Wie kann man einen SQL-Beitritt von vielen zu vielen andeuten?

9

Ich habe 3 "große" Tabellen, die auf einem Spaltenpaar (beide ints) verbunden sind.

  • Tabelle 1 enthält ~ 200 Millionen Zeilen
  • Tabelle 2 enthält ~ 1,5 Millionen Zeilen
  • Tabelle 3 enthält ~ 6 Millionen Zeilen

Jeder Tisch hat einen Clustered - Index auf Key1, Key2und dann eine weitere Spalte. Key1hat eine geringe Kardinalität und ist sehr schief. In der WHEREKlausel wird immer darauf verwiesen . Key2wird in der WHEREKlausel nie erwähnt . Jeder Join ist viele zu viele.

Das Problem liegt in der Kardinalitätsschätzung. Die Ausgabeschätzung jedes Joins wird kleiner statt größer . Dies führt zu endgültigen Schätzungen von niedrigen Hunderten, wenn das tatsächliche Ergebnis weit in die Millionen geht.

Gibt es eine Möglichkeit für mich, das CE zu besseren Schätzungen zu bewegen?

SELECT 1
FROM Table1 t1
     JOIN Table2 t2
       ON t1.Key1 = t2.Key1
          AND t1.Key2 = t2.Key2
     JOIN Table3 t3
       ON t1.Key1 = t3.Key1
          AND t1.Key2 = t3.Key2
WHERE t1.Key1 = 1;

Lösungen, die ich ausprobiert habe:

  • Erstellen mehrspaltiger Statistiken für Key1,Key2
  • Erstellen von Tonnen von gefilterten Statistiken auf Key1(Dies hilft ziemlich viel, aber ich habe am Ende Tausende von benutzerdefinierten Statistiken in der Datenbank.)

Maskierter Ausführungsplan (Entschuldigung für die schlechte Maskierung)

In dem Fall, den ich betrachte, hat das Ergebnis 9 Millionen Zeilen. Das neue CE schätzt 180 Zeilen; Das alte CE schätzt 6100 Zeilen.

Hier ist ein reproduzierbares Beispiel:

DROP TABLE IF EXISTS #Table1, #Table2, #Table3;
CREATE TABLE #Table1 (Key1 INT NOT NULL, Key2 INT NOT NULL, T1Key3 INT NOT NULL, CONSTRAINT pk_t1 PRIMARY KEY CLUSTERED (Key1, Key2, T1Key3));
CREATE TABLE #Table2 (Key1 INT NOT NULL, Key2 INT NOT NULL, T2Key3 INT NOT NULL, CONSTRAINT pk_t2 PRIMARY KEY CLUSTERED (Key1, Key2, T2Key3));
CREATE TABLE #Table3 (Key1 INT NOT NULL, Key2 INT NOT NULL, T3Key3 INT NOT NULL, CONSTRAINT pk_t3 PRIMARY KEY CLUSTERED (Key1, Key2, T3Key3));

-- Table1 
WITH Numbers
     AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
         FROM master..spt_values t1
              CROSS JOIN master..spt_values t2),
     DataSize (Key1, NumberOfRows)
     AS (SELECT 1, 2000 UNION
         SELECT 2, 10000 UNION
         SELECT 3, 25000 UNION
         SELECT 4, 50000 UNION
         SELECT 5, 200000)
INSERT INTO #Table1
SELECT Key1
     , Key2 = ROW_NUMBER() OVER (PARTITION BY Key1, T1Key3 ORDER BY Number)
     , T1Key3
FROM DataSize
     CROSS APPLY (SELECT TOP(NumberOfRows) 
                         Number
                       , T1Key3 = Number%(Key1*Key1) + 1 
                  FROM Numbers
                  ORDER BY Number) size;

-- Table2 (same Key1, Key2 values; smaller number of distinct third Key)
WITH Numbers
     AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
         FROM master..spt_values t1
              CROSS JOIN master..spt_values t2)
INSERT INTO #Table2
SELECT DISTINCT 
       Key1
     , Key2
     , T2Key3
FROM #Table1
     CROSS APPLY (SELECT TOP (Key1*10) 
                         T2Key3 = Number
                  FROM Numbers
                  ORDER BY Number) size;

-- Table2 (same Key1, Key2 values; smallest number of distinct third Key)
WITH Numbers
     AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
         FROM master..spt_values t1
              CROSS JOIN master..spt_values t2)
INSERT INTO #Table3
SELECT DISTINCT 
       Key1
     , Key2
     , T3Key3
FROM #Table1
     CROSS APPLY (SELECT TOP (Key1) 
                         T3Key3 = Number
                  FROM Numbers
                  ORDER BY Number) size;


DROP TABLE IF EXISTS #a;
SELECT col = 1 
INTO #a
FROM #Table1 t1
     JOIN #Table2 t2
       ON t1.Key1 = t2.Key1
          AND t1.Key2 = t2.Key2
WHERE t1.Key1 = 1;

DROP TABLE IF EXISTS #b;
SELECT col = 1 
INTO #b
FROM #Table1 t1
     JOIN #Table2 t2
       ON t1.Key1 = t2.Key1
          AND t1.Key2 = t2.Key2
     JOIN #Table3 t3
       ON t1.Key1 = t3.Key1
          AND t1.Key2 = t3.Key2
WHERE t1.Key1 = 1;
Steven Hibble
quelle

Antworten:

5

Um ganz klar zu sein, der Optimierer weiß bereits, dass es sich um eine Viele-zu-Viele-Verbindung handelt. Wenn Sie Zusammenführungsverknüpfungen erzwingen und einen geschätzten Plan anzeigen, wird eine Eigenschaft für den Verknüpfungsoperator angezeigt, die angibt, ob die Verknüpfung viele zu viele sein kann. Das Problem, das Sie hier lösen müssen, besteht darin, die Kardinalitätsschätzungen zu erhöhen, vermutlich, damit Sie einen effizienteren Abfrageplan für den Teil der Abfrage erhalten, den Sie ausgelassen haben.

Das erste, was ich versuchen würde, ist, die Ergebnisse des Joins aus Object3und Object5in eine temporäre Tabelle zu schreiben. Für den Plan, den Sie veröffentlicht haben, ist es nur eine einzelne Spalte in 51393 Zeilen, daher sollte er in tempdb kaum Platz beanspruchen. Sie können vollständige Statistiken auf dem temporären Tisch sammeln, und dies allein könnte ausreichen, um eine ausreichend genaue endgültige Kardinalitätsschätzung zu erhalten. Das Sammeln vollständiger Statistiken Object1kann ebenfalls hilfreich sein. Kardinalitätsschätzungen werden häufig schlechter, wenn Sie von einem Plan von rechts nach links wechseln.

Wenn dies nicht funktioniert, können Sie den ENABLE_QUERY_OPTIMIZER_HOTFIXESAbfragehinweis ausprobieren, wenn Sie ihn noch nicht auf Datenbank- oder Serverebene aktiviert haben. Microsoft sperrt planbeeinflussende Leistungskorrekturen für SQL Server 2016 hinter dieser Einstellung. Einige davon beziehen sich auf Kardinalitätsschätzungen. Vielleicht haben Sie Glück und eine der Korrekturen hilft Ihnen bei Ihrer Anfrage. Sie können auch versuchen, den älteren Kardinalitätsschätzer mit einem FORCE_LEGACY_CARDINALITY_ESTIMATIONAbfragehinweis zu verwenden. Bestimmte Datensätze können mit dem alten CE bessere Schätzungen erhalten.

Als letzten Ausweg können Sie die Kardinalitätsschätzung mithilfe der MANY()Funktion von Adam Machanic manuell um einen beliebigen Faktor erhöhen . Ich spreche in einer anderen Antwort darüber, aber es sieht so aus, als ob der Link tot ist. Wenn Sie interessiert sind, kann ich versuchen, etwas auszugraben.

Joe Obbish
quelle
Adams make_parallelFunktion wird verwendet, um das Problem zu lösen. Ich werde einen Blick darauf werfen many. Scheint ein ziemlich grobes Pflaster zu sein.
Steven Hibble
2

SQL Server-Statistiken enthalten nur ein Histogramm für die führende Spalte des Statistikobjekts. Daher können Sie gefilterte Statistiken erstellen, die ein Histogramm von Werten für Key2, jedoch nur zwischen Zeilen mit bereitstellen Key1 = 1. Das Erstellen dieser gefilterten Statistiken für jede Tabelle korrigiert die Schätzungen und führt zu dem Verhalten, das Sie für die Testabfrage erwarten: Jeder neue Join hat keinen Einfluss auf die endgültige Kardinalitätsschätzung (bestätigt in SQL 2016 SP1 und SQL 2017).

-- Note: Add "WITH FULLSCAN" to each if you want a perfect 20,000 row estimate
CREATE STATISTICS st_#Table1 ON #Table1 (Key2) WHERE Key1 = 1
CREATE STATISTICS st_#Table2 ON #Table2 (Key2) WHERE Key1 = 1
CREATE STATISTICS st_#Table3 ON #Table3 (Key2) WHERE Key1 = 1

Ohne diese gefilterten Statistiken verfolgt SQL Server einen heuristischeren Ansatz zur Schätzung der Kardinalität Ihres Joins. Das folgende Whitepaper enthält allgemeine Beschreibungen einiger der von SQL Server verwendeten Heuristiken: Optimieren Ihrer Abfragepläne mit dem SQL Server 2014 Cardinality Estimator .

Wenn Sie beispielsweise den USE HINT('ASSUME_JOIN_PREDICATE_DEPENDS_ON_FILTERS')Hinweis zu Ihrer Abfrage hinzufügen , wird die Heuristik des Join-Containments so geändert, dass eine gewisse Korrelation (und keine Unabhängigkeit) zwischen dem Key1Prädikat und dem Key2Join-Prädikat angenommen wird, was für Ihre Abfrage von Vorteil sein kann. Bei der abschließenden Testabfrage erhöht dieser Hinweis die Kardinalitätsschätzung von 1,175bis 7,551, ist jedoch immer noch ziemlich schüchtern gegenüber der korrekten 20,000Zeilenschätzung, die mit den gefilterten Statistiken erstellt wurde.

Ein anderer Ansatz, den wir in ähnlichen Situationen verwendet haben, besteht darin, die relevante Teilmenge der Daten in # temp-Tabellen zu extrahieren. Insbesondere jetzt, da neuere Versionen von SQL Server nicht mehr eifrig # temp-Tabellen auf die Festplatte schreiben , haben wir mit diesem Ansatz gute Ergebnisse erzielt. Ihre Beschreibung Ihres Viele-zu-Viele-Joins impliziert, dass jede einzelne # temp-Tabelle in Ihrem Fall relativ klein (oder zumindest kleiner als die endgültige Ergebnismenge) ist. Daher ist dieser Ansatz möglicherweise einen Versuch wert.

DROP TABLE IF EXISTS #Table1_extract, #Table2_extract, #Table3_extract, #c
-- Extract only the subset of rows that match the filter predicate
-- (Or better yet, extract only the subset of columns you need!)
SELECT * INTO #Table1_extract FROM #Table1 WHERE Key1 = 1
SELECT * INTO #Table2_extract FROM #Table2 WHERE Key1 = 1
SELECT * INTO #Table3_extract FROM #Table3 WHERE Key1 = 1
-- Now perform the join on those extracts, removing the filter predicate
SELECT col = 1
INTO #c 
FROM #Table1_extract t1
JOIN #Table2_extract t2
    ON t1.Key2 = t2.Key2
JOIN #Table3_extract t3
    ON t1.Key2 = t3.Key2
Geoff Patterson
quelle
Wir verwenden häufig gefilterte Statistiken, machen sie jedoch Key1für jede Tabelle zu einer pro Wert. Wir haben jetzt Tausende von ihnen.
Steven Hibble
2
@StevenHibble Guter Punkt, dass Tausende von gefilterten Statistiken die Verwaltung erschweren könnten. (Wir haben auch gesehen, dass sich dies negativ auf die Planerstellungszeit auswirkt.) Es passt möglicherweise nicht zu Ihrem Anwendungsfall, aber ich habe auch einen anderen # temp-Tabellenansatz hinzugefügt, den wir mehrmals erfolgreich verwendet haben.
Geoff Patterson
-1

Eine Reichweite. Keine andere wirkliche Basis als zu versuchen.

SELECT 1
FROM Table1 t1
     JOIN Table2 t2
       ON t1.Key2 = t2.Key2
      AND t1.Key1 = 1
      AND t2.Key1 = 1
     JOIN Table3 t3
       ON t2.Key2 = t3.Key2
      AND t3.Key1 = 1;
Paparazzo
quelle