Schlechter Ausführungsplan nach Aktualisierung der Statistiken aufgrund der temporären Tabelle

8

Eine Abfrage mit gespeicherten Prozeduren erhält manchmal einen schlechten Plan, nachdem die Statistiken für eine der Tabellen aktualisiert wurden, kann jedoch direkt danach wieder in den guten Plan kompiliert werden. Gleiche kompilierte Parameter.

Das Problem scheint von einer kleinen temporären Tabelle zu stammen, die im SP erstellt und dann verbunden wurde. Der fehlerhafte Plan enthält eine Warnung in der temporären Tabelle, dass die Join-Spalte keine Statistiken enthält. Was gibt?

SQL Server 2016 SP1 CU4 mit Kompatibilitätsstufe 2014

Schlechter Plan:

Screenshot mit schlechtem Plan

Guter Plan:

Guter Plan Screenshot

Gespeicherte Prozedur

USE AppDB
GO
SET QUOTED_IDENTIFIER ON
SET ANSI_NULLS ON
GO
CREATE PROCEDURE [MySchema].[MySP]
    @MyId VARCHAR(50),
    @Months INT
AS
BEGIN

    SET NOCOUNT ON

    SELECT * 
    INTO #MyTemp
    FROM AppDB.MySchema.View_Feeder vf WITH (NOLOCK)
    WHERE vf.MyId = @MyId AND vf.Status IS NOT NULL

    SELECT wd.Col1
         , vp.Col2
         , vp.Col3 
    FROM AppDB.MySchema.View_VP vp WITH (FORCESEEK)
    INNER JOIN #MyTemp wd ON wd.Col1 = vp.Col1
    WHERE vp.Col3 > DATEADD(MONTH, @Months * -1, GETDATE())

END

Innenansicht

USE AppDB
GO
SET QUOTED_IDENTIFIER ON
SET ANSI_NULLS ON
GO
CREATE VIEW [MySchema].[View_VP]
AS

    SELECT pp.Col1,
           pd.Col2 AS Col2, 
           MAX(pp.Col4) AS Col3
    FROM P_DB..LargeTable pp WITH (NOLOCK)
    INNER JOIN P_DB..SmallTable pd WITH (NOLOCK) ON pp.P_Id = pd.P_Id
    WHERE pp.[Status] IN (3, 4)
    GROUP BY pp.Col1, pd.Col2

Pläne

Guter und schlechter Plan überarbeitet .

Zusätzliche Information

Der FORCESEEKHinweis wurde zu der Zeit hinzugefügt, um zu versuchen, genau dieses Problem zu lösen und den Plan zu stabilisieren. Und trotzdem, mit oder ohne, würde ich wirklich gerne verstehen, was hier passiert.

Ich kann das Problem nicht nach Belieben reproduzieren, daher ist es schwer zu sagen, ob das Ersetzen des SELECT INTOdurch eine explizite Tabelle einen Unterschied machen würde. Ich glaube jedoch, dass es sich genauso verhalten sollte.

SELECT
    database_id, 
    is_auto_create_stats_on, 
    is_auto_update_stats_on, 
    is_auto_update_stats_async_on
FROM sys.databases
WHERE
    database_id IN (2, <relevant user databases>)

kehrt zurück:

  database_id   is_auto_create_stats_on   is_auto_update_stats_on   is_auto_update_stats_async_on  
 ------------- ------------------------- ------------------------- ------------------------------- 
  2             1                         1                         0                              
  7             1                         1                         1                              
  37            1                         1                         1                              

Es ist klar, dass diese Suche schrecklich ist, aber die Frage ist, warum sie die gute Suche überhaupt nicht macht.

Die Abfrage gibt keine 1 Million Zeilen zurück, die Schätzungen sind falsch. Es könnte geringfügige Änderungen in der Ausgabe geben, aber die Anzahl der Zeilen ist immer ziemlich gering (vielleicht höchstens Hunderte).

Sogar diejenigen, die relativ viele Zeilen zurückgeben, erzeugen Pläne, die nach dem Idund niemals nach dem suchen status(was nicht selektiv ist, wie Sie sehen können). Ich kann den Status-Suchplan scheinbar nicht reproduzieren, egal welche Werte zusammengestellt werden. Ich habe sogar versucht, eine waitfor delayzwischen der Erstellung der temporären Tabelle und der zweiten Abfrage hinzuzufügen und die Statistiken / Neukompilierung in einer zweiten Sitzung zu aktualisieren, ohne dass dies Auswirkungen hatte.

Alex Friedman
quelle

Antworten:

12

Der fehlerhafte Plan enthält eine Warnung in der temporären Tabelle, dass die Join-Spalte keine Statistiken enthält. Was gibt?

Es mag einen esoterischeren Grund dafür geben, aber es ist wahrscheinlicher, dass die Erstellung von Statistiken einfach fehlschlägt. Dies kann beispielsweise der Fall sein, wenn die Aufgabe nicht die benötigten Speicherressourcen abruft oder wenn die Erstellung von Statistiken gedrosselt wird (zu viele gleichzeitige Kompilierungen). Weitere Informationen finden Sie im Microsoft White Paper Statistics, das vom Abfrageoptimierer in Microsoft SQL Server 2008 verwendet wird . Möglicherweise können Sie dies weiter debuggen, indem Sie sich den Profiler für automatische Statistiken oder erweiterte Ereignisse und andere Ereignisse ungefähr zur gleichen Zeit ansehen.

Allerdings wären viel mehr Informationen und Untersuchungen erforderlich, um die Schuld für die Planauswahl an die Tür der fehlenden temporären Tabellenstatistik zu legen. Auch ohne detaillierte Statistiken kann der Optimierer die Gesamtkardinalität der temporären Tabelle sehen, und dies scheint hier ein wichtiger Faktor zu sein.

... kann aber gleich danach wieder auf den guten Plan umgestellt werden. Gleiche kompilierte Parameter.

Der @MonthsParameter ist möglicherweise derselbe, aber die Anzahl der Zeilen in der temporären Tabelle (aus der unbekannten Ansicht View_Feeder) ist unterschiedlich, und die bereitgestellten Pläne zeigen nicht den Wert von an @MyId.

Ausgehend von den verfügbaren Informationen: Der "gute" Plan (nur Schätzungen, keine Leistungsdaten angegeben) basiert auf einer temporären Tabelle mit 4 Zeilen . Der 'schlechte Plan' basiert auf einer temporären Tabelle mit 114 Zeilen . Ein Mangel an Dichte- und Histogramminformationen mag zwar nicht hilfreich sein, aber es ist leicht zu erkennen, wie der Optimierer einen anderen Plan für 4 gegenüber 114 Zeilen wählen kann, wenn auch solche mit unbekannter Dichte und Verteilung.

Wenn Schätzungen zu Planbetreibern, die nicht von der temporären Tabelle abhängig sind, stark abweichen, ist dies ein starkes Signal dafür, dass die aktuellen Haupttabellenstatistiken nicht repräsentativ für die zugrunde liegenden Daten sind. Der Mangel an Informationen in der Frage macht es unmöglich, dies zu bewerten.

Es ist jedoch erkennbar, dass der Optimierer hier aufgefordert wird, zwischen nicht optimalen Alternativen zu wählen . Keiner der vorgestellten Pläne stellt eine „offensichtlich gute“ Wahl dar, da beide Suchvorgänge (Fehlen eines „Deckungsindex“) und späte Filterung (siehe weiter unten) beinhalten. Insbesondere Lookups sind mit hohen Kosten verbunden, die empfindlich von Kardinalitätsschätzungen abhängen.

Die Verwendung einer Ansicht schränkt die Auswahlmöglichkeiten für Optimierer und Hinweise ein:

  • Die Ansicht enthält eine GROUP BY, die verhindert, dass das Prädikat vp.Col3 > DATEADD(MONTH, @Months * -1, GETDATE())nach unten gedrückt wird, obwohl die Transformation in diesem speziellen Fall gültig wäre.
    • Das Einfügen der Ansicht in die Abfrage bietet eine natürliche Möglichkeit, die Datums- / Uhrzeitspalte früher zu filtern (obwohl in der Frage nicht angegeben ist, ob eine Umgestaltung der Abfrage eine Option ist).
  • Es ist nicht möglich, einen Index in einer Ansicht anzugeben, und FORCESEEKder Optimierer wird lediglich aufgefordert, einen Indexsuchplan zu finden (nicht unbedingt unter Verwendung des von Ihnen bevorzugten Index). Das Entfernen der Ansicht würde diese Einschränkung ebenfalls aufheben.

Wenn Sie dem Prädikat erlauben, nach unten zu drücken, sollten sich auch Indexierungsmöglichkeiten für die große Tabelle eröffnen. Zum Beispiel:

CREATE INDEX give_me_a_good_name
ON dbo.LargeTable (Col1, [Status], Col4) 
INCLUDE (P_Id);

... bietet einen guten Zugriffspfad für die umgeschriebene Abfrage:

DECLARE @Date datetime = DATEADD(MONTH, @Months * -1, GETDATE());

SELECT
    MT.Col1,
    ST.Col2,
    MAX(LT.Col4)
FROM #MyTemp AS MT
JOIN dbo.LargeTable AS LT
    ON LT.Col1 = MT.Col1
JOIN dbo.SmallTable AS ST
    ON ST.P_id = LT.P_Id
WHERE
    LT.[Status] IN (3, 4)
    AND LT.Col4 > @Date
GROUP BY
    MT.Col1,
    ST.Col2
OPTION (RECOMPILE);

Beispielplan

Eine weitere Überlegung ist die Auswirkung des Zwischenspeicherns von temporären Tabellen und Statistiken, wie in meinen Artikeln Erläuterungen zu temporären Tabellen in gespeicherten Prozeduren und zum Zwischenspeichern von temporären Tabellen beschrieben . Wenn ein guter Plan vom aktuellen Inhalt des temporären Objekts abhängt , kann eine explizite vor der Hauptabfrage und das Hinzufügen zur Hauptabfrage eine gute Lösung sein.UPDATE STATISTICS #MyTemp;OPTION (RECOMPILE)

Wenn eine bestimmte Planform für diese Abfrage immer optimal ist, stehen Ihnen alternativ viele Optionen zur Verfügung, darunter eine Vielzahl von Hinweisen, Planleitfäden und das Erzwingen von Abfrage-Speicherplänen. Möglicherweise ist die Verwendung einer Tabellenvariablen anstelle einer temporären Tabelle die bessere Wahl, da sie den Fall niedriger Kardinalität bevorzugt und keine Statistiken bereitstellt (oder sich darauf stützt).

Zusammenfassend lässt sich sagen, dass einige allgemeine Verbesserungen vorgenommen werden sollten, bevor über die Gründe für (die Auswirkungen) gelegentlich fehlender Statistiken in der temporären Tabelle nachgedacht wird:

  • Stellen Sie sicher, dass Statistiken für den Optimierer repräsentativ und nützlich sind
  • Überprüfen Sie die Ist- und Schätzwerte für einen Bereich von Parameterwerten
  • Stellen Sie gute Datenzugriffspfade für die Abfrage bereit, indem Sie vorhandene Indizes verbessern
  • Entfernen Sie die Ansicht, wenn möglich. oder betrachten Sie eine 'parametrisierte Ansicht' (Inline-Tabellenwertfunktion) mit einem expliziten Prädikat für den Datums- / Zeitparameter.
  • Stellen Sie sicher, dass die automatische Erstellung von Statistiken nicht unnötig gedrosselt wird
  • Verwenden Sie die richtige Art von temporärem Objekt für die Aufgabe (Tabelle vs. Variable).
  • Überlegen Sie, RECOMPILEob die Planauswahl sehr empfindlich auf Parameterwerte reagiert
  • Hinzufügen UPDATE STATISTICSund RECOMPILEwenn zwischengespeicherte Statistiken ein Problem sind
  • Stellen Sie sich eine temporäre Tabelle mit einem Primärschlüssel vor, anstatt SELECT INTOdem Optimierer nützliche Informationen zu liefern
  • Überprüfen Sie das Schema, um sicherzustellen, dass das Optimierungsprogramm über die größtmöglichen Informationen verfügt (z. B. Fremdschlüssel, andere Einschränkungen).
  • Berücksichtigen Sie die Eignung gefilterter Indizes / Statistiken basierend auf Ihrem Wissen über die Daten
  • Streuen Sie keine NOLOCKHinweise, um die Leistung zu steigern

Repro

Folgendes wurde aus den begrenzten Informationen erstellt, die in den bereitgestellten redigierten Ausführungsplänen verfügbar sind:

DROP VIEW IF EXISTS dbo.View_VP;
DROP TABLE IF EXISTS dbo.SmallTable, dbo.LargeTable, #MyTemp;
GO
CREATE TABLE LargeTable (P_Id integer NOT NULL, Status integer NOT NULL, Col1 integer NOT NULL, Col4 datetime NOT NULL);
CREATE TABLE SmallTable (P_id integer NOT NULL, Col2 integer NOT NULL)
CREATE TABLE #MyTemp (Col1 integer NOT NULL);
GO
CREATE VIEW dbo.View_VP 
AS
    SELECT
        pp.Col1,
        pd.Col2 AS Col2,
        MAX(pp.Col4) AS Col3
    FROM LargeTable pp
    JOIN SmallTable pd
        ON pd.P_id = pp.P_Id
    WHERE 
        pp.[Status] IN (3, 4)
    GROUP BY 
        pp.Col1, pd.Col2;
GO
CREATE UNIQUE CLUSTERED INDEX PK_SmallTable ON dbo.SmallTable (P_id)
CREATE CLUSTERED INDEX ix_P_id ON dbo.LargeTable (P_Id)
CREATE INDEX ix_Col1 ON dbo.LargeTable (Col1)
CREATE INDEX ix_Status ON dbo.LargeTable ([Status])
GO
UPDATE STATISTICS dbo.LargeTable WITH ROWCOUNT = 32268200, PAGECOUNT = 322682;
UPDATE STATISTICS dbo.SmallTable WITH ROWCOUNT = 6349, PAGECOUNT = 63;
UPDATE STATISTICS #MyTemp WITH ROWCOUNT = 4;

Die Abfrage lautet:

DECLARE @Months integer = 6;

SELECT wd.Col1
         , vp.Col2
         , vp.Col3 
    FROM dbo.View_VP vp WITH (FORCESEEK)
    INNER JOIN #MyTemp wd ON wd.Col1 = vp.Col1
    WHERE vp.Col3 > DATEADD(MONTH, @Months * -1, GETDATE())

Ohne echte Statistiken zu den Basistabellen werden Pläne bevorzugt, die dem Beispiel "schlechter Plan" nahe kommen (unter Verwendung ix_Status):

Demo-Plan

Dies legt nahe, dass Informationen über die Selektivität von Col1ein wichtiger Faktor bei der Auswahl des Optimierers sind.

Paul White 9
quelle
Sie haben natürlich Recht mit dem Kardinalitätsunterschied der temporären Tabelle, aber ich füge nur die Daten hinzu, die selbst bei 114 Zeilen normalerweise einen Plan generieren, der nach ID und nicht nach Status erstellt wird. Vielen Dank!
Alex Friedman