Pengantar
Sejak diperkenalkan di SQL Server 2005, fungsi jendela seperti ROW_NUMBER
dan RANK
telah terbukti sangat berguna dalam memecahkan berbagai masalah umum T-SQL. Dalam upaya untuk menggeneralisasi solusi semacam itu, perancang basis data sering melihat untuk memasukkannya ke dalam tampilan untuk mempromosikan enkapsulasi kode dan penggunaan kembali. Sayangnya, batasan dalam pengoptimal kueri SQL Server sering kali berarti bahwa tampilan yang berisi fungsi jendela tidak berfungsi sebaik yang diharapkan. Postingan ini berfungsi melalui contoh ilustratif masalah, merinci alasannya, dan memberikan sejumlah solusi.
Masalah ini juga dapat terjadi pada tabel turunan, ekspresi tabel umum, dan fungsi sebaris, tetapi saya paling sering melihatnya dengan tampilan karena sengaja ditulis agar lebih umum.
Fungsi jendela
Fungsi jendela dibedakan dengan adanya OVER()
klausa dan datang dalam tiga varietas:
- Fungsi jendela peringkat
ROW_NUMBER
RANK
DENSE_RANK
NTILE
- Fungsi jendela gabungan
MIN
,MAX
,AVG
,SUM
COUNT
,COUNT_BIG
CHECKSUM_AGG
STDEV
,STDEVP
,VAR
,VARP
- Fungsi jendela analitik
LAG
,LEAD
FIRST_VALUE
,LAST_VALUE
PERCENT_RANK
,PERCENTILE_CONT
,PERCENTILE_DISC
,CUME_DIST
Fungsi jendela peringkat dan agregat diperkenalkan di SQL Server 2005, dan diperluas secara signifikan di SQL Server 2012. Fungsi jendela analitik baru untuk SQL Server 2012.
Semua fungsi jendela yang tercantum di atas rentan terhadap batasan pengoptimal yang dirinci dalam artikel ini.
Contoh
Menggunakan database sampel AdventureWorks, tugas yang ada adalah menulis kueri yang mengembalikan semua produk #878 transaksi yang terjadi pada tanggal terbaru yang tersedia. Ada berbagai cara untuk mengekspresikan persyaratan ini dalam T-SQL, tetapi kami akan memilih untuk menulis kueri yang menggunakan fungsi windowing. Langkah pertama adalah menemukan catatan transaksi untuk produk #878 dan mengurutkannya dalam urutan tanggal menurun:
PILIH th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.Quantity, rnk =RANK() OVER ( ORDER BY th.TransactionDate DESC)FROM Production.TransactionHistory AS thWHERE th.ProductID =878ORDER BY rnk;
Hasil kueri seperti yang diharapkan, dengan enam transaksi terjadi pada tanggal terbaru yang tersedia. Rencana eksekusi berisi segitiga peringatan, mengingatkan kita akan indeks yang hilang:
Seperti biasa untuk saran indeks yang hilang, kita perlu mengingat bahwa rekomendasi bukanlah hasil dari analisis kueri yang menyeluruh – ini lebih merupakan indikasi bahwa kita perlu memikirkan sedikit tentang bagaimana kueri ini mengakses data yang dibutuhkannya.
Indeks yang disarankan tentu akan lebih efisien daripada memindai tabel sepenuhnya, karena akan memungkinkan indeks mencari ke produk tertentu yang kita minati. Indeks juga akan mencakup semua kolom yang diperlukan, tetapi tidak akan menghindari pengurutan (oleh
TransactionDate
menurun). Indeks ideal untuk kueri ini akan memungkinkan pencarian diProductID
, kembalikan record yang dipilih secara terbalikTransactionDate
pesan, dan tutupi kolom yang dikembalikan lainnya:BUAT INDEKS NONCLUSTERED ixON Production.TransactionHistory (ProductID, TransactionDate DESC)TERMASUK (ReferenceOrderID, Quantity);Dengan indeks itu, rencana eksekusi jauh lebih efisien. Pemindaian indeks berkerumun telah digantikan oleh pencarian rentang, dan pengurutan eksplisit tidak lagi diperlukan:
Langkah terakhir untuk kueri ini adalah membatasi hasil hanya pada baris yang berperingkat #1. Kami tidak dapat memfilter secara langsung di
WHERE
klausa kueri kami karena fungsi jendela hanya dapat muncul diSELECT
danORDER BY
klausa.Kami dapat mengatasi pembatasan ini menggunakan tabel turunan, ekspresi tabel umum, fungsi, atau tampilan. Pada kesempatan ini, kita akan menggunakan ekspresi tabel umum (alias tampilan sebaris):
DENGAN RankedTransactions AS( SELECT th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.Quantity, rnk =RANK() OVER ( ORDER BY th.TransactionDate DESC) DARI Production.TransactionHistory AS th WHERE th.ProductID =878 )PILIH TransactionID, ReferenceOrderID, TransactionDate, QuantityFROM RankedTransactionsWHERE rnk =1;Rencana eksekusinya sama seperti sebelumnya, dengan Filter tambahan untuk mengembalikan hanya baris yang berperingkat #1:
Kueri mengembalikan enam baris dengan peringkat yang sama yang kami harapkan:
Menggeneralisasi kueri
Ternyata kueri kami sangat berguna, sehingga keputusan diambil untuk menggeneralisasi dan menyimpan definisi dalam tampilan. Agar ini berfungsi untuk produk apa pun, kita perlu melakukan dua hal:mengembalikan
ProductID
dari tampilan, dan mempartisi fungsi peringkat berdasarkan produk:BUAT TAMPILAN dbo.MostRecentTransactionsPerProductWITH SCHEMABINDINGASSELECT sq1.ProductID, sq1.TransactionID, sq1.ReferenceOrderID, sq1.TransactionDate, sq1.QuantityFROM ( SELECT th.ProductID, th.TransactionID.TransactionDate. rnk =RANK() OVER ( PARTITION BY th.ProductID ORDER BY th.TransactionDate DESC) DARI Production.TransactionHistory AS th) AS sq1WHERE sq1.rnk =1;Memilih semua baris dari tampilan menghasilkan rencana eksekusi berikut dan hasil yang benar:
Kami sekarang dapat menemukan transaksi terbaru untuk produk 878 dengan kueri yang lebih sederhana pada tampilan:
PILIH mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =878;Harapan kami adalah bahwa rencana eksekusi untuk kueri baru ini akan sama persis seperti sebelum kami membuat tampilan. Pengoptimal kueri harus dapat mendorong filter yang ditentukan dalam
WHERE
klausa turun ke tampilan, menghasilkan pencarian indeks.Namun, kita perlu berhenti dan berpikir sedikit pada titik ini. Pengoptimal kueri hanya dapat menghasilkan rencana eksekusi yang dijamin menghasilkan hasil yang sama dengan spesifikasi kueri logis – apakah aman untuk mendorong
WHERE
kami klausa ke tampilan?PARTITION BY klausa fungsi jendela dalam tampilan. Alasannya adalah bahwa menghilangkan grup (partisi) lengkap dari fungsi jendela tidak akan memengaruhi peringkat baris yang dikembalikan oleh kueri. Pertanyaannya adalah, apakah pengoptimal kueri SQL Server mengetahui hal ini? Jawabannya tergantung pada versi SQL Server yang kita jalankan. Rencana eksekusi SQL Server 2005
Melihat properti Filter dalam rencana ini menunjukkan bahwa itu menerapkan dua predikat:
ProductID = 878
predikat belum diturunkan ke tampilan, menghasilkan rencana yang memindai indeks kami, memberi peringkat setiap baris dalam tabel sebelum memfilter produk #878 dan baris peringkat #1.Pengoptimal kueri SQL Server 2005 tidak dapat mendorong predikat yang sesuai melewati fungsi jendela dalam lingkup kueri yang lebih rendah (tampilan, ekspresi tabel umum, fungsi sebaris, atau tabel turunan). Batasan ini berlaku untuk semua build SQL Server 2005.
Rencana eksekusi SQL Server 2008+
Ini adalah rencana eksekusi untuk kueri yang sama pada SQL Server 2008 atau yang lebih baru:
ProductID
predikat telah berhasil didorong melewati operator peringkat, menggantikan pemindaian indeks dengan pencarian indeks yang efisien.Pengoptimal kueri 2008 menyertakan aturan penyederhanaan baru
SelOnSeqPrj
(select on sequence project) yang mampu mendorong predikat safe outer-scope melewati fungsi window. Untuk menghasilkan rencana yang kurang efisien untuk kueri ini di SQL Server 2008 atau yang lebih baru, kami harus menonaktifkan fitur pengoptimal kueri ini untuk sementara:PILIH mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =878OPTION (QUERYRULEOFF SelOnSeqPrj);
Sayangnya,
SelOnSeqPrj
aturan penyederhanaan hanya berfungsi ketika predikat melakukan perbandingan dengan konstanta . Oleh karena itu, kueri berikut menghasilkan paket suboptimal di SQL Server 2008 dan yang lebih baru:MENYATAKAN @ProductID INT =878; PILIH mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =@ProductID;
Masalah masih dapat terjadi meskipun predikat menggunakan nilai konstan. SQL Server dapat memutuskan untuk membuat parameter otomatis kueri sepele (yang memiliki rencana terbaik yang jelas). Jika parameterisasi otomatis berhasil, pengoptimal melihat parameter, bukan konstanta, dan
SelOnSeqPrj
aturan tidak diterapkan.Untuk kueri di mana parameterisasi otomatis tidak dicoba (atau ditentukan tidak aman), pengoptimalan mungkin masih gagal, jika opsi basis data untuk
FORCED PARAMETERIZATION
aktif. Kueri pengujian kami (dengan nilai konstan 878) tidak aman untuk parameterisasi otomatis, tetapi pengaturan parameterisasi paksa menimpanya, sehingga menghasilkan rencana yang tidak efisien:ALTER DATABASE AdventureWorksSET PARAMETERIZATION FORCED;GOSELECT mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct SEBAGAI mrt WHEREID mrt.>
Solusi SQL Server 2008+
Agar pengoptimal 'melihat' nilai konstan untuk kueri yang mereferensikan variabel atau parameter lokal, kita dapat menambahkan
OPTION (RECOMPILE)
petunjuk kueri:MENYATAKAN @ProductID INT =878; PILIH mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =@ProductIDOPTION (RECOMPILE);Catatan: Rencana eksekusi pra-eksekusi ('perkiraan') masih menunjukkan pemindaian indeks karena nilai variabel belum benar-benar ditetapkan. Saat kueri dieksekusi , namun, rencana eksekusi menunjukkan rencana pencarian indeks yang diinginkan:
SelOnSeqPrj
aturan tidak ada di SQL Server 2005, jadiOPTION (RECOMPILE)
tidak dapat membantu di sana. Jika Anda bertanya-tanya,OPTION (RECOMPILE)
solusi menghasilkan pencarian meskipun opsi database untuk parameterisasi paksa aktif.Semua versi solusi #1
Dalam beberapa kasus, adalah mungkin untuk mengganti tampilan bermasalah, ekspresi tabel umum, atau tabel turunan dengan fungsi bernilai tabel in-line berparameter:
CREATE FUNCTION dbo.MostRecentTransactionsForProduct( @ProductID integer) MENGEMBALIKAN TABEL DENGAN SKEMABINDING ASRETURN SELECT sq1.ProductID, sq1.TransactionID, sq1.ReferenceOrderID, sq1.TransactionDate, sq1.TransactionID Quantity ReferenceOrderID, th.TransactionDate, th.Quantity, rnk =RANK() OVER ( PARTITION BY th.ProductID ORDER BY th.TransactionDate DESC) DARI Production.TransactionHistory AS th WHERE th.ProductID =@ProductID ) SEBAGAI sq1 WHERE sq1. 1;Fungsi ini secara eksplisit menempatkan
ProductID
predikat dalam lingkup yang sama dengan fungsi jendela, menghindari batasan pengoptimal. Ditulis untuk menggunakan fungsi in-line, contoh query kita menjadi:PILIH mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsForProduct(878) AS mrt;Ini menghasilkan rencana pencarian indeks yang diinginkan pada semua versi SQL Server yang mendukung fungsi jendela. Solusi ini menghasilkan pencarian bahkan di mana predikat mereferensikan parameter atau variabel lokal –
OPTION (RECOMPILE)
tidak diperlukan.PARTITION BY yang sekarang sudah redundan. klausa, dan untuk tidak lagi mengembalikan ProductID
kolom. Saya membiarkan definisinya sama dengan tampilan yang diganti untuk menggambarkan dengan lebih jelas penyebab perbedaan rencana eksekusi.Semua versi solusi #2
Solusi kedua hanya berlaku untuk fungsi jendela peringkat yang difilter untuk mengembalikan baris bernomor atau peringkat #1 (menggunakan
ROW_NUMBER
,RANK
, atauDENSE_RANK
). Namun, ini adalah penggunaan yang sangat umum, jadi perlu disebutkan.Manfaat tambahannya adalah solusi ini dapat menghasilkan rencana yang bahkan lebih efisien daripada rencana pencarian indeks yang terlihat sebelumnya. Sebagai pengingat, rencana terbaik sebelumnya terlihat seperti ini:
Rencana eksekusi itu berada di peringkat 1.918 baris meskipun pada akhirnya hanya mengembalikan 6 . Kami dapat meningkatkan rencana eksekusi ini dengan menggunakan fungsi jendela dalam
ORDER BY
klausa alih-alih peringkat baris dan kemudian memfilter peringkat #1:SELECT TOP (1) WITH TIES th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.QuantityFROM Production.TransactionHistory AS thWHERE th.ProductID =878ORDER BY RANK() OVER ( ORDER BY th.TransactionDate DESC);
Permintaan itu dengan baik menggambarkan penggunaan fungsi jendela di
ORDER BY
klausa, tapi kita bisa melakukan lebih baik lagi, menghilangkan fungsi jendela sepenuhnya:SELECT TOP (1) WITH TIES th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.QuantityFROM Production.TransactionHistory AS thWHERE th.ProductID =878ORDER BY th.TransactionDate DESC;
Paket ini hanya membaca 7 baris dari tabel untuk mengembalikan set hasil 6 baris yang sama. Mengapa 7 baris? Operator Top berjalan di
WITH TIES
modus:
Itu terus meminta satu baris pada satu waktu dari subpohonnya hingga Tanggal Transaksi berubah. Baris ketujuh diperlukan untuk Top untuk memastikan bahwa tidak ada lagi baris dengan nilai terikat yang memenuhi syarat.
Kami dapat memperluas logika kueri di atas untuk menggantikan definisi tampilan yang bermasalah:
ALTER VIEW dbo.MostRecentTransactionsPerProductWITH SCHEMABINDINGASSELECT p.ProductID, Peringkat1.TransactionID, Peringkat1.ReferenceOrderID, Peringkat1.TransactionDate, Ranked1.QuantityFROM -- Daftar ID produk (PILIH ProductID FROM Production.Product) SEBAGAI pCROSS rank BERLAKU #1 hasil untuk setiap ID produk SELECT TOP (1) WITH TIES th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.Quantity FROM Production.TransactionHistory AS th WHERE th.ProductID =p.ProductID ORDER BY th.TransactionDate DESC) AS Peringkat1;Tampilan sekarang menggunakan
CROSS APPLY
untuk menggabungkan hasilORDER BY
kami yang dioptimalkan permintaan untuk setiap produk. Kueri pengujian kami tidak berubah:DECLARE @ProductID integer;SET @ProductID =878; PILIH mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =@ProductID;Paket sebelum dan sesudah eksekusi menunjukkan pencarian indeks tanpa memerlukan
OPTION (RECOMPILE)
petunjuk kueri. Berikut ini adalah rencana pasca-eksekusi ('aktual'):
Jika tampilan telah menggunakan
ROW_NUMBER
bukannyaRANK
, tampilan pengganti hanya akan menghilangkanWITH TIES
klausa padaTOP (1)
. Tampilan baru juga dapat ditulis sebagai fungsi bernilai tabel in-line berparameter tentunya.Orang dapat berargumen bahwa rencana pencarian indeks asli dengan
rnk = 1
predikat juga dapat dioptimalkan untuk hanya menguji 7 baris. Bagaimanapun, pengoptimal harus tahu bahwa peringkat dihasilkan oleh operator Proyek Urutan dalam urutan menaik yang ketat, sehingga eksekusi dapat berakhir segera setelah baris dengan peringkat lebih besar dari satu terlihat. Namun, pengoptimal tidak mengandung logika ini hari ini.Pemikiran Terakhir
Orang sering kecewa dengan kinerja tampilan yang menggabungkan fungsi jendela. Alasannya sering dapat ditelusuri kembali ke batasan pengoptimal yang dijelaskan dalam posting ini (atau mungkin karena perancang tampilan tidak menghargai bahwa predikat yang diterapkan pada tampilan harus muncul di
PARTITION BY
klausa untuk didorong ke bawah dengan aman).Saya ingin menekankan bahwa batasan ini tidak hanya berlaku untuk tampilan, dan juga tidak terbatas pada
ROW_NUMBER
,RANK
, danDENSE_RANK
. Anda harus mengetahui batasan ini saat menggunakan fungsi apa pun denganOVER
klausa dalam tampilan, ekspresi tabel umum, tabel turunan, atau fungsi bernilai tabel sebaris.Pengguna SQL Server 2005 yang mengalami masalah ini dihadapkan dengan pilihan untuk menulis ulang tampilan sebagai fungsi bernilai tabel in-line berparameter, atau menggunakan
APPLY
teknik (jika ada).Pengguna SQL Server 2008 memiliki opsi tambahan untuk menggunakan
OPTION (RECOMPILE)
petunjuk kueri jika masalah dapat diselesaikan dengan mengizinkan pengoptimal melihat konstanta alih-alih referensi variabel atau parameter. Ingatlah untuk memeriksa rencana pasca-eksekusi saat menggunakan petunjuk ini:rencana pra-eksekusi umumnya tidak dapat menunjukkan rencana yang optimal.