Dizinim neden SELECT TOP içinde kullanılmıyor?


15

İşte halsiz: Bir seçme sorgusu yapıyorum. WHEREVe ORDER BYdeyimlerindeki her sütun IX_MachineryId_DateRecorded, anahtarın bir parçası olarak veya INCLUDEsütunlar olarak tek bir kümelenmemiş dizinde bulunur. Ben seçerek ediyorum bütün bu imi arama sonuçlanacaktır sütunlar, ama sadece alıyorum TOP (1), bu yüzden mutlaka sunucu araması yalnızca sonunda, bir kez yapılması gereken söyleyebilir.

En önemlisi, sorguyu dizin kullanmaya zorladığında, IX_MachineryId_DateRecordedbir saniyeden daha kısa sürede çalışır. Sunucunun hangi dizinin kullanılacağına karar vermesine izin verirsem IX_MachineryId, alır ve bir dakika kadar sürer. Bu gerçekten indeksi doğru yaptığımı gösteriyor ve sunucu sadece kötü bir karar veriyor. Neden?

CREATE TABLE [dbo].[MachineryReading] (
    [Id]                 INT              IDENTITY (1, 1) NOT NULL,
    [Location]           [sys].[geometry] NULL,
    [Latitude]           FLOAT (53)       NOT NULL,
    [Longitude]          FLOAT (53)       NOT NULL,
    [Altitude]           FLOAT (53)       NULL,
    [Odometer]           INT              NULL,
    [Speed]              FLOAT (53)       NULL,
    [BatteryLevel]       INT              NULL,
    [PinFlags]           BIGINT           NOT NULL,
    [DateRecorded]       DATETIME         NOT NULL,
    [DateReceived]       DATETIME         NOT NULL,
    [Satellites]         INT              NOT NULL,
    [HDOP]               FLOAT (53)       NOT NULL,
    [MachineryId]        INT              NOT NULL,
    [TrackerId]          INT              NOT NULL,
    [ReportType]         NVARCHAR (1)     NULL,
    [FixStatus]          INT              DEFAULT ((0)) NOT NULL,
    [AlarmStatus]        INT              DEFAULT ((0)) NOT NULL,
    [OperationalSeconds] INT              DEFAULT ((0)) NOT NULL,
    CONSTRAINT [PK_dbo.MachineryReading] PRIMARY KEY CLUSTERED ([Id] ASC),
    CONSTRAINT [FK_dbo.MachineryReading_dbo.Machinery_MachineryId] FOREIGN KEY ([MachineryId]) REFERENCES [dbo].[Machinery] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_dbo.MachineryReading_dbo.Tracker_TrackerId] FOREIGN KEY ([TrackerId]) REFERENCES [dbo].[Tracker] ([Id]) ON DELETE CASCADE
);

GO
CREATE NONCLUSTERED INDEX [IX_MachineryId]
    ON [dbo].[MachineryReading]([MachineryId] ASC);

GO
CREATE NONCLUSTERED INDEX [IX_TrackerId]
    ON [dbo].[MachineryReading]([TrackerId] ASC);

GO
CREATE NONCLUSTERED INDEX [IX_MachineryId_DateRecorded]
    ON [dbo].[MachineryReading]([MachineryId] ASC, [DateRecorded] ASC)
    INCLUDE([OperationalSeconds], [FixStatus]);

Tablo ay aralıklarına ayrılmıştır (yine de orada neler olduğunu gerçekten anlamıyorum).

ALTER PARTITION SCHEME PartitionSchemeMonthRange NEXT USED [Primary]
ALTER PARTITION FUNCTION [PartitionFunctionMonthRange]() SPLIT RANGE(N'2016-01-01T00:00:00.000') 

ALTER PARTITION SCHEME PartitionSchemeMonthRange NEXT USED [Primary]
ALTER PARTITION FUNCTION [PartitionFunctionMonthRange]() SPLIT RANGE(N'2016-02-01T00:00:00.000') 
...

CREATE UNIQUE CLUSTERED INDEX [PK_dbo.MachineryReadingPs] ON MachineryReading(DateRecorded, Id) ON PartitionSchemeMonthRange(DateRecorded)

Normalde çalıştıracağım sorgu:

SELECT TOP (1) [Id], [Location], [Latitude], [Longitude], [Altitude], [Odometer], [ReportType], [FixStatus], [AlarmStatus], [Speed], [BatteryLevel], [PinFlags], [DateRecorded], [DateReceived], [Satellites], [HDOP], [OperationalSeconds], [MachineryId], [TrackerId]
    FROM [dbo].[MachineryReading]
    --WITH(INDEX(IX_MachineryId_DateRecorded)) --This makes all the difference
    WHERE ([MachineryId] = @p__linq__0) AND ([DateRecorded] >= @p__linq__1) AND ([DateRecorded] < @p__linq__2) AND ([OperationalSeconds] > 0)
    ORDER BY [DateRecorded] ASC

Sorgu planı: https://www.brentozar.com/pastetheplan/?id=r1c-RpxNx

Zorunlu indeksli sorgu planı: https://www.brentozar.com/pastetheplan/?id=SywwTagVe

Dahil edilen planlar gerçek yürütme planlarıdır, ancak hazırlama veritabanında (canlı boyutunun yaklaşık 1 / 100'ü). Canlı veritabanıyla uğraşmakta tereddüt ediyorum çünkü bu şirkette sadece bir ay önce başladım.

Bu bölümleme nedeniyle bir duygu var ve benim sorgu genellikle her bölüm (örneğin OperationalSecondsbir makine için şimdiye kadar kaydedilen ilk veya son almak istediğinizde ) yayılır. Ancak, elle yazdığım sorguların hepsi EntityFramework'un ürettiğinden 10 - 100 kat daha iyi çalışıyor , bu yüzden sadece saklı bir prosedür yapacağım.


1
Merhaba @AndrewWilliamson, Bu bir istatistik sorunu olabilir. Uygulanmayan plandan gerçek planı görürseniz, tahmini satır sayısı 1.22 ve gerçek 19039'dur. Bu da daha sonra planta göreceğiniz anahtar aramaya yol açar. istatistikleri güncellemeye çalıştın mı? Değilse, hazırlama veritabanında tam taramayı deneyin.
jesijesi

Yanıtlar:


21

Sunucunun hangi dizinin kullanılacağına karar vermesine izin verirsem IX_MachineryId, alır ve bir dakika kadar sürer.

Bu dizin bölümlenmediğinden, optimize edici, sorguda belirtilen sıralamayı sıralama olmadan sağlamak için kullanılabileceğini kabul eder. Benzersiz olmayan kümelenmemiş bir dizin olarak, kümelenmiş dizinin anahtarlarını alt anahtarlar olarak da içerir, böylece dizin aramak MachineryIdve DateRecordedaralık için kullanılabilir :

Dizin Ara

Dizin içermez OperationalSeconds, bu nedenle planın test etmek için (bölümlenmiş) kümelenmiş dizinde satır başına bu değeri yukarı bakması gerekir OperationalSeconds > 0:

Yukarı Bak

Optimize edici, bir satırın kümelenmemiş dizinden okunması gerektiğini ve TOP (1). Bu hesaplama satır hedefine dayanır (bir satırı hızlıca bulun) ve değerlerin düzgün bir dağılımını varsayar.

Gerçek plandan, 1 satırın tahmininin yanlış olduğunu görebiliriz. Aslında, hiçbir satırın sorgu koşullarını karşılamadığını keşfetmek için 19.039 satırın işlenmesi gerekir. Bu, bir satır hedefi optimizasyonu için en kötü durumdur (1 satır tahmin edildi, tüm satırlar gerçekten gerekli):

Güncel / tahmin

4138 izleme bayrağıyla satır hedeflerini devre dışı bırakabilirsiniz . Bu büyük olasılıkla SQL Server farklı bir plan, muhtemelen zorlanan bir plan seçerek sonuçlanır. Her durumda, endeks IX_MachineryIddahil edilerek daha uygun hale getirilebilir OperationalSeconds.

Hizalı olmayan kümelenmemiş dizinlere sahip olmak oldukça sıra dışıdır (dizinler, temel tablodan farklı bir şekilde bölümlenmiştir, hiç de dahil değildir).

Bu gerçekten indeksi doğru yaptığımı gösteriyor ve sunucu sadece kötü bir karar veriyor. Neden?

Her zamanki gibi, optimizer dikkate aldığı en ucuz planı seçiyor.

IX_MachineryIdPlanın tahmini maliyeti, bir satırın test edileceği ve iade edileceği (yanlış) satır hedef varsayımına dayanarak 0.01 maliyet birimidir.

IX_MachineryId_DateRecordedPlanın tahmini maliyeti 0.27 birimde çok daha yüksektir, çünkü çoğunlukla dizinden 5.515 satır okumayı, sıralamayı ve en düşük sıralamayı (sıralamayı) döndürmeyi bekler DateRecorded:

İlk N Sırala

Bu dizin bölümlere ayrılmıştır ve satırları DateRecordeddoğrudan sırayla döndüremez (daha sonra bakın). Her bölüm içindeki aramayı MachineryIdve DateRecordedaralığı arayabilir , ancak bir Sıralama gereklidir:

Bölümlenmiş Ara

Bu dizin bölümlenmemiş olsaydı, bir sıralama gerekli olmazdı ve fazladan eklenen sütuna sahip diğer (bölümlenmemiş) dizine çok benzer olurdu. Bölümlenmemiş filtrelenmiş bir dizin yine de biraz daha verimli olacaktır.


Kaynak sorguyu , ve parametrelerinin veri türleri sütununla ( ) eşleşecek şekilde güncelleştirmelisiniz . Şu anda, SQL Server çalışma zamanında tür uyuşmazlığı nedeniyle bir dinamik aralık hesaplıyor (Birleştirme Aralığı işlecini ve alt ağacını kullanarak):@From@ToDateRecordeddatetime

<ScalarOperator ScalarString="GetRangeWithMismatchedTypes([@From],NULL,(22))">
<ScalarOperator ScalarString="GetRangeWithMismatchedTypes([@To],NULL,(22))">

Bu dönüşüm, optimize edicinin artan bölüm kimlikleri ( DateRecordedartan sırada bir dizi değeri kapsayan ) ve eşitsizliğin öngördüğü arasındaki ilişki hakkında doğru muhakeme yapmasını önler DateRecorded.

Bölüm kimliği, bölümlenmiş bir dizin için üstü kapalı bir anahtardır. Normalde, iyileştirici bölüm kimliğine göre sıralamanın (artan kimliklerin artan, ayrık değerlerle eşleştiği yerlerde DateRecorded) tek başına DateRecordedsıralamayla aynı DateRecordedolduğunu görebilir MachineryID(sabit olduğu göz önüne alındığında ). Bu akıl yürütme zinciri tür dönüşümü tarafından kırılır.

gösteri

Basit bölümlenmiş bir tablo ve dizin:

CREATE PARTITION FUNCTION PF (datetime)
AS RANGE LEFT FOR VALUES ('20160101', '20160201', '20160301');

CREATE PARTITION SCHEME PS AS PARTITION PF ALL TO ([PRIMARY]);

CREATE TABLE dbo.T (c1 integer NOT NULL, c2 datetime NOT NULL) ON PS (c2);

CREATE INDEX i ON dbo.T (c1, c2) ON PS (c2);

INSERT dbo.T (c1, c2) 
VALUES (1, '20160101'), (1, '20160201'), (1, '20160301');

Eşleşen türlerle sorgu

-- Types match (datetime)
DECLARE 
    @From datetime = '20010101',
    @To datetime = '20090101';

-- Seek with no sort
SELECT T2.c2 
FROM dbo.T AS T2 
WHERE T2.c1 = 1 
AND T2.c2 >= @From
AND T2.c2 < @To
ORDER BY 
    T2.c2;

Hiçbir şey aramayın

Uyuşmayan türlerle sorgu

-- Mismatched types (datetime2 vs datetime)
DECLARE 
    @From datetime2 = '20010101',
    @To datetime2 = '20090101';

-- Merge Interval and Sort
SELECT T2.c2 
FROM dbo.T AS T2 
WHERE T2.c1 = 1 
AND T2.c2 >= @From
AND T2.c2 < @To
ORDER BY 
    T2.c2;

Aralığı Birleştir ve Sırala


5

Dizin sorgu için oldukça iyi görünüyor ve neden optimizer tarafından seçilmediğinden emin değilim (istatistik? Bölümleme? Masmavi sınırlama ?, gerçekten bir fikir yok.)

Ancak, filtrelenmiş bir dizin , > 0sabit bir değerse ve bir sorgu yürütmesinden diğerine değişmezse , belirli sorgu için daha da iyi olur :

CREATE NONCLUSTERED INDEX IX_MachineryId_DateRecorded_filtered
    ON dbo.MachineryReading
        (MachineryId, DateRecorded) 
    WHERE (OperationalSeconds > 0) ;

Sahip olduğunuz dizin OperationalSeconds3. sütunun olduğu yerde ve filtrelenmiş dizin arasında iki fark vardır :

  • İlk olarak, filtrelenmiş dizin hem genişlik (daha dar) hem de satır sayısı olarak daha küçüktür.
    Bu, filtrelenmiş dizini genel olarak daha verimli hale getirir, çünkü SQL Server bellekte tutmak için daha az alana ihtiyaç duyar.

  • İkincisi ve bu sorgu için daha ince ve önemlidir, yalnızca sorguda kullanılan filtreyle eşleşen satırlara sahip olmasıdır. Bu 3. sütunun değerlerine bağlı olarak bu çok önemli olabilir.
    Örneğin, belirli bir parametre kümesi MachineryIdve DateRecorded1000 satır verebilir. Bu satırların tümü veya neredeyse tamamı (OperationalSeconds > 0)filtreyle eşleşirse , her iki dizin de iyi çalışır. Ancak filtreyle eşleşen satırlar çok azsa (veya yalnızca sonuncusu veya hiç yoksa), ilk dizinin bir eşleşme bulana kadar çok fazla veya bu 1000 satırdan geçmesi gerekir. Öte yandan, filtrelenmiş dizin, yalnızca filtreyle eşleşen satırlar depolandığından eşleşen bir satır bulmak (veya 0 satır döndürmek) için yalnızca bir arama gerekir.


1
Dizini eklemek sorguyu daha verimli hale getirdi mi?
ypercubeᵀᴹ

Evreleme veritabanına değil (gerçekten düzgün bir şekilde test etmek için daha fazla veriye ihtiyaç duyuyor), henüz canlı olarak denemedim, yeni dizinlerin üzerine inşa etmek bir saatten fazla sürüyor. Ayrıca, zaten yavaş çalıştığı için canlı veritabanımıza bir şey yapmakta oldukça tereddüt ediyorum. Yaşamımızı evrelemeye klonlamak için daha iyi bir sisteme ihtiyacımız var.
Andrew Williamson
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.