Ini adalah bagian ketiga dari seri tentang solusi untuk tantangan generator seri nomor. Di Bagian 1 saya membahas solusi yang menghasilkan baris dengan cepat. Di Bagian 2 saya membahas solusi yang menanyakan tabel dasar fisik yang Anda isi sebelumnya dengan baris. Bulan ini saya akan fokus pada teknik menarik yang dapat digunakan untuk menangani tantangan kita, tetapi juga memiliki aplikasi yang menarik jauh di luar itu. Saya tidak mengetahui nama resmi untuk teknik ini, tetapi konsepnya agak mirip dengan eliminasi partisi horizontal, jadi saya akan menyebutnya secara informal sebagai eliminasi unit horizontal teknik. Teknik ini dapat memiliki manfaat kinerja positif yang menarik, tetapi ada juga peringatan yang perlu Anda waspadai, di mana dalam kondisi tertentu dapat menimbulkan penalti kinerja.
Sekali lagi terima kasih 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 di tempdb, mengaktifkan statistik waktu:
SET NOCOUNT ON; USE tempdb; SET STATISTICS TIME ON;
Ide sebelumnya
Teknik eliminasi unit horizontal dapat digunakan sebagai alternatif logika eliminasi kolom, atau eliminasi unit vertikal teknik, yang saya andalkan dalam beberapa solusi yang saya bahas sebelumnya. Anda dapat membaca tentang dasar-dasar logika eliminasi kolom dengan ekspresi tabel di Dasar-dasar ekspresi tabel, Bagian 3 – Tabel turunan, pertimbangan pengoptimalan di bagian “Proyeksi kolom dan kata di SELECT *.”
Ide dasar teknik eliminasi unit vertikal adalah jika Anda memiliki ekspresi tabel bersarang yang mengembalikan kolom x dan y, dan kueri luar Anda hanya merujuk kolom x, proses kompilasi kueri menghilangkan y dari pohon kueri awal, dan oleh karena itu rencananya tidak perlu mengevaluasinya. Ini memiliki beberapa implikasi positif terkait pengoptimalan, seperti mencapai cakupan indeks dengan x saja, dan jika y adalah hasil komputasi, tidak perlu mengevaluasi ekspresi dasar y sama sekali. Ide ini merupakan inti dari solusi Alan Burstein. Saya juga mengandalkannya di beberapa solusi lain yang saya bahas, seperti dengan fungsi dbo.GetNumsAlanCharlieItzikBatch (dari Bagian 1), fungsi dbo.GetNumsJohn2DaveObbishAlanCharlieItzik dan dbo.GetNumsJohn2DaveObbishAlanCharlieItzik2 (dari Part 2), dan lainnya. Sebagai contoh, saya akan menggunakan dbo.GetNumsAlanCharlieItzikBatch sebagai solusi dasar dengan logika eliminasi vertikal.
Sebagai pengingat, solusi ini menggunakan 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
Saya menggunakan kode berikut untuk menguji kinerja fungsi dengan 100 juta baris, mengembalikan kolom hasil yang dihitung n (manipulasi hasil fungsi ROW_NUMBER), diurutkan oleh n:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieItzikBatch(1, 100000000) ORDER BY n OPTION(MAXDOP 1);
Berikut adalah statistik waktu yang saya dapatkan untuk tes ini:
Waktu CPU =9328 md, waktu yang berlalu =9330 md.Saya menggunakan kode berikut untuk menguji kinerja fungsi dengan 100 juta baris, mengembalikan kolom rn (langsung, tidak dimanipulasi, hasil dari fungsi ROW_NUMBER), diurutkan oleh rn:
DECLARE @n AS BIGINT; SELECT @n = rn FROM dbo.GetNumsAlanCharlieItzikBatch(1, 100000000) ORDER BY rn OPTION(MAXDOP 1);
Berikut adalah statistik waktu yang saya dapatkan untuk tes ini:
Waktu CPU =7296 mdtk, waktu berlalu =7291 mdtk.Mari kita tinjau ide-ide penting yang tertanam dalam solusi ini.
Mengandalkan logika eliminasi kolom, alan muncul dengan ide untuk mengembalikan tidak hanya satu kolom dengan seri angka, tetapi tiga:
- Kolom rn mewakili hasil yang tidak dimanipulasi dari fungsi ROW_NUMBER, yang dimulai dengan 1. Ini murah untuk dihitung. Ini adalah pelestarian urutan ketika Anda memberikan konstanta dan ketika Anda memberikan nonkonstanta (variabel, kolom) sebagai input ke fungsi. Ini berarti bahwa ketika kueri luar Anda menggunakan ORDER BY rn, Anda tidak mendapatkan operator Sortir dalam paket.
- Kolom n merepresentasikan komputasi berdasarkan @low, konstanta, dan rownum (hasil dari fungsi ROW_NUMBER). Ini adalah pelestarian urutan sehubungan dengan rownum ketika Anda memberikan konstanta sebagai input ke fungsi. Itu berkat wawasan Charlie tentang pelipatan konstan (lihat Bagian 1 untuk detailnya). Namun, ini bukan pelestarian urutan saat Anda memberikan nonkonstanta sebagai input, karena Anda tidak mendapatkan pelipatan konstan. Saya akan menunjukkannya nanti di bagian tentang peringatan.
- Kolom op mewakili n dalam urutan yang berlawanan. Ini adalah hasil dari perhitungan dan bukan pelestarian urutan.
Mengandalkan logika eliminasi kolom, jika Anda perlu mengembalikan rangkaian angka yang dimulai dengan 1, Anda meminta kolom rn, yang lebih murah daripada meminta n. Jika Anda memerlukan rangkaian angka yang dimulai dengan nilai selain 1, Anda meminta n dan membayar biaya tambahan. Jika Anda membutuhkan hasil yang diurutkan berdasarkan kolom angka, dengan konstanta sebagai input, Anda dapat menggunakan ORDER BY rn atau ORDER BY n. Tetapi dengan input nonkonstanta, Anda ingin memastikan untuk menggunakan ORDER BY rn. Mungkin ide yang baik untuk selalu menggunakan ORDER BY rn saat membutuhkan hasil yang dipesan agar aman.
Gagasan eliminasi satuan horizontal mirip dengan gagasan eliminasi satuan vertikal, hanya saja ini berlaku untuk kumpulan baris, bukan kumpulan kolom. Faktanya, Joe Obbish mengandalkan ide ini dalam fungsinya dbo.GetNumsObbish (dari Bagian 2), dan kami akan melangkah lebih jauh. Dalam solusinya, Joe menyatukan beberapa kueri yang mewakili subrentang angka yang terpisah, menggunakan filter dalam klausa WHERE dari setiap kueri untuk menentukan penerapan subrentang. Saat Anda memanggil fungsi dan meneruskan input konstan yang mewakili pembatas rentang yang Anda inginkan, SQL Server menghilangkan kueri yang tidak dapat diterapkan pada waktu kompilasi, sehingga rencana tersebut bahkan tidak mencerminkannya.
Penghapusan unit horizontal, waktu kompilasi versus waktu berjalan
Mungkin ide yang baik untuk memulai dengan mendemonstrasikan konsep eliminasi unit horizontal dalam kasus yang lebih umum, dan juga mendiskusikan perbedaan penting antara eliminasi waktu kompilasi dan waktu proses. Kemudian kita bisa mendiskusikan bagaimana menerapkan ide tersebut ke tantangan seri angka kita.
Saya akan menggunakan tiga tabel yang disebut dbo.T1, dbo.T2 dan dbo.T3 dalam contoh saya. Gunakan kode DDL dan DML berikut untuk membuat dan mengisi tabel ini:
DROP TABLE IF EXISTS dbo.T1, dbo.T2, dbo.T3; GO CREATE TABLE dbo.T1(col1 INT); INSERT INTO dbo.T1(col1) VALUES(1); CREATE TABLE dbo.T2(col1 INT); INSERT INTO dbo.T2(col1) VALUES(2); CREATE TABLE dbo.T3(col1 INT); INSERT INTO dbo.T3(col1) VALUES(3);
Misalkan Anda ingin menerapkan TVF inline yang disebut dbo.OneTable yang menerima salah satu dari tiga nama tabel di atas sebagai input, dan mengembalikan data dari tabel yang diminta. Berdasarkan konsep eliminasi unit horizontal, Anda dapat mengimplementasikan fungsi seperti ini:
CREATE OR ALTER FUNCTION dbo.OneTable(@WhichTable AS NVARCHAR(257)) RETURNS TABLE AS RETURN SELECT col1 FROM dbo.T1 WHERE @WhichTable = N'dbo.T1' UNION ALL SELECT col1 FROM dbo.T2 WHERE @WhichTable = N'dbo.T2' UNION ALL SELECT col1 FROM dbo.T3 WHERE @WhichTable = N'dbo.T3'; GO
Ingatlah bahwa TVF sebaris menerapkan penyematan parameter. Ini berarti bahwa ketika Anda meneruskan konstanta seperti N'dbo.T2' sebagai input, proses inlining mengganti semua referensi ke @WhichTable dengan konstanta sebelum pengoptimalan . Proses eliminasi kemudian dapat menghapus referensi ke T1 dan T3 dari pohon kueri awal, dan dengan demikian optimasi kueri menghasilkan rencana yang hanya mereferensikan T2. Mari kita uji ide ini dengan kueri berikut:
SELECT * FROM dbo.OneTable(N'dbo.T2');
Rencana untuk kueri ini ditunjukkan pada Gambar 1.
Gambar 1:Rencanakan dbo.OneTable dengan masukan konstan
Seperti yang Anda lihat, hanya tabel T2 yang muncul dalam rencana.
Hal-hal sedikit lebih rumit ketika Anda melewatkan nonconstant sebagai input. Ini bisa terjadi saat menggunakan variabel, parameter prosedur, atau melewatkan kolom melalui APPLY. Nilai input tidak diketahui pada waktu kompilasi, atau potensi penggunaan kembali rencana yang diparameterisasi perlu diperhitungkan.
Pengoptimal tidak dapat menghilangkan tabel apa pun dari paket, tetapi masih memiliki trik. Itu dapat menggunakan operator Filter startup di atas subpohon yang mengakses tabel, dan hanya menjalankan subpohon yang relevan berdasarkan nilai runtime dari @WhichTable. Gunakan kode berikut untuk menguji strategi ini:
DECLARE @T AS NVARCHAR(257) = N'dbo.T2'; SELECT * FROM dbo.OneTable(@T);
Rencana eksekusi ini ditunjukkan pada Gambar 2:
Gambar 2:Rencanakan dbo.OneTable dengan masukan tidak konstan
Plan Explorer membuatnya sangat jelas untuk melihat bahwa hanya subpohon yang berlaku yang dieksekusi (Eksekusi =1), dan membuat subpohon yang tidak dieksekusi menjadi abu-abu (Eksekusi =0). Selain itu, STATISTICS IO menampilkan info I/O hanya untuk tabel yang diakses:
Tabel 'T2'. Hitungan pemindaian 1, pembacaan logis 1, pembacaan fisik 0, server halaman membaca 0, pembacaan ke depan membaca 0, pembacaan server halaman pembacaan ke depan 0, pembacaan logika lob 0, pembacaan fisik lob 0, server halaman lob membaca 0, pembacaan lob- depan membaca 0, server halaman lob read-depan membaca 0.Menerapkan logika eliminasi unit horizontal ke tantangan deret angka
Seperti disebutkan, Anda dapat menerapkan konsep eliminasi unit horizontal dengan memodifikasi salah satu solusi sebelumnya yang saat ini menggunakan logika eliminasi vertikal. Saya akan menggunakan fungsi dbo.GetNumsAlanCharlieItzikBatch sebagai titik awal untuk contoh saya.
Ingatlah bahwa Joe Obbish menggunakan eliminasi unit horizontal untuk mengekstrak sub-rentang terpisah yang relevan dari deret bilangan. Kita akan menggunakan konsep untuk memisahkan secara horizontal komputasi yang lebih murah (rn) dimana @low =1 dari komputasi yang lebih mahal (n) dimana @low <> 1.
Saat melakukannya, kita dapat bereksperimen dengan menambahkan ide Jeff Moden dalam fungsi fnTally-nya, di mana ia menggunakan baris sentinel dengan nilai 0 untuk kasus di mana rentangnya dimulai dengan @low =0.
Jadi kita memiliki empat unit horizontal:
- Baris penjaga dengan 0 di mana @rendah =0, dengan n =0
- TOP (@high) baris di mana @low =0, dengan n =rownum murah, dan op =@high – rownum
- TOP (@high) baris di mana @low =1, dengan n =rownum murah, dan op =@high + 1 – rownum
- TOP(@high – @low + 1) baris di mana @low <> 0 AND @low <> 1, dengan lebih mahal n =@low – 1 + rownum, dan op =@high + 1 – rownum
Solusi ini menggabungkan ide dari Alan, Charlie, Joe, Jeff, dan saya sendiri, jadi kami akan memanggil versi mode batch dari fungsi dbo.GetNumsAlanCharlieJoeJeffItzikBatch.
Pertama, ingat untuk memastikan bahwa Anda masih memiliki tabel dbo.BatchMe yang ada untuk mendapatkan pemrosesan batch dalam solusi kami, atau gunakan kode berikut jika tidak:
DROP TABLE IF EXISTS dbo.BatchMe; GO CREATE TABLE dbo.BatchMe(col1 INT NOT NULL, INDEX idx_cs CLUSTERED COLUMNSTORE);
Berikut kode dengan definisi fungsi dbo.GetNumsAlanCharlieJoeJeffItzikBatch:
CREATE OR ALTER FUNCTION dbo.GetNumsAlanCharlieJoeJeffItzikBatch(@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 @low AS n, @high AS op WHERE @low = 0 AND @high > @low UNION ALL SELECT TOP(@high) rownum AS n, @high - rownum AS op FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0 WHERE @low = 0 ORDER BY rownum UNION ALL SELECT TOP(@high) rownum AS n, @high + 1 - rownum AS op FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0 WHERE @low = 1 ORDER BY rownum UNION ALL SELECT TOP(@high - @low + 1) @low - 1 + rownum AS n, @high + 1 - rownum AS op FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0 WHERE @low <> 0 AND @low <> 1 ORDER BY rownum; GO
Penting:Konsep eliminasi unit horizontal tidak diragukan lagi lebih kompleks untuk diterapkan daripada konsep vertikal, jadi mengapa repot-repot? Karena menghilangkan tanggung jawab untuk memilih kolom yang tepat dari pengguna. Pengguna hanya perlu khawatir tentang menanyakan kolom yang disebut n, dibandingkan dengan mengingat untuk menggunakan rn ketika rentang dimulai dengan 1, dan n sebaliknya.
Mari kita mulai dengan menguji solusi dengan input konstan 1 dan 100.000.000, meminta hasilnya untuk dipesan:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeJeffItzikBatch(1, 100000000) ORDER BY n OPTION(MAXDOP 1);
Rencana eksekusi ini ditunjukkan pada Gambar 3.
Gambar 3:Rencana untuk dbo.GetNumsAlanCharlieJoeJeffItzikBatch(1, 100M)
Perhatikan bahwa satu-satunya kolom yang dikembalikan didasarkan pada ekspresi ROW_NUMBER langsung, tidak dimanipulasi (Expr1313). Perhatikan juga bahwa tidak perlu menyortir dalam rencana.
Saya mendapatkan statistik waktu berikut untuk eksekusi ini:
Waktu CPU =7359 mdtk, waktu berlalu =7354 mdtk.Waktu proses secara memadai mencerminkan fakta bahwa paket menggunakan mode batch, ekspresi ROW_NUMBER yang tidak dimanipulasi, dan tidak ada penyortiran.
Selanjutnya, uji fungsi dengan rentang konstan 0 hingga 99.999.999:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeJeffItzikBatch(0, 99999999) ORDER BY n OPTION(MAXDOP 1);
Rencana eksekusi ini ditunjukkan pada Gambar 4.
Gambar 4:Rencanakan untuk dbo.GetNumsAlanCharlieJoeJeffItzikBatch(0, 99999999)
Rencananya menggunakan operator Merge Join (Concatenation) untuk menggabungkan baris sentinel dengan nilai 0 dan sisanya. Meskipun bagian kedua seefisien sebelumnya, logika penggabungan memakan korban yang cukup besar sekitar 26% pada waktu berjalan, menghasilkan statistik waktu berikut:
Waktu CPU =9265 md, waktu berlalu =9298 md.Mari kita uji fungsinya dengan rentang konstan 2 hingga 100.000.001:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeJeffItzikBatch(2, 100000001) ORDER BY n OPTION(MAXDOP 1);
Rencana eksekusi ini ditunjukkan pada Gambar 5.
Gambar 5:Rencanakan untuk dbo.GetNumsAlanCharlieJoeJeffItzikBatch(2, 100000001)
Kali ini tidak ada logika penggabungan yang mahal karena bagian baris sentinel tidak relevan. Namun, perhatikan bahwa kolom yang dikembalikan adalah ekspresi yang dimanipulasi @low – 1 + rownum, yang setelah parameter embedding/inlining dan folding konstan menjadi 1 + rownum.
Berikut adalah statistik waktu yang saya dapatkan untuk eksekusi ini:
Waktu CPU =9000 mdtk, waktu berlalu =9015 mdtk.Seperti yang diharapkan, ini tidak secepat dengan rentang yang dimulai dengan 1, tetapi yang menarik, lebih cepat daripada dengan rentang yang dimulai dengan 0.
Menghapus 0 baris penjaga
Mengingat bahwa teknik dengan baris sentinel dengan nilai 0 tampaknya lebih lambat daripada menerapkan manipulasi ke rownum, masuk akal untuk menghindarinya. Ini membawa kita ke solusi berbasis eliminasi horizontal yang disederhanakan yang menggabungkan ide-ide dari Alan, Charlie, Joe, dan saya sendiri. Saya akan memanggil fungsi dengan solusi ini dbo.GetNumsAlanCharlieJoeItzikBatch. Berikut definisi fungsinya:
CREATE OR ALTER FUNCTION dbo.GetNumsAlanCharlieJoeItzikBatch(@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) rownum AS n, @high + 1 - rownum AS op FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0 WHERE @low = 1 ORDER BY rownum UNION ALL SELECT TOP(@high - @low + 1) @low - 1 + rownum AS n, @high + 1 - rownum AS op FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0 WHERE @low <> 1 ORDER BY rownum; GO
Mari kita uji dengan rentang 1 hingga 100 juta:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeItzikBatch(1, 100000000) ORDER BY n OPTION(MAXDOP 1);
Rencananya sama dengan yang ditunjukkan sebelumnya pada Gambar 3, seperti yang diharapkan.
Dengan demikian, saya mendapatkan statistik waktu berikut:
Waktu CPU =7219 mdtk, waktu berlalu =7243 mdtk.Uji dengan rentang 0 hingga 99.999.999:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeItzikBatch(0, 99999999) ORDER BY n OPTION(MAXDOP 1);
Kali ini Anda mendapatkan rencana yang sama seperti yang ditunjukkan sebelumnya pada Gambar 5—bukan Gambar 4.
Berikut adalah statistik waktu yang saya dapatkan untuk eksekusi ini:
Waktu CPU =9313 md, waktu berlalu =9334 md.Uji dengan rentang 2 hingga 100.000.001:
DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeItzikBatch(2, 100000001) ORDER BY n OPTION(MAXDOP 1);
Sekali lagi Anda mendapatkan paket yang sama seperti yang ditunjukkan sebelumnya pada Gambar 5.
Saya mendapatkan statistik waktu berikut untuk eksekusi ini:
Waktu CPU =9125 mdtk, waktu berlalu =9148 mdtk.Peringatan saat menggunakan input nonkonstan
Dengan teknik eliminasi unit vertikal dan horizontal, semuanya bekerja dengan ideal selama Anda melewatkan konstanta sebagai input. Namun, Anda perlu menyadari peringatan yang dapat mengakibatkan penalti kinerja saat Anda memberikan input yang tidak konstan. Teknik eliminasi unit vertikal memiliki lebih sedikit masalah, dan masalah yang ada lebih mudah untuk diatasi, jadi mari kita mulai dengannya.
Ingat bahwa dalam artikel ini kita menggunakan fungsi dbo.GetNumsAlanCharlieItzikBatch sebagai contoh kita yang mengandalkan konsep eliminasi satuan vertikal. Mari kita jalankan serangkaian pengujian dengan input nonkonstan, seperti variabel.
Sebagai pengujian pertama kami, kami akan mengembalikan rn dan meminta data yang dipesan oleh rn:
DECLARE @mylow AS BIGINT = 1, @myhigh AS BIGINT = 100000000; DECLARE @n AS BIGINT; SELECT @n = rn FROM dbo.GetNumsAlanCharlieItzikBatch(@mylow, @myhigh) ORDER BY rn OPTION(MAXDOP 1);
Ingat bahwa rn mewakili ekspresi ROW_NUMBER yang tidak dimanipulasi, jadi fakta bahwa kita menggunakan input nonkonstan tidak memiliki signifikansi khusus dalam kasus ini. Tidak perlu penyortiran eksplisit dalam rencana.
Saya mendapatkan statistik waktu berikut untuk eksekusi ini:
Waktu CPU =7390 mdtk, waktu berlalu =7386 mdtk.Angka-angka ini mewakili kasus yang ideal.
Pada tes berikutnya, urutkan baris hasil dengan n:
DECLARE @mylow AS BIGINT = 1, @myhigh AS BIGINT = 100000000; DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieItzikBatch(@mylow, @myhigh) ORDER BY n OPTION(MAXDOP 1);
Rencana eksekusi ini ditunjukkan pada Gambar 6.
Gambar 6:Rencanakan pemesanan dbo.GetNumsAlanCharlieItzikBatch(@mylow, @myhigh) oleh n
Lihat masalahnya? Setelah inlining, @low diganti dengan @mylow—bukan dengan nilai di @mylow, yaitu 1. Akibatnya, pelipatan konstan tidak terjadi, dan oleh karena itu n bukan pelestarian urutan terhadap rownum. Hal ini mengakibatkan penyortiran eksplisit dalam rencana.
Berikut adalah statistik waktu yang saya dapatkan untuk eksekusi ini:
Waktu CPU =25141 mdtk, waktu berlalu =25628 mdtk.Waktu eksekusi hampir tiga kali lipat dibandingkan saat penyortiran eksplisit tidak diperlukan.
Solusi sederhana adalah dengan menggunakan ide asli Alan Burstein untuk selalu memesan berdasarkan rn saat Anda membutuhkan hasil yang dipesan, baik saat mengembalikan rn maupun saat mengembalikan n, seperti:
DECLARE @mylow AS BIGINT = 1, @myhigh AS BIGINT = 100000000; DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieItzikBatch(@mylow, @myhigh) ORDER BY rn OPTION(MAXDOP 1);
Kali ini tidak ada penyortiran eksplisit dalam rencana.
Saya mendapatkan statistik waktu berikut untuk eksekusi ini:
Waktu CPU =9156 mdtk, waktu berlalu =9184 mdtk.Angka tersebut cukup mencerminkan fakta bahwa Anda mengembalikan ekspresi yang dimanipulasi, tetapi tidak menimbulkan penyortiran eksplisit.
Dengan solusi yang didasarkan pada teknik eliminasi unit horizontal, seperti fungsi dbo.GetNumsAlanCharlieJoeItzikBatch kami, situasinya lebih rumit saat menggunakan input nonkonstan.
Mari kita uji dulu fungsinya dengan rentang 10 angka yang sangat kecil:
DECLARE @mylow AS BIGINT = 1, @myhigh AS BIGINT = 10; DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeItzikBatch(@mylow, @myhigh) ORDER BY n OPTION(MAXDOP 1);
Rencana eksekusi ini ditunjukkan pada Gambar 7.
Gambar 7:Rencanakan untuk dbo.GetNumsAlanCharlieJoeItzikBatch(@mylow, @myhigh)
Ada sisi yang sangat mengkhawatirkan dari rencana ini. Perhatikan bahwa operator filter muncul di bawah operator teratas! Dalam setiap panggilan yang diberikan ke fungsi dengan input nonkonstan, tentu saja salah satu cabang di bawah operator Penggabungan akan selalu memiliki kondisi filter palsu. Namun, kedua operator Top meminta jumlah baris yang bukan nol. Jadi operator Top di atas operator dengan kondisi false filter akan meminta baris, dan tidak akan pernah puas karena operator filter akan terus membuang semua baris yang didapatnya dari node anaknya. Pekerjaan di subpohon di bawah operator Filter harus dijalankan sampai selesai. Dalam kasus kami ini berarti bahwa subpohon akan melalui pekerjaan menghasilkan baris 4B, yang akan dibuang oleh operator Filter. Anda bertanya-tanya mengapa operator filter repot-repot meminta baris dari simpul anaknya, tetapi tampaknya itulah cara kerjanya saat ini. Sulit untuk melihat ini dengan rencana statis. Lebih mudah untuk melihat ini secara langsung, misalnya, dengan opsi eksekusi kueri langsung di SentryOne Plan Explorer, seperti yang ditunjukkan pada Gambar 8. Cobalah.
Gambar 8:Statistik Kueri Langsung untuk dbo.GetNumsAlanCharlieJoeItzikBatch(@mylow, @myhigh)
Tes ini membutuhkan waktu 9:15 menit untuk menyelesaikannya di mesin saya, dan ingat bahwa permintaannya adalah mengembalikan rentang 10 angka.
Mari kita pikirkan jika ada cara untuk menghindari pengaktifan subpohon yang tidak relevan secara keseluruhan. Untuk mencapai ini, Anda ingin operator Filter startup muncul di atas operator Top bukannya di bawah mereka. Jika Anda membaca Dasar-dasar ekspresi tabel, Bagian 4 – Tabel turunan, pertimbangan pengoptimalan, lanjutan, Anda tahu bahwa filter TOP mencegah penggabungan ekspresi tabel. Jadi, yang perlu Anda lakukan adalah menempatkan kueri TOP di tabel turunan, dan menerapkan filter di kueri luar terhadap tabel turunan.
Inilah fungsi modifikasi kami yang menerapkan trik ini:
CREATE OR ALTER FUNCTION dbo.GetNumsAlanCharlieJoeItzikBatch(@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 * FROM ( SELECT TOP(@high) rownum AS n, @high + 1 - rownum AS op FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0 ORDER BY rownum ) AS D1 WHERE @low = 1 UNION ALL SELECT * FROM ( SELECT TOP(@high - @low + 1) @low - 1 + rownum AS n, @high + 1 - rownum AS op FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0 ORDER BY rownum ) AS D2 WHERE @low <> 1; GO
Seperti yang diharapkan, eksekusi dengan konstanta tetap berperilaku dan melakukan hal yang sama seperti tanpa trik.
Adapun input nonconstant, sekarang dengan rentang kecil sangat cepat. Berikut tes dengan rentang 10 angka:
DECLARE @mylow AS BIGINT = 1, @myhigh AS BIGINT = 10; DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeItzikBatch(@mylow, @myhigh) ORDER BY n OPTION(MAXDOP 1);
Rencana eksekusi ini ditunjukkan pada Gambar 9.
Gambar 9:Rencana untuk meningkatkan dbo.GetNumsAlanCharlieJoeItzikBatch(@mylow, @myhigh)
Amati bahwa efek yang diinginkan dari menempatkan operator Filter di atas operator Top tercapai. Namun, kolom pemesanan n diperlakukan sebagai hasil manipulasi, dan oleh karena itu tidak dianggap sebagai kolom yang mempertahankan urutan sehubungan dengan jumlah baris. Akibatnya, ada penyortiran eksplisit dalam rencana.
Uji fungsi dengan rentang besar angka 100 juta:
DECLARE @mylow AS BIGINT = 1, @myhigh AS BIGINT = 100000000; DECLARE @n AS BIGINT; SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeItzikBatch(@mylow, @myhigh) ORDER BY n OPTION(MAXDOP 1);
Saya mendapatkan statistik waktu berikut:
Waktu CPU =29907 mdtk, waktu berlalu =29909 mdtk.Apa yang mengecewakan; itu hampir sempurna!
Ringkasan kinerja dan wawasan
Gambar 10 memiliki ringkasan statistik waktu untuk solusi yang berbeda.
Gambar 10:Ringkasan kinerja waktu dari solusi
Jadi apa yang telah kita pelajari dari semua ini? Saya kira untuk tidak melakukannya lagi! Cuma bercanda. Kami belajar bahwa lebih aman untuk menggunakan konsep eliminasi vertikal seperti di dbo.GetNumsAlanCharlieItzikBatch, yang memperlihatkan hasil ROW_NUMBER yang tidak dimanipulasi (rn) dan yang dimanipulasi (n). Pastikan saja ketika ingin mengembalikan hasil yang dipesan, selalu pesan dengan rn, apakah mengembalikan rn atau n.
Jika Anda benar-benar yakin bahwa solusi Anda akan selalu digunakan dengan konstanta sebagai input, Anda dapat menggunakan konsep eliminasi satuan horizontal. Ini akan menghasilkan solusi yang lebih intuitif bagi pengguna, karena mereka akan berinteraksi dengan satu kolom untuk nilai naik. Saya masih menyarankan menggunakan trik dengan tabel turunan untuk mencegah unnesting dan menempatkan operator Filter di atas operator Top jika fungsi tersebut pernah digunakan dengan input nonkonstan, hanya untuk berjaga-jaga.
Kami masih belum selesai. Bulan depan saya akan terus mencari solusi tambahan.