Ini adalah bagian kelima dan terakhir dalam seri yang mencakup solusi untuk tantangan generator seri nomor. Di Bagian 1, Bagian 2, Bagian 3 dan Bagian 4 saya membahas solusi T-SQL murni. Sejak awal ketika saya memposting teka-teki, beberapa orang berkomentar bahwa solusi dengan kinerja terbaik kemungkinan adalah solusi berbasis CLR. Dalam artikel ini kami akan menguji asumsi intuitif ini. Secara khusus, saya akan membahas solusi berbasis CLR yang diposting oleh Kamil Kosno dan Adam Machanic.
Terima kasih banyak kepada Alan Burstein, Joe Obbish, Adam Machanic, Christopher Ford, Jeff Moden, Charlie, NoamGr, Kamil Kosno, Dave Mason, John Nelson #2, Ed Wagner, Michael Burbea, dan Paul White untuk berbagi ide dan komentar Anda.
Saya akan melakukan pengujian saya di database bernama testdb. Gunakan kode berikut untuk membuat database jika tidak ada, dan untuk mengaktifkan I/O dan statistik waktu:
-- DB dan statsSET NOCOUNT ON;SET STATISTICS IO, TIME ON;GO IF DB_ID('testdb') IS NULL CREATE DATABASE testdb;GO GUNAKAN testdb;GO
Demi kesederhanaan, saya akan menonaktifkan keamanan ketat CLR dan membuat database dapat dipercaya menggunakan kode berikut:
-- Aktifkan CLR, nonaktifkan keamanan ketat CLR dan buat db dapat dipercayaEXEC sys.sp_configure 'tampilkan pengaturan lanjutan', 1;RECONFIGURE; EXEC sys.sp_configure 'clr diaktifkan', 1;EXEC sys.sp_configure 'clr ketat keamanan', 0;RECONFIGURE; EXEC sys.sp_configure 'tampilkan pengaturan lanjutan', 0;RECONFIGURE; ALTER DATABASE testdb SET TRUSTWORTHY ON; PERGI
Solusi sebelumnya
Sebelum saya membahas solusi berbasis CLR, mari kita tinjau kinerja dua solusi T-SQL dengan performa terbaik.
Solusi T-SQL berperforma terbaik yang tidak menggunakan tabel dasar tetap (selain tabel penyimpanan kolom kosong dummy untuk mendapatkan pemrosesan batch), dan oleh karena itu tidak melibatkan operasi I/O, adalah solusi yang diimplementasikan dalam fungsi dbo.GetNumsAlanCharlieItzikBatch. Saya membahas solusi ini di Bagian 1.
Berikut kode untuk membuat tabel kolom kosong dummy yang digunakan oleh kueri fungsi:
DROP TABLE JIKA ADA dbo.BatchMe;GO CREATE TABLE dbo.BatchMe(col1 INT NOT NULL, INDEX idx_cs CLUSTERED COLUMNSTORE);GO
Dan inilah kode dengan definisi fungsi:
BUAT ATAU ubah FUNGSI dbo.GetNumsAlanCharlieItzikBatch(@low AS BIGINT =1, @high AS BIGINT) MENGEMBALIKAN TABLEASRETURN DENGAN 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 ( PILIH 1 AS c DARI L0 SEBAGAI LINTAS GABUNG L0 AS B ), L2 AS ( PILIH 1 AS c DARI L1 SEBAGAI LINTAS GABUNG L1 AS B ), L3 AS ( PILIH 1 AS c FROM L2 AS A CROSS GABUNG 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
Mari kita uji dulu fungsi yang meminta serangkaian angka 100 juta, dengan agregat MAX diterapkan ke kolom n:
PILIH MAX(n) SEBAGAI mx DARI dbo.GetNumsAlanCharlieItzikBatch(1, 100000000) OPSI(MAXDOP 1);
Ingat, teknik pengujian ini menghindari transmisi 100 juta baris ke pemanggil, dan juga menghindari upaya mode baris yang terlibat dalam penetapan variabel saat menggunakan teknik penetapan variabel.
Berikut adalah statistik waktu yang saya dapatkan untuk pengujian ini di mesin saya:
Waktu CPU =6719 md, waktu berlalu =6742 md .Eksekusi fungsi ini tentu saja tidak menghasilkan pembacaan logis.
Selanjutnya, mari kita uji dengan urutan, menggunakan teknik penetapan variabel:
MENNYATAKAN @n SEBAGAI BIGINT; PILIH @n =n DARI dbo.GetNumsAlanCharlieItzikBatch(1, 100000000) ORDER BY n OPTION(MAXDOP 1);
Saya mendapatkan statistik waktu berikut untuk eksekusi ini:
Waktu CPU =9468 md, waktu berlalu =9531 md .Ingat bahwa fungsi ini tidak menghasilkan pengurutan saat meminta data yang diurutkan oleh n; Anda pada dasarnya mendapatkan paket yang sama apakah Anda meminta data yang dipesan atau tidak. Kami dapat mengaitkan sebagian besar waktu ekstra dalam pengujian ini dibandingkan dengan yang sebelumnya dengan penugasan variabel berbasis mode baris 100 juta.
Solusi T-SQL berperforma terbaik yang menggunakan tabel dasar tetap, dan karena itu menghasilkan beberapa operasi I/O, meskipun sangat sedikit, adalah solusi Paul White yang diimplementasikan dalam fungsi dbo.GetNums_SQLkiwi. Saya membahas solusi ini di Bagian 4.
Berikut kode Paul untuk membuat tabel columnstore yang digunakan oleh fungsi dan fungsi itu sendiri:
-- Helper columnstore tableDROP TABLE JIKA ADA dbo.CS; -- 64K baris (cukup untuk 4B baris saat digabungkan silang)-- kolom 1 selalu nol-- kolom 2 adalah (1...65536)PILIH -- ketik sebagai integer NOT NULL -- (semuanya dinormalisasi menjadi 64 bit dalam columnstore/modus batch) n1 =ISNULL(CONVERT(integer, 0), 0), n2 =ISNULL(CONVERT(integer, N.rn), 0)INTO dbo.CSFROM ( SELECT rn =ROW_NUMBER() OVER (ORDER BY @@SPID) DARI master.dbo.spt_values SEBAGAI SILANG SV1 GABUNG master.dbo.spt_values SEBAGAI SV2 ORDER OLEH rn ASC OFFSET 0 ROWS FETCH NEXT 65536 ROWS ONLY) SEBAGAI N; -- Satu grup baris terkompresi dari 65.536 barisCREATE CLUSTERED COLUMNSTORE INDEX CCI ON dbo.CS WITH (MAXDOP =1);GO -- The functionCREATE OR ALTER FUNCTION dbo.GetNums_SQLkiwi( @low bigint =1, @high bigint)RETURNS table ASRETURN SELECT N .rn, n =@low - 1 + N.rn, op =@high + 1 - N.rn FROM ( SELECT -- Gunakan @@TRANCOUNT alih-alih @@SPID jika Anda menyukai semua pertanyaan Anda serial rn =ROW_NUMBER() OVER (ORDER BY @@SPID ASC) FROM dbo.CS AS N1 GABUNG dbo.CS AS N2 -- Batch mode hash cross join -- Integer bukan null tipe data hindari hash probe residual -- Ini selalu 0 =0 PADA N2. n1 =N1.n1 WHERE -- Cobalah untuk menghindari SQRT pada bilangan negatif dan aktifkan penyederhanaan -- untuk pemindaian konstan tunggal jika @low> @high (dengan literal) -- Tidak ada filter penyalaan dalam mode batch @high>=@low -- Filter kasar:-- Batasi setiap sisi salib bergabung ke SQRT(target jumlah baris) -- IIF menghindari SQRT pada angka negatif dengan parameter AND N1.n2 <=CONVERT(integer, CEILING(SQRT(CONVERT(float, IIF(@high>=@low, @high) - @rendah + 1, 0))))) AND N2.n2 <=CONVERT(bilangan bulat, CEILING(SQRT(CONVERT(float, IIF(@high>=@low, @high - @low + 1, 0)) ))) ) AS N WHERE -- Filter presisi:-- Filter mode batch, gabungan silang terbatas ke jumlah baris yang tepat yang diperlukan -- Menghindari pengoptimal memperkenalkan mode baris Atas dengan mode baris berikut menghitung skalar @ rendah - 2 + N.rn <@high;GO
Mari kita uji terlebih dahulu tanpa urutan menggunakan teknik agregat, menghasilkan rencana mode semua batch:
PILIH MAX(n) AS mx FROM dbo.GetNums_SQLkiwi(1, 100000000) OPTION(MAXDOP 1);
Saya mendapatkan waktu dan statistik I/O berikut untuk eksekusi ini:
Waktu CPU =2922 md, waktu berlalu =2943 md .Tabel 'CS'. Hitungan pemindaian 2, pembacaan logis 0, pembacaan fisik 0, pembacaan server halaman 0, pembacaan ke depan 0, pembacaan ke depan server halaman pembacaan 0, pembacaan logika lob 44 , lob fisik membaca 0, server halaman lob membaca 0, lob membaca ke depan membaca 0, server halaman lob membaca ke depan membaca 0.
Tabel 'CS'. Segmen dibaca 2, segmen dilewati 0.
Mari kita uji fungsi dengan urutan menggunakan teknik penetapan variabel:
MENNYATAKAN @n SEBAGAI BIGINT; PILIH @n =n FROM dbo.GetNums_SQLkiwi(1, 100000000) ORDER BY n OPTION(MAXDOP 1);
Seperti solusi sebelumnya, solusi ini juga menghindari pengurutan eksplisit dalam rencana, dan karena itu mendapatkan rencana yang sama apakah Anda meminta data yang dipesan atau tidak. Tetapi sekali lagi, tes ini menimbulkan penalti ekstra terutama karena teknik penugasan variabel yang digunakan di sini, yang mengakibatkan bagian penugasan variabel dalam rencana diproses dalam mode baris.
Berikut adalah waktu dan statistik I/O yang saya dapatkan untuk eksekusi ini:
Waktu CPU =6985 md, waktu berlalu =7033 md .Tabel 'CS'. Hitungan pemindaian 2, pembacaan logis 0, pembacaan fisik 0, pembacaan server halaman 0, pembacaan ke depan 0, pembacaan ke depan server halaman pembacaan 0, pembacaan logika lob 44 , lob fisik membaca 0, server halaman lob membaca 0, lob membaca ke depan membaca 0, server halaman lob membaca ke depan membaca 0.
Tabel 'CS'. Segmen dibaca 2, segmen dilewati 0.
Solusi CLR
Baik Kamil Kosno dan Adam Machanic pertama-tama menyediakan solusi sederhana CLR saja, dan kemudian muncul dengan kombo CLR+T-SQL yang lebih canggih. Saya akan mulai dengan solusi Kamil dan kemudian membahas solusi Adam.
Solusi oleh Kamil Kosno
Berikut kode CLR yang digunakan dalam solusi pertama Kamil untuk mendefinisikan fungsi yang disebut GetNums_KamilKosno1:
menggunakan Sistem;menggunakan System.Data.SqlTypes;menggunakan System.Collections;kelas parsial publik GetNumsKamil1{ [Microsoft.SqlServer.Server.SqlFunction(FillRowMethodName ="GetNums_KamilKosno1_Fill", TableDefinition ="n BIGINT1_Kamil) GetNums_KamilKosno1_Kamil publik statis (SqlInt64 rendah, SqlInt64 tinggi) { kembali (rendah.IsNull || tinggi.IsNull) ? GetNumsCS baru (0, 0) :GetNumsCS baru (Nilai rendah, Nilai tinggi); } public static void GetNums_KamilKosno1_Fill(Object o, out SqlInt64 n) { n =(long)o; } private class GetNumsCS :IEnumerator { public GetNumsCS(panjang dari, panjang sampai) { _lowrange =from; _saat ini =_rentang rendah - 1; _highrange =untuk; } public bool MoveNext() { _current +=1; if (_current> _highrange) mengembalikan false; lain kembali benar; } objek publik Saat ini { get { return _current; } } public void Reset() { _current =_lowrange - 1; } panjang _rendah; panjang _ saat ini; panjang _highrange; }}
Fungsi menerima dua input yang disebut rendah dan tinggi dan mengembalikan tabel dengan kolom BIGINT yang disebut n. Fungsinya adalah jenis streaming, mengembalikan baris dengan nomor berikutnya dalam seri per baris permintaan dari kueri panggilan. Seperti yang Anda lihat, Kamil memilih metode yang lebih formal untuk mengimplementasikan antarmuka IEnumerator, yang melibatkan penerapan metode MoveNext (memajukan enumerator untuk mendapatkan baris berikutnya), Current (mendapatkan baris pada posisi enumerator saat ini), dan Reset (menetapkan pencacah ke posisi awalnya, yaitu sebelum baris pertama).
Variabel memegang nomor saat ini dalam seri disebut _current. Konstruktor menyetel _current ke batas rendah dari rentang yang diminta dikurangi 1, dan hal yang sama berlaku untuk metode Reset. Metode MoveNext memajukan _current sebesar 1. Kemudian, jika _current lebih besar dari batas tinggi dari rentang yang diminta, metode mengembalikan false, yang berarti tidak akan dipanggil lagi. Jika tidak, itu mengembalikan true, artinya akan dipanggil lagi. Metode Current secara alami mengembalikan _current. Seperti yang Anda lihat, logika yang cukup mendasar.
Saya menelepon proyek Visual Studio GetNumsKamil1, dan menggunakan jalur C:\Temp\ untuk itu. Berikut kode yang saya gunakan untuk menerapkan fungsi di database testdb:
FUNGSI DROP JIKA ADA dbo.GetNums_KamilKosno1; JATUHKAN ASSEMBLY JIKA ADA GetNumsKamil1;PERGI CREATE ASSEMBLY GetNumsKamil1 FROM 'C:\Temp\GetNumsKamil1\GetNumsKamil1\bin\Debug\GetNumsKamil1.dll';GO CREATE FUNCTION dbo.GetNums(@Kohigh AS) TABEL(n BIGINT) ORDER(n) SEBAGAI NAMA EKSTERNAL GetNumsKamil1.GetNumsKamil1.GetNums_KamilKosno1;GO
Perhatikan penggunaan klausa ORDER dalam pernyataan CREATE FUNCTION. Fungsi memancarkan baris dalam n pemesanan, jadi ketika baris perlu diserap dalam paket dalam n pemesanan, berdasarkan klausa ini SQL Server tahu bahwa ia dapat menghindari pengurutan dalam paket.
Mari kita uji fungsinya, pertama dengan teknik agregat, saat pemesanan tidak diperlukan:
PILIH MAX(n) SEBAGAI mx DARI dbo.GetNums_KamilKosno1(1, 100000000);
Saya mendapatkan rencana yang ditunjukkan pada Gambar 1.
Gambar 1:Rencanakan fungsi dbo.GetNums_KamilKosno1
Tidak banyak yang bisa dikatakan tentang rencana ini, selain fakta bahwa semua operator menggunakan mode eksekusi baris.
Saya mendapatkan statistik waktu berikut untuk eksekusi ini:
Waktu CPU =37375 md, waktu berlalu =37488 md .Dan tentu saja, tidak ada pembacaan logis yang terlibat.
Mari kita uji fungsi dengan urutan, menggunakan teknik penetapan variabel:
MENNYATAKAN @n SEBAGAI BIGINT; PILIH @n =n DARI dbo.GetNums_KamilKosno1(1, 10000000) ORDER OLEH n;
Saya mendapatkan rencana yang ditunjukkan pada Gambar 2 untuk eksekusi ini.
Gambar 2:Rencanakan fungsi dbo.GetNums_KamilKosno1 dengan ORDER BY
Perhatikan bahwa tidak ada pengurutan dalam rencana sejak fungsi dibuat dengan klausa ORDER(n). Namun, ada beberapa upaya untuk memastikan bahwa baris memang dipancarkan dari fungsi dalam urutan yang dijanjikan. Ini dilakukan dengan menggunakan operator Proyek Segmen dan Urutan, yang digunakan untuk menghitung nomor baris, dan operator Assert, yang membatalkan eksekusi kueri jika pengujian gagal. Pekerjaan ini memiliki penskalaan linier—tidak seperti penskalaan n log n yang akan Anda dapatkan jika penskalaan diperlukan—tetapi tetap saja tidak murah. Saya mendapatkan statistik waktu berikut untuk tes ini:
Waktu CPU =51531 md, waktu berlalu =51905 md .Hasilnya mungkin mengejutkan bagi sebagian orang—terutama mereka yang secara intuitif berasumsi bahwa solusi berbasis CLR akan berkinerja lebih baik daripada solusi T-SQL. Seperti yang Anda lihat, waktu eksekusi jauh lebih lama dibandingkan dengan solusi T-SQL kami yang berkinerja terbaik.
Solusi kedua Kamil adalah hibrida CLR-T-SQL. Di luar input rendah dan tinggi, fungsi CLR (GetNums_KamilKosno2) menambahkan input langkah, dan mengembalikan nilai antara rendah dan tinggi yang terpisah satu sama lain. Berikut kode CLR yang Kamil gunakan dalam solusi keduanya:
menggunakan Sistem;menggunakan System.Data.SqlTypes;menggunakan System.Collections; publik parsial kelas GetNumsKamil2{ [Microsoft.SqlServer.Server.SqlFunction(DataAccess =Microsoft.SqlServer.Server.DataAccessKind.None, IsDeterministic =true, IsPrecise =true, FillRowMethodName ="GetNums_Fill", TableDefinition")] public static IEnumerator GetNums_KamilKosno2(SqlInt64 rendah, SqlInt64 tinggi, SqlInt64 langkah) { kembali (rendah.IsNull || tinggi.IsNull) ? GetNumsCS baru(0, 0, step.Value) :GetNumsCS baru(low.Value, high.Value, step.Value); } public static void GetNums_Fill(Object o, out SqlInt64 n) { n =(panjang)o; } private class GetNumsCS :IEnumerator { public GetNumsCS(panjang dari, panjang ke, langkah panjang) { _lowrange =from; _langkah =langkah; _saat ini =_rentang rendah - _langkah; _highrange =untuk; } public bool MoveNext() { _current =_current + _step; if (_current> _highrange) mengembalikan false; lain kembali benar; } objek publik Saat ini { get { return _current; } } public void Reset() { _current =_lowrange - _step; } panjang _rendah; panjang _ saat ini; panjang _highrange; panjang _langkah; }}
Saya menamai proyek VS GetNumsKamil2, menempatkannya di jalur C:\Temp\ juga, dan menggunakan kode berikut untuk menerapkannya di database testdb:
-- Buat perakitan dan fungsiDROP FUNCTION JIKA ADA dbo.GetNums_KamilKosno2;DROP ASSEMBLY JIKA ADA GetNumsKamil2;GO CREATE ASSEMBLY GetNumsKamil2 DARI 'C:\Temp\GetNumsKamil2\GetNums'Kamil2\binsKamil2\binsKamil .GetNums_KamilKosno2 (@low AS BIGINT =1, @high AS BIGINT, @step AS BIGINT) MENGEMBALIKAN TABLE(n BIGINT) ORDER(n) SEBAGAI NAMA EKSTERNAL GetNumsKamil2.GetNumsKamil2.GetNums_KamilKosno2;GO
Sebagai contoh untuk menggunakan fungsi, berikut adalah permintaan untuk menghasilkan nilai antara 5 dan 59, dengan langkah 10:
PILIH dan DARI dbo.GetNums_KamilKosno2(5, 59, 10);
Kode ini menghasilkan output berikut:
n---51525354555
Sedangkan untuk bagian T-SQL, Kamil menggunakan fungsi bernama dbo.GetNums_Hybrid_Kamil2, dengan kode sebagai berikut:
BUAT ATAU ubah FUNGSI dbo.GetNums_Hybrid_Kamil2(@low SEBAGAI BIGINT, @high SEBAGAI BIGINT) MENGEMBALIKAN TABLEASRETURN SELECT TOP (@high - @low + 1) V.n FROM dbo.GetNums_KamilKosno2(@low, @high, 10) SEBAGAI GN LINTAS BERLAKU (NILAI(0+GN.n),(1+GN.n),(2+GN.n),(3+GN.n),(4+GN.n), (5+GN.n ),(6+GN.n),(7+GN.n),(8+GN.n),(9+GN.n)) AS V(n);GO
Seperti yang Anda lihat, fungsi T-SQL memanggil fungsi CLR dengan input @low dan @high yang sama, dan dalam contoh ini menggunakan ukuran langkah 10. Kueri menggunakan CROSS APPLY antara hasil fungsi CLR dan konstruktor tabel -nilai yang menghasilkan angka akhir dengan menambahkan nilai dalam rentang 0 hingga 9 ke awal langkah. Filter TOP digunakan untuk memastikan bahwa Anda tidak mendapatkan lebih dari jumlah nomor yang Anda minta.
Penting: Saya harus menekankan bahwa Kamil membuat asumsi di sini tentang filter TOP yang diterapkan berdasarkan urutan nomor hasil, yang tidak benar-benar dijamin karena kueri tidak memiliki klausa ORDER BY. Jika Anda menambahkan klausa ORDER BY untuk mendukung TOP, atau mengganti TOP dengan filter WHERE, untuk menjamin filter deterministik, ini dapat sepenuhnya mengubah profil kinerja solusi.
Bagaimanapun, pertama-tama mari kita uji fungsi tanpa urutan menggunakan teknik agregat:
PILIH MAX(n) SEBAGAI mx DARI dbo.GetNums_Hybrid_Kamil2(1, 100000000);
Saya mendapatkan rencana yang ditunjukkan pada Gambar 3 untuk eksekusi ini.
Gambar 3:Rencanakan fungsi dbo.GetNums_Hybrid_Kamil2
Sekali lagi, semua operator dalam paket menggunakan mode eksekusi baris.
Saya mendapatkan statistik waktu berikut untuk eksekusi ini:
Waktu CPU =13985 md, waktu berlalu =14069 md .Dan tentu saja tidak ada pembacaan logis.
Mari kita uji fungsinya dengan perintah:
MENNYATAKAN @n SEBAGAI BIGINT; PILIH @n =n DARI dbo.GetNums_Hybrid_Kamil2(1, 100000000) ORDER OLEH n;
Saya mendapatkan rencana yang ditunjukkan pada Gambar 4.
Gambar 4:Rencanakan fungsi dbo.GetNums_Hybrid_Kamil2 dengan ORDER BY
Karena angka hasil adalah hasil manipulasi batas bawah langkah yang dikembalikan oleh fungsi CLR dan delta yang ditambahkan dalam konstruktor nilai tabel, pengoptimal tidak percaya bahwa angka hasil dihasilkan dalam urutan yang diminta, dan menambahkan penyortiran eksplisit ke rencana.
Saya mendapatkan statistik waktu berikut untuk eksekusi ini:
Waktu CPU =68703 md, waktu berlalu =84538 md .Jadi tampaknya ketika tidak ada pesanan yang dibutuhkan, solusi kedua Kamil lebih baik daripada yang pertama. Tetapi ketika pesanan dibutuhkan, itu sebaliknya. Either way, solusi T-SQL lebih cepat. Secara pribadi, saya percaya kebenaran dari solusi pertama, tetapi tidak yang kedua.
Solusi oleh Adam Machanic
Solusi pertama Adam juga merupakan fungsi CLR dasar yang terus menambah penghitung. Hanya saja, alih-alih menggunakan pendekatan formal yang lebih terlibat seperti yang Kamil lakukan, Adam menggunakan pendekatan yang lebih sederhana yang memanggil perintah hasil per baris yang perlu dikembalikan.
Berikut kode CLR Adam untuk solusi pertamanya, yang mendefinisikan fungsi streaming yang disebut GetNums_AdamMachanic1:
menggunakan System.Data.SqlTypes;menggunakan System.Collections; kelas parsial publik GetNumsAdam1{ [Microsoft.SqlServer.Server.SqlFunction( FillRowMethodName ="GetNums_AdamMachanic1_fill", TableDefinition ="n BIGINT")] public static IEnumerable GetNums_AdamMachanic1(SqlInt64 min, SqlInt64 min, Sql. var max_int =max.Value; for (; min_int <=max_int; min_int++) { hasil kembali (min_int); } } public static void GetNums_AdamMachanic1_fill(object o, out long i) { i =(long)o; }};
Solusinya sangat elegan dalam kesederhanaannya. Seperti yang Anda lihat, fungsi menerima dua input yang disebut min dan max yang mewakili titik batas rendah dan tinggi dari rentang yang diminta, dan mengembalikan tabel dengan kolom BIGINT yang disebut n. Fungsi menginisialisasi variabel yang disebut min_int dan max_int dengan nilai parameter input masing-masing fungsi. Fungsi tersebut kemudian menjalankan loop selama min_int <=max_int, yang dalam setiap iterasi menghasilkan baris dengan nilai min_int saat ini dan menambahkan min_int sebanyak 1. Itu saja.
Saya menamai proyek GetNumsAdam1 di VS, menempatkannya di C:\Temp\, dan menggunakan kode berikut untuk menerapkannya:
-- Buat perakitan dan fungsiDROP FUNCTION JIKA ADA dbo.GetNums_AdamMachanic1; DROP ASSEMBLY JIKA ADA GetNumsAdam1;PERGI CREATE ASSEMBLY GetNumsAdam1 DARI 'C:\Temp\GetNumsAdam1\GetNumsAdam1\binsAdam1\bin .GetNums_AdamMachanic1(@low AS BIGINT =1, @high AS BIGINT) MENGEMBALIKAN TABEL(n BIGINT) ORDER(n) SEBAGAI NAMA EKSTERNAL GetNumsAdam1.GetNumsAdam1.GetNums_AdamMachanic1;GO
Saya menggunakan kode berikut untuk mengujinya dengan teknik agregat, untuk kasus-kasus ketika pesanan tidak penting:
PILIH MAX(n) SEBAGAI mx DARI dbo.GetNums_AdamMachanic1(1, 100000000);
Saya mendapatkan rencana yang ditunjukkan pada Gambar 5 untuk eksekusi ini.
Gambar 5:Rencanakan fungsi dbo.GetNums_AdamMachanic1
Rencananya sangat mirip dengan rencana yang Anda lihat sebelumnya untuk solusi pertama Kamil, dan hal yang sama berlaku untuk kinerjanya. Saya mendapatkan statistik waktu berikut untuk eksekusi ini:
Waktu CPU =36687 md, waktu berlalu =36952 md .Dan tentu saja tidak diperlukan pembacaan logis.
Mari kita uji fungsi dengan urutan, menggunakan teknik penetapan variabel:
MENNYATAKAN @n SEBAGAI BIGINT; PILIH @n =n DARI dbo.GetNums_AdamMachanic1(1, 10000000) ORDER OLEH n;
Saya mendapatkan rencana yang ditunjukkan pada Gambar 6 untuk eksekusi ini.
Gambar 6:Rencanakan fungsi dbo.GetNums_AdamMachanic1 dengan ORDER BY
Sekali lagi, rencananya terlihat mirip dengan yang Anda lihat sebelumnya untuk solusi pertama Kamil. Tidak perlu penyortiran eksplisit karena fungsi dibuat dengan klausa ORDER, tetapi rencana tersebut menyertakan beberapa pekerjaan untuk memverifikasi bahwa baris benar-benar dikembalikan dipesan seperti yang dijanjikan.
Saya mendapatkan statistik waktu berikut untuk eksekusi ini:
Waktu CPU =55047 md, waktu berlalu =55498 md .Dalam solusi keduanya, Adam juga menggabungkan bagian CLR dan bagian T-SQL. Berikut deskripsi Adam tentang logika yang dia gunakan dalam penyelesaiannya:
“Saya mencoba memikirkan cara mengatasi masalah obrolan SQLCLR, dan juga tantangan utama pembuat angka ini di T-SQL, yang merupakan fakta bahwa kita tidak bisa begitu saja membuat baris ajaib menjadi ada.
CLR adalah jawaban yang bagus untuk bagian kedua tetapi tentu saja terhambat oleh masalah pertama. Jadi sebagai kompromi saya membuat T-SQL TVF [disebut GetNums_AdamMachanic2_8192] hardcoded dengan nilai 1 hingga 8192. (Pilihan yang cukup arbitrer, tetapi terlalu besar dan QO mulai tersedak sedikit.) Selanjutnya saya memodifikasi fungsi CLR saya [ bernama GetNums_AdamMachanic2_8192_base] untuk menampilkan dua kolom, "max_base" dan "base_add", dan menghasilkan baris seperti:
- max_base, base_add
——————
8191, 1
8192, 8192
8192, 16384
…
8192, 99991552
257, 99999744
Sekarang loop sederhana. Output CLR dikirim ke T-SQL TVF, yang diatur untuk hanya mengembalikan hingga baris "max_base" dari set hardcodednya. Dan untuk setiap baris, ia menambahkan "base_add" ke nilai, sehingga menghasilkan angka yang diperlukan. Kuncinya di sini, menurut saya, adalah kita dapat menghasilkan N baris hanya dengan satu gabungan silang logis, dan fungsi CLR hanya harus mengembalikan 1/8192 baris, jadi cukup cepat untuk bertindak sebagai generator dasar.”
Logikanya tampaknya cukup mudah.
Berikut kode yang digunakan untuk mendefinisikan fungsi CLR yang disebut GetNums_AdamMachanic2_8192_base:
menggunakan System.Data.SqlTypes;menggunakan System.Collections; public partial class GetNumsAdam2{ private struct row { public long max_base; base_add panjang publik; } [Microsoft.SqlServer.Server.SqlFunction( FillRowMethodName ="GetNums_AdamMachanic2_8192_base_fill", TableDefinition ="max_base int, base_add int")] public static IEnumerable GetNums_AdamMachanic2_8192_base =min.64_base min var max_int =max.Value; var min_group =min_int / 8192; var max_group =max_int / 8192; for (; min_group <=max_group; min_group++) { if (min_int> max_int) menghasilkan break; var max_base =8192 - (min_int % 8192); if (min_group ==max_group &&max_int <(((max_int / 8192) + 1) * 8192) - 1) max_base =max_int - min_int + 1; hasil kembali ( baris baru() { max_base =max_base, base_add =min_int } ); min_int =(min_group + 1) * 8192; } } public static void GetNums_AdamMachanic2_8192_base_fill(object o, out long max_base, out long base_add) { var r =(baris)o; max_base =r.max_base; base_add =r.base_add; }};
Saya menamai proyek VS GetNumsAdam2 dan ditempatkan di jalur C:\Temp\ seperti dengan proyek lainnya. Berikut kode yang saya gunakan untuk menerapkan fungsi di database testdb:
SELECT * FROM dbo.GetNums_AdamMachanic2_8192_base(1, 100000000);
Kode ini menghasilkan output berikut, ditampilkan di sini dalam bentuk singkatan:
max_base base_add-------------------- --------------------8191 18192 81928192 163848192 245768192 32768...8192 999669768192 999751688192 999833608192 99991552257 99999744 (12208 baris terpengaruh)
Berikut kode dengan definisi fungsi T-SQL GetNums_AdamMachanic2_8192 (disingkat):
BUAT ATAUUBAH FUNGSI dbo.GetNums_AdamMachanic2_8192(@max_base SEBAGAI BIGINT, @add_base SEBAGAI BIGINT) MENGEMBALIKAN TABLEASRETURN SELECT TOP (@max_base) V.i + @add_base SEBAGAI val DARI ( NILAI (0), (1), (2), (3), (4), ... (8187), (8188), (8189), (8190), (8191) ) AS V(i);GO
Penting: Juga di sini, saya harus menekankan bahwa mirip dengan apa yang saya katakan tentang solusi kedua Kamil, Adam membuat asumsi di sini bahwa filter TOP akan mengekstrak baris teratas berdasarkan urutan tampilan baris di konstruktor nilai tabel, yang tidak benar-benar dijamin. Jika Anda menambahkan klausa ORDER BY untuk mendukung TOP atau mengubah filter ke filter WHERE, Anda akan mendapatkan filter deterministik, tetapi ini dapat sepenuhnya mengubah profil kinerja solusi.
Terakhir, inilah fungsi T-SQL terluar, dbo.GetNums_AdamMachanic2, yang dipanggil pengguna akhir untuk mendapatkan seri nomor:
BUAT ATAU UBAH FUNGSI dbo.GetNums_AdamMachanic2(@low AS BIGINT =1, @high AS BIGINT) KEMBALI TABELASRETURN SELECT Y.val AS n FROM ( SELECT max_base, base_add FROM dbo.GetNums_AdamMachanic)@low AS, @high(2_8192_base) X LINTAS BERLAKU dbo.GetNums_AdamMachanic2_8192(X.max_base, X.base_add) SEBAGAI YGO
Fungsi ini menggunakan operator CROSS APPLY untuk menerapkan fungsi T-SQL bagian dalam dbo.GetNums_AdamMachanic2_8192 per baris yang dikembalikan oleh fungsi CLR bagian dalam dbo.GetNums_AdamMachanic2_8192_base.
Mari kita uji solusi ini terlebih dahulu menggunakan teknik agregat saat urutan tidak menjadi masalah:
PILIH MAX(n) SEBAGAI mx DARI dbo.GetNums_AdamMachanic2(1, 100000000);
Saya mendapatkan rencana yang ditunjukkan pada Gambar 7 untuk eksekusi ini.
Gambar 7:Rencanakan fungsi dbo.GetNums_AdamMachanic2
Saya mendapatkan statistik waktu berikut untuk tes ini:
SQL Server waktu parsing dan kompilasi :Waktu CPU =313 md, waktu berlalu =339 md .SQL Server waktu eksekusi :waktu CPU =8859 md, waktu berlalu =8849 md .
Tidak diperlukan pembacaan logis.
Waktu eksekusi tidak buruk, tetapi perhatikan waktu kompilasi yang tinggi karena konstruktor nilai tabel besar yang digunakan. Anda akan membayar waktu kompilasi yang tinggi terlepas dari ukuran rentang yang Anda minta, jadi ini sangat rumit saat menggunakan fungsi dengan rentang yang sangat kecil. Dan solusi ini masih lebih lambat daripada yang T-SQL.
Mari kita uji fungsinya dengan perintah:
MENNYATAKAN @n SEBAGAI BIGINT; PILIH @n =n DARI dbo.GetNums_AdamMachanic2(1, 10000000) ORDER OLEH n;
Saya mendapatkan rencana yang ditunjukkan pada Gambar 8 untuk eksekusi ini.
Gambar 8:Rencanakan fungsi dbo.GetNums_AdamMachanic2 dengan ORDER BY
Seperti dengan solusi kedua Kamil, pengurutan eksplisit diperlukan dalam rencana, dengan memberikan penalti kinerja yang signifikan. Berikut adalah statistik waktu yang saya dapatkan untuk tes ini:
Waktu eksekusi:Waktu CPU =54891 md, waktu berlalu =60981 md .Plus, masih ada penalti waktu kompilasi yang tinggi sekitar sepertiga detik.
Kesimpulan
Sangat menarik untuk menguji solusi berbasis CLR untuk tantangan nomor seri karena banyak orang awalnya berasumsi bahwa solusi berkinerja terbaik kemungkinan akan menjadi solusi berbasis CLR. Kamil dan Adam menggunakan pendekatan serupa, dengan upaya pertama menggunakan loop sederhana yang menambah penghitung dan menghasilkan baris dengan nilai berikutnya per iterasi, dan upaya kedua yang lebih canggih yang menggabungkan bagian CLR dan T-SQL. Secara pribadi, saya tidak merasa nyaman dengan fakta bahwa dalam solusi kedua Kamil dan Adam mereka mengandalkan filter TOP nondeterministik, dan ketika saya mengubahnya menjadi filter deterministik dalam pengujian saya sendiri, itu berdampak buruk pada kinerja solusi. . Either way, dua solusi T-SQL kami berkinerja lebih baik daripada yang CLR, dan tidak menghasilkan penyortiran eksplisit dalam rencana saat Anda membutuhkan baris yang dipesan. Jadi saya tidak benar-benar melihat nilai dalam mengejar rute CLR lebih jauh. Gambar 9 memiliki ringkasan kinerja dari solusi yang saya sajikan dalam artikel ini.
Gambar 9:Perbandingan kinerja waktu
Bagi saya, GetNums_AlanCharlieItzikBatch harus menjadi solusi pilihan ketika Anda sama sekali tidak memerlukan jejak I/O, dan GetNums_SQKWiki harus lebih disukai jika Anda tidak keberatan dengan jejak I/O kecil. Tentu saja, kami selalu berharap bahwa suatu hari Microsoft akan menambahkan alat yang sangat berguna ini sebagai alat bawaan, dan mudah-mudahan jika/ketika mereka melakukannya, itu akan menjadi solusi berkinerja yang mendukung pemrosesan batch dan paralelisme. Jadi, jangan lupa untuk memilih permintaan peningkatan fitur ini, dan bahkan mungkin menambahkan komentar Anda tentang mengapa ini penting bagi Anda.
Saya sangat menikmati mengerjakan seri ini. Saya belajar banyak selama proses ini, dan berharap Anda juga melakukannya.