Die Hash-Verknüpfung zwischen Master- / Detailtabellen führt zu einer zu niedrigen Kardinalitätsschätzung

9

Wie kann ich SQL Server 2014 beim Verknüpfen einer Mastertabelle mit einer Detailtabelle dazu ermutigen, die Kardinalitätsschätzung der größeren (Detail-) Tabelle als Kardinalitätsschätzung der Verknüpfungsausgabe zu verwenden?

Wenn Sie beispielsweise 10K-Masterzeilen mit 100K-Detailzeilen verknüpfen, möchte SQL Server, dass der Join auf 100K-Zeilen geschätzt wird - genau wie die geschätzte Anzahl der Detailzeilen. Wie sollte ich meine Abfragen und / oder Tabellen und / oder Indizes strukturieren, damit der SQL Server-Schätzer die Tatsache nutzen kann, dass jede Detailzeile immer eine entsprechende Hauptzeile enthält? (Dies bedeutet, dass eine Verbindung zwischen ihnen niemals die Kardinalitätsschätzung verringern sollte.)

Hier finden Sie weitere Details. Unsere Datenbank verfügt über ein Master / Detail-Tabellenpaar: VisitTargetEine Zeile für jede Verkaufstransaktion und VisitSaleeine Zeile für jedes Produkt in jeder Transaktion. Es ist eine Eins-zu-Viele-Beziehung: eine VisitTarget-Zeile für durchschnittlich 10 VisitSale-Zeilen.

Die Tabellen sehen folgendermaßen aus: (Ich vereinfache nur die relevanten Spalten für diese Frage.)

-- "master" table
CREATE TABLE VisitTarget
(
  VisitTargetId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
  SaleDate date NOT NULL,
  StoreId int NOT NULL
  -- other columns omitted for clarity  
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitTarget_SaleDate 
    ON VisitTarget (SaleDate) INCLUDE (StoreId /*, ...more columns */);

-- "detail" table
CREATE TABLE VisitSale
(
  VisitSaleId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
  VisitTargetId int NOT NULL,
  SaleDate date NOT NULL, -- denormalized; copied from VisitTarget
  StoreId int NOT NULL, -- denormalized; copied from VisitTarget
  ItemId int NOT NULL,
  SaleQty int NOT NULL,
  SalePrice decimal(9,2) NOT NULL
  -- other columns omitted for clarity  
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitSale_SaleDate 
  ON VisitSale (SaleDate)
  INCLUDE (VisitTargetId, StoreId, ItemId, SaleQty, TotalSalePrice decimal(9,2) /*, ...more columns */
);
ALTER TABLE VisitSale 
  WITH CHECK ADD CONSTRAINT FK_VisitSale_VisitTargetId 
  FOREIGN KEY (VisitTargetId)
  REFERENCES VisitTarget (VisitTargetId);
ALTER TABLE VisitSale
  CHECK CONSTRAINT FK_VisitSale_VisitTargetId;

Aus Leistungsgründen haben wir teilweise denormalisiert, indem wir die am häufigsten verwendeten Filterspalten (z. B. SaleDate) aus der Mastertabelle in die einzelnen Detailtabellenzeilen kopiert haben. Anschließend haben wir Deckungsindizes für beide Tabellen hinzugefügt, um datumsgefilterte Abfragen besser zu unterstützen. Dies funktioniert hervorragend, um die E / A beim Ausführen datumsgefilterter Abfragen zu reduzieren. Ich denke jedoch, dass dieser Ansatz Probleme beim Schätzen der Kardinalität verursacht, wenn die Master- und die Detailtabelle zusammengefügt werden.

Wenn wir diese beiden Tabellen verbinden, sehen Abfragen folgendermaßen aus:

SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitTarget vt 
    JOIN VisitSale vs on vt.VisitTargetId = vs.VisitTargetId
WHERE
    vs.SaleDate BETWEEN '20170101' and '20171231'
    and vt.SaleDate BETWEEN '20170101' and '20171231'
    -- more filtering goes here, e.g. by store, by product, etc. 

Der Datumsfilter in der Detailtabelle ( VisitSale) ist redundant. Hiermit können Sie sequentielle E / A (auch als Indexsuchoperator bezeichnet) in der Detailtabelle für Abfragen aktivieren, die nach einem Datumsbereich gefiltert werden.

Der Plan für diese Art von Abfragen sieht folgendermaßen aus:

Geben Sie hier die Bildbeschreibung ein

Einen tatsächlichen Plan einer Abfrage mit demselben Problem finden Sie hier .

Wie Sie sehen können, ist die Kardinalitätsschätzung für die Verknüpfung (der Tooltip unten links im Bild) mehr als viermal zu niedrig: 2,1 Mio. tatsächlich vs. 0,5 Mio. geschätzt. Dies führt zu Leistungsproblemen (z. B. Verschütten in Tempdb), insbesondere wenn es sich bei dieser Abfrage um eine Unterabfrage handelt, die in einer komplexeren Abfrage verwendet wird.

Die Zeilenanzahlschätzungen für jeden Zweig des Joins liegen jedoch nahe an den tatsächlichen Zeilenzahlen. Die obere Hälfte des Joins beträgt 100.000 tatsächliche gegenüber geschätzten 164.000. Die untere Hälfte des Joins besteht aus 2,1 Millionen Zeilen im Vergleich zu geschätzten 3,7 Millionen. Die Hash-Bucket-Verteilung sieht auch gut aus. Diese Beobachtungen legen mir nahe, dass die Statistiken für jede Tabelle in Ordnung sind und dass das Problem die Schätzung der Verknüpfungskardinalität ist.

Zuerst dachte ich, dass das Problem darin bestand, dass SQL Server erwartete, dass SaleDate-Spalten in jeder Tabelle unabhängig sind, während sie tatsächlich identisch sind. Deshalb habe ich versucht, der Join-Bedingung oder der WHERE-Klausel einen Gleichheitsvergleich für die Verkaufsdaten hinzuzufügen, z

ON vt.VisitTargetId = vs.VisitTargetId and vt.SaleDate = vs.SaleDate

oder

WHERE vt.SaleDate = vs.SaleDate

Das hat nicht funktioniert. Es hat sogar die Kardinalitätsschätzungen verschlechtert! Entweder verwendet SQL Server diesen Gleichheitshinweis nicht oder etwas anderes ist die Hauptursache des Problems.

Haben Sie Ideen, wie Sie dieses Problem mit der Kardinalitätsschätzung beheben und hoffentlich beheben können? Mein Ziel ist es, dass die Kardinalität des Master- / Detail-Joins genauso geschätzt wird wie die Schätzung für die größere Eingabe ("Detailtabelle") des Joins.

Wenn es darauf ankommt, führen wir SQL Server 2014 Enterprise SP2 CU8 Build 12.0.5557.0 unter Windows Server aus. Es sind keine Ablaufverfolgungsflags aktiviert. Die Datenbankkompatibilitätsstufe ist SQL Server 2014. Wir sehen dasselbe Verhalten auf mehreren verschiedenen SQL Servern, sodass es unwahrscheinlich ist, dass es sich um ein serverspezifisches Problem handelt.

Im SQL Server 2014 Cardinality Estimator gibt es eine Optimierung, die genau das Verhalten ist, nach dem ich suche:

Das neue CE verwendet jedoch einen einfacheren Algorithmus, der davon ausgeht, dass zwischen einer großen und einer kleinen Tabelle eine Eins-zu-Viele-Verknüpfung besteht. Dies setzt voraus, dass jede Zeile in der großen Tabelle genau einer Zeile in der kleinen Tabelle entspricht. Dieser Algorithmus gibt die geschätzte Größe der größeren Eingabe als Join-Kardinalität zurück.

Im Idealfall könnte ich dieses Verhalten erhalten, bei dem die Kardinalitätsschätzung für den Join mit der Schätzung für die große Tabelle übereinstimmt, obwohl meine "kleine" Tabelle immer noch über 100.000 Zeilen zurückgibt!

Justin Grant
quelle

Antworten:

6

Unter der Annahme, dass keine Verbesserung erzielt werden kann, indem Sie etwas an Statistiken vornehmen oder das ältere CE verwenden, besteht der einfachste Weg, um Ihr Problem zu umgehen, darin, Ihr INNER JOINzu ändern in LEFT OUTER JOIN:

SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitSale vs
    LEFT OUTER JOIN VisitTarget vt on vt.VisitTargetId = vs.VisitTargetId
            AND vt.SaleDate BETWEEN '20170101' and '20171231'
WHERE vs.SaleDate BETWEEN '20170101' and '20171231'

Wenn Sie einen Fremdschlüssel zwischen Tabellen haben, filtern Sie immer nach demselben SaleDateBereich für beide Tabellen und SaleDatestimmen immer zwischen Tabellen überein, dann sollten sich die Ergebnisse Ihrer Abfrage nicht ändern. Es mag seltsam erscheinen, einen äußeren Join wie diesen zu verwenden, aber denken Sie daran, dass der Abfrageoptimierer darüber informiert wird, dass der Join zur VisitTargetTabelle niemals die Anzahl der von der Abfrage zurückgegebenen Zeilen verringert. Leider ändern Fremdschlüssel die Kardinalitätsschätzungen nicht, sodass Sie manchmal auf solche Tricks zurückgreifen müssen. (Microsoft Connect-Vorschlag: Optimieren Sie die Schätzungen des Optimierers mithilfe von Metadaten. )

Es ist möglich, dass das Schreiben der Abfrage in diesem Formular nicht gut funktioniert, je nachdem, was nach dem Join noch in der Abfrage passiert. Sie können versuchen, eine temporäre Tabelle zu verwenden, um die Zwischenergebnisse der Ergebnismenge mit der wichtigsten Kardinalitätsschätzung zu speichern.

Joe Obbish
quelle