Warum verwendet SQL Server einen besseren Ausführungsplan, wenn ich die Variable einfüge?

31

Ich habe eine SQL-Abfrage, die ich optimieren möchte:

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 hat zwei Indizes:

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)

Wenn ich die Abfrage genau wie oben beschrieben ausführe, durchsucht SQL Server den ersten Index, was zu 189.703 logischen Lesevorgängen und einer Dauer von 2-3 Sekunden führt.

Wenn ich die @IdVariable einfüge und die Abfrage erneut ausführe, sucht SQL Server nach dem zweiten Index, was zu nur 104 logischen Lesevorgängen und einer Dauer von 0,001 Sekunden (im Grunde genommen sofort) führt.

Ich brauche die Variable, aber ich möchte, dass SQL den guten Plan verwendet. Als vorübergehende Lösung habe ich einen Indexhinweis auf die Abfrage gesetzt, und die Abfrage erfolgt im Grunde genommen sofort. Ich versuche jedoch, mich von Indexhinweisen fernzuhalten, wenn dies möglich ist. Normalerweise gehe ich davon aus, dass ich etwas tun (oder aufhören) kann, wenn das Abfrageoptimierungsprogramm seine Arbeit nicht ausführen kann, ohne es ausdrücklich anzuweisen, was zu tun ist.

Warum hat SQL Server einen besseren Plan, wenn ich die Variable einfüge?

Regenblitz
quelle

Antworten:

43

In SQL Server gibt es drei gängige Formen von Nicht-Join-Prädikaten:

Mit einem wörtlichen Wert:

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

Mit einem Parameter :

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

Mit einer lokalen Variablen :

DECLARE @Reputation INT = 1

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

Ergebnisse

Wenn Sie einen Literalwert verwenden und Ihr Plan nicht a) trivial und b) einfach parametrisiert ist oder c) keine erzwungene Parametrisierung aktiviert ist, erstellt der Optimierer einen ganz speziellen Plan nur für diesen Wert.

Wenn Sie einen Parameter verwenden , erstellt das Optimierungsprogramm einen Plan für diesen Parameter (dies wird als Parameter-Sniffing bezeichnet ) und verwendet diesen Plan dann erneut, ohne Kompilierungshinweise, Plan-Cache-Räumung usw.

Wenn Sie eine lokale Variable verwenden , erstellt der Optimierer einen Plan für ... etwas .

Wenn Sie diese Abfrage ausführen würden:

DECLARE @Reputation INT = 1

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

Der Plan würde so aussehen:

NÜSSE

Und die geschätzte Anzahl der Zeilen für diese lokale Variable würde folgendermaßen aussehen:

NÜSSE

Obwohl die Abfrage eine Anzahl von 4.744.427 zurückgibt.

Da lokale Variablen unbekannt sind, verwenden Sie den "guten" Teil des Histogramms nicht zur Schätzung der Kardinalität. Sie verwenden eine Schätzung, die auf dem Dichtevektor basiert.

NÜSSE

SELECT 5.280389E-05 * 7250739 AS [poo]

Das gibt Ihnen 382.86722457471die Vermutung, die der Optimierer macht.

Diese unbekannten Vermutungen sind normalerweise sehr schlechte Vermutungen und können oft zu schlechten Plänen und schlechten Indexentscheidungen führen.

Es reparieren?

Ihre Optionen sind im Allgemeinen:

  • Spröde Index Hinweise
  • Potenziell teure Tipps zum Neukompilieren
  • Parametrisiertes dynamisches SQL
  • Eine gespeicherte Prozedur
  • Verbessere den aktuellen Index

Ihre Optionen sind insbesondere:

Um den aktuellen Index zu verbessern, müssen Sie ihn so erweitern, dass alle von der Abfrage benötigten Spalten abgedeckt werden:

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

Wenn Sie davon ausgehen, dass die IdWerte einigermaßen selektiv sind, erhalten Sie einen guten Plan und können dem Optimierer helfen, indem Sie ihm eine "offensichtliche" Datenzugriffsmethode zuweisen.

Mehr lesen

Weitere Informationen zum Einbetten von Parametern finden Sie hier:

Erik Darling
quelle
12

Ich gehe davon aus, dass Sie Daten verzerrt haben, dass Sie keine Abfragehinweise verwenden möchten, um das Optimierungsprogramm zu erzwingen, was zu tun ist, und dass Sie eine gute Leistung für alle möglichen Eingabewerte von benötigen @Id. Sie können einen Abfrageplan erstellen, für den garantiert nur einige wenige logische Lesevorgänge für einen möglichen Eingabewert erforderlich sind, wenn Sie bereit sind, das folgende Indexpaar (oder das entsprechende) zu erstellen:

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

Unten sind meine Testdaten. Ich habe 13 M Zeilen in die Tabelle eingefügt und dafür gesorgt, dass die Hälfte einen Wert '3A35EA17-CE7E-4637-8319-4C517B6E48CA'für die IdSpalte hat.

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;

Diese Abfrage könnte auf den ersten Blick etwas seltsam aussehen:

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;

Es nutzt die Reihenfolge der Indizes, um den minimalen oder maximalen Wert mit wenigen logischen Lesevorgängen zu ermitteln. Das CROSS JOINist es korrekte Ergebnisse zu erhalten , wenn es keine passenden Zeilen für den sind @IdWert. Selbst wenn ich nach dem beliebtesten Wert in der Tabelle filtere (6,5 Millionen Zeilen), erhalte ich nur 8 logische Lesevorgänge:

Tabelle 'MyTable'. Scananzahl 2, logische Lesevorgänge 8

Hier ist der Abfrageplan:

Bildbeschreibung hier eingeben

Beide Indexsuchen finden 0 oder 1 Zeilen. Es ist äußerst effizient, aber das Erstellen von zwei Indizes könnte für Ihr Szenario überfordert sein. Sie könnten stattdessen den folgenden Index in Betracht ziehen:

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

Jetzt MAXDOP 1sieht der Abfrageplan für die ursprüngliche Abfrage (mit einem optionalen Hinweis) etwas anders aus:

Bildbeschreibung hier eingeben

Die Schlüsselsuchen sind nicht mehr erforderlich. Mit einem besseren Zugriffspfad, der für alle Eingaben gut funktionieren sollte, sollten Sie sich keine Sorgen machen müssen, dass der Optimierer aufgrund des Dichtevektors den falschen Abfrageplan auswählt. Diese Abfrage und dieser Index sind jedoch nicht so effizient wie die andere, wenn Sie nach einem beliebten @IdWert suchen .

Tabelle 'MyTable'. Scananzahl 1, logische Lesevorgänge 33757

Joe Obbish
quelle
2

Ich kann hier nicht beantworten, warum , aber die schnelle und fehlerhafte Methode, um sicherzustellen, dass die Abfrage so ausgeführt wird, wie Sie es möchten, ist:

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

Dies birgt das Risiko, dass sich die Tabelle oder die Indizes in Zukunft ändern, sodass diese Optimierung nicht mehr funktioniert. Sie ist jedoch verfügbar, wenn Sie sie benötigen. Hoffentlich kann Ihnen jemand eine Antwort auf die Hauptursache anbieten, wie Sie es gewünscht haben, und nicht diese Problemumgehung.

Jon aller Berufe
quelle