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

Cara Menulis Query dengan Beberapa Perilaku

Seringkali, ketika kita menulis prosedur tersimpan, kita ingin berperilaku dengan cara yang berbeda berdasarkan input pengguna. Mari kita lihat contoh berikut:

  CREATE PROCEDURE
  	Sales.GetOrders
  (
  	@CustomerID	AS INT			= NULL ,
  	@SortOrder	AS SYSNAME		= N'OrderDate'
  )
  AS
  SELECT TOP (10)
  	SalesOrderID	         = SalesOrders.SalesOrderID ,
  	OrderDate		= CAST (SalesOrders.OrderDate AS DATE) ,
  	OrderStatus		= SalesOrders.[Status] ,
  	CustomerID		= SalesOrders.CustomerID ,
  	OrderTotal		= SUM (SalesOrderDetails.LineTotal)
  FROM
  	Sales.SalesOrderHeader AS SalesOrders
  INNER JOIN
  	Sales.SalesOrderDetail AS SalesOrderDetails
  ON
  	SalesOrders.SalesOrderID = SalesOrderDetails.SalesOrderID
  WHERE
  	SalesOrders.CustomerID = @CustomerID OR @CustomerID IS NULL
  GROUP BY
  	SalesOrders.SalesOrderID ,
  	SalesOrders.OrderDate ,
  	SalesOrders.DueDate ,
  	SalesOrders.[Status] ,
  	SalesOrders.CustomerID
  ORDER BY
  	CASE @SortOrder
  		WHEN N'OrderDate'
  			THEN SalesOrders.OrderDate
  		WHEN N'SalesOrderID'
  			THEN SalesOrders.SalesOrderID
  	END ASC;
  GO

Prosedur tersimpan ini, yang saya buat di database AdventureWorks2017, memiliki dua parameter:@CustomerID dan @SortOrder. Parameter pertama, @CustomerID, mempengaruhi baris yang akan dikembalikan. Jika ID pelanggan tertentu diteruskan ke prosedur tersimpan, maka ia mengembalikan semua pesanan (10 teratas) untuk pelanggan ini. Jika tidak, jika NULL, maka prosedur tersimpan mengembalikan semua pesanan (10 teratas), terlepas dari pelanggannya. Parameter kedua, @SortOrder, menentukan bagaimana data akan diurutkan—berdasarkan OrderDate atau SalesOrderID. Perhatikan bahwa hanya 10 baris pertama yang akan dikembalikan sesuai dengan urutan pengurutan.

Jadi, pengguna dapat memengaruhi perilaku kueri dengan dua cara—baris mana yang akan ditampilkan dan cara mengurutkannya. Untuk lebih tepatnya, ada 4 perilaku berbeda untuk kueri ini:

  1. Kembalikan 10 baris teratas untuk semua pelanggan yang diurutkan menurut TanggalPesanan (perilaku default)
  2. Kembalikan 10 baris teratas untuk pelanggan tertentu yang diurutkan menurut TanggalPesanan
  3. Kembalikan 10 baris teratas untuk semua pelanggan yang diurutkan menurut SalesOrderID
  4. Menampilkan 10 baris teratas untuk pelanggan tertentu yang diurutkan menurut SalesOrderID

Mari kita uji prosedur tersimpan dengan semua 4 opsi dan periksa rencana eksekusi dan statistik IO.

Kembalikan 10 Baris Teratas untuk Semua Pelanggan yang Diurutkan berdasarkan TanggalPesanan

Berikut ini adalah kode untuk menjalankan prosedur tersimpan:

  EXECUTE Sales.GetOrders;
  GO

Berikut rencana eksekusinya:

Karena kami belum memfilter berdasarkan pelanggan, kami perlu memindai seluruh tabel. Pengoptimal memilih untuk memindai kedua tabel menggunakan indeks pada SalesOrderID, yang memungkinkan Agregat Aliran yang efisien serta Penggabungan Penggabungan yang efisien.

Jika Anda memeriksa properti operator Clustered Index Scan pada tabel Sales.SalesOrderHeader, Anda akan menemukan predikat berikut:[AdventureWorks2017].[Sales].[SalesOrderHeader].[CustomerID] as [SalesOrders].[CustomerID]=[ @IDPelanggan] ATAU [@IDPelanggan] NULL. Pemroses kueri harus mengevaluasi predikat ini untuk setiap baris dalam tabel, yang tidak terlalu efisien karena akan selalu bernilai benar.

Kita masih perlu mengurutkan semua data berdasarkan OrderDate untuk mengembalikan 10 baris pertama. Jika ada indeks pada OrderDate, maka pengoptimal mungkin akan menggunakannya untuk memindai hanya 10 baris pertama dari Sales.SalesOrderHeader, tetapi tidak ada indeks seperti itu, jadi rencananya tampaknya baik-baik saja dengan mempertimbangkan indeks yang tersedia.

Berikut adalah output dari statistik IO:

  • Tabel 'SalesOrderHeader'. Hitungan pindai 1, pembacaan logis 689
  • Tabel 'SalesOrderDetail'. Pindai hitungan 1, pembacaan logis 1248

Jika Anda bertanya mengapa ada peringatan pada operator SELECT, maka itu adalah peringatan pemberian yang berlebihan. Dalam hal ini, bukan karena ada masalah dalam rencana eksekusi, melainkan karena pemroses kueri meminta 1.024KB (yang merupakan minimum secara default) dan hanya menggunakan 16KB.

Terkadang Merencanakan Caching Bukan Ide yang Bagus

Selanjutnya, kami ingin menguji skenario mengembalikan 10 baris teratas untuk pelanggan tertentu yang diurutkan berdasarkan TanggalPesanan. Di bawah ini adalah kodenya:

  EXECUTE Sales.GetOrders
  	@CustomerID	= 11006;
  GO

Rencana eksekusinya sama persis seperti sebelumnya. Kali ini, rencananya sangat tidak efisien karena memindai kedua tabel hanya untuk mengembalikan 3 pesanan. Ada banyak cara yang lebih baik untuk mengeksekusi kueri ini.

Alasannya, dalam hal ini, adalah rencana caching. Rencana eksekusi dibuat dalam eksekusi pertama berdasarkan nilai parameter dalam eksekusi spesifik itu—metode yang dikenal sebagai parameter sniffing. Paket tersebut disimpan dalam cache paket untuk digunakan kembali, dan, mulai sekarang, setiap panggilan ke prosedur tersimpan ini akan menggunakan kembali paket yang sama.

Ini adalah contoh di mana caching rencana bukanlah ide yang bagus. Karena sifat dari prosedur tersimpan ini, yang memiliki 4 perilaku berbeda, kami berharap mendapatkan rencana yang berbeda untuk setiap perilaku. Tapi kami terjebak dengan satu rencana, yang hanya bagus untuk salah satu dari 4 opsi, berdasarkan opsi yang digunakan dalam eksekusi pertama.

Mari kita nonaktifkan caching rencana untuk prosedur tersimpan ini, hanya agar kita dapat melihat rencana terbaik yang dapat dihasilkan oleh pengoptimal untuk masing-masing dari 3 perilaku lainnya. Kami akan melakukan ini dengan menambahkan WITH RECOMPILE ke perintah EXECUTE.

Kembalikan 10 Baris Teratas untuk Pelanggan Tertentu yang Diurutkan berdasarkan TanggalPesanan

Berikut adalah kode untuk mengembalikan 10 baris teratas untuk pelanggan tertentu yang diurutkan berdasarkan TanggalPesanan:

  EXECUTE Sales.GetOrders
  	@CustomerID	= 11006
  WITH
  	RECOMPILE;
  GO

Berikut rencana eksekusinya:

Kali ini, kami mendapatkan paket yang lebih baik, yang menggunakan indeks di CustomerID. Pengoptimal memperkirakan dengan benar 2,6 baris untuk ID Pelanggan =11006 (jumlah sebenarnya adalah 3). Tetapi perhatikan bahwa ia melakukan pemindaian indeks alih-alih pencarian indeks. Itu tidak dapat melakukan pencarian indeks karena harus mengevaluasi predikat berikut untuk setiap baris dalam tabel:[AdventureWorks2017].[Sales].[SalesOrderHeader].[CustomerID] as [SalesOrders].[CustomerID]=[@CustomerID ] ATAU [@CustomerID] NULL.

Berikut adalah output dari statistik IO:

  • Tabel 'SalesOrderDetail'. Pindai hitungan 3, pembacaan logis 9
  • Tabel 'SalesOrderHeader'. Pindai hitungan 1, pembacaan logis 66

Kembalikan 10 Baris Teratas untuk Semua Pelanggan yang Diurutkan berdasarkan SalesOrderID

Berikut adalah kode untuk mengembalikan 10 baris teratas untuk semua pelanggan yang diurutkan berdasarkan SalesOrderID:

  EXECUTE Sales.GetOrders
  	@SortOrder	= N'SalesOrderID'
  WITH
  	RECOMPILE;
  GO

Berikut rencana eksekusinya:

Hei, ini adalah rencana eksekusi yang sama seperti pada opsi pertama. Tapi kali ini, ada yang salah. Kita sudah tahu bahwa indeks berkerumun di kedua tabel diurutkan berdasarkan SalesOrderID. Kita juga tahu bahwa rencana memindai keduanya dalam urutan logis untuk mempertahankan urutan pengurutan (properti Dipesan disetel ke True). Operator Gabung Gabung juga mempertahankan urutan pengurutan. Karena kita sekarang diminta untuk mengurutkan hasil berdasarkan SalesOrderID, dan memang sudah diurutkan seperti itu, lalu mengapa kita harus membayar operator Sortir yang mahal?

Nah, jika Anda memeriksa operator Sortir, Anda akan melihat bahwa itu mengurutkan data menurut Expr1004. Dan, jika Anda memeriksa operator Hitung Skalar di sebelah kanan operator Sortir, maka Anda akan menemukan bahwa Expr1004 adalah sebagai berikut:

Itu bukan pemandangan yang indah, aku tahu. Ini adalah ekspresi yang kami miliki dalam klausa ORDER BY dari kueri kami. Masalahnya adalah pengoptimal tidak dapat mengevaluasi ekspresi ini pada waktu kompilasi, sehingga pengoptimal harus menghitungnya untuk setiap baris saat runtime, lalu mengurutkan seluruh kumpulan catatan berdasarkan itu.

Output dari IO statistik sama seperti pada eksekusi pertama:

  • Tabel 'SalesOrderHeader'. Hitungan pindai 1, pembacaan logis 689
  • Tabel 'SalesOrderDetail'. Pindai hitungan 1, pembacaan logis 1248

Kembalikan 10 Baris Teratas untuk Pelanggan Tertentu yang Diurutkan berdasarkan SalesOrderID

Berikut adalah kode untuk mengembalikan 10 baris teratas untuk pelanggan tertentu yang diurutkan berdasarkan SalesOrderID:

  EXECUTE Sales.GetOrders
  	@CustomerID	= 11006 ,
  	@SortOrder	= N'SalesOrderID'
  WITH
  	RECOMPILE;
  GO

Rencana eksekusi sama seperti pada opsi kedua (kembalikan 10 baris teratas untuk pelanggan tertentu yang diurutkan berdasarkan TanggalPesanan). Rencana tersebut memiliki dua masalah yang sama, yang telah kami sebutkan. Masalah pertama adalah melakukan pemindaian indeks daripada pencarian indeks karena ekspresi dalam klausa WHERE. Masalah kedua adalah melakukan pengurutan yang mahal karena ekspresi dalam klausa ORDER BY.

Jadi, Apa Yang Harus Kita Lakukan?

Mari kita ingatkan diri kita dulu apa yang sedang kita hadapi. Kami memiliki parameter, yang menentukan struktur kueri. Untuk setiap kombinasi nilai parameter, kami mendapatkan struktur kueri yang berbeda. Dalam kasus parameter @CustomerID, dua perilaku yang berbeda adalah NULL atau NOT NULL, dan mereka mempengaruhi klausa WHERE. Dalam kasus parameter @SortOrder, ada dua kemungkinan nilai, dan nilai tersebut mempengaruhi klausa ORDER BY. Hasilnya adalah 4 kemungkinan struktur kueri, dan kami ingin mendapatkan rencana yang berbeda untuk masing-masing struktur.

Kemudian kita memiliki dua masalah yang berbeda. Yang pertama adalah caching paket. Hanya ada satu rencana untuk prosedur tersimpan, dan itu akan dihasilkan berdasarkan nilai parameter dalam eksekusi pertama. Masalah kedua adalah bahwa bahkan ketika rencana baru dibuat, itu tidak efisien karena pengoptimal tidak dapat mengevaluasi ekspresi "dinamis" dalam klausa WHERE dan dalam klausa ORDER BY pada waktu kompilasi.

Kita dapat mencoba memecahkan masalah ini dengan beberapa cara:

  1. Gunakan serangkaian pernyataan IF-ELSE
  2. Pisahkan prosedur menjadi prosedur tersimpan yang terpisah
  3. Gunakan OPSI (REKOMPILASI)
  4. Buat kueri secara dinamis

Gunakan Serangkaian Pernyataan IF-ELSE

Idenya sederhana:alih-alih ekspresi "dinamis" dalam klausa WHERE dan dalam klausa ORDER BY, kita dapat membagi eksekusi menjadi 4 cabang menggunakan pernyataan IF-ELSE—satu cabang untuk setiap kemungkinan perilaku.

Sebagai contoh, berikut adalah kode untuk cabang pertama:

  IF
  	@CustomerID IS NULL
  AND
  	@SortOrder = N'OrderDate'
  BEGIN
  	SELECT TOP (10)
  		SalesOrderID	        = SalesOrders.SalesOrderID ,
  		OrderDate		= CAST (SalesOrders.OrderDate AS DATE) ,
  		OrderStatus		= SalesOrders.[Status] ,
  		CustomerID		= SalesOrders.CustomerID ,
  		OrderTotal		= SUM (SalesOrderDetails.LineTotal)
  	FROM
  		Sales.SalesOrderHeader AS SalesOrders
  	INNER JOIN
  		Sales.SalesOrderDetail AS SalesOrderDetails
  	ON
  		SalesOrders.SalesOrderID = SalesOrderDetails.SalesOrderID
  	GROUP BY
  		SalesOrders.SalesOrderID,
  		SalesOrders.OrderDate,
  		SalesOrders.DueDate,
  		SalesOrders.[Status],
  		SalesOrders.CustomerID
  	ORDER BY
  		SalesOrders.OrderDate ASC;
  END;

Pendekatan ini dapat membantu menghasilkan rencana yang lebih baik, tetapi memiliki beberapa keterbatasan.

Pertama, prosedur tersimpan menjadi cukup panjang, dan lebih sulit untuk menulis, membaca, dan memelihara. Dan ini adalah saat kita hanya memiliki dua parameter. Jika kami memiliki 3 parameter, kami akan memiliki 8 cabang. Bayangkan Anda perlu menambahkan kolom ke klausa SELECT. Anda harus menambahkan kolom dalam 8 kueri berbeda. Ini menjadi mimpi buruk pemeliharaan, dengan risiko kesalahan manusia yang tinggi.

Kedua, kami masih memiliki masalah rencana caching dan parameter sniffing sampai batas tertentu. Ini karena dalam eksekusi pertama, pengoptimal akan membuat rencana untuk semua 4 kueri berdasarkan nilai parameter dalam eksekusi itu. Katakanlah eksekusi pertama akan menggunakan nilai default untuk parameter. Secara khusus, nilai @CustomerID akan menjadi NULL. Semua kueri akan dioptimalkan berdasarkan nilai tersebut, termasuk kueri dengan klausa WHERE (SalesOrders.CustomerID =@CustomerID). Pengoptimal akan memperkirakan 0 baris untuk kueri ini. Sekarang, katakanlah eksekusi kedua akan menggunakan nilai non-null untuk @CustomerID. Paket yang di-cache, yang memperkirakan 0 baris, akan digunakan, meskipun pelanggan mungkin memiliki banyak pesanan dalam tabel.

Pisahkan Prosedur menjadi Prosedur Tersimpan Terpisah

Alih-alih 4 cabang dalam prosedur tersimpan yang sama, kita dapat membuat 4 prosedur tersimpan yang terpisah, masing-masing dengan parameter yang relevan dan kueri yang sesuai. Kemudian, kita dapat menulis ulang aplikasi untuk memutuskan prosedur tersimpan mana yang akan dijalankan sesuai dengan perilaku yang diinginkan. Atau, jika kita ingin transparan untuk aplikasi, kita dapat menulis ulang prosedur tersimpan asli untuk memutuskan prosedur mana yang akan dijalankan berdasarkan nilai parameter. Kami akan menggunakan pernyataan IF-ELSE yang sama, tetapi alih-alih mengeksekusi kueri di setiap cabang, kami akan menjalankan prosedur tersimpan yang terpisah.

Keuntungannya adalah kami memecahkan masalah caching rencana karena setiap prosedur tersimpan sekarang memiliki rencananya sendiri, dan rencana untuk setiap prosedur tersimpan akan dihasilkan dalam eksekusi pertamanya berdasarkan parameter sniffing.

Tapi kami masih memiliki masalah pemeliharaan. Beberapa orang mungkin mengatakan bahwa sekarang lebih buruk, karena kita perlu memelihara beberapa prosedur tersimpan. Sekali lagi, jika kita menambah jumlah parameter menjadi 3, kita akan mendapatkan 8 prosedur tersimpan yang berbeda.

Gunakan OPSI (REKOMPILASI)

OPTION (RECOMPILE) bekerja seperti sulap. Anda hanya perlu mengucapkan kata-kata (atau menambahkannya ke kueri), dan keajaiban terjadi. Sungguh, ini memecahkan begitu banyak masalah karena mengkompilasi kueri saat runtime, dan melakukannya untuk setiap eksekusi.

Tetapi Anda harus berhati-hati karena Anda tahu apa yang mereka katakan:"Dengan kekuatan yang besar, datanglah tanggung jawab yang besar." Jika Anda menggunakan OPTION (RECOMPILE) dalam kueri yang sangat sering dijalankan pada sistem OLTP yang sibuk, maka Anda mungkin mematikan sistem karena server perlu mengkompilasi dan menghasilkan rencana baru di setiap eksekusi, menggunakan banyak sumber daya CPU. Ini benar-benar berbahaya. Namun, jika kueri hanya dijalankan sesekali, katakanlah setiap beberapa menit sekali, maka mungkin aman. Tapi selalu uji dampaknya di lingkungan spesifik Anda.

Dalam kasus kami, dengan asumsi kami dapat menggunakan OPTION (RECOMPILE) dengan aman, yang harus kami lakukan adalah menambahkan kata-kata ajaib di akhir kueri kami, seperti yang ditunjukkan di bawah ini:

  ALTER PROCEDURE
  	Sales.GetOrders
  (
  	@CustomerID	AS INT			= NULL ,
  	@SortOrder	AS SYSNAME		= N'OrderDate'
  )
  AS
  SELECT TOP (10)
  	SalesOrderID	        = SalesOrders.SalesOrderID ,
  	OrderDate		= CAST (SalesOrders.OrderDate AS DATE) ,
  	OrderStatus		= SalesOrders.[Status] ,
  	CustomerID		= SalesOrders.CustomerID ,
  	OrderTotal		= SUM (SalesOrderDetails.LineTotal)
  FROM
  	Sales.SalesOrderHeader AS SalesOrders
  INNER JOIN
  	Sales.SalesOrderDetail AS SalesOrderDetails
  ON
  	SalesOrders.SalesOrderID = SalesOrderDetails.SalesOrderID
  WHERE
  	SalesOrders.CustomerID = @CustomerID OR @CustomerID IS NULL
  GROUP BY
  	SalesOrders.SalesOrderID ,
  	SalesOrders.OrderDate ,
  	SalesOrders.DueDate ,
  	SalesOrders.[Status] ,
  	SalesOrders.CustomerID
  ORDER BY
  	CASE @SortOrder
  		WHEN N'OrderDate'
  			THEN SalesOrders.OrderDate
  		WHEN N'SalesOrderID'
  			THEN SalesOrders.SalesOrderID
  	END ASC
  OPTION
  	(RECOMPILE);
  GO

Sekarang, mari kita lihat keajaiban beraksi. Misalnya, berikut ini adalah rencana untuk perilaku kedua:

  EXECUTE Sales.GetOrders
  	@CustomerID	= 11006;
  GO

Sekarang kita mendapatkan pencarian indeks yang efisien dengan estimasi 2,6 baris yang benar. Kita masih perlu mengurutkan menurut TanggalPesanan, tetapi sekarang pengurutannya langsung menurut TanggalPesanan, dan kita tidak perlu menghitung ekspresi CASE dalam klausa ORDER BY lagi. Ini adalah rencana terbaik untuk perilaku kueri ini berdasarkan indeks yang tersedia.

Berikut adalah output dari statistik IO:

  • Tabel 'SalesOrderDetail'. Pindai hitungan 3, pembacaan logis 9
  • Tabel 'SalesOrderHeader'. Pindai hitungan 1, pembacaan logis 11

Alasan OPTION (RECOMPILE) sangat efisien dalam kasus ini adalah karena OPTION (RECOMPILE) memecahkan persis dua masalah yang kita miliki di sini. Ingat bahwa masalah pertama adalah rencana caching. OPTION (RECOMPILE) menghilangkan masalah ini sama sekali karena mengkompilasi ulang kueri setiap saat. Masalah kedua adalah ketidakmampuan pengoptimal untuk mengevaluasi ekspresi kompleks dalam klausa WHERE dan dalam klausa ORDER BY pada waktu kompilasi. Karena OPTION (RECOMPILE) terjadi saat runtime, ini menyelesaikan masalah. Karena pada saat runtime, pengoptimal memiliki lebih banyak informasi dibandingkan dengan waktu kompilasi, dan itu membuat semua perbedaan.

Sekarang, mari kita lihat apa yang terjadi ketika kita mencoba perilaku ketiga:

  EXECUTE Sales.GetOrders
  	@SortOrder	= N'SalesOrderID';
  GO

Houston kita punya masalah. Paket masih memindai kedua tabel seluruhnya dan kemudian mengurutkan semuanya, alih-alih hanya memindai 10 baris pertama dari Sales.SalesOrderHeader dan menghindari pengurutan sama sekali. Apa yang terjadi?

Ini adalah "kasus" yang menarik, dan ini ada hubungannya dengan ekspresi CASE dalam klausa ORDER BY. Ekspresi CASE mengevaluasi daftar kondisi dan mengembalikan salah satu ekspresi hasil. Tetapi ekspresi hasil mungkin memiliki tipe data yang berbeda. Jadi, apa tipe data dari seluruh ekspresi CASE? Nah, ekspresi CASE selalu mengembalikan tipe data dengan prioritas tertinggi. Dalam kasus kami, kolom OrderDate memiliki tipe data DATETIME, sedangkan kolom SalesOrderID memiliki tipe data INT. Tipe data DATETIME memiliki prioritas yang lebih tinggi, sehingga ekspresi CASE selalu mengembalikan DATETIME.

Ini berarti bahwa jika kita ingin mengurutkan berdasarkan SalesOrderID, ekspresi CASE harus terlebih dahulu secara implisit mengonversi nilai SalesOrderID ke DATETIME untuk setiap baris sebelum mengurutkannya. Lihat operator Hitung Skalar di sebelah kanan operator Sortir dalam rencana di atas? Itulah tepatnya yang dilakukannya.

Ini adalah masalah tersendiri, dan ini menunjukkan betapa berbahayanya mencampur tipe data yang berbeda dalam satu ekspresi CASE.

Kita dapat mengatasi masalah ini dengan menulis ulang klausa ORDER BY dengan cara lain, tetapi itu akan membuat kode lebih jelek dan sulit untuk dibaca dan dipelihara. Jadi, saya tidak akan pergi ke arah itu.

Sebagai gantinya, mari kita coba metode selanjutnya…

Menghasilkan Kueri Secara Dinamis

Karena tujuan kami adalah untuk menghasilkan 4 struktur kueri yang berbeda dalam satu kueri, SQL dinamis bisa sangat berguna dalam kasus ini. Idenya adalah untuk membangun kueri secara dinamis berdasarkan nilai parameter. Dengan cara ini, kita dapat membangun 4 struktur kueri yang berbeda dalam satu kode, tanpa harus mempertahankan 4 salinan kueri. Setiap struktur kueri akan dikompilasi satu kali, saat pertama kali dieksekusi, dan akan mendapatkan rencana terbaik karena tidak mengandung ekspresi kompleks.

Solusi ini sangat mirip dengan solusi dengan beberapa prosedur tersimpan, tetapi alih-alih mempertahankan 8 prosedur tersimpan untuk 3 parameter, kami hanya memelihara satu kode yang membangun kueri secara dinamis.

Saya tahu, SQL dinamis juga jelek dan terkadang cukup sulit untuk dipertahankan, tetapi menurut saya ini masih lebih mudah daripada mempertahankan beberapa prosedur tersimpan, dan tidak menskalakan secara eksponensial karena jumlah parameter meningkat.

Berikut kodenya:

  ALTER PROCEDURE
  	Sales.GetOrders
  (
  	@CustomerID	AS INT			= NULL ,
  	@SortOrder	AS SYSNAME		= N'OrderDate'
  )
  AS
  DECLARE
  	@Command AS NVARCHAR(MAX);
  SET @Command =
  	N'
  		SELECT TOP (10)
  			SalesOrderID	        = SalesOrders.SalesOrderID ,
  			OrderDate		= CAST (SalesOrders.OrderDate AS DATE) ,
  			OrderStatus		= SalesOrders.[Status] ,
  			CustomerID		= SalesOrders.CustomerID ,
  			OrderTotal		= SUM (SalesOrderDetails.LineTotal)
  		FROM
  			Sales.SalesOrderHeader AS SalesOrders
  		INNER JOIN
  			Sales.SalesOrderDetail AS SalesOrderDetails
  		ON
  			SalesOrders.SalesOrderID = SalesOrderDetails.SalesOrderID
  		' +
  		CASE
  			WHEN @CustomerID IS NULL
  				THEN N''
  			ELSE
  				N'WHERE
  			SalesOrders.CustomerID = @pCustomerID
  		'
  		END +
  		N'GROUP BY
  			SalesOrders.SalesOrderID ,
  			SalesOrders.OrderDate ,
  			SalesOrders.DueDate ,
  			SalesOrders.[Status] ,
  			SalesOrders.CustomerID
  		ORDER BY
  			' +
  			CASE @SortOrder
  				WHEN N'OrderDate'
  					THEN N'SalesOrders.OrderDate'
  				WHEN N'SalesOrderID'
  					THEN N'SalesOrders.SalesOrderID'
  			END +
  		N' ASC;
  	';
  EXECUTE sys.sp_executesql
  	@stmt			= @Command ,
  	@params			= N'@pCustomerID AS INT' ,
  	@pCustomerID	= @CustomerID;
  GO

Perhatikan bahwa saya masih menggunakan parameter internal untuk ID Pelanggan, dan saya menjalankan kode dinamis menggunakan sys.sp_executesql untuk melewatkan nilai parameter. Ini penting karena dua alasan. Pertama, untuk menghindari beberapa kompilasi dari struktur kueri yang sama untuk nilai @CustomerID yang berbeda. Kedua, untuk menghindari injeksi SQL.

Jika Anda mencoba menjalankan prosedur tersimpan sekarang menggunakan nilai parameter yang berbeda, Anda akan melihat bahwa setiap perilaku kueri atau struktur kueri mendapatkan rencana eksekusi terbaik, dan masing-masing dari 4 paket hanya dikompilasi sekali.

Sebagai contoh, berikut ini adalah rencana untuk perilaku ketiga:

  EXECUTE Sales.GetOrders
  	@SortOrder	= N'SalesOrderID';
  GO

Sekarang, kami hanya memindai 10 baris pertama dari tabel Sales.SalesOrderHeader, dan kami juga hanya memindai 110 baris pertama dari tabel Sales.SalesOrderDetail. Selain itu, tidak ada operator Sortir karena data sudah diurutkan berdasarkan SalesOrderID.

Berikut adalah output dari statistik IO:

  • Tabel 'SalesOrderDetail'. Pindai hitungan 1, pembacaan logis 4
  • Tabel 'SalesOrderHeader'. Pindai hitungan 1, pembacaan logis 3

Kesimpulan

Saat Anda menggunakan parameter untuk mengubah struktur kueri Anda, jangan gunakan ekspresi kompleks dalam kueri untuk mendapatkan perilaku yang diharapkan. Dalam kebanyakan kasus, ini akan menyebabkan kinerja yang buruk, dan untuk alasan yang baik. Alasan pertama adalah bahwa rencana akan dibuat berdasarkan eksekusi pertama, dan kemudian semua eksekusi berikutnya akan menggunakan kembali rencana yang sama, yang hanya sesuai untuk satu struktur kueri. Alasan kedua adalah bahwa pengoptimal terbatas dalam kemampuannya untuk mengevaluasi ekspresi kompleks tersebut pada waktu kompilasi.

Ada beberapa cara untuk mengatasi masalah ini, dan kami memeriksanya di artikel ini. Dalam kebanyakan kasus, metode terbaik adalah membangun kueri secara dinamis berdasarkan nilai parameter. Dengan begitu, setiap struktur kueri akan dikompilasi satu kali dengan rencana terbaik.

Saat Anda membuat kueri menggunakan SQL dinamis, pastikan untuk menggunakan parameter yang sesuai dan verifikasi bahwa kode Anda aman.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Apakah Pengemudi Tenaga Penjualan Anda Mendukung Tindakan Massal?

  2. Cara Mengelompokkan Berdasarkan Tahun di SQL

  3. Menggunakan Kolom Pseudo dengan Server Tertaut

  4. Cara Menggunakan LIKE di SQL

  5. Bahasa Kueri Terstruktur – Pentingnya mempelajari SQL