对于某些类型的 sql 查询,辅助数字表可能非常有用。它可以创建为包含特定任务所需行数的表,也可以创建为返回每个查询所需行数的用户定义函数。
创建这样一个函数的最佳方法是什么?
对于某些类型的 sql 查询,辅助数字表可能非常有用。它可以创建为包含特定任务所需行数的表,也可以创建为返回每个查询所需行数的用户定义函数。
创建这样一个函数的最佳方法是什么?
嘿...对不起,我这么晚才回复旧帖子。而且,是的,我必须做出回应,因为该线程上最受欢迎的答案(当时,带有指向 14 种不同方法的链接的递归 CTE 答案)是,嗯......性能充其量是挑战。
首先,包含 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 方法在 Duration 和 CPU 方面仅次于 While 循环,其内存压力是 While 循环的 8 倍。它是类固醇上的 RBAR,应该不惜一切代价避免任何单行计算,就像应该避免 While 循环一样。 有些地方递归非常有价值,但这不是其中之一。
作为一个边栏,Denny 先生绝对是……正确大小的永久数字或 Tally 表是大多数事情的方式。大小合适是什么意思?好吧,大多数人使用 Tally 表来生成日期或在 VARCHAR(8000) 上进行拆分。如果您使用“N”上的正确聚集索引创建一个 11,000 行的 Tally 表,您将有足够的行来创建超过 30 年的日期(我经常使用抵押贷款,所以 30 年对我来说是一个关键数字) 并且肯定足以处理 VARCHAR(8000) 拆分。为什么“正确的尺寸”如此重要?如果 Tally 表被大量使用,它很容易放入缓存中,这使得它非常快,根本不会对内存造成太大压力。
最后但并非最不重要的一点是,每个人都知道,如果您创建一个永久的 Tally 表,那么您使用哪种方法来构建它并不重要,因为 1)它只会制作一次,2)如果它是 11,000 行表,所有方法都将运行“足够好”。 那么为什么我对使用哪种方法感到愤慨???
答案是,一些不知道更好,只需要完成他或她的工作的可怜人/女孩可能会看到类似递归 CTE 方法的东西,并决定将其用于比建筑更大、更频繁使用的东西一个永久的 Tally 表,我试图保护这些人,他们的代码运行的服务器,以及拥有这些服务器上数据的公司。是的……这有什么大不了的。它也应该适用于其他所有人。教导正确的做事方式,而不是“足够好”。在发布或使用帖子或书中的内容之前进行一些测试......事实上,您所拯救的生命可能是您自己的,特别是如果您认为递归 CTE 是实现此类目标的方法。;-)
感谢收听...
最优化的函数是使用表格而不是函数。使用函数会导致额外的 CPU 负载来为返回的数据创建值,尤其是在返回的值覆盖非常大的范围时。
本文给出了 14 种不同的可能解决方案,并对每种解决方案进行了讨论。重要的一点是:
关于效率和性能的建议通常是主观的。无论如何使用查询,物理实现都决定了查询的效率。因此,与其依赖有偏见的指导方针,不如测试查询并确定哪一个表现更好。
我个人喜欢:
WITH Nbrs ( n ) AS (
SELECT 1 UNION ALL
SELECT 1 + n FROM Nbrs WHERE n < 500 )
SELECT n FROM Nbrs
OPTION ( MAXRECURSION 500 )
这个视图超级快并且包含所有正值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
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),']'));
编辑:见下面康拉德的评论。
Jeff Moden 的回答很棒……但我在 Postgres 上发现 Itzik 方法会失败,除非您删除 E32 行。
在 postgres 上稍微快一点(40ms vs 100ms)是我在这里找到的另一种适用于 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()?顺序()?
很久以后,我想贡献一个稍微不同的“传统”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 上启用。