첫째, 마지막 의견 이후 답변이 지연된 것에 대해 사과드립니다.
주제는 재귀 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 Table을 작성하고 약 1 초 정도 클러스터 색인을 추가했습니다. rCTE로 THAT을 시도하고 시간이 얼마나 걸리는지 확인하십시오! ;-)
일부 테스트 데이터 구축
테스트 데이터도 필요합니다. 예, rCTE를 포함하여 테스트 할 모든 기능이 12 행 동안 밀리 초 이하로 실행되지만 많은 사람들이 빠지는 함정입니다. 나중에이 트랩에 대해 더 이야기 하겠지만 지금은 각 함수를 4 만 번 호출하는 것을 시뮬레이션 할 수 있습니다. 이는 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에는 Scalar 또는 mTVF (Multi-statement Table Valued Functions)와 같은 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
위의 테스트 하니스에서 주목해야 할 것은 모든 출력을 "throwaway"변수로 분류한다는 것입니다. 이는 디스크 또는 화면 왜곡 결과를 출력하지 않고 성능 측정을 가능한 한 순수하게 유지하는 것입니다.
설정된 통계에 대한주의 사항
또한 테스터들에게주의 할 점은 ... Scalar 또는 mTVF 함수를 테스트 할 때 SET STATISTICS를 사용해서는 안됩니다. 이 테스트와 같은 iTVF 기능에서만 안전하게 사용할 수 있습니다. SET STATISTICS는 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.
데이터를 선택하는 "BASELINE SELECT"(동일한 양의 리턴을 시뮬레이트하기 위해 각 행이 12 번 생성됨)는 약 1/5 초에 올랐습니다. 다른 모든 것은 약 1/4 초에 들어 왔습니다. 피의 rCTE 기능을 제외한 모든 것. 4 초 및 1/4 초 또는 16 배 더 오래 걸렸습니다 (1,600 % 느림).
논리적 읽기 (메모리 IO)를 살펴보십시오. rCTE는 무려 2,960,000 (거의 3 백만 건)을 소비했지만 다른 기능은 약 82,100 만 소비했습니다. 이는 rCTE가 다른 기능보다 34.3 배 더 많은 메모리 IO를 소비했음을 의미합니다.
생각을 닫기
요약하자. 이 "작은"12 행 작업을 수행하는 rCTE 방법은 다른 기능보다 16 TIMES (1,600 %) 더 많은 CPU (및 지속 시간)와 34.3 TIMES (3,430 %) 더 많은 메모리 IO를 사용했습니다.
허 .. 무슨 생각하는지 알아 "큰 거래! 하나의 기능 일뿐입니다."
예, 동의했지만 다른 기능은 몇 개입니까? 기능 이외의 다른 장소는 몇 개입니까? 그리고 한 번에 12 행 이상으로 작동하는 것들이 있습니까? 그리고 분석법을 찾고있는 누군가가 그 rCTE 코드를 훨씬 더 크게 복사 할 가능성이 있습니까?
좋아, 무딘 시간. 행 수나 사용량이 제한되어 있기 때문에 성능 문제가있는 코드를 정당화하는 것은 전혀 의미가 없습니다. MPP 상자를 수백만 달러에 구입할 때를 제외하고 (그런 기계에서 작동하도록 코드를 다시 작성하는 비용은 말할 것도 없습니다) 코드를 16 배 빠르게 실행하는 기계를 구입할 수는 없습니다 (SSD의 원화) 우리가 그것을 테스트 할 때이 모든 것들이 고속 메모리에있었습니다.) 성능은 코드에 있습니다. 좋은 성능은 좋은 코드입니다.
모든 코드가 16 배 더 빨리 실행되는지 상상할 수 있습니까?
행 수가 적거나 사용량이 적을 때 성능이 나쁘거나 성능이 떨어지는 코드를 정당화하지 마십시오. 그렇다면 CPU와 디스크를 충분히 시원하게 유지하기 위해 기울어 졌다고 비난받은 풍차 중 하나를 빌려야 할 수도 있습니다. ;-)
"전체적으로"라는 단어
네 ... 동의합니다. 의미 적으로 말해서 Tally Table은 "tallies"가 아닌 숫자를 포함합니다. 주제에 대한 나의 원래 기사 (기술에 관한 최초의 기사는 아니었지만 그것은 나의 첫 기사였다)에서 나는 그것이 포함 된 것이 아니라 그것이하는 것 때문에 "탈리 (Tally)"라고 불렀다. 루핑 대신 "계산"하고 무언가를 "계산"하는 데 사용됩니다. ;-) 당신이 무엇을할지 ... Numbers Table, Tally Table, Sequence Table, 뭐든간에. 상관 없어요 나를 위해, "Tally"는 더 많은 의미를 지니고 있으며 좋은 게으른 DBA이기 때문에 7 대신 5 글자 (2는 동일) 만 포함하며 대부분의 사람들에게 말하기가 더 쉽습니다. 또한 테이블에 대한 명명 규칙을 따르는 "단일"입니다. ;-) 그것은 또한 60 년대의 책에서 페이지를 포함하는 기사가 그것을 호출했습니다. 나는 항상 그것을 "탈리 테이블"이라고 부르며 여전히 당신이나 다른 사람이 무엇을 의미하는지 알게 될 것입니다. 나는 또한 전염병과 같은 헝가리 표기법을 피하지만 "fnTally"라는 함수를 호출하여 "음, 당신이 내가 보여준 eff-en Tally Function을 사용하면 실제로 성능 문제가 없을 것"이라고 말할 수 있습니다. HR 위반. ;-) 실제로 HR 위반이 아닙니다. ;-) 실제로 HR 위반이 아닙니다. ;-)
내가 더 걱정하는 것은 성능 문제가있는 rCTE 및 기타 형태의 숨겨진 RBAR과 같은 것에 의존하는 대신 올바르게 사용하는 법을 배우는 사람들입니다.