首先,对于自上次发表评论以来拖延回复我深表歉意。
该主题出现在评论中,由于行数少,因此使用递归CTE(此处为rCTE)运行速度足够快。尽管看起来可能是这样,但事实并非如此。
建立合计表和合计功能
在开始测试之前,我们需要使用适当的聚簇索引和Itzik Ben-Gan风格的提示功能构建物理提示表。我们还将在TempDB中进行所有这些操作,以免意外丢弃任何人的好东西。
这是构建Tally Table的代码,以及我当前生产的Itzik出色代码的版本。
--===== Do this in a nice, safe place that everyone has
USE tempdb
;
--===== Create/Recreate a Physical Tally Table
IF OBJECT_ID('dbo.Tally','U') IS NOT NULL
DROP TABLE dbo.Tally
;
-- Note that the ISNULL makes a NOT NULL column
SELECT TOP 1000001
N = ISNULL(ROW_NUMBER() OVER (ORDER BY (SELECT NULL))-1,0)
INTO dbo.Tally
FROM sys.all_columns ac1
CROSS JOIN sys.all_columns ac2
;
ALTER TABLE dbo.Tally
ADD CONSTRAINT PK_Tally PRIMARY KEY CLUSTERED (N)
;
--===== Create/Recreate a Tally Function
IF OBJECT_ID('dbo.fnTally','IF') IS NOT NULL
DROP FUNCTION dbo.fnTally
;
GO
CREATE FUNCTION [dbo].[fnTally]
/**********************************************************************************************************************
Purpose:
Return a column of BIGINTs from @ZeroOrOne up to and including @MaxN with a max value of 1 Trillion.
As a performance note, it takes about 00:02:10 (hh:mm:ss) to generate 1 Billion numbers to a throw-away variable.
Usage:
--===== Syntax example (Returns BIGINT)
SELECT t.N
FROM dbo.fnTally(@ZeroOrOne,@MaxN) t
;
Notes:
1. Based on Itzik Ben-Gan's cascading CTE (cCTE) method for creating a "readless" Tally Table source of BIGINTs.
Refer to the following URLs for how it works and introduction for how it replaces certain loops.
http://www.sqlservercentral.com/articles/T-SQL/62867/
http://sqlmag.com/sql-server/virtual-auxiliary-table-numbers
2. To start a sequence at 0, @ZeroOrOne must be 0 or NULL. Any other value that's convertable to the BIT data-type
will cause the sequence to start at 1.
3. If @ZeroOrOne = 1 and @MaxN = 0, no rows will be returned.
5. If @MaxN is negative or NULL, a "TOP" error will be returned.
6. @MaxN must be a positive number from >= the value of @ZeroOrOne up to and including 1 Billion. If a larger
number is used, the function will silently truncate after 1 Billion. If you actually need a sequence with
that many values, you should consider using a different tool. ;-)
7. There will be a substantial reduction in performance if "N" is sorted in descending order. If a descending
sort is required, use code similar to the following. Performance will decrease by about 27% but it's still
very fast especially compared with just doing a simple descending sort on "N", which is about 20 times slower.
If @ZeroOrOne is a 0, in this case, remove the "+1" from the code.
DECLARE @MaxN BIGINT;
SELECT @MaxN = 1000;
SELECT DescendingN = @MaxN-N+1
FROM dbo.fnTally(1,@MaxN);
8. There is no performance penalty for sorting "N" in ascending order because the output is explicity sorted by
ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
Revision History:
Rev 00 - Unknown - Jeff Moden
- Initial creation with error handling for @MaxN.
Rev 01 - 09 Feb 2013 - Jeff Moden
- Modified to start at 0 or 1.
Rev 02 - 16 May 2013 - Jeff Moden
- Removed error handling for @MaxN because of exceptional cases.
Rev 03 - 22 Apr 2015 - Jeff Moden
- Modify to handle 1 Trillion rows for experimental purposes.
**********************************************************************************************************************/
(@ZeroOrOne BIT, @MaxN BIGINT)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN WITH
E1(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) --10E1 or 10 rows
, E4(N) AS (SELECT 1 FROM E1 a, E1 b, E1 c, E1 d) --10E4 or 10 Thousand rows
,E12(N) AS (SELECT 1 FROM E4 a, E4 b, E4 c) --10E12 or 1 Trillion rows
SELECT N = 0 WHERE ISNULL(@ZeroOrOne,0)= 0 --Conditionally start at 0.
UNION ALL
SELECT TOP(@MaxN) N = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E12 -- Values from 1 to @MaxN
;
GO
顺便说一下,请注意,它建立了一个百万行的Tally表,并在大约一秒钟的时间内向其中添加了聚簇索引。用rCTE尝试一下,看看需要多长时间!;-)
建立一些测试数据
我们还需要一些测试数据。是的,我同意我们要测试的所有功能(包括rCTE)仅在一毫秒或更短的时间内才能运行12行,但这是很多人陷入的陷阱。稍后,我们将详细讨论该陷阱,但现在,让我们模拟每个函数调用40,000次,这相当于我的商店中的某些函数在一天8小时内被调用多少次。试想一下,在大型的在线零售业务中可能会调用这些功能多少次。
因此,这是构建具有随机日期的40,000行的代码,每个行都有一个行号仅用于跟踪目的。我没有花时间来整个小时,因为这无关紧要。
--===== Do this in a nice, safe place that everyone has
USE tempdb
;
--===== Create/Recreate a Test Date table
IF OBJECT_ID('dbo.TestDate','U') IS NOT NULL
DROP TABLE dbo.TestDate
;
DECLARE @StartDate DATETIME
,@EndDate DATETIME
,@Rows INT
;
SELECT @StartDate = '2010' --Inclusive
,@EndDate = '2020' --Exclusive
,@Rows = 40000 --Enough to simulate an 8 hour day where I work
;
SELECT RowNum = IDENTITY(INT,1,1)
,SomeDateTime = RAND(CHECKSUM(NEWID()))*DATEDIFF(dd,@StartDate,@EndDate)+@StartDate
INTO dbo.TestDate
FROM dbo.fnTally(1,@Rows)
;
建立一些功能来做12行小时的事情
接下来,我将rCTE代码转换为一个函数并创建了3个其他函数。它们都是作为高性能iTVF(内联表值函数)创建的。您总能说出来,因为iTVF从来没有像标量或mTVF(多语句表值函数)那样具有BEGIN。
这是构建这4个函数的代码...我用它们使用的方法命名它们,而不是仅仅为了使它们易于识别而做的事情。
--===== CREATE THE iTVFs
--===== Do this in a nice, safe place that everyone has
USE tempdb
;
-----------------------------------------------------------------------------------------
IF OBJECT_ID('dbo.OriginalrCTE','IF') IS NOT NULL
DROP FUNCTION dbo.OriginalrCTE
;
GO
CREATE FUNCTION dbo.OriginalrCTE
(@Date DATETIME)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
WITH Dates AS
(
SELECT DATEPART(HOUR,DATEADD(HOUR,-1,@Date)) [Hour],
DATEADD(HOUR,-1,@Date) [Date], 1 Num
UNION ALL
SELECT DATEPART(HOUR,DATEADD(HOUR,-1,[Date])),
DATEADD(HOUR,-1,[Date]), Num+1
FROM Dates
WHERE Num <= 11
)
SELECT [Hour], [Date]
FROM Dates
GO
-----------------------------------------------------------------------------------------
IF OBJECT_ID('dbo.MicroTally','IF') IS NOT NULL
DROP FUNCTION dbo.MicroTally
;
GO
CREATE FUNCTION dbo.MicroTally
(@Date DATETIME)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
SELECT [Hour] = DATEPART(HOUR,DATEADD(HOUR,t.N,@Date))
,[DATE] = DATEADD(HOUR,t.N,@Date)
FROM (VALUES (-1),(-2),(-3),(-4),(-5),(-6),(-7),(-8),(-9),(-10),(-11),(-12))t(N)
;
GO
-----------------------------------------------------------------------------------------
IF OBJECT_ID('dbo.PhysicalTally','IF') IS NOT NULL
DROP FUNCTION dbo.PhysicalTally
;
GO
CREATE FUNCTION dbo.PhysicalTally
(@Date DATETIME)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
SELECT [Hour] = DATEPART(HOUR,DATEADD(HOUR,-t.N,@Date))
,[DATE] = DATEADD(HOUR,-t.N,@Date)
FROM dbo.Tally t
WHERE N BETWEEN 1 AND 12
;
GO
-----------------------------------------------------------------------------------------
IF OBJECT_ID('dbo.TallyFunction','IF') IS NOT NULL
DROP FUNCTION dbo.TallyFunction
;
GO
CREATE FUNCTION dbo.TallyFunction
(@Date DATETIME)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
SELECT [Hour] = DATEPART(HOUR,DATEADD(HOUR,-t.N,@Date))
,[DATE] = DATEADD(HOUR,-t.N,@Date)
FROM dbo.fnTally(1,12) t
;
GO
建立测试线束以测试功能
最后但并非最不重要的一点是,我们需要一个测试工具。我进行基线检查,然后以相同的方式测试每个功能。
这是测试工具的代码...
PRINT '--========== Baseline Select =================================';
DECLARE @Hour INT, @Date DATETIME
;
SET STATISTICS TIME,IO ON;
SELECT @Hour = RowNum
,@Date = SomeDateTime
FROM dbo.TestDate
CROSS APPLY dbo.fnTally(1,12);
SET STATISTICS TIME,IO OFF;
GO
PRINT '--========== Orginal Recursive CTE ===========================';
DECLARE @Hour INT, @Date DATETIME
;
SET STATISTICS TIME,IO ON;
SELECT @Hour = fn.[Hour]
,@Date = fn.[Date]
FROM dbo.TestDate td
CROSS APPLY dbo.OriginalrCTE(td.SomeDateTime) fn;
SET STATISTICS TIME,IO OFF;
GO
PRINT '--========== Dedicated Micro-Tally Table =====================';
DECLARE @Hour INT, @Date DATETIME
;
SET STATISTICS TIME,IO ON;
SELECT @Hour = fn.[Hour]
,@Date = fn.[Date]
FROM dbo.TestDate td
CROSS APPLY dbo.MicroTally(td.SomeDateTime) fn;
SET STATISTICS TIME,IO OFF;
GO
PRINT'--========== Physical Tally Table =============================';
DECLARE @Hour INT, @Date DATETIME
;
SET STATISTICS TIME,IO ON;
SELECT @Hour = fn.[Hour]
,@Date = fn.[Date]
FROM dbo.TestDate td
CROSS APPLY dbo.PhysicalTally(td.SomeDateTime) fn;
SET STATISTICS TIME,IO OFF;
GO
PRINT'--========== Tally Function ===================================';
DECLARE @Hour INT, @Date DATETIME
;
SET STATISTICS TIME,IO ON;
SELECT @Hour = fn.[Hour]
,@Date = fn.[Date]
FROM dbo.TestDate td
CROSS APPLY dbo.TallyFunction(td.SomeDateTime) fn;
SET STATISTICS TIME,IO OFF;
GO
在上面的测试工具中要注意的一件事是,我将所有输出均分流到“丢弃”变量中。这样做是为了使性能测量尽可能纯净,而不会对磁盘或屏幕产生任何歪斜结果。
关于集合统计的注意事项
另外,对可能要测试的人也要当心……测试标量或mTVF函数时,切勿使用SET STATISTICS。它只能安全地用于iTVF功能(如本测试中的功能)。实践证明,SET STATISTICS使SCALAR函数的运行速度比不使用SCALAR函数时实际运行速度慢数百倍。是的,我正在尝试倾斜另一台风车,但这将是一篇完整的“篇幅长的文章”,而我没有时间。我在SQLServerCentral.com上有一篇文章都在谈论这一切,但是在此处发布链接毫无意义,因为有人会对此一无所知。
测试结果
因此,这是在装有6GB RAM的小型i5笔记本电脑上运行测试工具时的测试结果。
--========== Baseline Select =================================
Table 'Worktable'. Scan count 1, logical reads 82309, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestDate'. Scan count 1, logical reads 105, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 203 ms, elapsed time = 206 ms.
--========== Orginal Recursive CTE ===========================
Table 'Worktable'. Scan count 40001, logical reads 2960000, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestDate'. Scan count 1, logical reads 105, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 4258 ms, elapsed time = 4415 ms.
--========== Dedicated Micro-Tally Table =====================
Table 'Worktable'. Scan count 1, logical reads 81989, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestDate'. Scan count 1, logical reads 105, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 234 ms, elapsed time = 235 ms.
--========== Physical Tally Table =============================
Table 'Worktable'. Scan count 1, logical reads 81989, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestDate'. Scan count 1, logical reads 105, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Tally'. Scan count 1, logical reads 3, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 250 ms, elapsed time = 252 ms.
--========== Tally Function ===================================
Table 'Worktable'. Scan count 1, logical reads 81989, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestDate'. Scan count 1, logical reads 105, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 250 ms, elapsed time = 253 ms.
仅选择数据(每行创建12次以模拟相同的收益量)的“ BASELINE SELECT”大约在1/5秒之内。其他所有内容大约需要四分之一秒。好吧,除了血腥的rCTE功能外,其他所有功能。花了4到1/4秒或16倍的时间(慢了1600%)。
再看一下逻辑读取(内存IO)... rCTE消耗了296万(近300万次读取),而其他功能仅消耗了82100。这意味着rCTE消耗的内存IO是其他任何功能的34.3倍以上。
结束思想
让我们总结一下。用于执行此“小型” 12行操作的rCTE方法比其他任何功能使用的CPU(和持续时间)多了16倍(1600%),而内存IO则多了34.3倍(3,430%)。
嘿...我知道你在想什么。“大不了!这只是一项功能。”
是的,同意,但是您还有多少其他功能?除了功能,您还有其他几个地方?而且,您是否有其中每次运行仅超过12行的那些?而且,是否有可能某个人一头雾水地某个方法可能会复制rCTE代码以获得更大的东西?
好吧,时间直率。人们仅仅因为有限的行数或使用量来证明性能受到挑战的代码是绝对没有道理的。除了当您以数百万美元的价格购买MPP盒(更不用说重写代码以使其在这样的机器上工作而花费的费用)之外,您无法购买运行代码速度提高16倍的机器(赢得了SSD)也不做...所有这些东西在我们测试时都在高速内存中。性能在代码中。好的性能就是好的代码。
您是否可以想象所有代码的运行速度“快”了16倍?
不要以低的行数甚至低的使用率来证明糟糕的代码或性能受到挑战的代码。如果这样做,您可能不得不借用我被指控倾斜的一台风车,以保持CPU和磁盘足够凉爽。;-)
在单词“ TALLY”上的单词
是的,我同意。从语义上讲,理货表包含数字,而不是“总数”。在我关于该主题的原始文章中(不是有关该技术的原始文章,但这是我的第一篇文章),我之所以称其为“ Tally”,并不是因为它包含的内容,而是因为它的作用...它是用来“计数”而不是循环,用来“计数”某物就是“计数”某物。;-)称呼您将要...数字表,理货表,序列表等。我不在乎 对我来说,“ Tally”更有意义,它是一个很好的懒惰DBA,只包含5个字母(2个相同)而不是7个字母,对于大多数人来说更容易说出来。它也是“单数的”,遵循我对表的命名约定。;-) 包含60年代一本书中的一页的文章也称为它。我将始终将其称为“理货单”,您仍然会知道我或别人的意思。我也避免像瘟疫这样的匈牙利符号,而是将函数称为“ fnTally”,这样我可以说:“好吧,如果您使用了我展示给您的eff-en Tally函数,那么您就不会遇到性能问题”违反人力资源。;-) 没有实际违反人力资源。;-) 没有实际违反人力资源。;-)
我更关心的是人们学会正确使用它,而不是诉诸性能受到挑战的rCTE和其他形式的Hidden RBAR。