Neden değişkeni satır içi yaparken SQL Server daha iyi bir yürütme planı kullanıyor?


32

Optimize etmeye çalıştığım bir SQL sorgusu var:

DECLARE @Id UNIQUEIDENTIFIER = 'cec094e5-b312-4b13-997a-c91a8c662962'

SELECT 
  Id,
  MIN(SomeTimestamp),
  MAX(SomeInt)
FROM dbo.MyTable
WHERE Id = @Id
  AND SomeBit = 1
GROUP BY Id

MyTable iki dizin var:

CREATE NONCLUSTERED INDEX IX_MyTable_SomeTimestamp_Includes
ON dbo.MyTable (SomeTimestamp ASC)
INCLUDE(Id, SomeInt)

CREATE NONCLUSTERED INDEX IX_MyTable_Id_SomeBit_Includes
ON dbo.MyTable (Id, SomeBit)
INCLUDE (TotallyUnrelatedTimestamp)

Sorguyu tam olarak yukarıda yazıldığı gibi yürüttüğümde, SQL Server ilk dizini tarar, sonuçta 189.703 mantıksal okuma ve 2-3 saniye süre kalır.

@IdDeğişkeni satır içine aldığımda ve sorguyu tekrar yürüttüğümde, SQL Server ikinci dizini arayarak yalnızca 104 mantıksal okuma ve 0.001 saniye süre (temelde anlık) elde etti.

Değişkeye ihtiyacım var ama SQL'in iyi planı kullanmasını istiyorum. Geçici bir çözüm olarak, sorguya bir dizin ipucu koydum ve sorgu temelde anında. Ancak, mümkün olduğunda indeks ipuçlarından uzak durmaya çalışıyorum. Sorgu en iyi duruma getiricisi işini yapamazsa, ne yapılacağını açıkça söylemeden yardımcı olmak için yapabileceğim (veya yapmayı bıraktığım) bir şey olduğunu kabul ediyorum.

Peki, değişkeni satır içi yaptığımda SQL Server neden daha iyi bir plan ortaya koyuyor?

Yanıtlar:


44

SQL Server'da, birleştirme olmayan yükleminin üç genel formu vardır:

Bir ile birebir değeri:

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = 1;

Bir parametre ile :

CREATE PROCEDURE dbo.SomeProc(@Reputation INT)
AS
BEGIN
    SELECT COUNT(*) AS records
    FROM   dbo.Users AS u
    WHERE  u.Reputation = @Reputation;
END;

Bir ile yerel değişken :

DECLARE @Reputation INT = 1

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = @Reputation;

Çıktıları

Değişmez bir değer kullandığınızda ve planınız a) Önemsiz ve b) Basit Parametreli veya c) Zorunlu Parametreyi açmamışsanız, optimize edici sadece bu değer için çok özel bir plan yaratır.

Bir parametre kullandığınızda , en iyi duruma getirici bu parametre için bir plan oluşturacaktır (buna parametre koklama denir ) ve daha sonra bu planı yeniden kullanın, ipuçlarını yeniden derleme, önbellek tahliyesi vb.

Bir kullandığınızda yerel değişken , iyileştirici için ... bir plan yapar Something .

Bu sorguyu çalıştırmanız gerekirse:

DECLARE @Reputation INT = 1

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = @Reputation;

Plan şöyle gözükürdü:

FINDIK

Ve bu yerel değişken için tahmini satır sayısı şöyle görünür:

FINDIK

Sorgu 4.744.427 sayısını döndürse bile.

Bilinmeyen yerel değişkenler, kardinalite tahmini için histogramın 'iyi' kısmını kullanmaz. Yoğunluk vektörünü temel alan bir tahmin kullanırlar.

FINDIK

SELECT 5.280389E-05 * 7250739 AS [poo]

Bu size verecek 382.86722457471, bu da optimizer’in yaptığı tahmin.

Bu bilinmeyen tahminler genellikle çok kötü tahminlerdir ve sıklıkla kötü planlara ve kötü endeks seçeneklerine yol açabilir.

Düzeltiyorum?

Seçenekleriniz genellikle:

  • Gevrek indeks ipuçları
  • Potansiyel olarak pahalı yeniden derleme ipuçları
  • Parametreli dinamik SQL
  • Saklı bir prosedür
  • Mevcut dizini iyileştir

Seçenekleriniz özellikle:

Geçerli dizini iyileştirmek, sorgunun gerektirdiği tüm sütunları kapsayacak şekilde genişletmek anlamına gelir:

CREATE NONCLUSTERED INDEX IX_MyTable_Id_SomeBit_Includes
ON dbo.MyTable (Id, SomeBit)
INCLUDE (TotallyUnrelatedTimestamp, SomeTimestamp, SomeInt)
WITH (DROP_EXISTING = ON);

IdDeğerlerin makul derecede seçici olduğunu varsayarsak , bu size iyi bir plan verir ve 'belirgin' bir veri erişim yöntemi vererek optimizere yardımcı olur.

Daha fazla okuma

Burada parametre gömme hakkında daha fazla bilgi bulabilirsiniz:


12

Verilerinizi çarpıttığınızı, optimize ediciyi ne yapmaya zorlamak için sorgulama ipuçlarını kullanmak istemediğinizi ve tüm olası giriş değerleri için iyi bir performans elde etmeniz gerektiğini varsayacağım @Id. Aşağıdaki indeks çiftlerini (veya eşdeğerlerini) oluşturmak istiyorsanız, olası herhangi bir giriş değeri için yalnızca birkaç avuç mantıksal okuma gerektiren bir sorgu planı alabilirsiniz:

CREATE INDEX GetMinSomeTimestamp ON dbo.MyTable (Id, SomeTimestamp) WHERE SomeBit = 1;
CREATE INDEX GetMaxSomeInt ON dbo.MyTable (Id, SomeInt) WHERE SomeBit = 1;

Test verilerim aşağıdadır. Tabloya 13 M satır koydum ve bunların yarısının sütun '3A35EA17-CE7E-4637-8319-4C517B6E48CA'için bir değeri vardı Id.

DROP TABLE IF EXISTS dbo.MyTable;

CREATE TABLE dbo.MyTable (
    Id uniqueidentifier,
    SomeTimestamp DATETIME2,
    SomeInt INT,
    SomeBit BIT,
    FILLER VARCHAR(100)
);

INSERT INTO dbo.MyTable WITH (TABLOCK)
SELECT NEWID(), CURRENT_TIMESTAMP, 0, 1, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

INSERT INTO dbo.MyTable WITH (TABLOCK)
SELECT '3A35EA17-CE7E-4637-8319-4C517B6E48CA', CURRENT_TIMESTAMP, 0, 1, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

Bu sorgu ilk başta biraz garip görünebilir:

DECLARE @Id UNIQUEIDENTIFIER = '3A35EA17-CE7E-4637-8319-4C517B6E48CA'

SELECT
  @Id,
  st.SomeTimestamp,
  si.SomeInt
FROM (
    SELECT TOP (1) SomeInt, Id
    FROM dbo.MyTable
    WHERE Id = @Id
    AND SomeBit = 1
    ORDER BY SomeInt DESC
) si
CROSS JOIN (
    SELECT TOP (1) SomeTimestamp, Id
    FROM dbo.MyTable
    WHERE Id = @Id
    AND SomeBit = 1
    ORDER BY SomeTimestamp ASC
) st;

Birkaç mantıksal okuma ile min veya maks değerini bulmak için indekslerin sıralanmasından yararlanmak için tasarlanmıştır. CROSS JOINİçin herhangi bir eşleme satır yokken doğru sonuçlar elde etmek için orada @Iddeğer. Tablodaki en popüler değere filtre uygulasam bile (6.5 milyon satır eşleşiyor) Yalnızca 8 mantıksal okuma alıyorum:

'MyTable' tablosu. Tarama sayısı 2, mantıksal okuma 8

İşte sorgu planı:

görüntü tanımını buraya girin

Her iki dizin de 0 veya 1 satır bulmaya çalışır. Son derece verimli, ancak iki dizin oluşturmak, senaryonuz için aşırı yüklenebilir. Bunun yerine aşağıdaki dizini düşünebilirsiniz:

CREATE INDEX CoveringIndex ON dbo.MyTable (Id) INCLUDE (SomeTimestamp, SomeInt) WHERE SomeBit = 1;

Şimdi orijinal sorgu için sorgu planı (isteğe bağlı bir MAXDOP 1ipucu ile) biraz farklı görünüyor:

görüntü tanımını buraya girin

Anahtar aramaları artık gerekli değildir. Tüm girişler için iyi çalışması gereken daha iyi bir erişim yolu ile, yoğunluk vektöründen dolayı yanlış sorgu planını seçen optimizer konusunda endişelenmenize gerek yoktur. Ancak, bu sorgu ve dizin popüler bir @Iddeğere bakarsanız, diğeri kadar verimli olmayacaktır .

'MyTable' tablosu. Tarama sayısı 1, mantıksal okuma 33757


2

Neden burada yanıtlayamıyorum , ancak sorgunun istediğiniz gibi çalışmasını sağlamanın hızlı ve kirli yolu:

DECLARE @Id UNIQUEIDENTIFIER = 'cec094e5-b312-4b13-997a-c91a8c662962'
SELECT 
  Id,
  MIN(SomeTimestamp),
  MAX(SomeInt)
FROM dbo.MyTable WITH (INDEX(IX_MyTable_Id_SomeBit_Includes))
WHERE Id = @Id
  AND SomeBit = 1
GROUP BY Id

Bu, tablonun veya endekslerin gelecekte bu optimizasyonun işlevsiz hale gelebilmesi için değişebileceği riskini doğurur, ancak ihtiyacınız olursa kullanılabilir. Umarım birileri size bu geçici çözümü yerine istediğiniz gibi bir temel neden yanıtı sunabilir.

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.