SQL,辅助数字表

时间:2008-08-14 09:01:38

标签: sql sql-server

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

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

7 个答案:

答案 0 :(得分:106)

嘿......对不起,我对这篇老帖子的反应太迟了。并且,是的,我必须回应,因为在这个帖子上最受欢迎的答案(当时,递归CTE答案以及14种不同方法的链接),嗯......性能充其量挑战。

首先,使用14种不同解决方案的文章可以很好地查看动态创建Numbers / Tally表的不同方法,但正如文章和引用的帖子中指出的那样,非常重要的一句......

  

关于效率和效率的建议   表现往往是主观的。   无论查询是如何形成的   使用,物理实现   确定查询的效率。   因此,而不是依靠   有偏见的指导方针,势在必行   您测试查询并确定   哪一个表现得更好。“

具有讽刺意味的是,该文章本身包含许多主观陈述和“有偏见的指导方针”,例如“递归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方法仅次于While循环持续时间和CPU,并且逻辑读取形式的内存压力是While While循环的8倍。它是类固醇的RBAR,应该不惜一切代价避免任何单行计算,就像应该避免使用While循环一样。 有些地方递归很有价值,但这不是其中之一

作为一个侧边栏,丹尼先生绝对是现货......正确大小的永久号码或理货桌是大多数事情的方法。正确大小意味着什么?好吧,大多数人使用Tally表来生成日期或在VARCHAR(8000)上进行拆分。如果您在“N”上创建一个11,000行Tally表并使用正确的聚集索引,那么您将拥有足够的行来创建超过30年的日期(我可以使用抵押贷款,所以30年对我来说是一个关键数字)当然足以处理VARCHAR(8000)拆分。为什么“正确的尺寸”如此重要?如果Tally表被大量使用,它很容易适应缓存,这使得它的速度非常快,而且内存压力很大。

最后但并非最不重要的是,每个人都知道,如果你创建一个永久的Tally表,那么使用哪种方法构建它并不重要,因为1)它只会被制作一次而2)如果它像一个11,000行表,所有方法都将运行“足够好”。 那么为什么所有关于我使用哪种方法的选择

答案是,一些不太了解并且只需要完成工作的穷人/加仑人可能会看到类似递归CTE方法的东西,并决定将其用于更大更频繁的东西使用而不是构建一个永久的Tally表,我试图保护那些人,他们的代码运行的服务器,以及拥有这些服务器上的数据的公司。是的......这是一笔大买卖。它也应该适用于其他所有人。教导正确的做事方式,而不是“足够好”。在发布或使用帖子或书籍中的某些内容之前进行一些测试...实际上,您节省的生命可能是您自己的,特别是如果您认为递归CTE是这样的方式。 ; - )

感谢您的聆听...

答案 1 :(得分:10)

最佳功能是使用表而不是函数。使用函数会导致额外的CPU负载,从而为返回的数据创建值,尤其是在返回的值覆盖范围非常大的情况下。

答案 2 :(得分:5)

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 )

答案 3 :(得分:3)

此视图速度非常快,包含所有正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

答案 4 :(得分:1)

使用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?

的想法

答案 5 :(得分:0)

更久以后,我想提供一个略有不同的'传统'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比Itzik的CTE执行更多的READ,但是比传统的CTE少。 但是,它始终比其他查询执行更少的WRITES。 如你所知,写作总是比Reads贵得多。

持续时间在很大程度上取决于核心数(MAXDOP),但在我的8核上,执行速度更快(以ms为单位的持续时间更短),然后是其他查询。

我正在使用:

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。

答案 6 :(得分:0)

编辑:请参阅下面的康拉德评论。

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()? SEQUENCE()?