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

Kejutan dan Asumsi Kinerja :DATEADD

Kembali pada tahun 2013, saya menulis tentang bug di pengoptimal di mana argumen ke-2 dan ke-3 untuk DATEDIFF() dapat ditukar – yang dapat menyebabkan perkiraan jumlah baris yang salah dan, pada gilirannya, pemilihan rencana eksekusi yang buruk:

  • Kejutan dan Asumsi Kinerja :DATEDIFF

Akhir pekan terakhir ini, saya belajar tentang situasi yang sama, dan langsung berasumsi bahwa itu adalah masalah yang sama. Lagi pula, gejalanya tampak hampir identik:

  1. Ada fungsi tanggal/waktu di WHERE klausa.
    • Kali ini DATEADD() bukannya DATEDIFF() .
  2. Jelas ada perkiraan jumlah baris 1 yang salah, dibandingkan dengan jumlah baris sebenarnya lebih dari 3 juta.
    • Ini sebenarnya merupakan perkiraan 0, tetapi SQL Server selalu mengumpulkan perkiraan tersebut menjadi 1.
  3. Pemilihan rencana yang buruk dibuat (dalam hal ini, gabungan loop dipilih) karena perkiraan yang rendah.

Pola yang menyinggung terlihat seperti ini:

WHERE [datetime2(7) column] >= DATEADD(DAY, -365, SYSUTCDATETIME());

Pengguna mencoba beberapa variasi, tetapi tidak ada yang berubah; mereka akhirnya berhasil mengatasi masalah tersebut dengan mengubah predikat menjadi:

WHERE DATEDIFF(DAY, [column], SYSUTCDATETIME()) <= 365;

Ini mendapat perkiraan yang lebih baik (tebakan ketidaksetaraan 30% yang khas); jadi kurang tepat. Dan sementara itu menghilangkan loop join, ada dua masalah utama dengan predikat ini:

  1. Ini tidak kueri yang sama, karena sekarang mencari batas 365 hari yang telah berlalu, sebagai lawan lebih besar dari titik waktu tertentu 365 hari yang lalu. signifikan secara statistik? Mungkin tidak. Tapi ambang, secara teknis, tidak sama.
  2. Menerapkan fungsi pada kolom membuat seluruh ekspresi tidak dapat dimaklumi – mengarah ke pemindaian penuh. Ketika tabel hanya berisi sedikit lebih dari satu tahun data, ini bukan masalah besar, tetapi ketika tabel semakin besar, atau predikat menjadi lebih sempit, ini akan menjadi masalah.

Sekali lagi, saya menyimpulkan bahwa DATEADD() operasi adalah masalahnya, dan merekomendasikan pendekatan yang tidak bergantung pada DATEADD() – membangun datetime dari semua bagian waktu saat ini, memungkinkan saya untuk mengurangi satu tahun tanpa menggunakan DATEADD() :

WHERE [column] >= DATETIMEFROMPARTS(
      DATEPART(YEAR,   SYSUTCDATETIME())-1, 
      DATEPART(MONTH,  SYSUTCDATETIME()),
      DATEPART(DAY,    SYSUTCDATETIME()),
      DATEPART(HOUR,   SYSUTCDATETIME()), 
      DATEPART(MINUTE, SYSUTCDATETIME()),
      DATEPART(SECOND, SYSUTCDATETIME()), 0);

Selain menjadi besar, ini memiliki beberapa masalah sendiri, yaitu bahwa banyak logika harus ditambahkan untuk memperhitungkan tahun kabisat dengan benar. Pertama, agar tidak gagal jika kebetulan berjalan pada tanggal 29 Februari, dan kedua, untuk memasukkan tepat 365 hari dalam semua kasus (bukan 366 selama setahun setelah hari kabisat). Perbaikan mudah, tentu saja, tetapi membuat logika jauh lebih buruk – terutama karena kueri harus ada di dalam tampilan, di mana variabel perantara dan beberapa langkah tidak mungkin dilakukan.

Sementara itu, OP mengajukan item Connect, kecewa dengan perkiraan 1 baris:

  • Hubungkan #2567628 :Kendala dengan DateAdd() tidak memberikan perkiraan yang baik

Kemudian Paul White (@SQL_Kiwi) datang dan, seperti beberapa kali sebelumnya, menjelaskan beberapa masalah tambahan. Dia membagikan item Connect terkait yang diajukan oleh Erland Sommarskog pada tahun 2011:

  • Hubungkan #685903 :Perkiraan salah saat sysdatetime muncul dalam ekspresi dateadd()

Pada dasarnya, masalahnya adalah bahwa perkiraan yang buruk dapat dibuat tidak hanya ketika SYSDATETIME() (atau SYSUTCDATETIME() ) muncul, seperti yang dilaporkan Erland, tetapi ketika datetime2 ekspresi terlibat dalam predikat (dan mungkin hanya jika DATEADD() juga digunakan). Dan itu bisa berjalan dua arah – jika kita menukar >= untuk <= , perkiraan menjadi seluruh tabel, sehingga tampaknya pengoptimal melihat SYSDATETIME() nilai sebagai konstanta, dan sepenuhnya mengabaikan operasi apa pun seperti DATEADD() yang dilakukan terhadapnya.

Paul membagikan bahwa solusinya hanyalah menggunakan datetime setara saat menghitung tanggal, sebelum mengubahnya menjadi tipe data yang tepat. Dalam hal ini, kita dapat menukar SYSUTCDATETIME() dan ubah ke GETUTCDATE() :

WHERE [column] >= CONVERT(datetime2(7), DATEADD(DAY, -365, GETUTCDATE()));

Ya, ini menghasilkan sedikit kehilangan presisi, tetapi partikel debu juga dapat memperlambat jari Anda saat menekan F5 kunci. Yang penting seek masih bisa digunakan dan perkiraannya benar – hampir sempurna, sebenarnya:

Pembacaan serupa karena tabel berisi data hampir secara eksklusif dari tahun lalu, sehingga pencarian pun menjadi pemindaian rentang sebagian besar tabel. Jumlah baris tidak identik karena (a) kueri kedua terputus pada tengah malam dan (b) kueri ketiga menyertakan satu hari data tambahan karena hari kabisat awal tahun ini. Bagaimanapun, ini masih menunjukkan bagaimana kita bisa mendekati perkiraan yang tepat dengan menghilangkan DATEADD() , tetapi perbaikan yang tepat adalah menghapus kombinasi langsung dari DATEADD() dan datetime2 .

Untuk mengilustrasikan lebih lanjut bagaimana perkiraan menjadi salah, Anda dapat melihat bahwa jika kami memberikan argumen dan arah yang berbeda ke kueri asli dan penulisan ulang Paul, jumlah baris yang diperkirakan untuk yang pertama selalu didasarkan pada waktu saat ini – mereka tidak 't berubah dengan jumlah hari yang berlalu (sedangkan Paul relatif akurat setiap waktu):

Baris sebenarnya untuk kueri pertama sedikit lebih rendah karena ini dijalankan setelah tidur siang yang lama

Perkiraan tidak akan selalu sebaik ini; tabel saya hanya memiliki distribusi yang relatif stabil. Saya mengisinya dengan kueri berikut dan kemudian memperbarui statistik dengan pemindaian penuh, jika Anda ingin mencobanya sendiri:

-- OP's table definition:
CREATE TABLE dbo.DateaddRepro 
(
  SessionId  int IDENTITY(1, 1) NOT NULL PRIMARY KEY,
  CreatedUtc datetime2(7) NOT NULL DEFAULT SYSUTCDATETIME()
);
GO
 
CREATE NONCLUSTERED INDEX [IX_User_Session_CreatedUtc]
ON dbo.DateaddRepro(CreatedUtc) INCLUDE (SessionId);
GO
 
INSERT dbo.DateaddRepro(CreatedUtc)
SELECT dt FROM 
(
  SELECT TOP (3150000) dt = DATEADD(HOUR, (s1.[precision]-ROW_NUMBER()
    OVER (PARTITION BY s1.[object_id] ORDER BY s2.[object_id])) / 15, GETUTCDATE())
  FROM sys.all_columns AS s1 CROSS JOIN sys.all_objects AS s2
) AS x;
 
UPDATE STATISTICS dbo.DateaddRepro WITH FULLSCAN;
 
SELECT DISTINCT SessionId FROM dbo.DateaddRepro 
WHERE /* pick your WHERE clause to test */;

Saya mengomentari item Connect baru, dan kemungkinan akan kembali dan memperbaiki jawaban Stack Exchange saya.

Moral cerita

Cobalah untuk menghindari penggabungan DATEADD() dengan ekspresi yang menghasilkan datetime2 , terutama pada versi SQL Server yang lebih lama (ini ada di SQL Server 2012). Ini juga bisa menjadi masalah, bahkan di SQL Server 2016, saat menggunakan model estimasi kardinalitas yang lebih lama (karena tingkat kompatibilitas yang lebih rendah, atau penggunaan flag jejak 9481 secara eksplisit). Masalah seperti ini tidak kentara dan tidak selalu langsung terlihat, jadi semoga ini berfungsi sebagai pengingat (bahkan mungkin bagi saya saat saya menemukan skenario serupa di lain waktu). Seperti yang saya sarankan di posting terakhir, jika Anda memiliki pola kueri seperti ini, periksa apakah Anda mendapatkan perkiraan yang benar, dan buat catatan di suatu tempat untuk memeriksanya lagi setiap kali ada perubahan besar dalam sistem (seperti peningkatan atau paket layanan).


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. RDBMS vs NoSQL

  2. Tampilan SQL

  3. Cara menggunakan klausa HAVING dalam SQL

  4. Bagaimana Rencana Paralel Memulai – Bagian 5

  5. Cara Menambahkan Posisi Peringkat Baris dalam SQL dengan RANK()