SQL,数字辅助表

Posted

技术标签:

【中文标题】SQL,数字辅助表【英文标题】:SQL, Auxiliary table of numbers 【发布时间】:2010-09-05 20:46:25 【问题描述】:

对于某些类型的 sql 查询,辅助数字表可能非常有用。它可以创建为包含特定任务所需行数的表,也可以创建为返回每个查询所需行数的用户定义函数。

创建这样一个函数的最佳方法是什么?

【问题讨论】:

您能解释一下为什么要这样做而不是使用预先填满数字的表格吗? 以填写这样的表格为例。 并非所有 DBA 和/或第 3 方应用程序都允许添加永久表。 在https://feedback.azure.com/forums/908035-sql-server/suggestions/32890519-add-a-built-in-table-of-numbers投票支持不浪费内存和IO的内置虚拟数字表功能 【参考方案1】:

嘿...对不起,我这么晚才回复旧帖子。而且,是的,我必须做出回应,因为该线程上最受欢迎的答案(当时,带有指向 14 种不同方法的链接的递归 CTE 答案)是,嗯……充其量是对性能的挑战。

首先,包含 14 种不同解决方案的文章非常适合查看动态创建 Numbers/Tally 表的不同方法,但正如文章和引用线程中所指出的,有一个 非常 em> 重要的引述...

"关于效率和 性能往往是主观的。 不管查询是如何进行的 使用,物理实现 决定查询的效率。 因此,与其依赖 有偏见的指导方针,势在必行 您测试查询并确定 哪个表现更好。”

具有讽刺意味的是,文章本身包含许多主观陈述和“有偏见的指导方针”,例如“递归 CTE 可以生成一个数字列表相当有效”这是从 Itzik Ben-Gen 发布的新闻组中使用 WHILE 循环的一种有效的方法(我确信他发布的目的只是为了比较)。来吧,伙计们......仅仅提到Itzik的好名字可能会导致一些可怜的懒汉实际使用这种可怕的方法。作者应该实践他所宣扬的内容,并且应该在做出如此荒谬的错误陈述之前进行一些性能测试,尤其是在面对任何可扩展性时。

考虑到在对任何代码的功能或某人“喜欢”什么做出任何主观声明之前实际进行一些测试,这里有一些代码您可以自己进行测试。为您运行测试的 SPID 设置探查器并自行检查...只需为您的“最喜欢”号码执行数字 1000000 的“Search'n'Replace”并查看...

--===== Test for 1000000 rows ==================================
GO
--===== Traditional RECURSIVE CTE method
   WITH Tally (N) AS 
        ( 
         SELECT 1 UNION ALL 
         SELECT 1 + N FROM Tally WHERE N < 1000000 
        ) 
 SELECT N 
   INTO #Tally1 
   FROM Tally 
 OPTION (MAXRECURSION 0);
GO
--===== Traditional WHILE LOOP method
 CREATE TABLE #Tally2 (N INT);
    SET NOCOUNT ON;
DECLARE @Index INT;
    SET @Index = 1;
  WHILE @Index <= 1000000 
  BEGIN 
         INSERT #Tally2 (N) 
         VALUES (@Index);
            SET @Index = @Index + 1;
    END;
GO
--===== Traditional CROSS JOIN table method
 SELECT TOP (1000000)
        ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS N
   INTO #Tally3
   FROM Master.sys.All_Columns ac1
  CROSS JOIN Master.sys.ALL_Columns ac2;
GO
--===== Itzik's CROSS JOINED CTE method
   WITH E00(N) AS (SELECT 1 UNION ALL SELECT 1),
        E02(N) AS (SELECT 1 FROM E00 a, E00 b),
        E04(N) AS (SELECT 1 FROM E02 a, E02 b),
        E08(N) AS (SELECT 1 FROM E04 a, E04 b),
        E16(N) AS (SELECT 1 FROM E08 a, E08 b),
        E32(N) AS (SELECT 1 FROM E16 a, E16 b),
   cteTally(N) AS (SELECT ROW_NUMBER() OVER (ORDER BY N) FROM E32)
 SELECT N
   INTO #Tally4
   FROM cteTally
  WHERE N <= 1000000;
GO
--===== Housekeeping
   DROP TABLE #Tally1, #Tally2, #Tally3, #Tally4;
GO

当我们在这里的时候,这里是我从 SQL Profiler 中得到的数值,分别是 100、1000、10000、100000 和 1000000...

SPID TextData                                 Dur(ms) CPU   Reads   Writes
---- ---------------------------------------- ------- ----- ------- ------
  51 --===== Test for 100 rows ==============       8     0       0      0
  51 --===== Traditional RECURSIVE CTE method      16     0     868      0
  51 --===== Traditional WHILE LOOP method CR      73    16     175      2
  51 --===== Traditional CROSS JOIN table met      11     0      80      0
  51 --===== Itzik's CROSS JOINED CTE method        6     0      63      0
  51 --===== Housekeeping   DROP TABLE #Tally      35    31     401      0

  51 --===== Test for 1000 rows =============       0     0       0      0
  51 --===== Traditional RECURSIVE CTE method      47    47    8074      0
  51 --===== Traditional WHILE LOOP method CR      80    78    1085      0
  51 --===== Traditional CROSS JOIN table met       5     0      98      0
  51 --===== Itzik's CROSS JOINED CTE method        2     0      83      0
  51 --===== Housekeeping   DROP TABLE #Tally       6    15     426      0

  51 --===== Test for 10000 rows ============       0     0       0      0
  51 --===== Traditional RECURSIVE CTE method     434   344   80230     10
  51 --===== Traditional WHILE LOOP method CR     671   563   10240      9
  51 --===== Traditional CROSS JOIN table met      25    31     302     15
  51 --===== Itzik's CROSS JOINED CTE method       24     0     192     15
  51 --===== Housekeeping   DROP TABLE #Tally       7    15     531      0

  51 --===== Test for 100000 rows ===========       0     0       0      0
  51 --===== Traditional RECURSIVE CTE method    4143  3813  800260    154
  51 --===== Traditional WHILE LOOP method CR    5820  5547  101380    161
  51 --===== Traditional CROSS JOIN table met     160   140     479    211
  51 --===== Itzik's CROSS JOINED CTE method      153   141     276    204
  51 --===== Housekeeping   DROP TABLE #Tally      10    15     761      0

  51 --===== Test for 1000000 rows ==========       0     0       0      0
  51 --===== Traditional RECURSIVE CTE method   41349 37437 8001048   1601
  51 --===== Traditional WHILE LOOP method CR   59138 56141 1012785   1682
  51 --===== Traditional CROSS JOIN table met    1224  1219    2429   2101
  51 --===== Itzik's CROSS JOINED CTE method     1448  1328    1217   2095
  51 --===== Housekeeping   DROP TABLE #Tally       8     0     415      0

如您所见,递归 CTE 方法在 Duration 和 CPU 方面仅次于 While 循环,其内存压力是 While 循环的 8 倍。它是类固醇上的 RBAR,应该不惜一切代价避免任何单行计算,就像应该避免 While 循环一样。 有些地方递归非常有价值,但这不是其中之一

作为边栏,Denny 先生绝对是...正确大小的永久性 Numbers 或 Tally 表是处理大多数事情的方式。大小合适是什么意思?好吧,大多数人使用 Tally 表来生成日期或在 VARCHAR(8000) 上进行拆分。如果您使用“N”上的正确聚集索引创建一个 11,000 行的 Tally 表,您将有足够的行来创建超过 30 年的日期(我经常使用抵押贷款,所以 30 年对我来说是一个关键数字) 并且肯定足以处理 VARCHAR(8000) 拆分。为什么“正确的尺寸”如此重要?如果 Tally 表被大量使用,它很容易放入缓存中,这使得它非常快,根本不会对内存造成太大压力。

最后但并非最不重要的一点是,每个人都知道,如果您创建一个永久的 Tally 表,那么您使用哪种方法来构建它并不重要,因为 1)它只会制作一次,2)如果它类似于一个 11,000 行的表,所有方法都将运行“足够好”。 那么为什么我对使用哪种方法感到愤慨???

答案是,一些不知道更好,只需要完成他或她的工作的可怜的家伙/gal 可能会看到类似于递归 CTE 方法的东西,并决定将其用于更大、更频繁的事情使用而不是建立一个永久的 Tally 表,我试图保护这些人、他们的代码运行所在的服务器以及拥有这些服务器上数据的公司。是的……这有什么大不了的。它也应该适用于其他所有人。教导正确的做事方式,而不是“足够好”。在发布或使用帖子或书中的内容之前进行一些测试......事实上,您所拯救的生命可能是您自己的,特别是如果您认为递归 CTE 是实现此类目标的方法。 ;-)

感谢收听...

【讨论】:

我真的很希望更多的人有你的社会责任感。已经说过了,如果出于某种原因需要,则需要一次为所有类型的东西填充数字表,it seems SELECT INTO w/ IDENTITY is faster than CTE。 感谢您的友好反馈,安德烈。【参考方案2】:

最优化的函数是使用表格而不是函数。使用函数会导致额外的 CPU 负载来为返回的数据创建值,尤其是在返回的值覆盖非常大的范围时。

【讨论】:

我认为这取决于你的情况。在两个性能最佳的选项之间,您可以在 IO 和 CPU 成本之间进行权衡,具体取决于对您而言哪个更昂贵。 IO 几乎总是比 CPU 便宜,尤其是这个表很小并且可能已经在 budferpool 中。 @mrdenny I/O 总是方式比 CPU 更昂贵和更慢。近年来,SSD 对此有所改变,但在大多数生产架构中,这些 SSD 在它们和 CPU 之间都有网络链接。我看到的唯一真正受 CPU 限制的数据库是运行未经调优的仅 ORM 应用程序或繁重的机器学习。 @rmalayter 除非该表的使用频率足以让我们关心,它几乎肯定会在内存中,并且内存升级成本更低,并且通常不会像添加 CPU 内核那样影响许可. SQL Server 企业版将在每个核心 5 位数字的球场内,即添加核心可能会比在服务器中投入更多内存的全部成本花费更多的许可费用。【参考方案3】:

This article 给出了 14 种不同的可能解决方案,并对每种解决方案进行了讨论。重点是:

关于效率和 性能往往是主观的。 不管查询是如何进行的 使用,物理实现 决定查询的效率。 因此,与其依赖 有偏见的指导方针,势在必行 您测试查询并确定 哪个表现更好。

我个人喜欢:

WITH Nbrs ( n ) AS (
    SELECT 1 UNION ALL
    SELECT 1 + n FROM Nbrs WHERE n < 500 )
SELECT n FROM Nbrs
OPTION ( MAXRECURSION 500 )

【讨论】:

被接受的答案证明是错误的?虽然看起来很帅,但它不是“最佳”。【参考方案4】:

这个视图速度非常快,包含所有正的int 值。

CREATE VIEW dbo.Numbers
WITH SCHEMABINDING
AS
    WITH Int1(z) AS (SELECT 0 UNION ALL SELECT 0)
    , Int2(z) AS (SELECT 0 FROM Int1 a CROSS JOIN Int1 b)
    , Int4(z) AS (SELECT 0 FROM Int2 a CROSS JOIN Int2 b)
    , Int8(z) AS (SELECT 0 FROM Int4 a CROSS JOIN Int4 b)
    , Int16(z) AS (SELECT 0 FROM Int8 a CROSS JOIN Int8 b)
    , Int32(z) AS (SELECT TOP 2147483647 0 FROM Int16 a CROSS JOIN Int16 b)
    SELECT ROW_NUMBER() OVER (ORDER BY z) AS n
    FROM Int32
GO

【讨论】:

0 通常很有用。我可能会将最后一列转换为int。您还应该知道,该方法基本上包含在接受的答案中(没有0 或转换为int),名称为Itzik's CROSS JOINED CTE 方法 在视图中添加WITH SCHEMABINDING 有什么特别的原因吗? 添加“WITH SCHEMABINDING”可以加快查询速度。它帮助优化器知道没有数据被访问。 (见blogs.msdn.com/b/sqlprogrammability/archive/2006/05/12/…) 我想知道@AnthonyFaull 是否可以通过一些测量来支持这一点。【参考方案5】:

使用SQL Server 2016+ 生成数字表,您可以使用OPENJSON

-- range from 0 to @max - 1
DECLARE @max INT = 40000;

SELECT rn = CAST([key] AS INT) 
FROM OPENJSON(CONCAT('[1', REPLICATE(CAST(',1' AS VARCHAR(MAX)),@max-1),']'));

LiveDemo


想法来自How can we use OPENJSON to generate series of numbers?

【讨论】:

不错。我想,如果position() 在 SQL Server 的 XQuery 中得到完全支持,则可以使用与此类似的 XML。 对于迟到的评论感到抱歉,但与 Itik 的级联 CTE 方法相比,该代码使用了 11.4 倍的 CPU 和无限多的逻辑读取 (2,000,023)。【参考方案6】:

编辑:见下方康拉德的评论。

Jeff Moden 的回答很棒……但我在 Postgres 上发现 Itzik 方法会失败,除非您删除 E32 行。

在 postgres 上稍快(40ms vs 100ms)是我在 here 上找到的另一种适用于 postgres 的方法:

WITH 
    E00 (N) AS ( 
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 ),
    E01 (N) AS (SELECT a.N FROM E00 a CROSS JOIN E00 b),
    E02 (N) AS (SELECT a.N FROM E01 a CROSS JOIN E01 b ),
    E03 (N) AS (SELECT a.N FROM E02 a CROSS JOIN E02 b 
        LIMIT 11000  -- end record  11,000 good for 30 yrs dates
    ), -- max is 100,000,000, starts slowing e.g. 1 million 1.5 secs, 2 mil 2.5 secs, 3 mill 4 secs
    Tally (N) as (SELECT row_number() OVER (ORDER BY a.N) FROM E03 a)

SELECT N
FROM Tally

当我从 SQL Server 迁移到 Postgres 世界时,可能错过了在该平台上创建计数表的更好方法... INTEGER()?序列()?

【讨论】:

可能错过了在 [postgres] 上做计数表的更好方法 是的,你做到了 generate_series @Conrad Frix,对于迟到的问题(晚了 5 年多)表示歉意,但您是否进行过任何性能测试以将这个出色的内置工具与其他方法进行比较? @JeffModen 抱歉,不,但它很容易测试。获取 Ruskin 的查询并将其与生成系列的调用进行比较。 @Conrad Frix ,因为您声称性能并且您可以访问这两种环境(我没有)并且您还声称它很容易测试,我希望您会接受测试它的时间。 ;-) @Conrad Frix,呵呵...你已经设置好了,你不能花 5 分钟来测试你自己的性能声明。 NP。继续前进,【参考方案7】:

很久以后,我想贡献一个稍微不同的“传统”CTE(不接触基表来获取行数):

--===== Hans CROSS JOINED CTE method
WITH Numbers_CTE (Digit)
AS
(SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9)
SELECT HundredThousand.Digit * 100000 + TenThousand.Digit * 10000 + Thousand.Digit * 1000 + Hundred.Digit * 100 + Ten.Digit * 10 + One.Digit AS Number
INTO #Tally5
FROM Numbers_CTE AS One CROSS JOIN Numbers_CTE AS Ten CROSS JOIN Numbers_CTE AS Hundred CROSS JOIN Numbers_CTE AS Thousand CROSS JOIN Numbers_CTE AS TenThousand CROSS JOIN Numbers_CTE AS HundredThousand

此 CTE 执行的 READ 比 Itzik 的 CTE 多,但比传统 CTE 少。 但是,它执行的 WRITES 始终少于其他查询。 如您所知,写入始终比读取昂贵得多。

持续时间很大程度上取决于核心数 (MAXDOP),但在我的 8core 上,执行速度始终比其他查询更快(以毫秒为单位的持续时间更短)。

我正在使用:

Microsoft SQL Server 2012 - 11.0.5058.0 (X64) 
May 14 2014 18:34:29 
Copyright (c) Microsoft Corporation
Enterprise Edition (64-bit) on Windows NT 6.3 <X64> (Build 9600: )

在 Windows Server 2012 R2、32 GB、Xeon X3450 @2.67Ghz、4 核 HT 上启用。

【讨论】:

以上是关于SQL,数字辅助表的主要内容,如果未能解决你的问题,请参考以下文章

数字代码表

sql表如何添加从sql表中获取的数字数组

sql 字符串随地吐痰,数字表,理货表,

mysql 日期辅助表

ORA-06502: PL/SQL: 数字或值错误: NULL 索引表键值

在 SQL 中查找表之间缺失的数字