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

Pesanan Bersyarat Oleh

Skenario umum di banyak aplikasi client-server memungkinkan pengguna akhir untuk menentukan urutan hasil. Beberapa orang ingin melihat item dengan harga terendah terlebih dahulu, beberapa ingin melihat item terbaru terlebih dahulu, dan beberapa ingin melihatnya berdasarkan abjad. Ini adalah hal yang rumit untuk dicapai dalam Transact-SQL karena Anda tidak bisa hanya mengatakan:

BUAT PROSEDUR dbo.SortOnSomeTable @SortColumn NVARCHAR(128) =N'key_col', @SortDirection VARCHAR(4) ='ASC'ASBEGIN ... ORDER BY @SortColumn; -- atau ... PESAN OLEH @SortColumn @SortDirection;ENDGO

Ini karena T-SQL tidak mengizinkan variabel di lokasi ini. Jika Anda hanya menggunakan @SortColumn, Anda menerima:

Msg 1008, Level 16, State 1, Line x
Item SELECT yang diidentifikasi oleh ORDER BY nomor 1 berisi variabel sebagai bagian dari ekspresi yang mengidentifikasi posisi kolom. Variabel hanya diperbolehkan saat memesan dengan ekspresi yang mereferensikan nama kolom.

(Dan ketika pesan kesalahan mengatakan, "sebuah ekspresi yang mereferensikan nama kolom," Anda mungkin menganggapnya ambigu, dan saya setuju. Tapi saya dapat meyakinkan Anda bahwa ini tidak berarti variabel adalah ekspresi yang sesuai.)

Jika Anda mencoba menambahkan @SortDirection, pesan kesalahannya sedikit lebih buram:

Pesan 102, Level 15, Status 1, Baris x
Sintaks salah di dekat '@SortDirection'.

Ada beberapa cara untuk mengatasinya, dan insting pertama Anda mungkin menggunakan SQL dinamis, atau memperkenalkan ekspresi CASE. Tetapi seperti kebanyakan hal, ada komplikasi yang dapat memaksa Anda turun ke jalan yang satu atau yang lain. Jadi yang mana yang harus Anda gunakan? Mari kita pelajari cara kerja solusi ini, dan bandingkan dampaknya terhadap performa untuk beberapa pendekatan berbeda.

Contoh Data

Menggunakan tampilan katalog yang mungkin kita semua pahami dengan cukup baik, sys.all_objects, saya membuat tabel berikut berdasarkan gabungan silang, membatasi tabel hingga 100.000 baris (saya ingin data yang memenuhi banyak halaman tetapi tidak membutuhkan waktu yang signifikan untuk kueri dan tes):

BUAT DATABASE OrderBy;GOUSE OrderBy;GO SELECT TOP (100000) key_col =ROW_NUMBER() OVER (ORDER BY s1.[object_id]), -- sebuah BIGINT dengan indeks berkerumun s1.[object_id], -- sebuah INT tanpa nama indeks =s1.name -- sebuah NVARCHAR dengan indeks pendukung COLLATE SQL_Latin1_General_CP1_CI_AS, type_desc =s1.type_desc -- sebuah NVARCHAR(60) tanpa indeks COLLATE SQL_Latin1_General_CP1_CI_AS, s1.modify_date -- sebuah datetime tanpa indexINTO. sys.all_objects SEBAGAI SILANG s1 GABUNG sys.all_objects SEBAGAI s2ORDER OLEH s1.[object_id];

(Trik COLLATE adalah karena banyak tampilan katalog memiliki kolom yang berbeda dengan susunan yang berbeda, dan ini memastikan bahwa kedua kolom akan cocok untuk tujuan demo ini.)

Kemudian saya membuat pasangan indeks cluster/non-clustered khas yang mungkin ada pada tabel seperti itu, sebelum pengoptimalan (saya tidak dapat menggunakan object_id untuk kuncinya, karena gabungan silang membuat duplikat):

BUAT UNIK CLUSTERED INDEX key_col PADA dbo.sys_objects(key_col); BUAT nama INDEKS DI dbo.sys_objects(name);

Kasus Penggunaan

Seperti disebutkan di atas, pengguna mungkin ingin melihat data ini diurutkan dalam berbagai cara, jadi mari kita buat beberapa kasus penggunaan umum yang ingin kami dukung (dan dengan dukungan, maksud saya tunjukkan):

  • Diurutkan berdasarkan key_col ascending ** default jika pengguna tidak peduli
  • Diurutkan berdasarkan object_id (naik/turun)
  • Diurutkan berdasarkan nama (naik/turun)
  • Diurutkan menurut type_desc (naik/turun)
  • Diurutkan berdasarkan tanggal_modifikasi (naik/turun)

Kami akan membiarkan urutan key_col sebagai default karena ini akan menjadi yang paling efisien jika pengguna tidak memiliki preferensi; karena key_col adalah pengganti sewenang-wenang yang seharusnya tidak berarti apa-apa bagi pengguna (dan bahkan mungkin tidak diperlihatkan kepada mereka), tidak ada alasan untuk mengizinkan penyortiran terbalik pada kolom itu.

Pendekatan yang Tidak Berfungsi

Pendekatan paling umum yang saya lihat ketika seseorang pertama kali mulai mengatasi masalah ini adalah memperkenalkan logika kontrol aliran ke kueri. Mereka berharap dapat melakukan ini:

SELECT key_col, [object_id], nama, type_desc, modifikasi_dateFROM dbo.sys_objectsORDER BY JIKA @SortColumn ='key_col' key_colIF @SortColumn ='object_id' [object_id]JIKA @SortColumn ='nama' nama...IF @SortDirection ='ASC' ASCELSE DESC;

Ini jelas tidak berhasil. Selanjutnya saya melihat CASE diperkenalkan secara tidak benar, menggunakan sintaks yang sama:

SELECT key_col, [object_id], name, type_desc, memodifikasi_dateFROM dbo.sys_objectsORDER BY CASE @SortColumn KETIKA 'key_col' MAKA key_col KETIKA 'object_id' MAKA [object_id] SAAT 'nama' MAKA nama ... AKHIR KASUS 'ASC' LALU ASC ELSE DESC END;

Ini lebih dekat, tetapi gagal karena dua alasan. Salah satunya adalah bahwa CASE adalah ekspresi yang mengembalikan tepat satu nilai dari tipe data tertentu; ini menggabungkan tipe data yang tidak kompatibel dan oleh karena itu akan merusak ekspresi CASE. Yang lainnya adalah tidak ada cara untuk menerapkan arah pengurutan secara kondisional dengan cara ini tanpa menggunakan SQL dinamis.

Pendekatan yang Berhasil

Tiga pendekatan utama yang saya lihat adalah sebagai berikut:

Kelompokkan jenis dan arah yang kompatibel menjadi satu

Untuk menggunakan CASE dengan ORDER BY, harus ada ekspresi berbeda untuk setiap kombinasi tipe dan arah yang kompatibel. Dalam hal ini kita harus menggunakan sesuatu seperti ini:

CREATE PROCEDURE dbo.Sort_CaseExpanded @SortColumn NVARCHAR(128) =N'key_col', @SortDirection VARCHAR(4) ='ASC'ASBEGIN SET NOCOUNT ON; PILIH key_col, [object_id], nama, type_desc, modifikasi_tanggal DARI dbo.sys_objects ORDER BY CASE WHEN @SortDirection ='ASC' THEN CASE @SortColumn WHEN 'key_col' THEN key_col WHEN 'object_id' THEN [object_id' END EN_ SortDirection ='DESC' THEN CASE @SortColumn WHEN 'key_col' THEN key_col WHEN 'object_id' THEN [object_id] END END DESC, CASE WHEN @SortDirection ='ASC' THEN CASE @SortColumn WHEN 'name' THEN type_desc END END, CASE WHEN @SortDirection ='DESC' THEN CASE @SortColumn WHEN 'name' THEN name WHEN 'type_desc' THEN type_desc END END DESC, CASE WHEN @SortColumn ='modify_date' AND @'SortDirection END ='modify_date' AND @SortDirection END =' , KASUS KETIKA @SortColumn ='modify_date' AND @SortDirection ='DESC' MAKA modifikasi_date END DESC;END

Anda mungkin berkata, wow, itu kode yang jelek, dan saya setuju dengan Anda. Saya pikir inilah mengapa banyak orang men-cache data mereka di front end dan membiarkan tingkat presentasi menanganinya dengan mengaturnya dalam urutan yang berbeda. :-)

Anda dapat menciutkan logika ini sedikit lebih jauh dengan mengubah semua tipe non-string menjadi string yang akan mengurutkan dengan benar, mis.

CREATE PROCEDURE dbo.Sort_CaseCollapsed @SortColumn NVARCHAR(128) =N'key_col', @SortDirection VARCHAR(4) ='ASC'ASBEGIN SET NOCOUNT ON; PILIH key_col, [object_id], nama, type_desc, modifikasi_tanggal DARI dbo.sys_objects ORDER BY CASE WHEN @SortDirection ='ASC' THEN CASE @SortColumn WHEN 'key_col' THEN KANAN('000000000000' + RTRIM(WHEN_col '), 12 object_id' THEN KANAN(COALESCE(NULLIF(LEFT(RTRIM([object_id]),1),'-'),'0') + REPLICATE('0', 23) + RTRIM([object_id]), 24) WHEN 'name' KEMUDIAN nama KETIKA 'type_desc' MAKA type_desc KETIKA 'modify_date' THEN CONVERT(CHAR(19), memodifikasi_date, 120) END END, CASE WHEN @SortDirection ='DESC' THEN CASE @SortColumn WHEN 'key_col' THEN RIGHT(' 000000000000' + RTRIM(key_col), 12) KETIKA 'object_id' MAKA KANAN(COALESCE(NULLIF(LEFT(RTRIM([object_id]),1),'-'),'0') + REPLICATE('0', 23 ) + RTRIM([object_id]), 24) WHEN 'name' THEN name WHEN 'type_desc' THEN type_desc WHEN 'modify_date' THEN CONVERT(CHAR(19), modified_date, 120) END END DESC;END

Tetap saja, ini adalah kekacauan yang sangat buruk, dan Anda harus mengulangi ekspresi dua kali untuk menangani arah pengurutan yang berbeda. Saya juga menduga bahwa menggunakan OPTION RECOMPILE pada kueri itu akan mencegah Anda tersengat oleh parameter sniffing. Kecuali dalam kasus default, sebagian besar pekerjaan yang dilakukan di sini bukanlah kompilasi.

Terapkan peringkat menggunakan fungsi jendela

Saya menemukan trik rapi ini dari AndriyM, meskipun ini paling berguna dalam kasus di mana semua kolom pemesanan potensial adalah tipe yang kompatibel, jika tidak, ekspresi yang digunakan untuk ROW_NUMBER() sama rumitnya. Bagian yang paling pintar adalah untuk beralih antara urutan menaik dan menurun, kita cukup mengalikan ROW_NUMBER() dengan 1 atau -1. Kita dapat menerapkannya dalam situasi ini sebagai berikut:

BUAT PROSEDUR dbo.Sort_RowNumber @SortColumn NVARCHAR(128) =N'key_col', @SortDirection VARCHAR(4) ='ASC'ASBEGIN SET NOCOUNT ON;;DENGAN x AS ( SELECT key_col, [object_id], nama, type_desc, modifikasi_date, rn =ROW_NUMBER() OVER ( ORDER BY CASE @SortColumn WHEN 'key_col' THEN RIGHT('000000000000' + RTRIM(key_col), 12) WHEN ' object_id' THEN KANAN(COALESCE(NULLIF(LEFT(RTRIM([object_id]),1),'-'),'0') + REPLICATE('0', 23) + RTRIM([object_id]), 24) WHEN 'name' MAKA nama WHEN 'type_desc' THEN type_desc WHEN 'modify_date' THEN CONVERT(CHAR(19),modify_date, 120) END ) * CASE @SortDirection WHEN 'ASC' THEN 1 ELSE -1 END FROM dbo.sys_objects ) SELECT key_col , [object_id], nama, type_desc, modifikasi_tanggal DARI x ORDER OLEH rn;ENDGO

Sekali lagi, OPTION RECOMPILE dapat membantu di sini. Juga, Anda mungkin memperhatikan dalam beberapa kasus ini bahwa ikatan ditangani secara berbeda oleh berbagai rencana – ketika memesan berdasarkan nama, misalnya, Anda biasanya akan melihat key_col muncul dalam urutan menaik dalam setiap set nama duplikat, tetapi Anda juga dapat melihat nilai-nilai bercampur. Untuk memberikan perilaku yang lebih dapat diprediksi jika terjadi seri, Anda selalu dapat menambahkan klausa ORDER BY tambahan. Perhatikan bahwa jika Anda ingin menambahkan key_col ke contoh pertama, Anda harus membuatnya menjadi ekspresi sehingga key_col tidak terdaftar dalam ORDER BY dua kali (misalnya Anda dapat melakukannya menggunakan key_col + 0, misalnya).

SQL Dinamis

Banyak orang memiliki keraguan tentang SQL dinamis – tidak mungkin untuk membaca, ini adalah tempat berkembang biak untuk injeksi SQL, itu mengarah ke rencana cache bloat, itu mengalahkan tujuan menggunakan prosedur tersimpan… Beberapa di antaranya tidak benar, dan beberapa di antaranya mudah untuk dimitigasi. Saya telah menambahkan beberapa validasi di sini yang dapat dengan mudah ditambahkan ke salah satu prosedur di atas:

CREATE PROCEDURE dbo.Sort_DynamicSQL @SortColumn NVARCHAR(128) =N'key_col', @SortDirection VARCHAR(4) ='ASC'ASBEGIN SET NOCOUNT ON; -- tolak semua arah pengurutan yang tidak valid:IF UPPER(@SortDirection) NOT IN ('ASC','DESC') BEGIN RAISERROR('Parameter tidak valid untuk @SortDirection:%s', 11, 1, @SortDirection); KEMBALI -1; END -- tolak nama kolom yang tidak diharapkan:JIKA LOWER(@SortColumn) NOT IN (N'key_col', N'object_id', N'name', N'type_desc', N'modify_date') BEGIN RAISERROR('Parameter tidak valid untuk @SortColumn:%s', 11, 1, @SortColumn); KEMBALI -1; SET AKHIR @SortColumn =QUOTENAME(@SortColumn); MENYATAKAN @sql NVARCHAR(MAX); SET @sql =N'SELECT key_col, [object_id], nama, type_desc, modifikasi_date FROM dbo.sys_objects ORDER BY ' + @SortColumn + ' ' + @SortDirection + ';'; EXEC sp_executesql @sql;END

Perbandingan Kinerja

Saya membuat prosedur tersimpan pembungkus untuk setiap prosedur di atas, sehingga saya dapat dengan mudah menguji semua skenario. Empat prosedur pembungkus terlihat seperti ini, dengan nama prosedur yang bervariasi tentunya:

BUAT PROSEDUR dbo.Test_Sort_CaseExpandedASBEGIN AKTIFKAN NOCOUNT; EXEC dbo.Sort_CaseExpanded; -- default EXEC dbo.Sort_CaseExpanded N'name', 'ASC'; EXEC dbo.Sort_CaseExpanded N'name', 'DESC'; EXEC dbo.Sort_CaseExpanded N'object_id', 'ASC'; EXEC dbo.Sort_CaseExpanded N'object_id', 'DESC'; EXEC dbo.Sort_CaseExpanded N'type_desc', 'ASC'; EXEC dbo.Sort_CaseExpanded N'type_desc', 'DESC'; EXEC dbo.Sort_CaseExpanded N'modify_date', 'ASC'; EXEC dbo.Sort_CaseExpanded N'modify_date', 'DESC';END

Dan kemudian menggunakan SQL Sentry Plan Explorer, saya membuat rencana eksekusi aktual (dan metrik yang menyertainya) dengan kueri berikut, dan mengulangi proses 10 kali untuk menjumlahkan total durasi:

DBCC DROPCLEANBUFFERS;DBCC FREEPROCCACHE;EXEC dbo.Test_Sort_CaseExpanded;--EXEC dbo.Test_Sort_CaseCollapsed;--EXEC dbo.Test_Sort_RowNumber;--EXEC dbo.Test_Sort_Sort_SQL> 

Saya juga menguji tiga kasus pertama dengan OPTION RECOMPILE (tidak masuk akal untuk kasus SQL dinamis, karena kami tahu itu akan menjadi rencana baru setiap kali), dan keempat kasus dengan MAXDOP 1 untuk menghilangkan gangguan paralelisme. Berikut adalah hasilnya:

Kesimpulan

Untuk kinerja langsung, SQL dinamis menang setiap saat (meskipun hanya dengan margin kecil pada kumpulan data ini). Pendekatan ROW_NUMBER(), meskipun pintar, adalah yang kalah dalam setiap pengujian (maaf AndriyM).

Itu menjadi lebih menyenangkan ketika Anda ingin memperkenalkan klausa WHERE, apalagi paging. Ketiganya seperti badai yang sempurna untuk memperkenalkan kompleksitas pada apa yang dimulai sebagai permintaan pencarian sederhana. Semakin banyak permutasi yang dimiliki kueri Anda, semakin besar kemungkinan Anda ingin membuang keterbacaan keluar jendela dan menggunakan SQL dinamis dalam kombinasi dengan pengaturan "optimalkan beban kerja ad hoc" untuk meminimalkan dampak paket sekali pakai dalam cache paket Anda.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Migrasi Data Menggunakan Network_link

  2. Visualisasi Data di Microsoft Power BI

  3. Penggabungan Nested Loops dan Performance Spool

  4. Serialisasi Penghapusan Dari Indeks Columnstore Clustered

  5. 911/112:Model Data Layanan Panggilan Darurat