Warum wird mein Index nicht in einem SELECT TOP verwendet?

15

Hier ist die Übersicht: Ich mache eine Auswahlabfrage. Jede Spalte in den Klauseln WHEREund ORDER BYbefindet sich in einem einzelnen nicht gruppierten Index IX_MachineryId_DateRecorded, entweder als Teil des Schlüssels oder als INCLUDESpalten. Ich wähle alle Spalten aus, so dass eine Lesezeichensuche durchgeführt wird, aber ich nehme nur TOP (1), so dass der Server sicher sagen kann, dass die Suche am Ende nur einmal durchgeführt werden muss.

Vor allem, wenn ich die Abfrage zwinge, den Index zu verwenden, dauert IX_MachineryId_DateRecordedes weniger als eine Sekunde. Wenn ich den Server entscheiden lasse, welcher Index verwendet werden soll, wählt er IX_MachineryIdund es dauert bis zu einer Minute. Das deutet wirklich darauf hin, dass ich den Index richtig gemacht habe und der Server gerade eine schlechte Entscheidung trifft. Warum?

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

Die Tabelle ist in Monatsbereiche unterteilt (obwohl ich immer noch nicht wirklich verstehe, was dort vor sich geht).

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)

Die Abfrage, die ich normalerweise ausführen würde:

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

Abfrageplan: https://www.brentozar.com/pastetheplan/?id=r1c-RpxNx

Abfrageplan mit erzwungenem Index: https://www.brentozar.com/pastetheplan/?id=SywwTagVe

Die enthaltenen Pläne sind die tatsächlichen Ausführungspläne, befinden sich jedoch in der Staging-Datenbank (ungefähr 1/100 der Livegröße). Ich zögere, an der Live-Datenbank herumzuspielen, da ich erst vor einem Monat bei dieser Firma angefangen habe.

Ich habe das Gefühl, es liegt an der Partitionierung, und meine Abfrage erstreckt sich normalerweise über jede einzelne Partition (z. B. wenn ich die erste oder letzte OperationalSecondsAufzeichnung für eine Maschine erhalten möchte ). Die Abfragen, die ich von Hand geschrieben habe, werden jedoch alle gut 10 bis 100 Mal schneller ausgeführt als das, was EntityFramework generiert hat. Ich werde also nur eine gespeicherte Prozedur erstellen .

Andrew Williamson
quelle
1
Hi @AndrewWilliamson, Es könnte ein Statistikproblem sein. Wenn Sie den aktuellen Plan aus dem nicht erzwungenen Plan sehen, beträgt die geschätzte Anzahl der Zeilen 1,22 und die tatsächliche Anzahl 19039. Dies führt wiederum zu der Schlüsselsuche, die Sie später im Plan sehen. Haben Sie versucht, die Statistiken zu aktualisieren? Wenn nicht, versuchen Sie es mit einem vollständigen Scan der Staging-Datenbank.
Jesijesi

Antworten:

21

Wenn ich den Server entscheiden lasse, welcher Index verwendet werden soll, wählt er IX_MachineryIdund es dauert bis zu einer Minute.

Dieser Index ist nicht partitioniert, sodass das Optimierungsprogramm erkennt, dass er verwendet werden kann, um die in der Abfrage angegebene Reihenfolge ohne Sortieren bereitzustellen. Als nicht eindeutiger nicht gruppierter Index enthält er auch die Schlüssel des gruppierten Index als Unterschlüssel, sodass der Index zum Suchen verwendet werden kannMachineryId und der DateRecordedBereich:

Index-Suche

Der Index enthält nicht OperationalSeconds , daher muss der Plan diesen Wert pro Zeile im (partitionierten) Clustered-Index nachschlagen, um Folgendes zu testen OperationalSeconds > 0:

Nachsehen

Das Optimierungsprogramm schätzt, dass eine Zeile aus dem nicht gruppierten Index gelesen und nachgeschlagen werden muss, um die Anforderungen zu erfüllen TOP (1). Diese Berechnung basiert auf dem Zeilenziel (schnell eine Zeile finden) und geht von einer gleichmäßigen Werteverteilung aus.

Aus dem tatsächlichen Plan können wir sehen, dass die Schätzung von 1 Zeile ungenau ist. Tatsächlich müssen 19.039 Zeilen verarbeitet werden, um festzustellen, dass keine Zeilen die Abfragebedingungen erfüllen. Dies ist der schlimmste Fall für eine Zeilenzieloptimierung (1 Zeile geschätzt, alle Zeilen tatsächlich benötigt):

Ist / Schätzung

Sie können Zeilenziele mit dem Ablaufverfolgungsflag 4138 deaktivieren . Dies würde höchstwahrscheinlich dazu führen, dass SQL Server einen anderen Plan auswählt, möglicherweise den, den Sie erzwungen haben. In jedem Fall IX_MachineryIdkönnte der Index durch Einbeziehen optimaler gemacht werden OperationalSeconds.

Es ist ziemlich ungewöhnlich, nicht ausgerichtete Nonclustered-Indizes zu haben (Indizes, die anders als die Basistabelle partitioniert sind, auch überhaupt nicht).

Das deutet wirklich darauf hin, dass ich den Index richtig gemacht habe und der Server gerade eine schlechte Entscheidung trifft. Warum?

Wie üblich wählt der Optimierer den günstigsten Plan aus, den er berücksichtigt.

Die geschätzten Kosten der IX_MachineryId Plans betragen 0,01 Kosteneinheiten, basierend auf der (falschen) Zeilenzielannahme, dass eine Zeile getestet und zurückgegeben wird.

Die geschätzten Kosten des IX_MachineryId_DateRecordedPlans sind mit 0,27 Einheiten viel höher, hauptsächlich, weil davon ausgegangen wird, dass 5.515 Zeilen aus dem Index gelesen, sortiert und die niedrigste (nach DateRecorded) Sortierung zurückgegeben werden :

Top N Sortieren

Dieser Index ist partitioniert und kann keine Zeilen in der angegebenen DateRecordedReihenfolge direkt zurückgeben (siehe später). Es kann nach MachineryIddem DateRecordedBereich in jeder Partition suchen , aber eine Sortierung ist erforderlich:

Partitionierter Suchlauf

Wenn dieser Index nicht partitioniert wäre, wäre keine Sortierung erforderlich, und er wäre dem anderen (nicht partitionierten) Index mit der zusätzlich enthaltenen Spalte sehr ähnlich. Ein nicht partitionierter gefilterter Index wäre noch etwas effizienter.


Sie sollten die Quellabfrage aktualisieren, damit die Datentypen der Parameter @Fromund mit der Spalte ( ) übereinstimmen . Momentan berechnet SQL Server einen dynamischen Bereich, da der Typ zur Laufzeit nicht übereinstimmt (unter Verwendung des Operators "Zusammenführungsintervall" und seiner Unterstruktur):@ToDateRecordeddatetime

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

Diese Konvertierung verhindert, dass der Optimierer die Beziehung zwischen aufsteigenden Partitions- IDs (die einen Wertebereich DateRecordedin aufsteigender Reihenfolge abdecken ) und den Ungleichungs-Prädikaten korrekt beurteiltDateRecorded .

Die Partitions-ID ist ein impliziter führender Schlüssel für einen partitionierten Index. Normalerweise kann der Optimierer erkennen, dass die Reihenfolge nach Partitions-ID (wobei aufsteigende IDs aufsteigenden, nicht zusammenhängenden Werten von entsprechen DateRecorded) DateRecordedder Reihenfolge nach DateRecordedallein entspricht (vorausgesetzt, dies MachineryIDist konstant). Diese Argumentationskette wird durch die Typkonvertierung unterbrochen.

Demo

Eine einfache partitionierte Tabelle und ein Index:

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

Abfrage mit übereinstimmenden Typen

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

Suche keine Sorte

Abfrage mit nicht übereinstimmenden Typen

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

Zusammenführungsintervall und Sortierung

Paul White Monica wieder einsetzen
quelle
5

Der Index scheint für die Abfrage recht gut zu sein, und ich bin mir nicht sicher, warum er nicht vom Optimierer ausgewählt wurde (Statistik? Partitionierung? Azurblau-Beschränkung ?, keine Ahnung.)

Ein gefilterter Index wäre jedoch für die jeweilige Abfrage noch besser, wenn er > 0ein fester Wert ist und sich nicht von einer Abfrageausführung zur nächsten ändert:

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

Es gibt zwei Unterschiede zwischen dem Index, bei dem OperationalSecondses sich um die 3. Spalte handelt, und dem gefilterten Index:

  • Erstens ist der gefilterte Index kleiner, sowohl in der Breite (schmaler) als auch in der Anzahl der Zeilen.
    Dies macht den gefilterten Index im Allgemeinen effizienter, da SQL Server weniger Speicherplatz benötigt, um ihn im Speicher zu behalten.

  • Zweitens ist dies subtiler und wichtig für die Abfrage, da nur Zeilen vorhanden sind, die mit dem in der Abfrage verwendeten Filter übereinstimmen. Dies kann in Abhängigkeit von den Werten dieser dritten Spalte äußerst wichtig sein.
    Beispielsweise kann ein bestimmter Parametersatz für MachineryIdund DateRecorded1000 Zeilen ergeben. Wenn alle oder fast alle dieser Zeilen mit dem (OperationalSeconds > 0)Filter übereinstimmen , verhalten sich beide Indizes gut. Wenn die mit dem Filter übereinstimmenden Zeilen jedoch sehr klein sind (oder nur die letzte oder gar keine), muss der erste Index viele oder alle dieser 1000 Zeilen durchlaufen, bis eine Übereinstimmung gefunden wird. Der gefilterte Index benötigt andererseits nur eine Suche, um eine übereinstimmende Zeile zu finden (oder 0 Zeilen zurückzugeben), da nur Zeilen gespeichert werden, die mit dem Filter übereinstimmen.

ypercubeᵀᴹ
quelle
1
Hat das Hinzufügen des Index die Abfrage effizienter gemacht?
Ypercubeᵀᴹ
Nicht für die Staging-Datenbank (es werden wirklich mehr Daten benötigt, um richtig zu testen), ich habe es noch nicht live ausprobiert. Neue Indizes brauchen über eine Stunde, um darauf aufzubauen. Ich zögere auch ziemlich, irgendetwas mit unserer Live-Datenbank zu machen, da sie bereits langsam läuft. Wir brauchen ein besseres System, um unser Leben in die Inszenierung zu klonen.
Andrew Williamson