Ini adalah bagian kedua dari seri tentang solusi untuk tantangan generator seri nomor. Bulan lalu saya membahas solusi yang menghasilkan baris dengan cepat menggunakan konstruktor nilai tabel dengan baris berdasarkan konstanta. Tidak ada operasi I/O yang terlibat dalam solusi tersebut. Bulan ini saya fokus pada solusi yang menanyakan tabel dasar fisik yang Anda isi sebelumnya dengan baris. Untuk alasan ini, selain melaporkan profil waktu solusi seperti yang saya lakukan bulan lalu, saya juga akan melaporkan profil I/O dari solusi baru. Sekali lagi terima kasih kepada Alan Burstein, Joe Obbish, Adam Machanic, Christopher Ford, Jeff Moden, Charlie, NoamGr, Kamil Kosno, Dave Mason, John Nelson #2, dan Ed Wagner untuk berbagi ide dan komentar Anda.
Solusi tercepat sejauh ini
Pertama, sebagai pengingat singkat, mari kita tinjau solusi tercepat dari artikel bulan lalu, yang diimplementasikan sebagai TVF inline yang disebut dbo.GetNumsAlanCharlieItzikBatch.
Saya akan melakukan pengujian saya di tempdb, mengaktifkan statistik IO dan WAKTU:
SET NOCOUNT ON; USE tempdb; SET STATISTICS IO, TIME ON;
Solusi tercepat dari bulan lalu menerapkan join dengan tabel dummy yang memiliki indeks columnstore untuk mendapatkan pemrosesan batch. Berikut kode untuk membuat tabel dummy:
DROP TABLE IF EXISTS dbo.BatchMe; GO CREATE TABLE dbo.BatchMe(col1 INT NOT NULL, INDEX idx_cs CLUSTERED COLUMNSTORE);
Dan berikut kode dengan definisi fungsi dbo.GetNumsAlanCharlieItzikBatch:
CREATE OR ALTER FUNCTION dbo.GetNumsAlanCharlieItzikBatch(@low AS BIGINT = 1, @high AS BIGINT) RETURNS TABLE AS RETURN WITH L0 AS ( SELECT 1 AS c FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1), (1),(1),(1),(1),(1),(1),(1),(1)) AS D(c) ), L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ), L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ), L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ), Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum FROM L3 ) SELECT TOP(@high - @low + 1) rownum AS rn, @high + 1 - rownum AS op, @low - 1 + rownum AS n FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0 ORDER BY rownum; GO
Bulan lalu saya menggunakan kode berikut untuk menguji kinerja fungsi dengan 100 juta baris, setelah mengaktifkan hasil Buang setelah eksekusi di SSMS untuk menekan pengembalian baris keluaran:
SELECT n FROM dbo.GetNumsAlanCharlieItzikBatch(1, 100000000) OPTION(MAXDOP 1);
Berikut adalah statistik waktu yang saya dapatkan untuk eksekusi ini:
Waktu CPU =16031 md, waktu berlalu =17172 md.Joe Obbish dengan benar mencatat bahwa tes ini mungkin kurang mencerminkan beberapa skenario kehidupan nyata dalam arti bahwa sebagian besar waktu proses disebabkan oleh jaringan async I/O menunggu (jenis tunggu ASYNC_NETWORK_IO). Anda dapat mengamati waktu tunggu tertinggi dengan melihat halaman properti simpul akar dari rencana kueri aktual, atau menjalankan sesi acara yang diperpanjang dengan info tunggu. Fakta bahwa Anda mengaktifkan Buang hasil setelah eksekusi di SSMS tidak mencegah SQL Server mengirimkan baris hasil ke SSMS; itu hanya mencegah SSMS mencetaknya. Pertanyaannya adalah, seberapa besar kemungkinan Anda akan mengembalikan kumpulan hasil besar ke klien dalam skenario kehidupan nyata bahkan ketika Anda menggunakan fungsi untuk menghasilkan rangkaian angka besar? Mungkin lebih sering Anda menulis hasil kueri ke tabel, atau menggunakan hasil fungsi sebagai bagian dari kueri yang akhirnya menghasilkan kumpulan hasil kecil. Anda perlu mencari tahu ini. Anda dapat menulis hasil yang ditetapkan ke dalam tabel sementara menggunakan pernyataan SELECT INTO, atau, Anda dapat menggunakan trik Alan Burstein dengan pernyataan SELECT penugasan, yang memberikan nilai kolom hasil ke variabel.
Inilah cara Anda mengubah tes terakhir untuk menggunakan opsi penetapan variabel:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieItzikBatch(1, 100000000) OPTION(MAXDOP 1);
Berikut adalah statistik waktu yang saya dapatkan untuk tes ini:
Waktu CPU =8641 md, waktu yang berlalu =8645 md.Kali ini info tunggu tidak memiliki waktu tunggu I/O jaringan asinkron, dan Anda dapat melihat penurunan waktu proses yang signifikan.
Uji fungsinya lagi, kali ini tambahkan pemesanan:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieItzikBatch(1, 100000000) ORDER BY n OPTION(MAXDOP 1);
Saya mendapatkan statistik kinerja berikut untuk eksekusi ini:
Waktu CPU =9360 md, waktu berlalu =9551 md.Ingat bahwa tidak diperlukan operator Sortir dalam rencana untuk kueri ini karena kolom n didasarkan pada ekspresi yang mempertahankan urutan sehubungan dengan jumlah baris kolom. Itu berkat trik lipat konstan Charli, yang saya bahas bulan lalu. Rencana untuk kedua kueri—yang tanpa memesan dan yang dengan memesan adalah sama, sehingga kinerjanya cenderung serupa.
Gambar 1 merangkum angka kinerja yang saya dapatkan untuk solusi bulan lalu, hanya kali ini menggunakan penetapan variabel dalam pengujian alih-alih membuang hasil setelah eksekusi.
Gambar 1:Ringkasan kinerja sejauh ini dengan penetapan variabel
Saya akan menggunakan teknik penetapan variabel untuk menguji sisa solusi yang akan saya sajikan dalam artikel ini. Pastikan Anda menyesuaikan tes Anda untuk mencerminkan situasi kehidupan nyata terbaik Anda, menggunakan penetapan variabel, SELECT INTO, Buang hasil setelah eksekusi atau teknik lainnya.
Kiat untuk memaksakan paket serial tanpa MAXDOP 1
Sebelum saya menyajikan solusi baru, saya hanya ingin membahas tip kecil. Ingatlah bahwa beberapa solusi berkinerja terbaik saat menggunakan paket serial. Cara yang jelas untuk memaksa ini adalah dengan petunjuk kueri MAXDOP 1. Dan itulah cara yang tepat jika terkadang Anda ingin mengaktifkan paralelisme dan terkadang tidak. Namun, bagaimana jika Anda selalu ingin memaksakan rencana serial saat menggunakan fungsi, meskipun skenarionya kecil?
Ada trik untuk mencapai ini. Penggunaan UDF skalar noninlineable dalam kueri adalah penghambat paralelisme. Salah satu inhibitor inlining skalar UDF menjalankan fungsi intrinsik yang bergantung pada waktu, seperti SYSDATETIME. Jadi, inilah contoh UDF skalar tak-berbaris:
CREATE OR ALTER FUNCTION dbo.MySYSDATETIME() RETURNS DATETIME2 AS BEGIN RETURN SYSDATETIME(); END; GO
Opsi lainnya adalah mendefinisikan UDF hanya dengan beberapa konstanta sebagai nilai yang dikembalikan, dan menggunakan opsi INLINE =OFF di headernya. Tetapi opsi ini hanya tersedia dimulai dengan SQL Server 2019, yang memperkenalkan inlining skalar UDF. Dengan fungsi yang disarankan di atas, Anda dapat membuatnya apa adanya dengan versi SQL Server yang lebih lama.
Selanjutnya, ubah definisi fungsi dbo.GetNumsAlanCharlieItzikBatch menjadi panggilan dummy ke dbo.MySYSDATETIME (tentukan kolom berdasarkan itu tetapi jangan merujuk ke kolom dalam kueri yang dikembalikan), seperti:
CREATE OR ALTER FUNCTION dbo.GetNumsAlanCharlieItzikBatch(@low AS BIGINT = 1, @high AS BIGINT) RETURNS TABLE AS RETURN WITH L0 AS ( SELECT 1 AS c FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1), (1),(1),(1),(1),(1),(1),(1),(1)) AS D(c) ), L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ), L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ), L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ), Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum, dbo.MySYSDATETIME() AS dontinline FROM L3 ) SELECT TOP(@high - @low + 1) rownum AS rn, @high + 1 - rownum AS op, @low - 1 + rownum AS n FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0 ORDER BY rownum; GO
Anda sekarang dapat menjalankan kembali uji kinerja tanpa menentukan MAXDOP 1, dan masih mendapatkan paket serial:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieItzikBatch(1, 100000000) ORDER BY n;
Penting untuk ditekankan bahwa setiap kueri yang menggunakan fungsi ini sekarang akan mendapatkan paket serial. Jika ada kemungkinan fungsi tersebut akan digunakan dalam kueri yang akan mendapat manfaat dari paket paralel, sebaiknya jangan gunakan trik ini, dan saat Anda membutuhkan paket serial, cukup gunakan MAXDOP 1.
Solusi oleh Joe Obbish
Solusi Joe cukup kreatif. Berikut deskripsinya sendiri tentang solusinya:
“Saya memilih untuk membuat clustered columnstore index (CCI) dengan 134.217.728 baris bilangan bulat berurutan. Fungsi mereferensikan tabel hingga 32 kali untuk mendapatkan semua baris yang diperlukan untuk kumpulan hasil. Saya memilih CCI karena data akan terkompresi dengan baik (kurang dari 3 byte per baris), Anda mendapatkan mode batch "gratis", dan pengalaman sebelumnya menunjukkan bahwa membaca nomor urut dari CCI akan lebih cepat daripada membuatnya melalui beberapa metode lain. ”Seperti yang disebutkan sebelumnya, Joe juga mencatat bahwa pengujian kinerja asli saya secara signifikan miring karena menunggu I/O jaringan asinkron yang dihasilkan dengan mentransmisikan baris ke SSMS. Jadi semua tes yang akan saya lakukan di sini akan menggunakan ide Alan dengan penugasan variabel. Pastikan untuk menyesuaikan tes Anda berdasarkan apa yang paling mencerminkan situasi kehidupan nyata Anda.
Berikut kode yang digunakan Joe untuk membuat tabel dbo.GetNumsObbishTable dan mengisinya dengan 134.217.728 baris:
DROP TABLE IF EXISTS dbo.GetNumsObbishTable; CREATE TABLE dbo.GetNumsObbishTable (ID BIGINT NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE); GO SET NOCOUNT ON; DECLARE @c INT = 0; WHILE @c < 128 BEGIN INSERT INTO dbo.GetNumsObbishTable SELECT TOP (1048576) @c * 1048576 - 1 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values t1 CROSS JOIN master..spt_values t2 OPTION (MAXDOP 1); SET @c = @c + 1; END; GO
Butuh waktu 1:04 menit untuk menyelesaikan kode ini di komputer saya.
Anda dapat memeriksa penggunaan ruang tabel ini dengan menjalankan kode berikut:
EXEC sys.sp_spaceused @objname = N'dbo.GetNumsObbishTable';
Saya mendapat sekitar 350 MB ruang yang digunakan. Dibandingkan dengan solusi lain yang akan saya sajikan dalam artikel ini, solusi ini menggunakan lebih banyak ruang secara signifikan.
Dalam arsitektur columnstore SQL Server, grup baris dibatasi hingga 2^20 =1.048.576 baris. Anda dapat memeriksa berapa banyak grup baris yang dibuat untuk tabel ini menggunakan kode berikut:
SELECT COUNT(*) AS numrowgroups FROM sys.column_store_row_groups WHERE object_id = OBJECT_ID('dbo.GetNumsObbishTable');
Saya mendapat 128 grup baris.
Berikut kode dengan definisi fungsi dbo.GetNumsObbish:
CREATE OR ALTER FUNCTION dbo.GetNumsObbish(@low AS BIGINT, @high AS BIGINT) RETURNS TABLE AS RETURN SELECT @low + ID AS n FROM dbo.GetNumsObbishTable WHERE ID <= @high - @low UNION ALL SELECT @low + ID + CAST(134217728 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(134217728 AS BIGINT) AND ID <= @high - @low - CAST(134217728 AS BIGINT) UNION ALL SELECT @low + ID + CAST(268435456 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(268435456 AS BIGINT) AND ID <= @high - @low - CAST(268435456 AS BIGINT) UNION ALL SELECT @low + ID + CAST(402653184 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(402653184 AS BIGINT) AND ID <= @high - @low - CAST(402653184 AS BIGINT) UNION ALL SELECT @low + ID + CAST(536870912 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(536870912 AS BIGINT) AND ID <= @high - @low - CAST(536870912 AS BIGINT) UNION ALL SELECT @low + ID + CAST(671088640 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(671088640 AS BIGINT) AND ID <= @high - @low - CAST(671088640 AS BIGINT) UNION ALL SELECT @low + ID + CAST(805306368 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(805306368 AS BIGINT) AND ID <= @high - @low - CAST(805306368 AS BIGINT) UNION ALL SELECT @low + ID + CAST(939524096 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(939524096 AS BIGINT) AND ID <= @high - @low - CAST(939524096 AS BIGINT) UNION ALL SELECT @low + ID + CAST(1073741824 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(1073741824 AS BIGINT) AND ID <= @high - @low - CAST(1073741824 AS BIGINT) UNION ALL SELECT @low + ID + CAST(1207959552 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(1207959552 AS BIGINT) AND ID <= @high - @low - CAST(1207959552 AS BIGINT) UNION ALL SELECT @low + ID + CAST(1342177280 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(1342177280 AS BIGINT) AND ID <= @high - @low - CAST(1342177280 AS BIGINT) UNION ALL SELECT @low + ID + CAST(1476395008 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(1476395008 AS BIGINT) AND ID <= @high - @low - CAST(1476395008 AS BIGINT) UNION ALL SELECT @low + ID + CAST(1610612736 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(1610612736 AS BIGINT) AND ID <= @high - @low - CAST(1610612736 AS BIGINT) UNION ALL SELECT @low + ID + CAST(1744830464 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(1744830464 AS BIGINT) AND ID <= @high - @low - CAST(1744830464 AS BIGINT) UNION ALL SELECT @low + ID + CAST(1879048192 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(1879048192 AS BIGINT) AND ID <= @high - @low - CAST(1879048192 AS BIGINT) UNION ALL SELECT @low + ID + CAST(2013265920 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(2013265920 AS BIGINT) AND ID <= @high - @low - CAST(2013265920 AS BIGINT) UNION ALL SELECT @low + ID + CAST(2147483648 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(2147483648 AS BIGINT) AND ID <= @high - @low - CAST(2147483648 AS BIGINT) UNION ALL SELECT @low + ID + CAST(2281701376 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(2281701376 AS BIGINT) AND ID <= @high - @low - CAST(2281701376 AS BIGINT) UNION ALL SELECT @low + ID + CAST(2415919104 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(2415919104 AS BIGINT) AND ID <= @high - @low - CAST(2415919104 AS BIGINT) UNION ALL SELECT @low + ID + CAST(2550136832 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(2550136832 AS BIGINT) AND ID <= @high - @low - CAST(2550136832 AS BIGINT) UNION ALL SELECT @low + ID + CAST(2684354560 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(2684354560 AS BIGINT) AND ID <= @high - @low - CAST(2684354560 AS BIGINT) UNION ALL SELECT @low + ID + CAST(2818572288 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(2818572288 AS BIGINT) AND ID <= @high - @low - CAST(2818572288 AS BIGINT) UNION ALL SELECT @low + ID + CAST(2952790016 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(2952790016 AS BIGINT) AND ID <= @high - @low - CAST(2952790016 AS BIGINT) UNION ALL SELECT @low + ID + CAST(3087007744 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(3087007744 AS BIGINT) AND ID <= @high - @low - CAST(3087007744 AS BIGINT) UNION ALL SELECT @low + ID + CAST(3221225472 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(3221225472 AS BIGINT) AND ID <= @high - @low - CAST(3221225472 AS BIGINT) UNION ALL SELECT @low + ID + CAST(3355443200 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(3355443200 AS BIGINT) AND ID <= @high - @low - CAST(3355443200 AS BIGINT) UNION ALL SELECT @low + ID + CAST(3489660928 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(3489660928 AS BIGINT) AND ID <= @high - @low - CAST(3489660928 AS BIGINT) UNION ALL SELECT @low + ID + CAST(3623878656 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(3623878656 AS BIGINT) AND ID <= @high - @low - CAST(3623878656 AS BIGINT) UNION ALL SELECT @low + ID + CAST(3758096384 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(3758096384 AS BIGINT) AND ID <= @high - @low - CAST(3758096384 AS BIGINT) UNION ALL SELECT @low + ID + CAST(3892314112 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(3892314112 AS BIGINT) AND ID <= @high - @low - CAST(3892314112 AS BIGINT) UNION ALL SELECT @low + ID + CAST(4026531840 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(4026531840 AS BIGINT) AND ID <= @high - @low - CAST(4026531840 AS BIGINT) UNION ALL SELECT @low + ID + CAST(4160749568 AS BIGINT) AS n FROM dbo.GetNumsObbishTable WHERE @high - @low + 1 > CAST(4160749568 AS BIGINT) AND ID <= @high - @low - CAST(4160749568 AS BIGINT); GO
32 kueri individual menghasilkan subrentang 134.217.728-bilangan bulat yang terputus-putus yang, ketika disatukan, menghasilkan rentang lengkap tanpa gangguan 1 hingga 4.294.967.296. Apa yang benar-benar cerdas tentang solusi ini adalah predikat filter WHERE yang digunakan kueri individu. Ingatlah bahwa ketika SQL Server memproses TVF sebaris, pertama-tama ia menerapkan penyematan parameter, menggantikan parameter dengan konstanta input. SQL Server kemudian dapat mengoptimalkan kueri yang menghasilkan subrentang yang tidak berpotongan dengan rentang input. Misalnya, saat Anda meminta rentang input 1 hingga 100.000.000, hanya kueri pertama yang relevan, dan sisanya dioptimalkan. Rencananya kemudian dalam hal ini akan melibatkan referensi ke hanya satu contoh tabel. Itu cukup brilian!
Mari kita uji kinerja fungsi dengan rentang 1 hingga 100.000.000:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsObbish(1, 100000000);
Rencana untuk kueri ini ditunjukkan pada Gambar 2.
Gambar 2:Rencana untuk dbo.GetNumsObbish, 100 juta baris, tidak berurutan
Perhatikan bahwa memang hanya satu referensi ke CCI tabel yang diperlukan dalam rencana ini.
Saya mendapatkan statistik waktu berikut untuk eksekusi ini:
Itu cukup mengesankan, dan jauh lebih cepat daripada apa pun yang telah saya uji.
Berikut adalah statistik I/O yang saya dapatkan untuk eksekusi ini:
Tabel 'GetNumsObbishTable'. Hitungan pemindaian 1, pembacaan logis 0, pembacaan fisik 0, pembacaan server halaman 0, pembacaan ke depan 0, pembacaan ke depan server halaman pembacaan 0, pembacaan logis lob 32928 , lob fisik membaca 0, server halaman lob membaca 0, lob read-ahead membaca 0, server halaman lob membaca-depan 0.Tabel 'GetNumsObbishTable'. Segmen dibaca 96 , segmen dilewati 32.
Profil I/O dari solusi ini adalah salah satu kelemahannya dibandingkan dengan yang lain, menimbulkan lebih dari 30 ribu lob pembacaan logis untuk eksekusi ini.
Untuk melihat bahwa ketika Anda melewati beberapa subrentang 134.217.728-bilangan bulat, rencana akan melibatkan beberapa referensi ke tabel, kueri fungsi dengan rentang 1 hingga 400.000.000, misalnya:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsObbish(1, 400000000);
Rencana eksekusi ini ditunjukkan pada Gambar 3.
Gambar 3:Rencana untuk dbo.GetNumsObbish, 400 juta baris, tidak berurutan
Rentang yang diminta melintasi tiga subrentang 134.217.728 bilangan bulat, oleh karena itu rencana tersebut menunjukkan tiga referensi ke CCI tabel.
Berikut adalah statistik waktu yang saya dapatkan untuk eksekusi ini:
Waktu CPU =20610 md, waktu yang berlalu =20628 md.Dan inilah statistik I/O-nya:
Tabel 'GetNumsObbishTable'. Jumlah pemindaian 3, pembacaan logis 0, pembacaan fisik 0, pembacaan server halaman 0, pembacaan ke depan 0, pembacaan ke depan server halaman pembacaan 0, pembacaan logis lob 131026 , lob fisik membaca 0, server halaman lob membaca 0, lob read-ahead membaca 0, server halaman lob membaca-depan 0.Tabel 'GetNumsObbishTable'. Segmen dibaca 382 , segmen dilewati 2.
Kali ini eksekusi kueri menghasilkan lebih dari 130 ribu pembacaan logika lob.
Jika Anda dapat menahan biaya I/O, dan tidak perlu memproses seri angka secara berurutan, ini adalah solusi yang bagus. Namun, jika Anda perlu memproses rangkaian secara berurutan, solusi ini akan menghasilkan operator Sortir dalam rencana. Berikut adalah tes yang meminta hasil yang dipesan:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsObbish(1, 100000000) ORDER BY n;
Rencana eksekusi ini ditunjukkan pada Gambar 4.
Gambar 4:Rencana untuk dbo.GetNumsObbish, 100 juta baris, dipesan
Berikut adalah statistik waktu yang saya dapatkan untuk eksekusi ini:
Waktu CPU =44516 md, waktu berlalu =34836 md.Seperti yang Anda lihat, performa menurun secara signifikan seiring dengan peningkatan waktu proses berdasarkan urutan besarnya karena penyortiran eksplisit.
Berikut adalah statistik I/O yang saya dapatkan untuk eksekusi ini:
Tabel 'GetNumsObbishTable'. Jumlah pemindaian 4, pembacaan logis 0, pembacaan fisik 0, pembacaan server halaman 0, pembacaan ke depan 0, pembacaan ke depan server halaman pembacaan 0, pembacaan logis lob 32928 , lob fisik membaca 0, server halaman lob membaca 0, lob read-ahead membaca 0, server halaman lob membaca-depan 0.Tabel 'GetNumsObbishTable'. Segmen dibaca 96 , segmen dilewati 32.
Meja 'Meja Kerja'. Hitungan pemindaian 0, pembacaan logis 0, pembacaan fisik 0, pembacaan server halaman 0, pembacaan ke depan 0, pembacaan ke depan server halaman pembacaan 0, pembacaan logika lob 0, pembacaan fisik lob 0, pembacaan server halaman lob 0, pembacaan lob- ke depan membaca 0, server halaman lob membaca ke depan membaca 0.
Amati bahwa Meja Kerja muncul di output STATISTICS IO. Itu karena semacam berpotensi tumpah ke tempdb, dalam hal ini akan menggunakan meja kerja. Eksekusi ini tidak tumpah, oleh karena itu semua angka dalam entri ini adalah nol.
Solusi oleh John Nelson #2, Dave, Joe, Alan, Charlie, Itzik
John Nelson #2 memposting solusi yang sangat indah dalam kesederhanaannya. Selain itu, ini mencakup ide dan saran dari solusi lain oleh Dave, Joe, Alan, Charlie, dan saya sendiri.
Seperti solusi Joe, John memutuskan untuk menggunakan CCI untuk mendapatkan kompresi tingkat tinggi dan pemrosesan batch "gratis". Hanya John yang memutuskan untuk mengisi tabel dengan 4B baris dengan beberapa penanda NULL tiruan di kolom bit, dan membuat fungsi ROW_NUMBER menghasilkan angka. Karena nilai yang disimpan semuanya sama, dengan kompresi nilai berulang, Anda membutuhkan ruang yang jauh lebih sedikit, menghasilkan I/O yang jauh lebih sedikit dibandingkan dengan solusi Joe. Kompresi penyimpanan kolom menangani nilai berulang dengan sangat baik karena dapat mewakili setiap bagian berurutan tersebut dalam segmen kolom grup baris hanya sekali bersama dengan jumlah kejadian berulang yang berurutan. Karena semua baris memiliki nilai yang sama (penanda NULL), secara teoritis Anda hanya memerlukan satu kemunculan per grup baris. Dengan baris 4B, Anda akan mendapatkan 4.096 grup baris. Masing-masing harus memiliki segmen kolom tunggal, dengan persyaratan penggunaan ruang yang sangat sedikit.
Berikut kode untuk membuat dan mengisi tabel, diimplementasikan sebagai CCI dengan kompresi arsip:
DROP TABLE IF EXISTS dbo.NullBits4B; CREATE TABLE dbo.NullBits4B ( b BIT NULL, INDEX cc_NullBits4B CLUSTERED COLUMNSTORE WITH (DATA_COMPRESSION = COLUMNSTORE_ARCHIVE) ); GO WITH L0 AS (SELECT CAST(NULL AS BIT) AS b FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS D(b)), L1 AS (SELECT A.b FROM L0 AS A CROSS JOIN L0 AS B), L2 AS (SELECT A.b FROM L1 AS A CROSS JOIN L1 AS B), nulls(b) AS (SELECT A.b FROM L2 AS A CROSS JOIN L2 AS B) INSERT INTO dbo.NullBits4B WITH (TABLOCK) (b) SELECT b FROM nulls; GO
Kelemahan utama dari solusi ini adalah waktu yang dibutuhkan untuk mengisi tabel ini. Butuh kode ini 12:32 menit untuk menyelesaikan pada mesin saya ketika mengizinkan paralelisme, dan 15:17 menit ketika memaksa rencana serial.
Perhatikan bahwa Anda dapat berupaya mengoptimalkan pemuatan data. Misalnya, John menguji solusi yang memuat baris menggunakan 32 koneksi simultan dengan OSTRESS.EXE, masing-masing menjalankan 128 putaran penyisipan 2^20 baris (ukuran grup baris maksimum). Solusi ini menurunkan waktu muat John menjadi sepertiga. Berikut kode yang digunakan John:
ostress -S(local)\YourSQLInstance -E -dtempdb -n32 -r128 -Q"DENGAN L0 AS (SELECT CAST(NULL AS BIT) AS b FROM (VALUES(1),(1),(1),(1) ,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) SEBAGAI D(b)), L1 AS (PILIH A.b DARI L0 SEBAGAI SILANG GABUNG L0 AS B), L2 AS (PILIH A.b DARI L1 SEBAGAI LINTAS GABUNG L1 AS B), nulls(b) AS (PILIH A.b DARI L2 SEBAGAI A CROSS JOIN L2 AS B) INSERT INTO dbo.NullBits4B(b) SELECT TOP(1048576) b FROM nulls OPTION(MAXDOP 1);"Namun, waktu buka dalam hitungan menit. Kabar baiknya adalah Anda hanya perlu melakukan pemuatan data ini sekali.
Berita baiknya adalah sedikit ruang yang dibutuhkan oleh meja. Gunakan kode berikut untuk memeriksa penggunaan ruang:
EXEC sys.sp_spaceused @objname = N'dbo.NullBits4B';
Saya mendapat 1,64 MB. Sungguh menakjubkan mengingat fakta bahwa tabel memiliki 4B baris!
Gunakan kode berikut untuk memeriksa berapa banyak grup baris yang dibuat:
SELECT COUNT(*) AS numrowgroups FROM sys.column_store_row_groups WHERE object_id = OBJECT_ID('dbo.NullBits4B');
Seperti yang diharapkan, jumlah grup baris adalah 4.096.
Definisi fungsi dbo.GetNumsJohn2DaveObbishAlanCharlieItzik kemudian menjadi sangat sederhana:
CREATE OR ALTER FUNCTION dbo.GetNumsJohn2DaveObbishAlanCharlieItzik (@low AS BIGINT = 1, @high AS BIGINT) RETURNS TABLE AS RETURN WITH Nums AS (SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum FROM dbo.NullBits4B) SELECT TOP(@high - @low + 1) rownum AS rn, @high + 1 - rownum AS op, @low - 1 + rownum AS n FROM Nums ORDER BY rownum; GO
Seperti yang Anda lihat, kueri sederhana terhadap tabel menggunakan fungsi ROW_NUMBER untuk menghitung nomor baris dasar (kolom rownum), lalu kueri luar menggunakan ekspresi yang sama seperti di dbo.GetNumsAlanCharlieItzikBatch untuk menghitung rn, op, dan n. Juga di sini, baik rn dan n adalah pelestarian urutan sehubungan dengan rownum.
Mari kita uji kinerja fungsi:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsJohn2DaveObbishAlanCharlieItzik(1, 100000000);
Saya mendapatkan rencana yang ditunjukkan pada Gambar 5 untuk eksekusi ini.
Gambar 5:Rencanakan untuk dbo.GetNumsJohn2DaveObbishAlanCharlieItzik
Berikut adalah statistik waktu yang saya dapatkan untuk tes ini:
Waktu CPU =7593 ms, waktu berlalu =7590 ms.
Seperti yang Anda lihat, waktu eksekusi tidak secepat solusi Joe, tetapi masih lebih cepat daripada semua solusi lain yang saya uji.
Berikut adalah statistik I/O yang saya dapatkan untuk pengujian ini:
Tabel 'NullBits4B'. Segmen dibaca 96 , segmen dilewati 0
Perhatikan bahwa persyaratan I/O jauh lebih rendah dibandingkan dengan solusi Joe.
Hal hebat lainnya tentang solusi ini adalah ketika Anda perlu memproses seri nomor yang dipesan, Anda tidak perlu membayar ekstra. Itu karena itu tidak akan menghasilkan operasi pengurutan eksplisit dalam rencana, terlepas dari apakah Anda memesan hasilnya dengan rn atau n.
Berikut adalah tes untuk menunjukkan ini:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsJohn2DaveObbishAlanCharlieItzik(1, 100000000) ORDER BY n;
Anda mendapatkan paket yang sama seperti yang ditunjukkan sebelumnya pada Gambar 5.
Berikut adalah statistik waktu yang saya dapatkan untuk tes ini;
Waktu CPU =7578 ms, waktu berlalu =7582 ms.Dan berikut adalah statistik I/O:
Tabel 'NullBits4B'. Hitungan pemindaian 1, pembacaan logis 0, pembacaan fisik 0, pembacaan server halaman 0, pembacaan ke depan 0, pembacaan ke depan server halaman pembacaan 0, pembacaan logis lob 194 , lob fisik membaca 0, server halaman lob membaca 0, lob read-ahead membaca 0, server halaman lob membaca-depan 0.Tabel 'NullBits4B'. Segmen dibaca 96 , segmen dilewati 0.
Mereka pada dasarnya sama seperti dalam pengujian tanpa urutan.
Solution 2 oleh John Nelson #2, Dave Mason, Joe Obbish, Alan, Charlie, Itzik
Solusi John cepat dan sederhana. Itu luar biasa. Satu-satunya kelemahan adalah waktu buka. Terkadang ini tidak menjadi masalah karena pemuatan hanya terjadi sekali. Tetapi jika ini merupakan masalah, Anda dapat mengisi tabel dengan 102.400 baris alih-alih 4B baris, dan menggunakan gabungan silang antara dua contoh tabel dan filter TOP untuk menghasilkan maksimum baris 4B yang diinginkan. Perhatikan bahwa untuk mendapatkan baris 4B, cukup untuk mengisi tabel dengan 65.536 baris dan kemudian menerapkan gabungan silang; namun, agar data segera dikompresi—bukan dimuat ke toko delta berbasis rowstore—Anda perlu memuat tabel dengan minimal 102.400 baris.
Berikut kode untuk membuat dan mengisi tabel:
DROP TABLE IF EXISTS dbo.NullBits102400; GO CREATE TABLE dbo.NullBits102400 ( b BIT NULL, INDEX cc_NullBits102400 CLUSTERED COLUMNSTORE WITH (DATA_COMPRESSION = COLUMNSTORE_ARCHIVE) ); GO WITH L0 AS (SELECT CAST(NULL AS BIT) AS b FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS D(b)), L1 AS (SELECT A.b FROM L0 AS A CROSS JOIN L0 AS B), nulls(b) AS (SELECT A.b FROM L1 AS A CROSS JOIN L1 AS B CROSS JOIN L1 AS C) INSERT INTO dbo.NullBits102400 WITH (TABLOCK) (b) SELECT TOP(102400) b FROM nulls; GO
Waktu muat dapat diabaikan — 43 mdtk di mesin saya.
Periksa ukuran tabel pada disk:
EXEC sys.sp_spaceused @objname = N'dbo.NullBits102400';
Saya mendapat ruang 56 KB yang dibutuhkan untuk data.
Periksa jumlah grup baris, statusnya (terkompresi atau terbuka) dan ukurannya:
SELECT state_description, total_rows, size_in_bytes FROM sys.column_store_row_groups WHERE object_id = OBJECT_ID('dbo.NullBits102400');
Saya mendapatkan output berikut:
state_description total_rows size_in_bytes ------------------ ----------- -------------- COMPRESSED 102400 293
Hanya satu grup baris yang dibutuhkan di sini; itu dikompresi, dan ukurannya diabaikan 293 byte.
Jika Anda mengisi tabel dengan satu baris lebih sedikit (102.399), Anda mendapatkan toko delta terbuka tanpa kompresi berbasis rowstore. Dalam kasus seperti itu sp_spaceused melaporkan ukuran data pada disk lebih dari 1 MB, dan sys.column_store_row_groups melaporkan info berikut:
state_description total_rows size_in_bytes ------------------ ----------- -------------- OPEN 102399 1499136
Jadi, pastikan Anda mengisi tabel dengan 102.400 baris!
Berikut definisi fungsi dbo.GetNumsJohn2DaveObbishAlanCharlieItzik2:
CREATE OR ALTER FUNCTION dbo.GetNumsJohn2DaveObbishAlanCharlieItzik2 (@low AS BIGINT = 1, @high AS BIGINT) RETURNS TABLE AS RETURN WITH Nums AS (SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum FROM dbo.NullBits102400 AS A CROSS JOIN dbo.NullBits102400 AS B) SELECT TOP(@high - @low + 1) rownum AS rn, @high + 1 - rownum AS op, @low - 1 + rownum AS n FROM Nums ORDER BY rownum; GO
Let’s test the function's performance:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsJohn2DaveObbishAlanCharlieItzik2(1, 100000000) OPTION(MAXDOP 1);
I got the plan shown in Figure 6 for this execution.
Figure 6:Plan for dbo.GetNumsJohn2DaveObbishAlanCharlieItzik2
I got the following time statistics for this test:
CPU time =9188 ms, elapsed time =9188 ms.As you can see, the execution time increased by ~ 26%. It’s still pretty fast, but not as fast as the single-table solution. So that’s a tradeoff that you’ll need to evaluate.
I got the following I/O stats for this test:
Table 'NullBits102400'. Scan count 2, logical reads 0, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 8 , lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.Table 'NullBits102400'. Segment reads 2, segment skipped 0.
The I/O profile of this solution is excellent.
Let’s add order to the test:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsJohn2DaveObbishAlanCharlieItzik2(1, 100000000) ORDER BY n OPTION(MAXDOP 1);
You get the same plan as shown earlier in Figure 6 since there’s no explicit sorting needed.
I got the following time statistics for this test:
CPU time =9140 ms, elapsed time =9237 ms.And the following I/O stats:
Table 'NullBits102400'. Scan count 2, logical reads 0, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 8 , lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.Table 'NullBits102400'. Segment reads 2, segment skipped 0.
Again, the numbers are very similar to the test without the ordering.
Performance summary
Figure 7 has a summary of the time statistics for the different solutions.
Figure 7:Time performance summary of solutions
Figure 8 has a summary of the I/O statistics.
Figure 8:I/O performance summary of solutions
Thanks to all of you who posted ideas and suggestions in effort to create a fast number series generator. It’s a great learning experience!
We’re not done yet. Next month I’ll continue exploring additional solutions.