Sangat mudah untuk membuktikan bahwa dua ekspresi berikut menghasilkan hasil yang sama persis:hari pertama bulan ini.
SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0), CONVERT(DATE, DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE()));
Dan mereka membutuhkan waktu yang hampir sama untuk menghitung:
SELECT SYSDATETIME(); GO DECLARE @d DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0); GO 1000000 GO SELECT SYSDATETIME(); GO DECLARE @d DATE = DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE()); GO 1000000 SELECT SYSDATETIME();
Di sistem saya, kedua batch membutuhkan waktu sekitar 175 detik untuk diselesaikan.
Jadi, mengapa Anda lebih memilih satu metode daripada yang lain? Ketika salah satu dari mereka benar-benar mengacaukan estimasi kardinalitas .
Sebagai primer cepat, mari kita bandingkan dua nilai ini:
SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0), -- today: 2013-09-01 DATEADD(MONTH, DATEDIFF(MONTH, GETDATE(), 0), 0); -- today: 1786-05-01 --------------------------------------^^^^^^^^^^^^ notice how these are swapped
(Perhatikan bahwa nilai aktual yang diwakili di sini akan berubah, tergantung pada saat Anda membaca posting ini - "hari ini" yang dirujuk dalam komentar adalah 5 September 2013, hari posting ini ditulis. Pada Oktober 2013, misalnya, hasilnya akan menjadi 2013-10-01
dan 1786-04-01
.)
Dengan itu, izinkan saya menunjukkan kepada Anda apa yang saya maksud…
Repro
Mari kita buat tabel yang sangat sederhana, dengan hanya mengelompokkan DATE
kolom, dan muat 15.000 baris dengan nilai 1786-05-01
dan 50 baris dengan nilai 2013-09-01
:
CREATE TABLE dbo.DateTest ( CreateDate DATE ); CREATE CLUSTERED INDEX x ON dbo.DateTest(CreateDate); INSERT dbo.DateTest(CreateDate) SELECT TOP (15000) DATEADD(MONTH, DATEDIFF(MONTH, GETDATE(), 0), 0) FROM sys.all_objects AS s1 CROSS JOIN sys.all_objects AS s2 UNION ALL SELECT TOP (50) DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0) FROM sys.all_objects;
Dan kemudian mari kita lihat rencana sebenarnya untuk dua pertanyaan ini:
SELECT /* Query 1 */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0); SELECT /* Query 2 */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = DATEADD(MONTH, DATEDIFF(MONTH, GETDATE(), 0), 0);
Rencana grafis terlihat benar:
Rencana grafis untuk DATEDIFF(MONTH, 0, GETDATE()) permintaan
Rencana grafis untuk DATEDIFF(MONTH, GETDATE(), 0) permintaan
Namun perkiraan biayanya tidak cukup – perhatikan seberapa tinggi perkiraan biaya untuk kueri pertama, yang hanya mengembalikan 50 baris, dibandingkan dengan kueri kedua, yang mengembalikan 15.000 baris!
Kisi laporan menunjukkan perkiraan biaya
Dan tab Operasi Teratas menunjukkan bahwa kueri pertama (mencari 2013-09-01
) diperkirakan akan menemukan 15.000 baris, padahal sebenarnya hanya menemukan 50; kueri kedua menunjukkan kebalikannya:ia diharapkan menemukan 50 baris yang cocok dengan 1786-05-01
, tetapi menemukan 15.000. Berdasarkan perkiraan kardinalitas yang salah seperti ini, saya yakin Anda dapat membayangkan efek drastis seperti apa yang dapat terjadi pada kueri yang lebih kompleks terhadap kumpulan data yang jauh lebih besar.
Tab Operasi Teratas untuk kueri pertama [DATEDIFF(BULAN, 0, GETDATE())]
Tab Operasi Teratas untuk kueri kedua [DATEDIFF(BULAN, 0, GETDATE())]
Variasi kueri yang sedikit berbeda, menggunakan ekspresi yang berbeda untuk menghitung awal bulan (disinggung di awal posting), tidak menunjukkan gejala ini:
SELECT /* Query 3 */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = CONVERT(DATE, DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE()));
Paket ini sangat mirip dengan kueri 1 di atas, dan jika Anda tidak melihat lebih dekat, Anda akan berpikir bahwa paket ini setara:
Rencana grafis untuk kueri non-DATEDIFF
Namun, ketika Anda melihat tab Operasi Teratas di sini, Anda melihat bahwa perkiraannya tepat:
Tab Operasi Teratas menampilkan taksiran akurat
Pada ukuran dan kueri data khusus ini, dampak kinerja bersih (terutama durasi dan pembacaan) sebagian besar tidak relevan. Dan penting untuk dicatat bahwa kueri itu sendiri masih mengembalikan data yang benar; hanya saja perkiraannya salah (dan bisa mengarah ke rencana yang lebih buruk daripada yang saya tunjukkan di sini). Yang mengatakan, jika Anda menurunkan konstanta menggunakan DATEDIFF dalam kueri Anda dengan cara ini, Anda benar-benar harus menguji dampak ini di lingkungan Anda.
Jadi mengapa ini terjadi?
Sederhananya, SQL Server memiliki DATEDIFF
bug di mana ia menukar argumen kedua dan ketiga saat mengevaluasi ekspresi untuk estimasi kardinalitas. Ini tampaknya melibatkan pelipatan konstan, setidaknya secara periferal; ada lebih banyak detail tentang pelipatan konstan dalam artikel Buku Daring ini tetapi, sayangnya, artikel tersebut tidak mengungkapkan informasi apa pun tentang bug khusus ini.
Ada perbaikan – atau ada?
Ada artikel basis pengetahuan (KB #2481274) yang mengklaim dapat mengatasi masalah tersebut, tetapi artikel tersebut memiliki beberapa masalah sendiri:
- Artikel KB mengklaim bahwa masalah telah diperbaiki di berbagai paket layanan atau pemutakhiran kumulatif untuk SQL Server 2005, 2008 dan 2008 R2. Namun, gejala itu masih ada di cabang-cabang yang tidak disebutkan secara eksplisit di sana, meskipun mereka telah melihat banyak CU tambahan sejak artikel itu diterbitkan. Saya masih dapat mereproduksi masalah ini pada SQL Server 2008 SP3 CU #8 (10.0.5828) dan SQL Server 2012 SP1 CU #5 (11.0.3373).
- Abaikan untuk menyebutkan bahwa, untuk mendapatkan manfaat dari perbaikan, Anda perlu mengaktifkan tanda pelacakan 4199 (dan "menguntungkan" dari semua cara lain yang dapat memengaruhi tanda pelacakan tertentu pada pengoptimal). Fakta bahwa tanda pelacakan ini diperlukan untuk perbaikan disebutkan dalam item Connect terkait, #630583, tetapi informasi ini belum kembali ke artikel KB. Baik artikel KB maupun item Hubungkan tidak memberikan wawasan apa pun tentang penyebabnya (bahwa argumen ke
DATEDIFF
telah ditukar selama evaluasi). Di sisi positifnya, menjalankan kueri di atas dengan tanda pelacakan aktif (menggunakanOPTION (QUERYTRACEON 4199)
) menghasilkan rencana yang tidak memiliki masalah perkiraan yang salah.
- Ini menyarankan Anda menggunakan SQL dinamis untuk mengatasi masalah ini. Dalam pengujian saya, menggunakan ekspresi yang berbeda (seperti yang di atas yang tidak menggunakan
DATEDIFF
) mengatasi masalah dalam versi modern dari SQL Server 2008 dan SQL Server 2012. Merekomendasikan SQL dinamis di sini tidak perlu rumit dan mungkin berlebihan, mengingat ekspresi yang berbeda dapat memecahkan masalah. Tetapi jika Anda menggunakan SQL dinamis, saya akan melakukannya dengan cara ini daripada cara yang mereka rekomendasikan di artikel KB, yang paling penting untuk meminimalkan risiko injeksi SQL:DECLARE @date DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0), @sql NVARCHAR(MAX) = N'SELECT COUNT(*) FROM dbo.DateTest WHERE CreateDate = @date;'; EXEC sp_executesql @sql, N'@date DATE', @date;
(Dan Anda dapat menambahkan
OPTION (RECOMPILE)
di sana, tergantung pada bagaimana Anda ingin SQL Server menangani parameter sniffing.)Ini mengarah ke paket yang sama dengan kueri sebelumnya yang tidak menggunakan
DATEDIFF
, dengan perkiraan yang tepat dan 99,1% dari biaya dalam pencarian indeks berkerumun.Pendekatan lain yang mungkin menggoda Anda (dan oleh Anda, maksud saya saya, ketika saya pertama kali mulai menyelidiki) adalah dengan menggunakan variabel untuk menghitung nilai sebelumnya:
DECLARE @d DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0); SELECT COUNT(*) FROM dbo.DateTest WHERE CreateDate = @d;
Masalah dengan pendekatan ini adalah bahwa, dengan variabel, Anda akan berakhir dengan rencana yang stabil, tetapi kardinalitas akan didasarkan pada tebakan (dan jenis tebakan akan bergantung pada ada atau tidaknya statistik) . Dalam hal ini, berikut adalah perkiraan vs. aktual:
Tab Operasi Teratas untuk kueri yang menggunakan variabelIni jelas tidak benar; tampaknya SQL Server telah menebak bahwa variabel akan cocok dengan 50% dari baris dalam tabel.
SQL Server 2014
Saya menemukan masalah yang sedikit berbeda di SQL Server 2014. Dua kueri pertama telah diperbaiki (dengan perubahan pada penaksir kardinalitas atau perbaikan lainnya), artinya DATEDIFF
argumen tidak lagi dialihkan. Ya!
Namun, regresi tampaknya telah diperkenalkan ke solusi penggunaan ekspresi yang berbeda – sekarang ia mengalami perkiraan yang tidak akurat (berdasarkan tebakan 50% yang sama dengan menggunakan variabel). Ini adalah pertanyaan yang saya jalankan:
SELECT /* 0, GETDATE() (2013) */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0); SELECT /* GETDATE(), 0 (1786) */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = DATEADD(MONTH, DATEDIFF(MONTH, GETDATE(), 0), 0); SELECT /* Non-DATEDIFF */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = CONVERT(DATE, DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE())); DECLARE @d DATE = DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE()); SELECT /* Variable */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = @d; DECLARE @date DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0), @sql NVARCHAR(MAX) = N'SELECT /* Dynamic SQL */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = @date;'; EXEC sp_executesql @sql, N'@date DATE', @date;
Berikut adalah kisi pernyataan yang membandingkan perkiraan biaya dan metrik waktu proses aktual:
Perkiraan biaya untuk 5 kueri spesimen di SQL Server 2014
Dan ini adalah perkiraan dan jumlah baris yang sebenarnya (dirakit menggunakan Photoshop):
Perkiraan dan jumlah baris aktual untuk 5 kueri di SQL Server 2014
Jelas dari keluaran ini bahwa ekspresi yang sebelumnya memecahkan masalah kini telah memperkenalkan ekspresi yang berbeda. Saya tidak yakin apakah ini gejala berjalan di CTP (misalnya sesuatu yang akan diperbaiki) atau apakah ini benar-benar regresi.
Dalam hal ini, bendera jejak 4199 (sendiri) tidak berpengaruh; penaksir kardinalitas baru membuat tebakan dan tidak benar. Apakah itu mengarah ke masalah kinerja aktual sangat bergantung pada banyak faktor lain di luar cakupan posting ini.
Jika Anda menemukan masalah ini, Anda dapat – setidaknya dalam CTP saat ini – memulihkan perilaku lama menggunakan OPTION (QUERYTRACEON 9481, QUERYTRACEON 4199)
. Trace flag 9481 menonaktifkan estimator kardinalitas baru, seperti yang dijelaskan dalam catatan rilis ini (yang pasti akan hilang atau setidaknya bergerak di beberapa titik). Ini pada gilirannya mengembalikan perkiraan yang benar untuk non-DATEDIFF
versi kueri, tetapi sayangnya masih tidak menyelesaikan masalah saat tebakan dibuat berdasarkan variabel (dan menggunakan TF9481 saja, tanpa TF4199, memaksa dua kueri pertama untuk mundur ke perilaku pertukaran argumen lama).
Kesimpulan
Saya akui ini adalah kejutan besar bagi saya. Kudos to Martin Smith dan t-clausen.dk untuk bertahan dan meyakinkan saya bahwa ini adalah nyata dan bukan masalah yang dibayangkan. Juga terima kasih banyak kepada Paul White (@SQL_Kiwi) yang membantu saya menjaga kewarasan saya dan mengingatkan saya akan hal-hal yang tidak boleh saya katakan. :-)
Karena tidak mengetahui bug ini, saya bersikeras bahwa rencana kueri yang lebih baik dihasilkan hanya dengan mengubah teks kueri sama sekali, bukan karena perubahan spesifik. Ternyata, terkadang ada perubahan pada kueri yang Anda asumsikan tidak akan membuat perbedaan, sebenarnya akan. Jadi saya menyarankan jika Anda memiliki pola kueri serupa di lingkungan Anda, Anda mengujinya dan memastikan perkiraan kardinalitas keluar dengan benar. Dan buat catatan untuk mengujinya lagi saat Anda meningkatkan versi.