Pertama, permintaan maaf saya atas keterlambatan respons saya sejak komentar terakhir saya.
Subjek muncul dalam komentar yang menggunakan CTE Rekursif (rCTE dari sini) berjalan cukup cepat karena rendahnya jumlah baris. Meskipun mungkin terlihat seperti itu, tidak ada yang lebih jauh dari kebenaran.
BANGUN TALLY TABLE DAN FUNGSI TALLY
Sebelum kita memulai pengujian, kita perlu membuat Tabel Tally fisik dengan Indeks Clustered yang sesuai dan Fungsi Tally gaya Itzik Ben-Gan. Kami juga akan melakukan semua ini di TempDB sehingga kami tidak sengaja menjatuhkan barang siapa pun.
Berikut kode untuk membuat Tally Table dan versi produksi saya saat ini dari kode hebat 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
Ngomong-ngomong ... perhatikan bahwa membangun jutaan dan satu baris Tabel Tally dan menambahkan Indeks Clustered ke dalam sekitar satu detik atau lebih. Coba ITU dengan rCTE dan lihat berapa lama! ;-)
BANGUN BEBERAPA DATA UJI
Kami juga membutuhkan beberapa data uji. Ya, saya setuju bahwa semua fungsi yang akan kami uji, termasuk rCTE, berjalan dalam milidetik atau kurang untuk hanya 12 baris tetapi itulah jebakan yang banyak orang jatuh ke dalamnya. Kita akan berbicara lebih banyak tentang jebakan itu nanti, tetapi, untuk saat ini, mari kita simulasikan memanggil setiap fungsi 40.000 kali, yaitu berapa kali fungsi-fungsi tertentu di toko saya dipanggil dalam 8 jam sehari. Bayangkan saja berapa kali fungsi seperti itu dapat disebut dalam bisnis ritel online besar.
Jadi, inilah kode untuk membangun 40.000 baris dengan tanggal acak, masing-masing memiliki Nomor Baris hanya untuk tujuan pelacakan. Saya tidak meluangkan waktu untuk membuat waktu penuh karena itu tidak masalah di sini.
--===== 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)
;
MEMBANGUN BEBERAPA FUNGSI UNTUK MELAKUKAN HAL 12 JAM
Selanjutnya, saya mengonversi kode rCTE ke fungsi dan membuat 3 fungsi lainnya. Semuanya telah dibuat sebagai iTVF kinerja tinggi (Inline Table Valued Functions). Anda selalu bisa tahu karena iTVF tidak pernah memiliki BEGIN di dalamnya seperti Scalar atau mTVF (Multi-statement Table Valued Functions).
Inilah kode untuk membangun 4 fungsi itu ... Saya menamainya setelah metode yang mereka gunakan dan bukan apa yang mereka lakukan hanya untuk membuatnya lebih mudah untuk mengidentifikasi mereka.
--===== 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
MEMBANGUN UJI HARNESS UNTUK MENGUJI FUNGSI
Terakhir namun tidak kalah pentingnya, kami membutuhkan test harness. Saya melakukan pemeriksaan awal dan kemudian menguji setiap fungsi dengan cara yang identik.
Berikut kode untuk alat uji ...
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
Satu hal yang perlu diperhatikan dalam test harness di atas adalah bahwa saya shunt semua output menjadi variabel "throwaway". Itu untuk mencoba menjaga pengukuran kinerja semurni mungkin tanpa output ke disk atau hasil skewing layar.
KATA PERHATIAN PADA SET STATISTIK
Juga, kata hati-hati untuk calon penguji ... Anda TIDAK HARUS menggunakan STATISTIK SET ketika menguji fungsi Scalar atau mTVF. Itu hanya dapat digunakan dengan aman pada fungsi iTVF seperti yang ada dalam tes ini. SET STATISTIK telah terbukti membuat fungsi SCALAR berjalan ratusan kali lebih lambat dari yang sebenarnya mereka lakukan tanpanya. Ya, saya mencoba memiringkan kincir angin lain, tetapi itu akan menjadi posting panjang artikel yang lebih gila dan saya tidak punya waktu untuk itu. Saya memiliki artikel tentang SQLServerCentral.com yang membicarakan semua itu tetapi tidak ada gunanya memposting tautan di sini karena seseorang akan merasa tidak nyaman dengan hal itu.
HASIL UJI
Jadi, inilah hasil tes ketika saya menjalankan test harness pada laptop i5 kecil saya dengan RAM 6GB.
--========== 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", yang hanya memilih data (setiap baris dibuat 12 kali untuk mensimulasikan volume pengembalian yang sama), muncul tepat sekitar 1/5 detik. Yang lainnya masuk sekitar seperempat detik. Ya, semuanya kecuali fungsi RCTE yang berdarah. Butuh 4 dan 1/4 detik atau 16 kali lebih lama (1.600% lebih lambat).
Dan lihat bacaan logis (memori IO) ... RCTE menghabiskan 2.960.000 (hampir 3 JUTI berbunyi) sedangkan fungsi lainnya hanya mengkonsumsi sekitar 82.100. Itu berarti rCTE mengkonsumsi IO memori lebih dari 34,3 kali lebih banyak daripada fungsi lainnya.
PIKIRAN PENUTUP
Mari kita simpulkan. Metode rCTE untuk melakukan hal "kecil" 12 baris ini menggunakan 16 KALI (1.600%) lebih banyak CPU (dan durasi) dan 34.3 KALI (3.430%) lebih banyak memori IO daripada fungsi lainnya.
Heh ... Saya tahu apa yang Anda pikirkan. "Kesepakatan Besar! Hanya satu fungsi."
Ya, setuju, tetapi berapa banyak fungsi lain yang Anda miliki? Berapa banyak tempat lain di luar fungsi yang Anda miliki? Dan apakah Anda memiliki salah satu dari mereka yang bekerja dengan lebih dari 12 baris setiap kali dijalankan? Dan, apakah ada kemungkinan bahwa seseorang yang sedang dalam kesulitan untuk metode dapat menyalin kode rCTE untuk sesuatu yang jauh lebih besar?
Ok, waktunya untuk berterus terang. Sangat tidak masuk akal bagi orang untuk membenarkan kode yang ditantang kinerja hanya karena dugaan jumlah baris atau penggunaan yang terbatas. Kecuali ketika Anda membeli kotak MPP dengan harga jutaan dolar (belum lagi biaya penulisan ulang kode untuk membuatnya bekerja pada mesin seperti itu), Anda tidak dapat membeli mesin yang menjalankan kode Anda 16 kali lebih cepat (SSD menang lakukan juga ... semua ini ada dalam memori berkecepatan tinggi ketika kami mengujinya). Kinerja ada dalam kode. Performa bagus dalam kode yang baik.
Bisakah Anda bayangkan jika semua kode Anda berjalan "hanya" 16 kali lebih cepat?
Jangan sekali-kali menjustifikasi kode yang bermasalah atau kinerja bermasalah pada jumlah baris rendah atau bahkan penggunaan rendah. Jika Anda melakukannya, Anda mungkin harus meminjam salah satu kincir angin yang saya tuduh miringkan agar CPU dan disk Anda cukup dingin. ;-)
KATA TENTANG KATA "BENAR-BENAR"
Ya saya setuju. Berbicara secara semantik, Tally Table berisi angka, bukan "penghitungan". Dalam artikel asli saya pada subjek (itu bukan artikel asli tentang teknik tetapi itu adalah pertama saya di atasnya), saya menyebutnya "Tally" bukan karena apa yang dikandungnya, tetapi karena apa yang dilakukannya ... itu digunakan untuk "menghitung" alih-alih perulangan dan "Tally" sesuatu berarti "Menghitung" sesuatu. ;-) Sebut saja apa yang Anda akan ... Tabel Angka, Tabel Penghitungan, Tabel Urutan, apa pun. Saya tidak peduli. Bagi saya, "Tally" lebih berarti penuh dan, karena menjadi DBA malas yang baik, hanya berisi 5 huruf (2 identik) daripada 7 dan lebih mudah untuk mengatakan bagi kebanyakan orang. Ini juga "tunggal", yang mengikuti konvensi penamaan saya untuk tabel. ;-) Itu' S juga apa artikel yang berisi halaman dari buku dari 60-an menyebutnya. Saya akan selalu menyebutnya sebagai "Tally Table" dan Anda akan tetap tahu apa yang saya atau orang lain maksudkan. Saya juga menghindari Notasi Hongaria seperti wabah tetapi menyebut fungsi "fnTally" sehingga saya bisa mengatakan "Yah, jika Anda menggunakan fungsi Tally ef-en yang saya tunjukkan, Anda tidak akan memiliki masalah kinerja" tanpa benar-benar menjadi Pelanggaran SDM. ;-) tanpa itu sebenarnya menjadi pelanggaran SDM. ;-) tanpa itu sebenarnya menjadi pelanggaran SDM. ;-)
Yang lebih saya pedulikan adalah orang-orang yang belajar menggunakannya dengan benar alih-alih menggunakan hal-hal seperti rCTE yang menantang kinerja dan bentuk-bentuk lain dari RBAR Tersembunyi.