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

Bug T-SQL, perangkap, dan praktik terbaik – fungsi jendela

Artikel ini adalah angsuran keempat dalam seri tentang bug T-SQL, perangkap dan praktik terbaik. Sebelumnya saya membahas determinisme, subquery, dan join. Fokus artikel bulan ini adalah bug, jebakan, dan praktik terbaik yang terkait dengan fungsi jendela. Terima kasih Erland Sommarskog, Aaron Bertrand, Alejandro Mesa, Umachandar Jayachandran (UC), Fabiano Neves Amorim, Milos Radivojevic, Simon Sabin, Adam Machanic, Thomas Grohser, Chan Ming Man, dan Paul White karena telah menawarkan ide Anda!

Dalam contoh saya, saya akan menggunakan database sampel yang disebut TSQLV5. Anda dapat menemukan skrip yang membuat dan mengisi database ini di sini, dan diagram ER-nya di sini.

Ada dua perangkap umum yang melibatkan fungsi jendela, yang keduanya merupakan hasil dari default implisit berlawanan yang dipaksakan oleh standar SQL. Satu perangkap berkaitan dengan perhitungan total berjalan di mana Anda mendapatkan bingkai jendela dengan opsi RANGE implisit. Jebakan lain agak terkait, tetapi memiliki konsekuensi yang lebih parah, yang melibatkan definisi bingkai implisit untuk fungsi FIRST_VALUE dan LAST_VALUE.

Bingkai jendela dengan opsi RANGE implisit

Jebakan pertama kami melibatkan perhitungan total berjalan menggunakan fungsi jendela agregat, di mana Anda secara eksplisit menentukan klausa urutan jendela, tetapi Anda tidak secara eksplisit menentukan unit bingkai jendela (ROWS atau RANGE) dan luas bingkai jendela terkait, misalnya, ROWS SEBELUMNYA TIDAK TERBATAS. Default implisit berlawanan dengan intuisi dan konsekuensinya bisa mengejutkan dan menyakitkan.

Untuk mendemonstrasikan perangkap ini, saya akan menggunakan tabel yang disebut Transaksi yang menampung dua juta transaksi rekening bank dengan kredit (nilai positif) dan debit (nilai negatif). Jalankan kode berikut untuk membuat tabel Transaksi dan mengisinya dengan contoh data:

 SET NOCOUNT AKTIF; GUNAKAN TSQLV5; -- http://tsql.solidq.com/SampleDatabases/TSQLV5.zip DROP TABLE JIKA ADA dbo.Transactions; CREATE TABLE dbo.Transactions ( actid INT NOT NULL, tranid INT NOT NULL, val MONEY NOT NULL, CONSTRAINT PK_Transactions PRIMARY KEY(actid, tranid) -- membuat indeks POC ); MENYATAKAN @num_partitions SEBAGAI INT =100, @rows_per_partition SEBAGAI INT =20000; INSERT INTO dbo.Transactions WITH (TABLOCK) (actid, tranid, val) SELECT NP.n, RPP.n, (ABS(CHECKSUM(NEWID())%2)*2-1) * (1 + ABS(CHECKSUM( NEWID())%5)) DARI dbo.GetNums(1, @num_partitions) SEBAGAI NP CROSS GABUNG dbo.GetNums(1, @rows_per_partition) SEBAGAI RPP;

Jebakan kami memiliki sisi logis dengan potensi bug logis serta sisi kinerja dengan penalti kinerja. Penalti kinerja hanya relevan jika fungsi jendela dioptimalkan dengan operator pemrosesan mode baris. SQL Server 2016 memperkenalkan operator Agregat Jendela mode batch, yang menghilangkan bagian penalti kinerja dari jebakan, tetapi sebelum SQL Server 2019 operator ini digunakan hanya jika Anda memiliki indeks penyimpanan kolom yang ada pada data. SQL Server 2019 memperkenalkan mode batch pada dukungan rowstore, sehingga Anda bisa mendapatkan pemrosesan mode batch meskipun tidak ada indeks columnstore yang ada pada data. Untuk mendemonstrasikan penalti kinerja dengan pemrosesan mode baris, jika Anda menjalankan contoh kode di artikel ini di SQL Server 2019 atau yang lebih baru, atau di Azure SQL Database, gunakan kode berikut untuk mengatur tingkat kompatibilitas database ke 140 sehingga belum mengaktifkan mode batch di toko baris:

 ALTER DATABASE TSQLV5 SET COMPATIBILITY_LEVEL =140;

Gunakan kode berikut untuk mengaktifkan statistik waktu dan I/O dalam sesi:

 SET WAKTU STATISTIK, IO AKTIF;

Untuk menghindari menunggu dua juta baris untuk dicetak di SSMS, saya sarankan menjalankan contoh kode di bagian ini dengan opsi Buang hasil setelah eksekusi diaktifkan (buka Opsi Kueri, Hasil, Kotak, dan centang Buang hasil setelah eksekusi).

Sebelum kita masuk ke perangkap, pertimbangkan kueri berikut (sebut saja Kueri 1) yang menghitung saldo rekening bank setelah setiap transaksi dengan menerapkan total berjalan menggunakan fungsi agregat jendela dengan spesifikasi bingkai eksplisit:

 SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ROWS UNBOUNDED PRECEDING ) SEBAGAI saldo DARI dbo.Transactions;

Rencana untuk kueri ini, menggunakan pemrosesan mode baris, ditunjukkan pada Gambar 1.

Gambar 1:Rencana untuk Kueri 1, pemrosesan mode baris

Paket menarik data yang dipesan sebelumnya dari indeks berkerumun tabel. Kemudian menggunakan operator Proyek Segmen dan Urutan untuk menghitung nomor baris untuk mengetahui baris mana yang termasuk dalam bingkai baris saat ini. Kemudian menggunakan operator Segment, Window Spool, dan Stream Aggregate untuk menghitung fungsi agregat jendela. Operator Window Spool digunakan untuk menggulung baris frame yang kemudian perlu diagregasi. Tanpa pengoptimalan khusus, rencana harus menulis per baris semua baris bingkai yang berlaku ke spool, dan kemudian menggabungkannya. Ini akan menghasilkan kuadrat, atau N, kompleksitas. Kabar baiknya adalah ketika frame dimulai dengan UNBOUNDED PRECEDING, SQL Server mengidentifikasi kasus tersebut sebagai jalur cepat kasus, di mana ia hanya mengambil total berjalan baris sebelumnya dan menambahkan nilai baris saat ini untuk menghitung total berjalan baris saat ini, menghasilkan penskalaan linier. Dalam mode jalur cepat ini, paket hanya menulis dua baris ke spool per baris input—satu dengan agregat, dan satu lagi dengan detail.

Window Spool dapat diimplementasikan secara fisik dengan salah satu dari dua cara. Baik sebagai spool dalam memori yang cepat yang dirancang khusus untuk fungsi jendela, atau sebagai spool pada disk yang lambat, yang pada dasarnya adalah tabel sementara di tempdb. Jika jumlah baris yang perlu ditulis ke spool per baris yang mendasarinya bisa melebihi 10.000, atau jika SQL Server tidak dapat memprediksi jumlahnya, itu akan menggunakan spool pada disk yang lebih lambat. Dalam rencana kueri kami, kami memiliki tepat dua baris yang ditulis ke spool per baris yang mendasarinya, jadi SQL Server menggunakan spool dalam memori. Sayangnya, tidak ada cara untuk mengetahui dari paket jenis gulungan apa yang Anda dapatkan. Ada dua cara untuk mengetahui hal ini. Salah satunya adalah dengan menggunakan acara tambahan yang disebut window_spool_ondisk_warning. Opsi lainnya adalah mengaktifkan STATISTICS IO, dan memeriksa jumlah pembacaan logis yang dilaporkan untuk tabel yang disebut Worktable. Angka yang lebih besar dari nol berarti Anda mendapatkan spool pada disk. Nol berarti Anda mendapatkan gulungan dalam memori. Berikut statistik I/O untuk kueri kami:

Logika Tabel 'Meja Kerja' berbunyi:0. Logika Tabel 'Transaksi' berbunyi:6208.

Seperti yang Anda lihat, kami menggunakan spool dalam memori. Itu umumnya terjadi saat Anda menggunakan unit bingkai jendela ROWS dengan UNBOUNDED PRECEDING sebagai pembatas pertama.

Berikut adalah statistik waktu untuk kueri kami:

Waktu CPU:4297 md, waktu berlalu:4441 md.

Butuh sekitar 4,5 detik untuk menyelesaikan kueri ini di mesin saya dengan hasil yang dibuang.

Sekarang untuk menangkap. Jika Anda menggunakan opsi RANGE alih-alih ROWS, dengan pembatas yang sama, mungkin ada sedikit perbedaan makna, tetapi perbedaan besar dalam kinerja dalam mode baris. Perbedaan makna hanya relevan jika Anda tidak memiliki pemesanan total, yaitu jika Anda memesan dengan sesuatu yang tidak unik. Opsi ROWS UNBOUNDED PRECEDING berhenti dengan baris saat ini, jadi dalam kasus seri, perhitungannya nondeterministik. Sebaliknya, opsi RANGE UNBOUNDED PRECEDING terlihat di depan baris saat ini, dan menyertakan ikatan jika ada. Ini menggunakan logika yang mirip dengan opsi TOP WITH TIES. Ketika Anda memiliki total pemesanan, yaitu, Anda memesan dengan sesuatu yang unik, tidak ada ikatan untuk disertakan, dan oleh karena itu ROWS dan RANGE menjadi setara secara logis dalam kasus seperti itu. Masalahnya adalah ketika Anda menggunakan RANGE, SQL Server selalu menggunakan spool pada disk di bawah pemrosesan mode baris karena saat memproses baris tertentu, ia tidak dapat memprediksi berapa banyak lagi baris yang akan disertakan. Ini dapat memiliki hukuman kinerja yang parah.

Perhatikan query berikut (sebut saja Query 2), yang sama dengan Query 1, hanya menggunakan opsi RANGE bukan ROWS:

 SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid RANGE UNBOUNDED PRECEDING ) SEBAGAI saldo DARI dbo.Transactions;

Rencana untuk kueri ini ditunjukkan pada Gambar 2.

Gambar 2:Rencana untuk Kueri 2, pemrosesan mode baris

Kueri 2 secara logis setara dengan Kueri 1 karena kami memiliki total pesanan; namun, karena menggunakan RANGE, ia dioptimalkan dengan spool di disk. Perhatikan bahwa dalam paket untuk Kueri 2, Kumparan Jendela terlihat sama dengan paket untuk Kueri 1, dan perkiraan biayanya sama.

Berikut adalah waktu dan statistik I/O untuk eksekusi Query 2:

Waktu CPU:19515 md, waktu berlalu:20201 md.
Logika Tabel 'Meja Kerja' berbunyi:12044701. Logika Tabel 'Transaksi' berbunyi:6208.

Perhatikan banyaknya pembacaan logis terhadap Worktable, yang menunjukkan bahwa Anda mendapatkan spool pada disk. Waktu proses lebih dari empat kali lebih lama daripada untuk Kueri 1.

Jika Anda berpikir bahwa jika itu masalahnya, Anda hanya akan menghindari menggunakan opsi RANGE, kecuali jika Anda benar-benar perlu menyertakan dasi, itu pemikiran yang bagus. Masalahnya adalah jika Anda menggunakan fungsi jendela yang mendukung bingkai (agregat, FIRST_VALUE, LAST_VALUE) dengan klausa urutan jendela eksplisit, tetapi tidak menyebutkan unit bingkai jendela dan luasnya yang terkait, Anda mendapatkan RANGE UNBOUNDED PRECEDING secara default . Default ini ditentukan oleh standar SQL, dan standar memilihnya karena umumnya lebih memilih opsi yang lebih deterministik sebagai default. Kueri berikut (sebut saja Kueri 3) adalah contoh yang termasuk dalam jebakan ini:

 SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ) AS balance FROM dbo.Transactions;

Seringkali orang menulis seperti ini dengan asumsi mereka mendapatkan PRECEDING BARIS UNBOUNDED secara default, tidak menyadari bahwa mereka sebenarnya mendapatkan RANGE UNBOUNDED PRECEDING. Soalnya karena fungsi ini menggunakan total order, Anda mendapatkan hasil yang sama seperti dengan ROWS, jadi Anda tidak bisa mengatakan bahwa ada masalah dari hasilnya. Tetapi angka kinerja yang akan Anda dapatkan seperti untuk Kueri 2. Saya melihat orang-orang selalu jatuh ke dalam jebakan ini.

Praktik terbaik untuk menghindari masalah ini adalah dalam kasus di mana Anda menggunakan fungsi jendela dengan bingkai, secara eksplisit tentang unit bingkai jendela dan luasnya, dan umumnya lebih memilih BARIS. Cadangan penggunaan RANGE hanya untuk kasus di mana pemesanan tidak unik dan Anda perlu menyertakan ikatan.

Pertimbangkan kueri berikut yang mengilustrasikan kasus ketika ada perbedaan konseptual antara ROWS dan RANGE:

 SELECT orderdate, orderid, val, SUM(val) OVER( ORDER BY orderdate ROWS UNBOUNDED PRECEDING ) AS sumrows, SUM(val) OVER( ORDER BY orderdate RANGE UNBOUNDED PRECEDING ) AS sumrange FROM Sales.OrderValues ​​ORDER BY orderdate; 

Kueri ini menghasilkan keluaran berikut:

 orderdate orderid val sumrows sumrange ---------- -------- -------- -------- -------- - 07-04-2017 10248 440.00 440.00 440.00 07-07-05 10249 1863.40 2303.40 2303.40 2017-07-08 10250 1552.60 3856.00 4510.06 2017-07-08 10251 654.06 4510.06 4510.06 2017-07-09 10252 3597.90 8107.96 8107.96 ... 

Amati perbedaan hasil untuk baris di mana tanggal pemesanan yang sama muncul lebih dari satu kali, seperti yang terjadi pada 8 Juli 2017. Perhatikan bagaimana opsi ROWS tidak menyertakan ikatan dan karenanya bersifat nondeterministik, dan bagaimana opsi RANGE melakukannya menyertakan ikatan, dan karenanya selalu deterministik.

Ini dipertanyakan meskipun jika dalam praktiknya Anda memiliki kasus di mana Anda memesan dengan sesuatu yang tidak unik, dan Anda benar-benar membutuhkan penyertaan ikatan untuk membuat perhitungan deterministik. Apa yang mungkin jauh lebih umum dalam praktiknya adalah melakukan salah satu dari dua hal. Salah satunya adalah untuk memutuskan hubungan dengan menambahkan sesuatu ke jendela pemesanan untuk membuatnya unik dan cara ini menghasilkan perhitungan deterministik, seperti:

 SELECT orderdate, orderid, val, SUM(val) OVER( ORDER BY orderdate, orderid ROWS UNBOUNDED PRECEDING ) AS runningsum FROM Sales.OrderValues ​​ORDER BY orderdate;

Kueri ini menghasilkan keluaran berikut:

 orderdate orderid val runningsum ---------- -------- --------- ----------- 2017-07-04 10248 440.00 440.00 2017-07-05 10249 1863.40 2303.40 2017-07-08 10250 1552.60 3856.00 2017-07-08 10251 654.06 4510.06 2017-07-09 10252 3597.90 8107.96 ...

Pilihan lain adalah menerapkan pengelompokan awal, dalam kasus kami, berdasarkan tanggal pemesanan, seperti:

 SELECT orderdate, SUM(val) AS daytotal, SUM(SUM(val)) OVER( ORDER BY orderdate ROWS UNBOUNDED PRECEDING ) AS runningsum FROM Sales.OrderValues ​​GROUP BY orderdate ORDER BY orderdate;

Kueri ini menghasilkan keluaran berikut di mana setiap tanggal pemesanan hanya muncul sekali:

 orderdate daytotal runningsum ---------- --------- ----------- 2017-07-04 440.00 440.00 2017-07-05 1863.40 2303.40 08-07-2017 2206.66 4510.06 2017-07-09 3597.90 8107.96 ...

Bagaimanapun, pastikan untuk mengingat praktik terbaik di sini!

Kabar baiknya adalah jika Anda menjalankan SQL Server 2016 atau yang lebih baru dan memiliki indeks columnstore yang ada pada data (bahkan jika itu adalah indeks columnstore yang difilter palsu), atau jika Anda menjalankan SQL Server 2019 atau yang lebih baru, atau di Azure SQL Database, terlepas dari keberadaan indeks columnstore, ketiga kueri yang disebutkan di atas dioptimalkan dengan operator Agregat Jendela mode batch. Dengan operator ini, banyak inefisiensi pemrosesan mode baris dihilangkan. Operator ini tidak menggunakan spool sama sekali, jadi tidak ada masalah in-memory versus on-disk spool. Ini menggunakan pemrosesan yang lebih canggih di mana ia dapat menerapkan beberapa lintasan paralel melalui jendela baris dalam memori untuk ROWS dan RANGE.

Untuk mendemonstrasikan penggunaan pengoptimalan mode batch, pastikan tingkat kompatibilitas database Anda disetel ke 150 atau lebih tinggi:

 ALTER DATABASE TSQLV5 SET COMPATIBILITY_LEVEL =150;

Jalankan Kueri 1 lagi:

 SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ROWS UNBOUNDED PRECEDING ) SEBAGAI saldo DARI dbo.Transactions;

Rencana untuk kueri ini ditunjukkan pada Gambar 3.

Gambar 3:Rencana untuk Kueri 1, pemrosesan mode batch

Berikut adalah statistik kinerja yang saya dapatkan untuk kueri ini:

Waktu CPU:937 md, waktu berlalu:983 md.
Logika Tabel 'Transaksi' berbunyi:6208.

Waktu berjalan turun menjadi 1 detik!

Jalankan Kueri 2 dengan opsi RANGE eksplisit lagi:

 SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid RANGE UNBOUNDED PRECEDING ) SEBAGAI saldo DARI dbo.Transactions;

Rencana untuk kueri ini ditunjukkan pada Gambar 4.

Gambar 2:Rencana untuk Kueri 2, pemrosesan mode batch

Berikut adalah statistik kinerja yang saya dapatkan untuk kueri ini:

Waktu CPU:969 md, waktu berlalu:1048 md.
Logika Tabel 'Transaksi' berbunyi:6208.

Performanya sama dengan Query 1.

Jalankan Kueri 3 lagi, dengan opsi RANGE implisit:

 SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ) AS balance FROM dbo.Transactions;

Jumlah rencana dan kinerjanya tentu saja sama dengan Kueri 2.

Setelah selesai, jalankan kode berikut untuk mematikan statistik performa:

 SET WAKTU STATISTIK, IO OFF;

Juga, jangan lupa untuk mematikan opsi Buang hasil setelah eksekusi di SSMS.

Bingkai implisit dengan FIRST_VALUE dan LAST_VALUE

Fungsi FIRST_VALUE dan LAST_VALUE adalah fungsi jendela offset yang masing-masing mengembalikan ekspresi dari baris pertama atau terakhir dalam bingkai jendela. Bagian yang sulit tentang mereka adalah sering kali ketika orang menggunakannya untuk pertama kalinya, mereka tidak menyadari bahwa mereka mendukung sebuah bingkai, melainkan berpikir bahwa mereka berlaku untuk seluruh partisi.

Pertimbangkan upaya berikut untuk mengembalikan info pesanan, ditambah nilai pesanan pertama dan terakhir pelanggan:

 PILIH custid, orderdate, orderid, val, FIRST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid ) AS firstval, LAST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid ) SEBAGAI lastval DARI Penjualan. OrderValues ​​ORDER BY custid, orderdate, orderid;

Jika Anda salah percaya bahwa fungsi ini beroperasi di seluruh partisi jendela, yang merupakan kepercayaan banyak orang yang menggunakan fungsi ini untuk pertama kalinya, Anda tentu mengharapkan FIRST_VALUE untuk mengembalikan nilai pesanan pesanan pertama pelanggan, dan LAST_VALUE untuk mengembalikan nilai pesanan pesanan terakhir pelanggan. Namun, dalam praktiknya, fungsi-fungsi ini mendukung sebuah bingkai. Sebagai pengingat, dengan fungsi yang mendukung bingkai, ketika Anda menentukan klausa urutan jendela tetapi bukan unit bingkai jendela dan luasnya yang terkait, Anda mendapatkan RANGE UNBOUNDED PRECEDING secara default. Dengan fungsi FIRST_VALUE, Anda akan mendapatkan hasil yang diharapkan, tetapi jika kueri Anda dioptimalkan dengan operator mode baris, Anda akan membayar penalti menggunakan spool di disk. Dengan fungsi LAST_VALUE itu bahkan lebih buruk. Tidak hanya itu Anda akan membayar penalti spool pada disk, tetapi alih-alih mendapatkan nilai dari baris terakhir di partisi, Anda akan mendapatkan nilai dari baris saat ini!

Berikut adalah output dari query di atas:

 custid orderdate orderid val firstval lastval ------- ---------- -------- ---------- ------ ---- ---------- 1 2018-08-25 10643 814.50 814.50 814.50 1 2018-10-03 10692 878.00 814.50 878.00 1 2018-10-13 10702 330.00 814.50 330.00 1 2019-01-15 10835 845,80 814,50 845,80 1 2019-03-16 10952 471,20 814,50 471,20 1 2019-04-09 11011 933,50 814,50 933,50 2 2017-09-18 10308 88,80 88,80 88,80 2 2018-08-08 10625 479,75 88,80-11-28,75 2 10759 320.00 88.80 320.00 2 2019-03-04 10926 514,40 88,80 514,40 3 2017-11-27 10365 403,20 403,20 403,20 3 2018-04-15 10507 749,06 403,20 749,06 3 2018-05-13 10535 1940,85 403,20 1940,85 3 2018-06-19 10573 2082.00 403.20 2082.00 3 2018-09-22 10677 813,37 403,20 813,37 3 25-09-2018 10682 375.50 403.20 375.50 3 28-01-2019 10856 660.00 403.20 660.00 ...

Seringkali ketika orang melihat output seperti itu untuk pertama kalinya, mereka berpikir bahwa SQL Server memiliki bug. Tapi tentu saja tidak; itu hanya default standar SQL. Ada bug dalam kueri. Menyadari bahwa ada bingkai yang terlibat, Anda ingin secara eksplisit tentang spesifikasi bingkai, dan menggunakan bingkai minimum yang menangkap baris yang Anda cari. Juga, pastikan Anda menggunakan unit ROWS. Jadi, untuk mendapatkan baris pertama dalam partisi, gunakan fungsi FIRST_VALUE dengan frame ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Untuk mendapatkan baris terakhir dalam partisi, gunakan fungsi LAST_VALUE dengan frame BARIS ANTARA CURRENT ROW DAN UNBOUNDED FOLLOWING.

Inilah kueri kami yang telah direvisi dengan bug yang diperbaiki:

 PILIH custid, orderdate, orderid, val, FIRST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid BARIS ANTARA UNBOUNDED PRECEDING DAN CURRENT ROW ) SEBAGAI firstval, LAST_VALUE(val) OVER( PARTITION BY custid order orderid BARIS ANTARA BARIS LANCAR DAN UNBOUNDED BERIKUT ) SEBAGAI lastval DARI Penjualan.OrderValues ​​ORDER BY custid, orderdate, orderid;

Kali ini Anda mendapatkan hasil yang benar:

 custid orderdate orderid val firstval lastval ------- ---------- -------- ---------- ------ ---- ---------- 1 2018-08-25 10643 814.50 814.50 933.50 1 2018-10-03 10692 878.00 814.50 933.50 1 2018-10-13 10702 330.00 814.50 933.50 1 2019-01-15 10835 845,80 814,50 933,50 1 2019-03-16 10952 471,20 814,50 933,50 1 2019-04-09 11011 933,50 814,50 933,50 2 2017-09-18 10308 88,80 88,80 514,40 2 2018-08-08 10625 479,75 88,80 11-28,40 2 10759 320.00 88.80 514.40 2 03-04-2019 10926 514.40 88.80 514.40 3 27-11-2017 10365 403.20 403.20 660.00 3 2018-04-15 10507 749.06 403.20 660.00 3 2018-05-13 10535 1940.85 403.20 660.00 3 2018-06-19 10573 2082.00 403.20 660.00 3 22-09-2018 10677 813.37 403.20 660.00 3 25-09-2018 10682 375.50 403.20 660.00 3 2019-01-28 10856 660.00 403.20 660.00 ...

Orang bertanya-tanya apa motivasi standar untuk mendukung bingkai dengan fungsi-fungsi ini. Jika Anda memikirkannya, Anda sebagian besar akan menggunakannya untuk mendapatkan sesuatu dari baris pertama atau terakhir di partisi. Jika Anda memerlukan nilai dari, katakanlah, dua baris sebelum arus, daripada menggunakan FIRST_VALUE dengan bingkai yang dimulai dengan 2 PRECEDING, bukankah lebih mudah menggunakan LAG dengan offset eksplisit 2, seperti:

 PILIH custid, orderdate, orderid, val, LAG(val, 2) OVER( PARTITION BY custid ORDER BY orderdate, orderid ) AS prevtwoval FROM Sales.OrderValues ​​ORDER BY custid, orderdate, orderid;

Kueri ini menghasilkan keluaran berikut:

 custid orderdate orderid val prevtwoval ------- ---------- -------- ---------- ------- ---- 1 2018-08-25 10643 814.50 NULL 1 2018-10-03 10692 878.00 NULL 1 2018-10-13 10702 330.00 814.50 1 2019-01-15 10835 845,80 878.00 1 2019-03-16 10952 471.20 330.00 1 2019-04-09 11011 933.50 845.80 2 2017-09-18 10308 88,80 NULL 2 2018-08-08 10625 479,75 NULL 2 2018-11-28 10759 320.00 88,80 2 2019-03-04 10926 514,40 479,75 3 11-11-27 10365 403.20 NULL 3 2018-04-15 10507 749,06 NULL 3 2018-05-13 10535 1940,85 403,20 3 2018-06-19 10573 2082.00 749.06 3 2018-09-22 10677 813.37 1940.85 3 2018-09-25 10682 375.50 2082.00 3 2019 -01-28 10856 660.00 813.37 ...

Rupanya, ada perbedaan semantik antara penggunaan fungsi LAG di atas dan FIRST_VALUE dengan bingkai yang dimulai dengan 2 PRECEDING. Dengan yang pertama, jika baris tidak ada di offset yang diinginkan, Anda mendapatkan NULL secara default. Dengan yang terakhir, Anda masih mendapatkan nilai dari baris pertama yang ada, yaitu nilai dari baris pertama di partisi. Pertimbangkan kueri berikut:

 PILIH custid, orderdate, orderid, val, FIRST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid BARIS ANTARA 2 SEBELUMNYA DAN BARIS LANCAR ) SEBAGAI prevtwoval DARI Sales.OrderValues ​​ORDER BY custid, orderdate; 

Kueri ini menghasilkan keluaran berikut:

 custid orderdate orderid val prevtwoval ------- ---------- -------- ---------- ------- ---- 1 2018-08-25 10643 814.50 814.50 1 2018-10-03 10692 878.00 814.50 1 2018-10-13 10702 330.00 814.50 1 2019-01-15 10835 845.80 878.00 1 2019-03-16 10952 471.20 330.00 1 2019-04-09 11011 933.50 845.80 2 2017-09-18 10308 88,80 88,80 2 2018-08 10625 479,75 88,80 2 2018-11-28 10759 320,00 88,80 2 2019-03-04 10926 514,40 479,75 3 11-11-27 10365 403,20 403,20 3 2018-04-15 10507 749,06 403,20 3 2018-05-13 10535 1940,85 403,20 3 2018-06-19 10573 2082.00 749.06 3 2018-09-22 10677 813.37 1940.85 3 2018-09-25 10682 375.50 2082.00 3 2019 -01-28 10856 660.00 813.37 ...

Perhatikan bahwa kali ini tidak ada NULL dalam output. Jadi ada beberapa nilai dalam mendukung bingkai dengan FIRST_VALUE dan LAST_VALUE. Pastikan Anda mengingat praktik terbaik untuk selalu eksplisit tentang spesifikasi bingkai dengan fungsi-fungsi ini, dan menggunakan opsi ROWS dengan bingkai minimal yang berisi baris yang Anda cari.

Kesimpulan

Artikel ini berfokus pada bug, jebakan, dan praktik terbaik yang terkait dengan fungsi jendela. Ingat bahwa kedua fungsi agregat jendela dan fungsi offset jendela FIRST_VALUE dan LAST_VALUE mendukung bingkai, dan jika Anda menentukan klausa urutan jendela tetapi Anda tidak menentukan unit bingkai jendela dan luasnya yang terkait, Anda mendapatkan RANGE UNBOUNDED PRECEDING oleh bawaan. Ini menimbulkan penalti kinerja saat kueri dioptimalkan dengan operator mode baris. Dengan fungsi LAST_VALUE ini menghasilkan nilai dari baris saat ini alih-alih baris terakhir di partisi. Ingatlah untuk secara eksplisit tentang bingkai dan umumnya lebih memilih opsi BARIS daripada RANGE. Sangat menyenangkan melihat peningkatan kinerja dengan operator Agregat Jendela mode batch. Jika dapat diterapkan, setidaknya jebakan kinerja dihilangkan.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Cara Menghitung Rata-Rata Pergerakan dalam Pergeseran Merah

  2. Analisis Data vs. Ilmu Data:Apa Bedanya?

  3. Kunci Utama Dalam SQL:Semua yang Perlu Anda Ketahui Tentang Operasi Kunci Utama

  4. SQL ORDER BY Klausa untuk Pemula

  5. Menghindari Pemecahan Masalah Performa Lutut