Database
 sql >> Teknologi Basis Data >  >> RDS >> Database

Masalah dengan Fungsi dan Tampilan Jendela

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 di ProductID , kembalikan record yang dipilih secara terbalik TransactionDate 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 di SELECT dan ORDER 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, jadi OPTION (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 , atau DENSE_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 hasil ORDER 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 bukannya RANK , tampilan pengganti hanya akan menghilangkan WITH TIES klausa pada TOP (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 , dan DENSE_RANK . Anda harus mengetahui batasan ini saat menggunakan fungsi apa pun dengan OVER 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.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Manfaat dan Keamanan di Amazon Relational Database Service

  2. Menggunakan Pola Alur Kerja untuk Mengelola Status Entitas Apa Pun

  3. Inisialisasi File Instan:Dampak Selama Penyiapan

  4. SQL INSERT untuk Pemula

  5. Menggunakan Langkah Unpivot untuk membuat Tabel Tabular dari Tabel Crosstab