Jak wskazać przyłączenie wielu do wielu w SQL Server?

9

Mam 3 „duże” tabele, które łączą się w parę kolumn (obie int).

  • Tabela 1 ma ~ 200 milionów wierszy
  • Tabela 2 ma około 1,5 miliona wierszy
  • Tabela 3 ma około 6 milionów wierszy

Każda tabela ma indeks klastrowany Key1, Key2, a następnie jeszcze jedną kolumnę. Key1ma niską liczność i jest bardzo wypaczony. Jest to zawsze przywoływane w WHEREklauzuli. Key2nigdy nie jest wspomniany w WHEREklauzuli. Każde dołączenie jest wiele do wielu.

Problem polega na oszacowaniu liczności. Szacunkowe dane wyjściowe dla każdego złączenia stają się mniejsze niż większe . Powoduje to ostateczne oszacowanie niskich setek, gdy faktyczny wynik należy do milionów.

Czy jest jakiś sposób, aby wskazać CE, aby dokonać lepszych szacunków?

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;

Rozwiązania, które wypróbowałem:

  • Tworzenie statystyk wielokolumnowych Key1,Key2
  • Tworzenie ton filtrowanych statystyk na Key1(pomaga to trochę, ale w końcu mam tysiące statystyk stworzonych przez użytkowników w bazie danych).

Zamaskowany plan wykonania (przepraszam za złe maskowanie)

W przypadku, na który patrzę, wynik ma 9 milionów wierszy. Nowy CE szacuje 180 rzędów; starsze CE szacuje 6100 wierszy.

Oto powtarzalny przykład:

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
źródło

Odpowiedzi:

5

Dla jasności optymalizator wie już, że jest to połączenie wielu do wielu. Jeśli wymusisz połączenie podczas scalania i spojrzysz na szacunkowy plan, zobaczysz właściwość dla operatora łączenia, która mówi, czy połączenie może być wiele do wielu. Problemem, który musisz rozwiązać tutaj, jest podszycie szacunków liczności, prawdopodobnie po to, aby uzyskać bardziej wydajny plan zapytań dla części pominiętego zapytania.

Pierwszą rzeczą, którą chciałbym spróbować jest umieszczenie wyników łączenia z Object3i Object5do tabeli temp. W opublikowanym planie jest to tylko jedna kolumna w 51393 wierszach, więc nie powinna zajmować miejsca w tempdb. Możesz zebrać pełne statystyki w tabeli tymczasowej i to samo może być wystarczające, aby uzyskać wystarczającą dokładną ostateczną ocenę liczności. Gromadzenie pełnych statystyk również Object1może pomóc. Szacunki dotyczące liczności często pogarszają się podczas przechodzenia od planu od prawej do lewej.

Jeśli to nie zadziała, możesz wypróbować ENABLE_QUERY_OPTIMIZER_HOTFIXESwskazówkę dotyczącą zapytania, jeśli jeszcze go nie włączono na poziomie bazy danych lub serwera. Microsoft blokuje wpływające na plan poprawki wydajności dla programu SQL Server 2016 za tym ustawieniem. Niektóre z nich dotyczą szacunków liczności, więc być może będziesz miał szczęście i jedna z poprawek pomoże w twoim zapytaniu. Możesz także spróbować użyć starszego estymatora liczności z FORCE_LEGACY_CARDINALITY_ESTIMATIONpodpowiedzią do zapytania. Niektóre zestawy danych mogą uzyskać lepsze oszacowania dzięki starszej wersji CE.

W ostateczności możesz ręcznie zwiększyć oszacowanie liczności o dowolny czynnik, korzystając z MANY()funkcji Adama Machanica . Mówię o tym w innej odpowiedzi, ale wygląda na to, że link nie działa. Jeśli jesteś zainteresowany, mogę spróbować coś wykopać.

Joe Obbish
źródło
make_parallelFunkcja Adama przyzwyczaja się do łagodzenia problemu. Rzucę okiem many. Wydaje się, że to dość obrzydliwa pomoc.
Steven Hibble
2

Statystyka programu SQL Server zawiera tylko histogram dla wiodącej kolumny obiektu statystyki. Dlatego możesz tworzyć filtrowane statystyki, które zapewniają histogram wartości dla Key2, ale tylko między wierszami z Key1 = 1. Utworzenie tych filtrowanych statystyk w każdej tabeli naprawia szacunki i prowadzi do oczekiwanego zachowania dla zapytania testowego: każde nowe sprzężenie nie wpływa na ostateczną ocenę liczności (potwierdzoną zarówno w SQL 2016 SP1, jak i 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

Bez tych filtrowanych statystyk SQL Server podejmie bardziej heurystyczne podejście do szacowania liczności twojego łączenia. Poniższy oficjalny dokument zawiera dobre opisy niektórych heurystyk używanych przez SQL Server na wysokim poziomie: Optymalizacja planów zapytań za pomocą programu SQL Server 2014 Cardinality Estimator .

Na przykład dodanie USE HINT('ASSUME_JOIN_PREDICATE_DEPENDS_ON_FILTERS')podpowiedzi do zapytania spowoduje zmianę heurystyki zawierania złączeń, aby przyjąć pewną korelację (a nie niezależność) między Key1predykatem a Key2predykatem łączenia, co może być korzystne dla zapytania. W przypadku ostatniego zapytania testowego ta podpowiedź zwiększa szacunkową liczność od 1,175do 7,551, ale wciąż jest nieco nieśmiała w stosunku do poprawnego 20,000oszacowania wiersza uzyskanego z filtrowanymi statystykami.

Innym podejściem zastosowanym w podobnych sytuacjach jest wyodrębnienie odpowiedniego podzbioru danych do tabel #temp. Zwłaszcza teraz, gdy nowsze wersje SQL Servera nie chętnie zapisują na dysku tabele #temp , osiągnęliśmy dobre wyniki dzięki temu podejściu. Twój opis łączenia wielu do wielu sugeruje, że każda indywidualna tabela #temp w twoim przypadku byłaby względnie mała (lub przynajmniej mniejsza niż końcowy zestaw wyników), więc to podejście może być warte wypróbowania.

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
źródło
Używamy filtrowanych statystyk w szerokim zakresie, ale sprawiamy, że są one po jednej Key1wartości w każdej tabeli. Teraz mamy ich tysiące.
Steven Hibble
2
@StevenHibble Dobrze, że tysiące odfiltrowanych statystyk może utrudnić zarządzanie. (Widzieliśmy również, że ma to negatywny wpływ na czas kompilacji planu). Może nie pasować do twojego przypadku użycia, ale dodałem również inne podejście do tabeli #temp, z którego korzystaliśmy kilka razy.
Geoff Patterson
-1

Zasięg Nie ma prawdziwej podstawy poza próbą.

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
źródło