Ada fitur yang banyak dari kita hindari, seperti kursor, pemicu, dan SQL dinamis. Tidak diragukan lagi mereka memiliki kasus penggunaan masing-masing, tetapi ketika kita melihat pemicu dengan kursor di dalam SQL dinamis, itu bisa membuat kita ngeri (tiga kali pukulan).
Panduan rencana dan sp_prepare berada di perahu yang sama:jika Anda melihat saya menggunakan salah satunya, Anda akan mengangkat alis; jika Anda melihat saya menggunakannya bersama-sama, Anda mungkin akan memeriksa suhu saya. Namun, seperti halnya kursor, pemicu, dan SQL dinamis, mereka memiliki kasus penggunaannya. Dan baru-baru ini saya menemukan skenario di mana menggunakannya bersama-sama bermanfaat.
Latar Belakang
Kami memiliki banyak data. Dan banyak aplikasi yang berjalan melawan data tersebut. Beberapa dari aplikasi tersebut sulit atau tidak mungkin untuk diubah, terutama aplikasi yang tersedia dari pihak ketiga. Jadi, ketika aplikasi yang dikompilasi mengirimkan kueri ad hoc ke SQL Server, khususnya sebagai pernyataan yang telah disiapkan, dan ketika kita tidak memiliki kebebasan untuk menambah atau mengubah indeks, beberapa peluang penyetelan akan segera dihapus.
Dalam hal ini, kami memiliki tabel dengan beberapa juta baris. Versi yang disederhanakan dan disanitasi:
CREATE TABLE dbo.TheThings ( ThingID bigint NOT NULL, TypeID uniqueidentifier NOT NULL, dt1 datetime NOT NULL DEFAULT sysutcdatetime(), dt2 datetime NOT NULL DEFAULT sysutcdatetime(), dt3 datetime NOT NULL DEFAULT sysutcdatetime(), CONSTRAINT PK_TheThings PRIMARY KEY (ThingID) ); CREATE INDEX ix_type ON dbo.TheThings(TypeID); SET NOCOUNT ON; GO DECLARE @guid1 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4', @guid2 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; INSERT dbo.TheThings(ThingID, TypeID) SELECT TOP (1000) 1000 + ROW_NUMBER() OVER (ORDER BY name), @guid1 FROM sys.all_columns; INSERT dbo.TheThings(ThingID, TypeID) SELECT TOP (1) 2500, @guid2 FROM sys.all_columns; INSERT dbo.TheThings(ThingID, TypeID) SELECT TOP (1000) 3000 + ROW_NUMBER() OVER (ORDER BY name), @guid1 FROM sys.all_columns;
Pernyataan yang disiapkan dari aplikasi terlihat seperti ini (seperti yang terlihat di cache paket):
(@P0 varchar(8000))SELECT * FROM dbo.TheThings WHERE TypeID = @P0
Masalahnya adalah, untuk beberapa nilai TypeID
, akan ada ribuan baris. Untuk nilai lain, akan ada kurang dari 10. Jika rencana yang salah dipilih (dan digunakan kembali) berdasarkan satu jenis parameter, ini bisa menjadi masalah bagi yang lain. Untuk kueri yang mengambil beberapa baris, kami ingin pencarian indeks dengan pencarian untuk mengambil kolom tambahan yang tidak tercakup, tetapi untuk kueri yang mengembalikan 700 ribu baris, kami hanya ingin pemindaian indeks berkerumun. (Idealnya, indeks akan mencakup, tetapi opsi ini tidak ada dalam kartu kali ini.)
Dalam praktiknya, aplikasi selalu mendapatkan variasi pemindaian, meskipun itu yang dibutuhkan sekitar 1% dari waktu. 99% kueri menggunakan pemindaian 2 juta baris padahal sebenarnya bisa menggunakan pencarian + 4 atau 5 pencarian.
Kami dapat dengan mudah mereproduksi ini di Management Studio dengan menjalankan kueri ini:
DBCC FREEPROCCACHE; DECLARE @P0 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4'; SELECT * FROM dbo.TheThings WHERE TypeID = @P0; GO DBCC FREEPROCCACHE; DECLARE @P0 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; SELECT * FROM dbo.TheThings WHERE TypeID = @P0; GO
Rencananya kembali seperti ini:
Perkiraan dalam kedua kasus adalah 1.000 baris; peringatan di sebelah kanan disebabkan oleh I/O sisa.
Bagaimana kami bisa memastikan kueri membuat pilihan yang tepat tergantung pada parameternya? Kita perlu mengkompilasi ulang, tanpa menambahkan petunjuk ke kueri, mengaktifkan tanda pelacakan, atau mengubah setelan database.
Jika saya menjalankan kueri secara mandiri menggunakan OPTION (RECOMPILE)
, saya akan mendapatkan pencarian jika perlu:
DBCC FREEPROCCACHE; DECLARE @guid1 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4', @guid2 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; SELECT * FROM dbo.TheThings WHERE TypeID = @guid1 OPTION (RECOMPILE); SELECT * FROM dbo.TheThings WHERE TypeID = @guid2 OPTION (RECOMPILE);
Dengan RECOMPILE, kami mendapatkan perkiraan yang lebih akurat, dan mencari saat kami membutuhkannya.
Namun, sekali lagi, kami tidak dapat menambahkan petunjuk ke kueri secara langsung.
Mari kita coba panduan rencana
Banyak orang memperingatkan terhadap panduan rencana, tapi kami agak terpojok di sini. Kami pasti lebih suka mengubah kueri, atau indeks, jika kami bisa. Tapi ini mungkin hal terbaik berikutnya.
EXEC sys.sp_create_plan_guide @name = N'TheThingGuide', @stmt = N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0', @type = N'SQL', @params = N'@P0 varchar(8000)', @hints = N'OPTION (RECOMPILE)';
Tampaknya langsung; mengujinya adalah masalahnya. Bagaimana kita mensimulasikan pernyataan yang disiapkan di Management Studio? Bagaimana kami bisa yakin bahwa aplikasi mendapatkan paket terpandu, dan itu secara eksplisit karena panduan paket?
Jika kami mencoba untuk mensimulasikan kueri ini di SSMS, ini akan diperlakukan sebagai pernyataan ad hoc, bukan pernyataan yang disiapkan, dan saya tidak bisa mendapatkan ini untuk mengambil panduan rencana:
DECLARE @P0 varchar(8000) = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; -- also tried uniqueidentifier SELECT * FROM dbo.TheThings WHERE TypeID = @P0
SQL Dinamis juga tidak berfungsi (ini juga diperlakukan sebagai pernyataan ad hoc):
DECLARE @sql nvarchar(max) = N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0', @params nvarchar(max) = N'@P0 varchar(8000)', -- also tried uniqueidentifier @P0 varchar(8000) = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; EXEC sys.sp_executesql @sql, @params, @P0;
Dan saya tidak bisa melakukan ini, karena itu juga tidak akan mengambil panduan rencana (parameterisasi mengambil alih di sini, dan saya tidak memiliki kebebasan untuk mengubah pengaturan basis data, bahkan jika ini diperlakukan seperti pernyataan yang disiapkan) :
SELECT * FROM TheThings WHERE TypeID = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
Saya tidak dapat memeriksa cache paket untuk kueri yang dijalankan dari aplikasi, karena paket yang di-cache tidak menunjukkan apa pun tentang penggunaan panduan paket (SSMS memasukkan informasi itu ke dalam XML untuk Anda saat Anda membuat paket sebenarnya). Dan jika kueri benar-benar mengamati petunjuk RECOMPILE yang saya berikan ke panduan paket, bagaimana saya bisa melihat bukti apa pun di cache paket?
Ayo coba sp_prepare
Saya telah menggunakan sp_prepare lebih sedikit dalam karir saya daripada panduan rencana, dan saya tidak akan merekomendasikan menggunakannya untuk kode aplikasi. (Seperti yang ditunjukkan Erik Darling, estimasi dapat ditarik dari vektor kepadatan, bukan dari mengendus parameter.)
Dalam kasus saya, saya tidak ingin menggunakannya karena alasan kinerja, saya ingin menggunakannya (bersama dengan sp_execute) untuk mensimulasikan pernyataan siap yang berasal dari aplikasi.
DECLARE @o int; EXEC sys.sp_prepare @o OUTPUT, N'@P0 varchar(8000)', N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0'; EXEC sys.sp_execute @o, 'EE81197A-B2EA-41F4-882E-4A5979ACACE4'; -- PK scan EXEC sys.sp_execute @o, 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; -- IX seek + lookup
SSMS menunjukkan kepada kita panduan rencana yang digunakan dalam kedua kasus.
Anda tidak akan dapat memeriksa cache paket untuk hasil ini, karena kompilasi ulang. Tetapi dalam skenario seperti saya, Anda harus dapat melihat efek dalam pemantauan, secara eksplisit memeriksa melalui Peristiwa yang Diperpanjang, atau mengamati pengurangan gejala yang membuat Anda menyelidiki kueri ini sejak awal (perhatikan bahwa rata-rata runtime, kueri stats dll. mungkin terpengaruh oleh kompilasi tambahan).
Kesimpulan
Ini adalah satu kasus di mana panduan rencana bermanfaat, dan sp_prepare berguna dalam memvalidasi bahwa itu akan berfungsi untuk aplikasi. Ini tidak sering berguna, dan lebih jarang bersama-sama, tetapi bagi saya itu adalah kombinasi yang menarik. Bahkan tanpa panduan rencana, jika Anda ingin menggunakan SSMS untuk mensimulasikan aplikasi yang mengirimkan pernyataan yang telah disiapkan, sp_prepare adalah teman Anda. (Juga lihat sp_prepexec, yang dapat menjadi jalan pintas jika Anda tidak mencoba memvalidasi dua paket berbeda untuk kueri yang sama.)
Perhatikan bahwa latihan ini tidak selalu untuk mendapatkan kinerja yang lebih baik setiap saat – itu untuk meratakan varians kinerja. Kompilasi ulang jelas tidak gratis, tetapi saya akan membayar denda kecil untuk membuat 99% kueri saya dieksekusi dalam 250 ms dan 1% dieksekusi dalam 5 detik, daripada terjebak dengan rencana yang benar-benar mengerikan untuk 99% kueri atau 1% dari kueri.