Erstens entschuldige ich mich für die Verzögerung meiner Antwort seit meinen letzten Kommentaren.
In den Kommentaren wurde darauf hingewiesen, dass die Verwendung eines rekursiven CTE (ab hier rCTE) aufgrund der geringen Zeilenanzahl schnell genug ist. Auch wenn es so scheint, könnte nichts weiter von der Wahrheit entfernt sein.
BUILD TALLY TABLE UND TALLY FUNCTION
Bevor wir mit dem Testen beginnen, müssen wir eine physische Tally-Tabelle mit dem entsprechenden Clustered-Index und einer Tally-Funktion im Itzik-Ben-Gan-Stil erstellen. Wir werden das alles auch in TempDB machen, damit wir nicht versehentlich die Goodies von irgendjemandem fallen lassen.
Hier ist der Code zum Erstellen der Tally-Tabelle und meine aktuelle Produktionsversion von Itziks wunderbarem Code.
--===== 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
Übrigens ... Beachten Sie, dass in ungefähr einer Sekunde eine Million und eine Zeile Tally Table erstellt und ein Clustered Index hinzugefügt wurde. Versuchen Sie das mit einem rCTE und sehen Sie, wie lange es dauert! ;-)
ERSTELLEN SIE EINIGE TESTDATEN
Wir brauchen auch einige Testdaten. Ja, ich stimme zu, dass alle Funktionen, die wir testen werden, einschließlich des rCTE, in einer Millisekunde oder weniger für nur 12 Zeilen ausgeführt werden, aber das ist die Falle, in die viele Menschen geraten. Wir werden später mehr über diese Falle sprechen, aber lassen Sie uns vorerst jeden Funktionsaufruf 40.000 Mal simulieren, dh wie oft bestimmte Funktionen in meinem Shop an einem 8-Stunden-Tag aufgerufen werden. Stellen Sie sich vor, wie oft solche Funktionen in einem großen Online-Einzelhandelsgeschäft aufgerufen werden könnten.
Hier ist also der Code zum Erstellen von 40.000 Zeilen mit zufälligen Datumsangaben, von denen jede eine Zeilennummer hat, nur zu Verfolgungszwecken. Ich habe mir nicht die Zeit genommen, ganze Stunden zu arbeiten, weil es hier keine Rolle spielt.
--===== 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)
;
BAUEN SIE EINIGE FUNKTIONEN, UM DIE 12-REIHEN-STUNDEN-SACHE ZU TUN
Als nächstes habe ich den rCTE-Code in eine Funktion konvertiert und 3 weitere Funktionen erstellt. Sie wurden alle als leistungsstarke iTVFs (Inline Table Valued Functions) erstellt. Sie können immer sagen, dass iTVFs keinen BEGIN enthalten, wie dies bei Scalar oder mTVFs (Multi-Statement Table Valued Functions) der Fall ist.
Hier ist der Code zum Erstellen dieser 4 Funktionen ... Ich habe sie nach der Methode benannt, die sie verwenden, und nicht nach dem, was sie tun, um es einfacher zu machen, sie zu identifizieren.
--===== 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
BAUEN SIE DAS TESTGURT, UM DIE FUNKTIONEN ZU TESTEN
Last but not least brauchen wir ein Testgeschirr. Ich mache einen Baseline-Check und teste dann jede Funktion auf identische Weise.
Hier ist der Code für das Testgeschirr ...
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
Eine Sache, die im obigen Testgeschirr zu beachten ist, ist, dass ich alle Ausgaben in "Wegwerf" -Variablen umleite. Das ist, um zu versuchen, die Leistungsmessungen so rein wie möglich zu halten, ohne dass Ergebnisse auf der Festplatte oder auf dem Bildschirm angezeigt werden.
VORSICHT BEI STATISTIKEN
Ein Hinweis zur Vorsicht für angehende Tester ... Sie dürfen SET STATISTICS NICHT verwenden, wenn Sie entweder die Skalar- oder die mTVF-Funktion testen. Es kann nur mit iTVF-Funktionen wie den in diesem Test beschriebenen sicher verwendet werden. SET STATISTICS führt nachweislich dazu, dass SCALAR-Funktionen hunderte Male langsamer ausgeführt werden, als dies tatsächlich der Fall ist. Ja, ich versuche, eine andere Windmühle zu kippen, aber das wäre ein ganzer Artikel, für den ich keine Zeit habe. Ich habe einen Artikel auf SQLServerCentral.com, in dem das alles behandelt wird, aber es macht keinen Sinn, den Link hier zu posten, da sich irgendjemand in dieser Hinsicht aus der Form bringen wird.
DIE TESTERGEBNISSE
Hier sind die Testergebnisse, wenn ich das Testkabel auf meinem kleinen i5-Laptop mit 6 GB RAM laufen lasse.
--========== 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.
Die "BASELINE SELECT", die nur Daten auswählt (jede Zeile wurde 12-mal erstellt, um dasselbe Rücklaufvolumen zu simulieren), kam ungefähr in einer Fünftelsekunde. Alles andere kam in einer Viertelsekunde. Nun, alles außer dieser verdammten rCTE-Funktion. Es dauerte 4 und 1/4 Sekunden oder 16 Mal länger (1.600% langsamer).
Und sehen Sie sich die logischen Lesevorgänge (Speicher-E / A) an ... Der rCTE verbrauchte satte 2.960.000 (fast 3 MILLION Lesevorgänge), während die anderen Funktionen nur etwa 82.100 verbrauchten. Das bedeutet, dass der rCTE mehr als 34,3-mal mehr Speicher-E / A verbraucht als alle anderen Funktionen.
SCHLIESSENDE GEDANKEN
Lassen Sie uns zusammenfassen. Die rCTE-Methode für diese "kleine" 12-Zeilen-Sache verwendete 16-mal (1.600%) mehr CPU (und Dauer) und 34,3-mal (3.430%) mehr Speicher-E / A als jede der anderen Funktionen.
Heh ... ich weiß was du denkst. "Big Deal! Es ist nur eine Funktion."
Ja, einverstanden, aber wie viele andere Funktionen haben Sie? Wie viele andere Orte außerhalb von Funktionen haben Sie? Und haben Sie welche, die mit mehr als nur 12 Zeilen pro Lauf arbeiten? Und gibt es eine Chance, dass jemand, der für eine Methode im Stich ist, diesen rCTE-Code für etwas viel Größeres kopiert?
Ok, Zeit stumpf zu sein. Es macht absolut keinen Sinn, Code mit Leistungsproblemen zu rechtfertigen, nur weil die Anzahl der Zeilen oder deren Verwendung begrenzt sein soll. Abgesehen davon, dass Sie eine MPP-Box für vielleicht Millionen von Dollar kaufen (ganz zu schweigen von den Kosten für das Umschreiben von Code, damit dieser auf einem solchen Computer funktioniert), können Sie keine Maschine kaufen, auf der Ihr Code 16-mal schneller ausgeführt wird (SSDs haben gewonnen) tu es auch nicht ... all dieses Zeug war im Hochgeschwindigkeitsspeicher (als wir es getestet haben). Leistung ist im Code. Gute Leistung steckt in gutem Code.
Können Sie sich vorstellen, dass Ihr gesamter Code "nur" 16-mal schneller lief?
Begründen Sie niemals schlechten oder leistungsgestörten Code bei geringer Zeilenanzahl oder sogar geringer Nutzung. Wenn Sie dies tun, müssen Sie möglicherweise eine der Windmühlen ausleihen, bei denen mir vorgeworfen wurde, sie zu neigen, um Ihre CPUs und Festplatten kühl genug zu halten. ;-)
EIN WORT ÜBER DAS WORT "TALLY"
Ja ich stimme zu. Semantisch gesehen enthält die Tally-Tabelle Zahlen, keine "Tallies". In meinem ursprünglichen Artikel zum Thema (es war nicht der ursprüngliche Artikel zur Technik, aber es war mein erster) habe ich es "Tally" genannt, nicht aufgrund dessen, was es enthält, sondern aufgrund dessen, was es tut ... es ist verwendet, um zu "zählen", anstatt zu schleifen, und um etwas zu "zählen", um etwas zu "zählen". ;-) Nenn es wie du willst ... Nummerntabelle, Tallytabelle, Sequenztabelle, was auch immer. Ist mir egal Für mich bedeutet "Tally" mehr "voll" und enthält als guter fauler DBA nur 5 Buchstaben (2 sind identisch) anstelle von 7, was für die meisten Leute einfacher zu sagen ist. Es ist auch "Singular", was meiner Namenskonvention für Tabellen folgt. ;-) Es ist Es ist auch das, was der Artikel, der eine Seite aus einem Buch aus den 60er Jahren enthielt, so nannte. Ich werde es immer als "Tally Table" bezeichnen und Sie werden immer noch wissen, was ich oder jemand anderes bedeutet. Ich vermeide auch die ungarische Notation wie die Pest, nenne aber die Funktion "fnTally", so dass ich sagen könnte "Wenn Sie die von mir gezeigte eff-en Tally-Funktion verwenden würden, hätten Sie kein Leistungsproblem", ohne dass es tatsächlich eine wäre HR Verletzung. ;-) ohne dass es sich tatsächlich um eine HR-Verletzung handelt. ;-) ohne dass es sich tatsächlich um eine HR-Verletzung handelt. ;-)
Was mich mehr beunruhigt, ist, dass die Leute lernen, es richtig zu verwenden, anstatt auf Dinge wie leistungsbehinderte rCTEs und andere Formen von versteckten RBAR zurückzugreifen.