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: VisitTarget
Eine Zeile für jede Verkaufstransaktion und VisitSale
eine 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:
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!