SARGable WHERE klausa untuk dua kolom tanggal

24

Saya memiliki pertanyaan menarik tentang SARGability. Dalam hal ini, ini tentang menggunakan predikat pada perbedaan antara dua kolom tanggal. Ini pengaturannya:

USE [tempdb]
SET NOCOUNT ON  

IF OBJECT_ID('tempdb..#sargme') IS NOT NULL
BEGIN
DROP TABLE #sargme
END

SELECT TOP 1000
IDENTITY (BIGINT, 1,1) AS ID,
CAST(DATEADD(DAY, [m].[severity] * -1, GETDATE()) AS DATE) AS [DateCol1],
CAST(DATEADD(DAY, [m].[severity], GETDATE()) AS DATE) AS [DateCol2]
INTO #sargme
FROM sys.[messages] AS [m]

ALTER TABLE [#sargme] ADD CONSTRAINT [pk_whatever] PRIMARY KEY CLUSTERED ([ID])
CREATE NONCLUSTERED INDEX [ix_dates] ON [#sargme] ([DateCol1], [DateCol2])

Apa yang akan sering saya lihat, adalah sesuatu seperti ini:

/*definitely not sargable*/
SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [#sargme] AS [s]
WHERE
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) >= 48;

... yang pastinya bukan SARGable. Ini menghasilkan pemindaian indeks, membaca semua 1000 baris, tidak baik. Baris yang diperkirakan bau. Anda tidak akan pernah memproduksinya.

Tidak pak, saya tidak suka itu.

Alangkah baiknya jika kita bisa mewujudkan CTE, karena itu akan membantu kita membuat ini, yah, lebih SARGable-er, secara teknis. Tapi tidak, kami mendapatkan rencana eksekusi yang sama seperti di bagian atas.

/*would be nice if it were sargable*/
WITH    [x] AS ( SELECT
                * ,
                DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) AS [ddif]
               FROM
                [#sargme] AS [s])
     SELECT
        *
     FROM
        [x]
     WHERE
        [x].[ddif] >= 48;

Dan tentu saja, karena kita tidak menggunakan konstanta, kode ini tidak mengubah apa pun, dan bahkan tidak separuh SARGable. Tidak menyenangkan. Paket eksekusi yang sama.

/*not even half sargable*/
SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [#sargme] AS [s]
WHERE
    [s].[DateCol2] >= DATEADD(DAY, 48, [s].[DateCol1])

Jika Anda merasa beruntung, dan mematuhi semua opsi SET ANSI di string koneksi Anda, Anda dapat menambahkan kolom yang dihitung, dan mencarinya ...

ALTER TABLE [#sargme] ADD [ddiff] AS 
DATEDIFF(DAY, DateCol1, DateCol2) PERSISTED

CREATE NONCLUSTERED INDEX [ix_dates2] ON [#sargme] ([ddiff], [DateCol1], [DateCol2])

SELECT [s].[ID] ,
       [s].[DateCol1] ,
       [s].[DateCol2]
FROM [#sargme] AS [s]
WHERE [ddiff] >= 48

Ini akan membuat Anda mencari indeks dengan tiga pertanyaan. Orang aneh keluar adalah tempat kami menambahkan 48 hari ke DateCol1. Kueri dengan DATEDIFFdalam WHEREklausa, CTEdan, kueri akhir dengan predikat pada kolom yang dikomputasi semua memberi Anda rencana yang jauh lebih baik dengan perkiraan yang jauh lebih bagus, dan semua itu.

Saya bisa hidup dengan ini.

Yang membawa saya ke pertanyaan: dalam satu permintaan, apakah ada cara SARGable untuk melakukan pencarian ini?

Tidak ada tabel temp, tidak ada variabel tabel, tidak mengubah struktur tabel, dan tidak ada tampilan.

Saya baik-baik saja dengan self-joins, CTE, subqueries, atau multiple pass atas data. Dapat bekerja dengan versi SQL Server apa pun.

Menghindari kolom yang dikomputasi adalah batasan buatan karena saya lebih tertarik pada solusi kueri daripada yang lainnya.

Erik Darling
sumber

Jawaban:

16

Hanya menambahkan ini dengan cepat sehingga ada sebagai jawaban (meskipun saya tahu itu bukan jawaban yang Anda inginkan).

Sebuah kolom dihitung diindeks biasanya merupakan solusi yang tepat untuk jenis masalah ini.

Saya t:

  • membuat predikat ekspresi yang dapat diindeks
  • memungkinkan statistik otomatis dibuat untuk perkiraan kardinalitas yang lebih baik
  • tidak perlu mengambil ruang di tabel dasar

Agar lebih jelas tentang poin terakhir itu, kolom yang dikomputasi tidak diharuskan bertahan dalam kasus ini:

-- Note: not PERSISTED, metadata change only
ALTER TABLE #sargme
ADD DayDiff AS DATEDIFF(DAY, DateCol1, DateCol2);

-- Index the expression
CREATE NONCLUSTERED INDEX index_name
ON #sargme (DayDiff)
INCLUDE (DateCol1, DateCol2);

Sekarang kueri:

SELECT
    S.ID,
    S.DateCol1,
    S.DateCol2,
    DATEDIFF(DAY, S.DateCol1, S.DateCol2)
FROM
    #sargme AS S
WHERE
    DATEDIFF(DAY, S.DateCol1, S.DateCol2) >= 48;

... memberikan rencana sepele berikut :

Rencana eksekusi

Seperti yang dikatakan Martin Smith, jika Anda memiliki koneksi menggunakan opsi set yang salah, Anda bisa membuat kolom reguler dan mempertahankan nilai yang dihitung menggunakan pemicu.

Semua ini hanya penting (mengabaikan kode) jika ada masalah nyata untuk dipecahkan, tentu saja, seperti yang Aaron katakan dalam jawabannya .

Ini menyenangkan untuk dipikirkan, tetapi saya tidak tahu cara untuk mencapai apa yang Anda inginkan secara wajar mengingat kendala dalam pertanyaan. Sepertinya setiap solusi optimal akan memerlukan struktur data baru dari beberapa jenis; perkiraan terdekat kita adalah perkiraan 'fungsi indeks' yang disediakan oleh indeks pada kolom yang dihitung non-persisten seperti di atas.

Paul White mengatakan GoFundMonica
sumber
12

Menertawakan ejekan dari beberapa nama terbesar di komunitas SQL Server, saya akan mencuat dan berkata, tidak.

Agar kueri Anda menjadi SARGable, Anda harus membuat kueri yang pada dasarnya dapat menentukan baris awal dalam berbagai baris berturut-turut dalam indeks. Dengan indeks ix_dates, baris tidak diurutkan berdasarkan perbedaan tanggal antara DateCol1dan DateCol2, sehingga baris target Anda dapat tersebar di mana saja dalam indeks.

Bergabung sendiri, beberapa lintasan, dll. Semua memiliki kesamaan bahwa mereka menyertakan setidaknya satu Pemindaian Indeks, meskipun bergabung (loop bersarang) mungkin menggunakan Pencarian Indeks. Tapi saya tidak bisa melihat bagaimana mungkin untuk menghilangkan Scan.

Sedangkan untuk mendapatkan perkiraan baris yang lebih akurat, tidak ada statistik tentang perbedaan tanggal.

Berikut ini, konstruksi CTE rekursif yang cukup jelek tidak secara teknis menghilangkan pemindaian seluruh tabel, meskipun itu memperkenalkan Nested Loop Join dan sejumlah (mungkin sangat besar) Indeks yang dicari.

DECLARE @from date, @count int;
SELECT TOP 1 @from=DateCol1 FROM #sargme ORDER BY DateCol1;
SELECT TOP 1 @count=DATEDIFF(day, @from, DateCol1) FROM #sargme WHERE DateCol1<=DATEADD(day, -48, {d '9999-12-31'}) ORDER BY DateCol1 DESC;

WITH cte AS (
    SELECT 0 AS i UNION ALL
    SELECT i+1 FROM cte WHERE i<@count)

SELECT b.*
FROM cte AS a
INNER JOIN #sargme AS b ON
    b.DateCol1=DATEADD(day, a.i, @from) AND
    b.DateCol2>=DATEADD(day, 48+a.i, @from)
OPTION (MAXRECURSION 0);

Ini membuat Indeks Kumparan berisi setiap DateCol1dalam tabel, kemudian melakukan Pencarian Indeks (pemindaian rentang) untuk masing-masing DateCol1dan DateCol2yang setidaknya 48 hari ke depan.

Lebih banyak IO, waktu eksekusi yang sedikit lebih lama, estimasi baris masih jauh, dan nol peluang paralelisasi karena rekursi: Saya menduga pertanyaan ini mungkin berguna jika Anda memiliki jumlah nilai yang sangat besar dalam waktu yang relatif sedikit berbeda, berturut-turut DateCol1(Menjaga jumlah mencari).

Rencana permintaan CTE rekursif gila

Daniel Hutmacher
sumber
9

Saya mencoba banyak variasi aneh, tetapi tidak menemukan versi yang lebih baik dari salah satu versi Anda. Masalah utama adalah bahwa indeks Anda terlihat seperti ini dalam hal bagaimana date1 dan date2 diurutkan bersama. Kolom pertama akan berada di garis rak yang bagus sementara celah di antara mereka akan sangat bergerigi. Anda ingin ini terlihat lebih seperti corong daripada yang sebenarnya:

Date1    Date2
-----    -------
*             *
*             *
*              *
 *       * 
 *        *
 *         *
  *      *
  *           *

Sebenarnya tidak ada cara yang dapat saya pikirkan untuk membuatnya dapat dicari untuk delta tertentu (atau kisaran delta) antara dua poin. Dan maksud saya satu pencarian yang dijalankan sekali + pemindaian kisaran, bukan pencarian yang dieksekusi untuk setiap baris. Itu akan melibatkan pemindaian dan / atau semacam di beberapa titik, dan ini adalah hal-hal yang ingin Anda hindari dengan jelas. Sayang sekali Anda tidak dapat menggunakan ekspresi seperti DATEADD/ DATEDIFFdalam indeks yang difilter, atau melakukan modifikasi skema apa pun yang memungkinkan jenis produk pada tanggal berbeda (seperti menghitung delta pada waktu insert / update). Seperti, ini tampaknya menjadi salah satu kasus di mana pemindaian sebenarnya adalah metode pengambilan yang optimal.

Anda mengatakan bahwa kueri ini tidak menyenangkan, tetapi jika Anda melihat lebih dekat, ini adalah yang terbaik (dan akan lebih baik jika Anda tidak menyertakan output skalar komputasi):

SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [#sargme] AS [s]
WHERE
    [s].[DateCol2] >= DATEADD(DAY, 48, [s].[DateCol1])

Alasannya adalah bahwa menghindari DATEDIFFkemungkinan mencukur beberapa CPU dibandingkan dengan perhitungan terhadap hanya kolom kunci tidak-terkemuka dalam indeks, dan juga menghindari beberapa konversi implisit jahat ke datetimeoffset(7)(jangan tanya mengapa mereka ada di sana, tetapi mereka). Ini DATEDIFFversinya:

<Predicate>
<ScalarOperator ScalarString = "Dateiff (hari, CONVERT_IMPLICIT (datetimeoffset (7), [splunge]. [Dbo]. [Sargme]. [DateCol1] sebagai [s]. [DateCol1], 0), CONVERT_IMPLICIT (tanggal lamanya) 7), [splunge]. [Dbo]. [Sargme]. [DateCol2] sebagai [s]. [DateCol2], 0))> = (48) ">

Dan inilah yang tanpa DATEDIFF:

<Predicate>
<ScalarOperator ScalarString = "[splunge]. [Dbo]. [Sargme]. [DateCol2] sebagai [s]. [DateCol2]> = dateadd (hari, (48), [splunge]. [Dbo]. [ sargme]. [DateCol1] sebagai [s]. [DateCol1]) ">

Saya juga menemukan hasil yang sedikit lebih baik dalam hal durasi ketika saya mengubah indeks hanya menyertakan DateCol2 (dan ketika kedua indeks hadir, SQL Server selalu memilih satu dengan satu kunci dan satu termasuk kolom vs multi-kunci). Untuk kueri ini, karena bagaimanapun kami harus memindai semua baris untuk menemukan rentang, tidak ada untungnya memiliki kolom tanggal kedua sebagai bagian dari kunci dan diurutkan dengan cara apa pun. Dan sementara saya tahu kita tidak bisa mendapatkan pencarian di sini, ada sesuatu yang secara inheren baik perasaan tidak menghalangi kemampuan untuk mendapatkannya dengan memaksa perhitungan terhadap kolom kunci utama, dan hanya melakukan mereka terhadap kolom sekunder atau termasuk.

Jika itu saya, dan saya menyerah untuk menemukan solusi yang cukup besar, saya tahu mana yang akan saya pilih - salah satu yang membuat SQL Server melakukan pekerjaan paling sedikit (bahkan jika delta hampir tidak ada). Atau lebih baik lagi saya akan melonggarkan batasan saya tentang perubahan skema dan sejenisnya.

Dan seberapa penting semua itu? Saya tidak tahu Saya membuat tabel 10 juta baris dan semua variasi kueri di atas masih selesai dalam waktu kurang dari satu detik. Dan ini pada VM di laptop (diberikan, dengan SSD).

Aaron Bertrand
sumber
3

Semua cara yang saya pikirkan untuk membuat WHERE sarg-mampu mampu rumit dan merasa seperti bekerja menuju indeks mencari sebagai tujuan akhir daripada sarana. Jadi, tidak, saya pikir itu (secara pragmatis) tidak mungkin.

Saya tidak yakin apakah "tidak mengubah struktur tabel" berarti tidak ada indeks tambahan. Berikut adalah solusi yang sepenuhnya menghindari pemindaian indeks, tetapi menghasilkan BANYAK indeks yang dicari secara terpisah, yaitu satu untuk setiap tanggal DateCol1 yang mungkin dalam rentang nilai tanggal Min / Max dalam tabel. (Tidak seperti Daniel yang menghasilkan satu pencarian untuk setiap tanggal berbeda yang benar-benar muncul di tabel). Secara teoritis itu adalah kandidat untuk paralelisme b / c itu menghindari rekursi. Tapi jujur, sulit untuk melihat distribusi data di mana hal ini lebih cepat daripada hanya memindai dan melakukan DATEIFF. (Mungkin DOP sangat tinggi?) Dan ... kodenya jelek. Saya kira upaya ini dianggap sebagai "latihan mental".

--Add this index to avoid the scan when determining the @MaxDate value
--CREATE NONCLUSTERED INDEX [ix_dates2] ON [#sargme] ([DateCol2]);
DECLARE @MinDate DATE, @MaxDate DATE;
SELECT @MinDate=DateCol1 FROM (SELECT TOP 1 DateCol1 FROM #sargme ORDER BY DateCol1 ASC) ss;
SELECT @MaxDate=DateCol2 FROM (SELECT TOP 1 DateCol2 FROM #sargme ORDER BY DateCol2 DESC) ss;

--Used 44 just to get a few more rows to test my logic
DECLARE @DateDiffSearchValue INT = 44, 
    @MinMaxDifference INT = DATEDIFF(DAY, @MinDate, @MaxDate);

--basic data profile in the table
SELECT [MinDate] = @MinDate, 
        [MaxDate] = @MaxDate, 
        [MinMaxDifference] = @MinMaxDifference, 
        [LastDate1SearchValue] = DATEADD(DAY, 0-@DateDiffSearchValue, @MaxDate);

;WITH rn_base AS (
SELECT [col1] = 0
        UNION ALL SELECT 0
        UNION ALL SELECT 0
        UNION ALL SELECT 0
),
rn_1 AS (
    SELECT t0.col1 FROM rn_base t0
        CROSS JOIN rn_base t1
        CROSS JOIN rn_base t2
        CROSS JOIN rn_base t3
),
rn_2 AS (
    SELECT rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM rn_1 t0
        CROSS JOIN rn_1 t1
),
candidate_searches AS (
    SELECT 
        [Date1_EqualitySearch] = DATEADD(DAY, t.rn-1, @MinDate),
        [Date2_RangeSearch] = DATEADD(DAY, t.rn-1+@DateDiffSearchValue, @MinDate)
    FROM rn_2 t
    WHERE DATEADD(DAY, t.rn-1, @MinDate) <= DATEADD(DAY, 0-@DateDiffSearchValue, @MaxDate)
    /* Of course, ignore row-number values that would result in a
       Date1_EqualitySearch value that is < @DateDiffSearchValue days before @MaxDate */
)
--select * from candidate_searches

SELECT c.*, xapp.*, dd_rows = DATEDIFF(DAY, xapp.DateCol1, xapp.DateCol2)
FROM candidate_searches c
    cross apply (
        SELECT t.*
        FROM #sargme t
        WHERE t.DateCol1 = c.date1_equalitysearch
        AND t.DateCol2 >= c.date2_rangesearch
    ) xapp
ORDER BY xapp.ID asc --xapp.DateCol1, xapp.DateCol2 
Aaron Morelli
sumber
3

Jawaban Wiki Komunitas awalnya ditambahkan oleh penulis pertanyaan sebagai edit pada pertanyaan

Setelah membiarkan ini duduk sebentar, dan beberapa orang yang benar-benar pintar berdebat, pemikiran awal saya tentang ini tampaknya benar: tidak ada cara yang waras dan SARGable untuk menulis permintaan ini tanpa menambahkan kolom, baik dihitung, atau dikelola melalui mekanisme lain, yaitu pemicu.

Saya memang mencoba beberapa hal lain, dan saya memiliki beberapa pengamatan lain yang mungkin atau mungkin tidak menarik bagi siapa pun yang membaca.

Pertama, jalankan kembali pengaturan menggunakan tabel biasa dan bukan tabel temp

  • Meskipun saya tahu reputasi mereka, saya ingin mencoba statistik multi-kolom. Mereka tidak berguna.
  • Saya ingin melihat statistik mana yang digunakan

Inilah pengaturan baru:

USE [tempdb]
SET NOCOUNT ON  

DBCC FREEPROCCACHE

IF OBJECT_ID('tempdb..sargme') IS NOT NULL
BEGIN
DROP TABLE sargme
END

SELECT TOP 1000
IDENTITY (BIGINT, 1,1) AS ID,
CAST(DATEADD(DAY, [m].[severity] * -1, GETDATE()) AS DATE) AS [DateCol1],
CAST(DATEADD(DAY, [m].[severity], GETDATE()) AS DATE) AS [DateCol2]
INTO sargme
FROM sys.[messages] AS [m]

ALTER TABLE [sargme] ADD CONSTRAINT [pk_whatever] PRIMARY KEY CLUSTERED ([ID])
CREATE NONCLUSTERED INDEX [ix_dates] ON [sargme] ([DateCol1], [DateCol2])

CREATE STATISTICS [s_sargme] ON [sargme] ([DateCol1], [DateCol2])

Kemudian, menjalankan kueri pertama, ia menggunakan indeks ix_dates, dan memindai, seperti sebelumnya. Tidak ada perubahan di sini. Ini sepertinya berlebihan, tetapi tetaplah dengan saya.

SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [sargme] AS [s]
WHERE
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) >= 48

Jalankan kueri CTE lagi, masih sama ...

WITH    [x] AS ( SELECT
                * ,
                DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) AS [ddif]
               FROM
                [sargme] AS [s])
     SELECT
        *
     FROM
        [x]
     WHERE
        [x].[ddif] >= 48;

Baik! Jalankan kueri tidak-setengah-setengah lagi:

SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [sargme] AS [s]
WHERE
    [s].[DateCol2] >= DATEADD(DAY, 48, [s].[DateCol1])

Sekarang tambahkan kolom yang dihitung, dan jalankan kembali ketiganya, bersama dengan kueri yang mengenai kolom yang dihitung:

ALTER TABLE [sargme] ADD [ddiff] AS 
DATEDIFF(DAY, DateCol1, DateCol2) PERSISTED

CREATE NONCLUSTERED INDEX [ix_dates2] ON [sargme] ([ddiff], [DateCol1], [DateCol2])

SELECT [s].[ID] ,
       [s].[DateCol1] ,
       [s].[DateCol2]
FROM [sargme] AS [s]
WHERE [ddiff] >= 48

Jika Anda terjebak dengan saya di sini, terima kasih. Ini adalah bagian pengamatan yang menarik dari postingan ini.

Menjalankan kueri dengan tanda jejak tidak berdokumen oleh Fabiano Amorim untuk melihat statistik mana setiap kueri yang digunakan cukup keren. Melihat bahwa tidak ada rencana yang menyentuh objek statistik hingga kolom yang dihitung dibuat dan diindeks tampak aneh.

Apa gumpalan darah

Heck, bahkan kueri yang mengenai kolom dihitung HANYA tidak menyentuh objek statistik sampai saya menjalankannya beberapa kali dan mendapat parameterisasi sederhana. Jadi meskipun mereka semua awalnya memindai indeks ix_dates, mereka menggunakan perkiraan kardinalitas hard-coded (30% dari tabel) daripada objek statistik yang tersedia untuk mereka.

Satu hal lain yang mengangkat alis di sini adalah bahwa ketika saya menambahkan hanya indeks yang tidak tercakup, kueri rencana semua memindai HEAP, daripada menggunakan indeks yang tidak tercakup pada kedua kolom tanggal.

Terima kasih kepada semua orang yang merespons. Anda semua luar biasa.

Paul White mengatakan GoFundMonica
sumber