Hier ist die Übersicht: Ich mache eine Auswahlabfrage. Jede Spalte in den Klauseln WHERE
und ORDER BY
befindet sich in einem einzelnen nicht gruppierten Index IX_MachineryId_DateRecorded
, entweder als Teil des Schlüssels oder als INCLUDE
Spalten. 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_DateRecorded
es weniger als eine Sekunde. Wenn ich den Server entscheiden lasse, welcher Index verwendet werden soll, wählt er IX_MachineryId
und 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 OperationalSeconds
Aufzeichnung 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 .
quelle
Antworten:
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 kann
MachineryId
und derDateRecorded
Bereich:Der Index enthält nicht
OperationalSeconds
, daher muss der Plan diesen Wert pro Zeile im (partitionierten) Clustered-Index nachschlagen, um Folgendes zu testenOperationalSeconds > 0
: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):
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_MachineryId
könnte der Index durch Einbeziehen optimaler gemacht werdenOperationalSeconds
.Es ist ziemlich ungewöhnlich, nicht ausgerichtete Nonclustered-Indizes zu haben (Indizes, die anders als die Basistabelle partitioniert sind, auch überhaupt nicht).
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_DateRecorded
Plans 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 (nachDateRecorded
) Sortierung zurückgegeben werden :Dieser Index ist partitioniert und kann keine Zeilen in der angegebenen
DateRecorded
Reihenfolge direkt zurückgeben (siehe später). Es kann nachMachineryId
demDateRecorded
Bereich in jeder Partition suchen , aber eine Sortierung ist erforderlich: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
@From
und 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):@To
DateRecorded
datetime
Diese Konvertierung verhindert, dass der Optimierer die Beziehung zwischen aufsteigenden Partitions- IDs (die einen Wertebereich
DateRecorded
in 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
)DateRecorded
der Reihenfolge nachDateRecorded
allein entspricht (vorausgesetzt, diesMachineryID
ist konstant). Diese Argumentationskette wird durch die Typkonvertierung unterbrochen.Demo
Eine einfache partitionierte Tabelle und ein Index:
Abfrage mit übereinstimmenden Typen
Abfrage mit nicht übereinstimmenden Typen
quelle
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
> 0
ein fester Wert ist und sich nicht von einer Abfrageausführung zur nächsten ändert:Es gibt zwei Unterschiede zwischen dem Index, bei dem
OperationalSeconds
es 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
MachineryId
undDateRecorded
1000 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.quelle