İki tarih sütunu için SARGable WHERE yantümcesi


24

SARGability hakkında ilginç bir soru olan ben var. Bu durumda, iki tarih sütunu arasındaki farkın bir tahminini kullanmakla ilgilidir. İşte kurulum:

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])

Çok sık göreceğim şey şunun gibi:

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

... kesinlikle SARGable değil. İndeks taramasıyla sonuçlanır, tüm 1000 satırları okur, hiç iyi değil. Tahmini satırlar kokuyor. Bunu asla üretime sokmazdın.

Hayır efendim, hoşuma gitmedi.

CTE'leri gerçekleştirebilmemiz iyi olurdu, çünkü bu teknik olarak daha iyi, daha SARGable yapmamıza yardımcı olacak. Fakat hayır, aynı uygulama planını üstümüze alıyoruz.

/*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;

Ve elbette, sabitleri kullanmadığımız için, bu kod hiçbir şeyi değiştirmez ve yarı SARGable bile değildir. Eğlenceli değil. Aynı uygulama planı.

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

Kendinizi şanslı hissediyorsanız ve bağlantı dizelerinizdeki tüm ANSI SET seçeneklerine uyuyorsanız, bir hesaplanmış sütun ekleyebilir ve üzerinde arama yapabilirsiniz ...

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

Bu size üç sorgudan oluşan bir endeks araması yapacaktır. Tuhaf adam, DateCol1'e 48 gün eklediğimiz yer. İle sorgu DATEDIFFiçinde WHEREfıkra, CTEhesaplanmış bir sütun üzerinde bir yüklemi ve son sorgu tüm size çok daha güzel tahminleri ile çok daha hoş bir plan vermek, vesaire.

Bununla yaşayabilirim.

Hangisi soruyu gündeme getiriyor: tek bir sorguda, bu aramayı gerçekleştirmek için SARGable bir yol var mı?

Temp tabloları yok, tablo değişkenleri yok, tablo yapısını değiştirmek yok ve görünüm yok.

Ben kendiliğinden bir araya gelme, CTE'ler, alt sorgular veya veri üzerinden çoklu geçişler konusunda iyiyim. SQL Server'ın herhangi bir sürümü ile çalışabilir.

Hesaplanan sütundan kaçınmak yapay bir sınırlamadır, çünkü bir sorgulama çözümüyle her şeyden daha çok ilgileniyorum.

Yanıtlar:


16

Bunu hızlıca ekleyerek bir cevap olarak var olmasını sağlayın (bunun istediğiniz cevap olmadığını bilmeme rağmen).

Bir endeksli hesaplanan sütun genellikle bu tür bir sorun için doğru çözümdür.

O:

  • yüklemi dizinlenebilir bir ifade haline getirir
  • daha iyi kardinalite tahmini için otomatik istatistik oluşturulmasına izin verir
  • gelmez gerek baz tablosundaki herhangi boşluk almak

Bu son noktada açıklığa kavuşmak için, hesaplanan sütunun bu durumda devam etmesi gerekli değildir :

-- 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);

Şimdi sorgu:

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;

... aşağıdaki önemsiz planı verir :

Yürütme planı

Martin Smith’in dediği gibi, yanlış ayar seçeneklerini kullanarak bağlantılarınız varsa, normal bir sütun oluşturabilir ve tetikleyicileri kullanarak hesaplanan değeri koruyabilirsiniz.

Bunların hepsi gerçekten, ( cevabı bir kenara koyarsanız), Aaron'un cevabında söylediği gibi çözülmesi gereken gerçek bir sorun varsa elbette .

Bu düşünmek için eğlenceli, ancak sorudaki makul kısıtlamalar göz önüne alındığında istediğinizi elde etmenin hiçbir yolunu bilmiyorum. Herhangi bir en uygun çözüm, bir tür yeni veri yapısı gerektirecek gibi görünüyor; En yakın olanımız, yukarıdaki gibi kalıcı olmayan bir hesaplanan sütun üzerinde bir indeks tarafından sağlanan 'fonksiyon indeksi' yaklaşımıdır.


12

SQL Server topluluğunun en büyük isimlerinden bazılarını riske atmak, boynumu sokacağım ve hayır diyeceğim.

Sorgunuzun SARGable olabilmesi için, temel olarak , bir dizindeki ardışık satır aralığındaki bir başlangıç ​​satırını tam olarak belirleyebilecek bir sorgu oluşturmanız gerekir . Endeksi ile ix_dates, satırlar arasındaki tarih farkı tarafından sipariş edilmez DateCol1ve DateCol2hedef satırları endeksinde yerde yayılmış olabilir, böylece.

Kendiliğinden birleşimler, çoklu geçişler, vb., Ortak olarak, en az bir İndeks Taraması içerdiklerinde ortaktır, bununla birlikte (iç içe bir döngü) birleşmesi bir İndeks Arayışı'nı iyi kullanabilir. Ancak, Taramayı ortadan kaldırmanın nasıl mümkün olacağını göremiyorum.

Daha doğru satır tahminleri almak için tarih farkına dair istatistik yoktur.

Aşağıdaki, oldukça çirkin özyinelemeli CTE yapısı teknik olarak tüm masanın taranmasını elimine etse de, Nested Loop Join'i ve (potansiyel olarak çok büyük) bir Index Seeks tanıtır.

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);

Her içeren bir Ana biriktirme oluşturur DateCol1tabloda, daha sonra bir İndeksi, her biri için (aralık tarama) aln gerçekleştirir DateCol1ve DateCol2ileri olarak en az 48 gün.

Daha fazla IO, biraz daha uzun yürütme süresi, satır tahmini hala yolunda değil ve özyinelemeden dolayı sıfır paralellik şansı: Sanırım bu sorgu, göreceli olarak az sayıda birbirini takip eden birkaç ardışık içinde çok fazla sayıda değere sahipseniz faydalı olabilir. DateCol1(Aran sayısını azaltır).

Çılgın özyinelemeli CTE sorgu planı


9

Bir sürü tuhaf varyasyon denedim, ancak sizinkinden daha iyi bir sürüm bulamadım. Asıl sorun, dizininizin tarih1 ve tarih2'nin nasıl birarada sıralandığına göre görünmesidir. İlk sütun güzel bir raf çizgisinde olacak ve aralarındaki boşluk çok pürüzlü olacak. Bunun gerçekten olacağından çok bir huni gibi görünmesini istiyorsun:

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

İki nokta arasında belirli bir delta (veya bir dizi delta) için aranabilir hale getirmeyi düşünebileceğim bir yol yok. Ve demek istediğim, bir kez yürütülen tek bir arama + aralık taraması, her satır için yürütülen bir arama değil. Bu, bir noktada bir tarama ve / veya bir sıralama içerecektir ve bunlar açıkça kaçınmak istediğiniz şeylerdir. Filtrelenmiş dizinler gibi DATEADD/ DATEDIFFiçinde ifadeler kullanamamanız veya tarih farkının ürününde bir sıralama yapabilecek (şema / güncelleme zamanında deltayı hesaplamak gibi) olası şema değişikliklerini yapmamak çok kötü . Olduğu gibi, bu bir taramanın aslında en uygun alma yöntemi olduğu durumlardan biri gibi görünüyor.

Bu sorgunun eğlenceli olmadığını söylediniz, ancak daha yakından bakarsanız, bu en iyisidir (ve hesaplama skalar çıktısını bıraksanız daha iyi olur):

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

Bunun nedeni, endeksteki sadece öncü olmayan kilit sütuna DATEDIFFkarşı yapılan hesaplamaya kıyasla bazı CPU'ları potansiyel olarak tıraş etmekten kaçınmak ve ayrıca bazı gizli örtük dönüşümlerden kaçınmaktır (neden orada olduklarını sorma bana). İşte sürüm:datetimeoffset(7)DATEDIFF

<Predicate>
<ScalarOperator ScalarString = "datediff (gün, CONVERT_IMPLICIT (datetimeoffset (7), [bölme]. [Dbo]. [Sargme]. [DateCol1] [s]. [DateCol1], 0), [DateCol1] olarak [s]. 7), [ayrıl]. [Dbo]. [Sargme]. [DateCol2] [s] olarak. [DateCol2], 0))> = (48) ">

Ve işte olmayan DATEDIFF:

<Predicate>
<ScalarOperator ScalarString = "[ayırma]. [Dbo]. [Sargme]. [DateCol2] [s] olarak [DateCol2]> = dateadd (gün, (48), [splunge]. sargme]. [DateCol1] [s] olarak [DateCol1]) ">

Ayrıca, dizini yalnızca içerecek şekilde değiştirdiğimde süre açısından biraz daha iyi sonuçlar buldum DateCol2(ve her iki dizinin de bulunduğu durumlarda, SQL Server her zaman bir anahtarlı birini ve bir sütun ve çoklu anahtar içerenleri seçti). Bu sorgu için, aralığı yine de bulmak için tüm satırları taramamız gerektiğinden, ikinci tarih sütununu anahtarın bir parçası olarak ayırmanın ve herhangi bir şekilde sıralamanın faydası yoktur. Bir süredir bildiğim biz hakkında doğal bir şey iyi-duygu vardır, bir buraya aramaya alamayan değil lider anahtar sütunu karşı hesaplamalar zorlamak ve sadece ikincil veya dahil edilen sütunların karşı onları gerçekleştirerek tane almak yeteneğini engelleyen.

Eğer ben olsaydım ve sarsılabilir bir çözüm bulmaktan vazgeçtim, hangisini seçeceğimi biliyorum - SQL Server'ı en az miktarda iş yapmasını sağlayan (delta neredeyse bulunmasa bile). Ya da daha iyisi, şema değişikliği ve benzerleriyle ilgili kısıtlamalarımı gevşetirim.

Ve bunların hepsi ne kadar önemli? Bilmiyorum. Tabloyu 10 milyon satır yaptım ve yukarıdaki sorgu varyasyonlarının tümü hala bir saniyenin altında tamamlandı. Ve bu bir dizüstü bilgisayardaki bir VM'de (SSD ile verilir).


3

NEREDE yan tümcesinin sarg mümkün kılmasını sağlamak için düşündüğüm bütün yollar karmaşık ve endekse doğru çalışmak bir araçtan ziyade bir amaç gibi görünmek istiyor. Yani, hayır, bunun (pragmatik) mümkün olduğunu sanmıyorum.

"Tablo yapısını değiştirmeden" ek indeks anlamına gelmediğinden emin değildim. İşte indeks taramalarını tamamen önleyen, fakat LOT ayrı indeks aramaları ile sonuçlanan , yani tablodaki Min / Maks tarih aralığında her olası DateCol1 tarihi için bir sonuç veren bir çözüm . (Birinde sonuçlanan Daniel’lerin aksine, gerçekte tabloda görünen her ayrı tarihi ararlar). Teorik olarak, yinelemeden kaçındığı b / c paralelliğine adaydır. Ama dürüst olmak gerekirse, bu şeyin DATEDIFF'i taramak ve yapmaktan daha hızlı olduğu bir veri dağılımı görmek zor. (Belki gerçekten yüksek bir DOP?) Ve ... kod çirkin. Sanırım bu çaba bir "zihinsel alıştırma" olarak sayılıyor.

--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 

3

Asıl soru, yazar tarafından soruya düzenleme olarak eklenen Topluluk Wiki yanıtı

Bunun bir süre beklemesine izin verdikten ve bazı akıllı insanlar içeri girdikten sonra, bu konudaki ilk düşüncem doğru görünüyor: Bu sorguyu, hesaplanan veya hesaplanan başka bir mekanizma ile bir sütun eklemeden yazmanın akıllıca ve SARGable bir yolu yok. tetikleyiciler.

Birkaç şey daha denedim ve okuyanların ilgisini çekebilecek ya da çekmeyecek başka gözlemlerim var.

İlk olarak, temp şablonu yerine normal bir tablo kullanarak kurulumu yeniden çalıştırma

  • Onların itibarını bilmeme rağmen, çok sütunlu istatistikleri denemek istedim. Onlar işe yaramazdı.
  • Hangi istatistiklerin kullanıldığını görmek istedim.

İşte yeni kurulum:

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])

Ardından, ilk sorguyu çalıştırarak, daha önce olduğu gibi ix_dates dizinini ve taramaları kullanır. Burada değişiklik yok. Bu gereksiz görünüyor, ama benimle kal.

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

CTE sorgusunu tekrar çalıştırın, yine de aynı ...

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

Peki! Yarım bile değil-sargable sorguyu tekrar çalıştırın:

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

Şimdi hesaplanan sütunu ekleyin ve hesaplanan sütuna isabet eden sorgunun yanı sıra üçünü de yeniden çalıştırın:

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

Buraya benimle sıkışırsan, teşekkürler. Bu, gönderinin ilginç gözlem kısmıdır.

Her bir sorgunun hangi istatistikleri kullandığını görmek için Fabiano Amorim tarafından belgelenmemiş iz bayrağıyla bir sorgu yapmak . Hiçbir planın hesaplanan sütun oluşturulup dizine eklenene kadar bir istatistik nesnesine dokunmadığını görmek garip görünüyordu.

Kan lekesi ne

Heck, SADECE hesaplanan sütuna çarpan sorgu bile birkaç kez koyana kadar basit bir parametreleme yapana kadar bir istatistik nesnesine dokunmadı. Bu nedenle, hepsi başlangıçta ix_dates endeksini taramasına rağmen, kendileri için mevcut herhangi bir istatistik nesnesinden ziyade kodlanmış önem derecesi tahminleri (tablonun% 30'u) kullandılar.

Buraya kaş ekleyen başka bir nokta, yalnızca kümelenmemiş dizini eklediğimde, sorgunun her iki tarih sütununda da kümelenmemiş dizini kullanmak yerine HEAP taranan planları olmasıdır.

Yanıt veren herkese teşekkürler. Hepiniz harikasınız.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.