In primo luogo, mi scuso per il ritardo nella mia risposta dai miei ultimi commenti.
L'argomento è emerso nei commenti secondo cui l'utilizzo di un CTE ricorsivo (da qui in poi rCTE) funziona abbastanza velocemente a causa del basso numero di righe. Sebbene possa apparire in questo modo, nulla potrebbe essere più lontano dalla verità.
COSTRUISCI TALLY TABLE E TALLY FUNCTION
Prima di iniziare i test, dobbiamo creare una tabella di conteggio fisica con l'indice cluster appropriato e una funzione di conteggio in stile Itzik Ben-Gan. Faremo anche tutto questo in TempDB in modo da non eliminare accidentalmente le chicche di nessuno.
Ecco il codice per costruire la Tally Table e la mia attuale versione di produzione del meraviglioso codice di 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
A proposito ... notate che ha creato un Tally Table da un milione e una riga e aggiunto un indice cluster ad esso in circa un secondo circa. Provalo con un rCTE e vedi quanto tempo impiega! ;-)
COSTRUISCI ALCUNI DATI DI PROVA
Abbiamo anche bisogno di alcuni dati di prova. Sì, sono d'accordo sul fatto che tutte le funzioni che testeremo, incluso l'rCTE, verranno eseguite in un millisecondo o meno per solo 12 righe, ma questa è la trappola in cui cadono molte persone. Parleremo di più di quella trappola più tardi ma, per ora, permettiamo di simulare la chiamata di ciascuna funzione 40.000 volte, ovvero quante volte alcune funzioni del mio negozio vengono chiamate in un giorno di 8 ore. Immagina quante volte tali funzioni potrebbero essere chiamate in una grande impresa di vendita al dettaglio online.
Quindi, ecco il codice per costruire 40.000 righe con date casuali, ognuna con un numero di riga solo a scopo di tracciamento. Non mi sono preso il tempo di fare ore intere perché non importa qui.
--===== 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)
;
COSTRUISCI ALCUNE FUNZIONI PER FARE LA COSA DELLE 12 ORE DI RIGA
Successivamente, ho convertito il codice rCTE in una funzione e ho creato altre 3 funzioni. Sono stati tutti creati come iTVF ad alte prestazioni (Inline Table Valued Functions). Puoi sempre dirlo perché iTVF non ha mai un INIZIO in loro come fanno Scalar o mTVF (funzioni con valori di tabella multiistruzione).
Ecco il codice per costruire quelle 4 funzioni ... Le ho chiamate in base al metodo che usano e non a quello che fanno solo per rendere più facile identificarle.
--===== 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
COSTRUISCI IL CABLAGGIO DI PROVA PER TESTARE LE FUNZIONI
Ultimo ma non meno importante, abbiamo bisogno di un'imbracatura di prova. Eseguo un controllo di base e quindi collaudo ogni funzione in modo identico.
Ecco il codice per il cablaggio di prova ...
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
Una cosa da notare nel cablaggio di prova sopra è che shunt tutto l'output in variabili "usa e getta". Questo per cercare di mantenere le misurazioni delle prestazioni il più pure possibile senza alcun output sul disco o risultati di inclinazione dello schermo.
UNA PAROLA DI ATTENZIONE SULLE STATISTICHE IMPOSTATE
Inoltre, un avvertimento per gli aspiranti tester ... NON DEVI utilizzare SET STATISTICS durante il test delle funzioni scalare o mTVF. Può essere utilizzato in modo sicuro solo su funzioni iTVF come quelle di questo test. SET STATISTICS ha dimostrato di far funzionare le funzioni SCALAR centinaia di volte più lentamente di quanto ne facciano effettivamente senza. Sì, sto cercando di inclinare un altro mulino a vento, ma sarebbe un post più lungo di un articolo e non ho tempo per farlo. Ho un articolo su SQLServerCentral.com che parla di questo, ma non ha senso pubblicare qui il link perché qualcuno si deformerà.
I RISULTATI DELLA PROVA
Quindi, ecco i risultati del test quando eseguo il cablaggio di prova sul mio piccolo laptop i5 con 6 GB di RAM.
--========== 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.
Il "BASELINE SELECT", che seleziona solo i dati (ogni riga creata 12 volte per simulare lo stesso volume di ritorno), è arrivato giusto circa 1/5 di secondo. Tutto il resto arrivò a circa un quarto di secondo. Bene, tutto tranne quella sanguinosa funzione rCTE. Ci sono voluti 4 e 1/4 secondi o 16 volte più a lungo (1.600% più lento).
E guarda le letture logiche (memoria IO) ... L'rCTE ha consumato ben 2.960.000 (quasi 3 MILIONI di letture) mentre le altre funzioni hanno consumato solo circa 82.100. Ciò significa che l'rCTE ha consumato più di 34,3 volte più memoria IO rispetto a qualsiasi altra funzione.
PENSIERI DI CHIUSURA
Riassumiamo. Il metodo rCTE per fare questa "piccola" cosa a 12 righe ha usato 16 TIMES (1.600%) più CPU (e durata) e 34.3 TIMES (3.430%) più memoria IO rispetto a qualsiasi altra funzione.
Heh ... So cosa stai pensando. "Un grosso problema! È solo una funzione."
Sì, d'accordo, ma quante altre funzioni hai? Quanti altri posti al di fuori delle funzioni hai? E hai qualcuno di quelli che funzionano con più di solo 12 righe per ogni corsa? E, c'è qualche possibilità che qualcuno in difficoltà per un metodo possa copiare quel codice rCTE per qualcosa di molto più grande?
Ok, è tempo di essere schietti. Non ha assolutamente senso per le persone giustificare il codice con problemi di prestazioni solo a causa del numero limitato di righe o dell'utilizzo. Tranne quando acquisti una scatola MPP per forse milioni di dollari (per non parlare delle spese di riscrittura del codice per farlo funzionare su una macchina del genere), non puoi acquistare una macchina che esegue il tuo codice 16 volte più velocemente (SSD ha vinto non farlo neanche ... tutte queste cose erano nella memoria ad alta velocità quando le abbiamo testate). Le prestazioni sono nel codice. Le buone prestazioni sono in un buon codice.
Riesci a immaginare se tutto il tuo codice fosse "solo" 16 volte più veloce?
Non giustificare mai codice difettoso o con prestazioni sfavorevoli su conteggi di riga bassi o utilizzo basso. Se lo fai, potresti dover prendere in prestito uno dei mulini a vento di cui sono stato accusato di inclinarmi per mantenere le tue CPU e i tuoi dischi abbastanza freschi. ;-)
UNA PAROLA SULLA PAROLA "TALLY"
Sì sono d'accordo. Semanticamente parlando, la Tally Table contiene numeri, non "conteggi". Nel mio articolo originale sull'argomento (non era l'articolo originale sulla tecnica ma era il mio primo su di esso), l'ho chiamato "Tally" non per ciò che contiene, ma per quello che fa ... è abituato a "contare" invece di eseguire il looping e a "Tally" qualcosa significa "contare" qualcosa. ;-) Chiamalo come vuoi ... Tabella dei numeri, Tabella dei riscontri, Tabella delle sequenze, qualunque cosa. Non mi interessa. Per me, "Tally" è più pieno e, essendo un buon DBA pigro, contiene solo 5 lettere (2 sono identiche) anziché 7 ed è più facile da dire per la maggior parte delle persone. È anche "singolare", che segue la mia convenzione di denominazione per i tavoli. ;-) E ' s anche come lo chiamava l'articolo che conteneva una pagina di un libro degli anni '60. Mi riferirò sempre ad esso come una "Tabella dei riscontri" e saprai ancora cosa intendo io o qualcun altro. Evito anche la notazione ungherese come la peste, ma ho chiamato la funzione "fnTally" in modo da poter dire "Beh, se avessi usato la funzione Tally eff-en che ti ho mostrato, non avresti un problema di prestazioni" senza che in realtà fosse un Violazione delle risorse umane. ;-) senza che si tratti effettivamente di una violazione delle risorse umane. ;-) senza che si tratti effettivamente di una violazione delle risorse umane. ;-)
Quello che mi preoccupa di più è che le persone imparano a usarlo correttamente invece di ricorrere a cose come rCTE con prestazioni limitate e altre forme di RBAR nascosto.