Cara meningkatkan taksiran 1 baris dalam Tampilan yang dibatasi oleh DateAdd () terhadap indeks

8

Menggunakan Microsoft SQL Server 2012 (SP3) (KB3072779) - 11.0.6020.0 (X64).

Diberikan tabel dan indeks:

create table [User].[Session] 
(
  SessionId int identity(1, 1) not null primary key
  CreatedUtc datetime2(7) not null default sysutcdatetime())
)

create nonclustered index [IX_User_Session_CreatedUtc]
on [User].[Session]([CreatedUtc]) include (SessionId)

Baris Aktual untuk masing-masing kueri berikut adalah 3,1M, baris yang diperkirakan ditampilkan sebagai komentar.

Ketika kueri ini memberi makan kueri lain dalam Tampilan , pengoptimal memilih loop bergabung karena perkiraan 1 baris. Bagaimana cara meningkatkan taksiran pada level dasar ini untuk menghindari mengesampingkan kueri gabungan join atau beralih ke SP?

Menggunakan tanggal hardcoded sangat bagus:

 select distinct SessionId from [User].Session -- 2.9M (great)
  where CreatedUtc > '04/08/2015'  -- but hardcoded

Kueri yang setara ini kompatibel dengan tampilan tetapi semua perkiraan 1 baris:

select distinct SessionId from [User].Session -- 1
 where CreatedUtc > dateadd(day, -365, sysutcdatetime())         

select distinct SessionId from [User].Session  -- 1
 where dateadd(day, 365, CreatedUtc) > sysutcdatetime();          

select distinct SessionId from [User].Session s  -- 1
 inner loop join  (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
    on d.MinCreatedUtc < s.CreatedUtc    
    -- (also tried reversing join order, not shown, no change)

select distinct SessionId from [User].Session s -- 1
 cross apply (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
 where d.MinCreatedUtc < s.CreatedUtc
    -- (also tried reversing join order, not shown, no change)

Cobalah beberapa petunjuk (tetapi Tidak Ada untuk Dilihat):

 select distinct SessionId from [User].Session -- 1
  where CreatedUtc > dateadd(day, -365, sysutcdatetime())
 option (recompile);

select distinct SessionId from [User].Session  -- 1
 where CreatedUtc > (select dateadd(day, -365, sysutcdatetime()))
 option (recompile, optimize for unknown);

select distinct SessionId                     -- 1
  from (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
 inner loop join [User].Session s    
    on s.CreatedUtc > d.MinCreatedUtc  
option (recompile);

Coba gunakan Parameter / Petunjuk (tapi N / A untuk Melihat):

declare
    @minDate datetime2(7) = dateadd(day, -365, sysutcdatetime());

select distinct SessionId from [User].Session  -- 1.2M (adequate)
 where CreatedUtc > @minDate;

select distinct SessionId from [User].Session  -- 2.96M (great)
 where CreatedUtc > @minDate
option (recompile);

select distinct SessionId from [User].Session  -- 1.2M (adequate)
 where CreatedUtc > @minDate
option (optimize for unknown);

Perkirakan vs Aktual

Statistiknya terkini.

DBCC SHOW_STATISTICS('user.Session', 'IX_User_Session_CreatedUtc') with histogram;

Beberapa baris terakhir dari histogram (total 189 baris) ditunjukkan:

masukkan deskripsi gambar di sini

crokusek
sumber

Jawaban:

6

Jawaban yang kurang komprehensif daripada Aaron tetapi masalah intinya adalah bug estimasi kardinalitas DATEADDketika menggunakan tipe datetime2 :

Connect: Perkiraan yang salah ketika sysdatetime muncul dalam ekspresi dateadd ()

Salah satu solusinya adalah menggunakan GETUTCDATE(yang mengembalikan datetime):

WHERE CreatedUtc > CONVERT(datetime2(7), DATEADD(DAY, -365, GETUTCDATE()))

Perhatikan konversi ke datetime2 harus di luar DATEADDuntuk menghindari bug.

Masalah estimasi kardinalitas 1 baris mereproduksi bagi saya di semua versi SQL Server hingga dan termasuk 2016 RC0 di mana 70 penduga kardinalitas model digunakan.

Aaron Bertrand telah menulis artikel tentang ini untuk SQLPerformance.com:

Paul White 9
sumber
6

Dalam beberapa skenario SQL Server dapat memiliki perkiraan yang benar-benar liar untuk DATEADD/ DATEDIFF, tergantung pada apa argumennya dan seperti apa data aktual Anda. Saya menulis tentang ini DATEDIFFketika berhadapan dengan awal bulan, dan beberapa solusi, di sini:

Tapi, saran khas saya adalah berhenti menggunakan DATEADD/ DATEDIFFdi mana / bergabung dengan klausa.

Pendekatan berikut, walaupun tidak super akurat ketika tahun kabisat ada dalam rentang yang difilter (itu akan mencakup satu hari ekstra dalam kasus itu), dan sementara dibulatkan ke hari itu, akan mendapatkan perkiraan yang lebih baik (tapi masih tidak hebat!), Sama seperti non-sargable Anda DATEDIFFterhadap pendekatan kolom, dan masih memungkinkan pencarian untuk digunakan:

DECLARE @start date = DATEFROMPARTS
(
  YEAR(GETUTCDATE())-1, 
  MONTH(GETUTCDATE()), 
  DAY(GETUTCDATE())
);

SELECT ... WHERE CreatedUtc >= @start;

Anda bisa memanipulasi input untuk DATEFROMPARTSmenghindari masalah pada hari kabisat, gunakan DATETIMEFROMPARTSuntuk mendapatkan lebih presisi daripada pembulatan ke hari, dll. Ini hanya untuk menunjukkan bahwa Anda dapat mengisi variabel dengan tanggal di masa lalu tanpa menggunakan DATEADD(itu hanya sedikit lebih banyak pekerjaan), dan karenanya hindari bagian yang lebih banyak dari bug estimasi (yang diperbaiki pada 2014+).

Untuk menghindari kesalahan pada hari kabisat, Anda dapat melakukan ini, mulai dari 28 Februari tahun lalu alih-alih 29:

DECLARE @start date = DATEFROMPARTS
(
  YEAR(GETUTCDATE())-1, 
  MONTH(GETUTCDATE()), 
  CASE WHEN DAY(GETUTCDATE()) = 29 AND MONTH(GETUTCDATE()) = 2 
    THEN 28 ELSE DAY(GETUTCDATE()) END
);

Anda juga bisa mengatakan menambahkan hari dengan memeriksa untuk melihat apakah kami melewati hari kabisat tahun ini, dan jika demikian, tambahkan hari ke awal (yang menarik, menggunakan di DATEADD sini masih memungkinkan untuk perkiraan yang akurat):

DECLARE @base date = GETUTCDATE();
IF GETUTCDATE() >= DATEFROMPARTS(YEAR(GETUTCDATE()),3,1) AND 
  TRY_CONVERT(datetime, DATEFROMPARTS(YEAR(GETUTCDATE()),2,29)) IS NOT NULL
BEGIN
  SET @base = DATEADD(DAY, 1, GETUTCDATE());
END

DECLARE @start date = DATEFROMPARTS
(
  YEAR(@base)-1, 
  MONTH(@base),
  CASE WHEN DAY(@base) = 29 AND MONTH(@base) = 2 
    THEN 28 ELSE DAY(@base) END
);

SELECT ... WHERE CreatedUtc >= @start;

Jika Anda perlu lebih akurat daripada hari di tengah malam, maka Anda bisa menambahkan lebih banyak manipulasi sebelum pilih:

DECLARE @accurate_start datetime2(7) = DATETIME2FROMPARTS
(
  YEAR(@start), MONTH(@start), DAY(@start),
  DATEPART(HOUR,  SYSUTCDATETIME()), 
  DATEPART(MINUTE,SYSUTCDATETIME()),
  DATEPART(SECOND,SYSUTCDATETIME()), 
  0,0
);

SELECT ... WHERE CreatedUtc >= @accurate_start;

Sekarang, Anda dapat menghentikan semua ini dalam tampilan, dan masih akan menggunakan pencarian dan perkiraan 30% tanpa memerlukan petunjuk atau jejak bendera, tapi itu tidak cantik. CTE bersarang hanya agar saya tidak perlu mengetik SYSUTCDATETIME()seratus kali atau mengulangi ekspresi yang digunakan kembali - mereka masih dapat dievaluasi beberapa kali.

CREATE VIEW dbo.v5 
AS
  WITH d(d) AS ( SELECT SYSUTCDATETIME() ),
  base(d) AS
  (
    SELECT DATEADD(DAY,CASE WHEN d >= DATEFROMPARTS(YEAR(d),3,1) 
      AND TRY_CONVERT(datetime,RTRIM(YEAR(d))+RIGHT('0'+RTRIM(MONTH(d)),2)
      +RIGHT('0'+RTRIM(DAY(d)),2)) IS NOT NULL THEN 1 ELSE 0 END, d)
    FROM d
  ),
  src(d) AS
  (
    SELECT DATETIME2FROMPARTS
    (
      YEAR(d)-1, 
      MONTH(d),
      CASE WHEN MONTH(d) = 2 AND DAY(d) = 29
        THEN 28 ELSE DAY(d) END,
      DATEPART(HOUR,d), 
      DATEPART(MINUTE,d),
      DATEPART(SECOND,d),
      10*DATEPART(MICROSECOND,d),
      7
    ) FROM base
  )
  SELECT DISTINCT SessionId FROM [User].[Session]
    WHERE CreatedUtc >= (SELECT d FROM src);

Ini jauh lebih banyak bertele-tele daripada DATEDIFFkolom Anda, tetapi seperti yang saya sebutkan di komentar , pendekatan itu tidak masuk akal, dan mungkin akan tampil kompetitif sementara sebagian besar tabel harus tetap dibaca, tapi saya curiga itu akan menjadi beban. sebagai "tahun lalu" menjadi persentase yang lebih rendah dari tabel.

Juga, hanya untuk referensi, berikut adalah beberapa metrik yang saya dapatkan ketika saya mencoba mereproduksi:

masukkan deskripsi gambar di sini

Saya tidak bisa mendapatkan perkiraan 1 baris, dan saya berusaha sangat keras untuk mencocokkan distribusi Anda (3,13 juta baris, 2,89 juta dari tahun lalu). Tapi Anda bisa melihat:

  • kedua solusi kami melakukan pembacaan yang kurang lebih setara.
  • solusi Anda sedikit kurang akurat karena hanya memperhitungkan batas-batas hari (dan itu mungkin baik-baik saja, pandangan saya bisa dibuat kurang tepat untuk mencocokkan).
  • 4199 + kompilasi ulang tidak benar-benar mengubah perkiraan (atau rencana).

Jangan menggambar terlalu banyak dari angka durasi - mereka sudah dekat sekarang, tetapi mungkin tidak tetap dekat saat tabel bertambah (sekali lagi, saya percaya karena bahkan pencarian masih harus membaca sebagian besar tabel).

Berikut adalah paket-paket untuk v4 (dateiff Anda terhadap kolom) dan v5 (versi saya):

masukkan deskripsi gambar di sini

masukkan deskripsi gambar di sini

Aaron Bertrand
sumber
Singkatnya, seperti yang dinyatakan dalam blog Anda . jawaban ini memberikan perkiraan yang dapat digunakan dan mencari rencana berbasis. Jawaban oleh @PaulWhite memberikan estimasi terbaik. Mungkin perkiraan 1 baris yang saya dapatkan (vs 1500) bisa jadi karena tabel tidak memiliki baris dalam ~ 24 jam terakhir.
crokusek
@crokusek Jika Anda mengatakan >= DATEADD(DAY, -365, SYSDATETIME())bahwa bug tersebut berdasarkan perkiraan >= SYSDATETIME(). Jadi secara teknis estimasi didasarkan pada berapa banyak baris dalam tabel memiliki CreatedUtcdi masa depan. Ini kemungkinan 0, tetapi SQL Server selalu membulatkan 0 hingga 1 untuk baris yang diperkirakan.
Aaron Bertrand
1

Ganti dateadd () dengan Dateiff () untuk mendapatkan perkiraan yang memadai (30% ish).

 select distinct SessionId from [User].Session     -- 1.2M est, 3.0M act.
  where datediff(day, CreatedUtc, sysutcdatetime()) <= 365

Ini tampaknya merupakan bug yang mirip dengan MS Connect 630583 .

Opsi kompilasi ulang tidak ada bedanya.

Rencanakan Statistik

crokusek
sumber
2
Perhatikan bahwa menerapkan tanggaliff pada kolom membuat ekspresi tidak dapat ditagih, jadi Anda harus memindai. Yang mungkin baik-baik saja ketika 90 +% dari tabel perlu dibaca, tetapi karena tabel semakin besar ini akan terbukti lebih mahal.
Aaron Bertrand
Poin yang bagus. Saya berpikir itu bisa mengubahnya secara internal. Diverifikasi bahwa ia melakukan pemindaian.
crokusek