Łączenie skrótem między tabelami wzorcowymi / szczegółowymi daje zbyt niskie oszacowanie liczności

9

W jaki sposób zachęcić SQL Server 2014 do łączenia szacunkowej wielkości większej (szczegółowej) tabeli przy dołączaniu głównej tabeli do szczegółowej tabeli jako oszacowania liczności wyników łączenia?

Na przykład, łącząc 10 000 wierszy wzorcowych z 100 tys. Wierszy szczegółowych, chcę, aby SQL Server oszacował łączenie przy 100 tys. Wierszy - tyle samo, ile szacowana liczba wierszy szczegółowych. Jak powinienem ustrukturyzować moje zapytania i / lub tabele i / lub indeksy, aby pomóc estymatorowi SQL Server wykorzystać fakt, że każdy wiersz szczegółów ma zawsze odpowiedni wiersz główny? (Oznacza to, że połączenie między nimi nigdy nie powinno zmniejszać oszacowania liczności).

Oto więcej szczegółów. Nasza baza danych ma parę tabel głównych / szczegółowych: VisitTargetma jeden wiersz dla każdej transakcji sprzedaży i VisitSalema jeden wiersz dla każdego produktu w każdej transakcji. Jest to relacja jeden do wielu: jeden wiersz VisitTarget na średnio 10 wierszy VisitSale.

Tabele wyglądają tak: (Upraszczam tylko odpowiednie kolumny dla tego pytania)

-- "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;

Ze względu na wydajność częściowo zdenormalizowaliśmy, kopiując najpopularniejsze kolumny filtrujące (np. SaleDate) Z tabeli głównej do każdego wiersza tabeli szczegółów, a następnie dodaliśmy indeksy pokrywające w obu tabelach, aby lepiej obsługiwać zapytania filtrowane według dat. Działa to świetnie w celu zmniejszenia liczby operacji we / wy podczas uruchamiania zapytań filtrowanych według daty, ale myślę, że takie podejście powoduje problemy z oszacowaniem liczności podczas łączenia ze sobą tabel głównych i szczegółowych.

Kiedy dołączymy do tych dwóch tabel, zapytania wyglądają następująco:

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. 

Filtr daty w tabeli szczegółów ( VisitSale) jest zbędny. Ma tam włączyć sekwencyjne operacje we / wy (zwane także operatorem indeksowania) w tabeli szczegółów dla zapytań filtrowanych według zakresu dat.

Plan dla tego rodzaju zapytań wygląda następująco:

wprowadź opis zdjęcia tutaj

Aktualny plan zapytania z tym samym problemem można znaleźć tutaj .

Jak widać, oszacowanie liczności sprzężenia (etykietka w lewym dolnym rogu na zdjęciu) jest ponad 4x zbyt niskie: 2,1 mln faktycznych vs. 0,5 mln oszacowanych. Powoduje to problemy z wydajnością (np. Rozlewanie do tempdb), szczególnie gdy to zapytanie jest podzapytaniem używanym w bardziej złożonym zapytaniu.

Ale szacunki liczby wierszy dla każdej gałęzi złączenia są zbliżone do faktycznej liczby wierszy. Górna połowa sprzężenia to 100 000 rzeczywistych w porównaniu z 164 000 oszacowanymi. Dolna połowa złączenia to 2,1 mln wierszy rzeczywistych względem 3,7 mln szacowanych. Rozkład łyżki mieszania również wygląda dobrze. Te obserwacje sugerują mi, że statystyki są prawidłowe dla każdej tabeli i że problemem jest oszacowanie liczności złączeń.

Na początku myślałem, że problem polega na tym, że SQL Server spodziewał się, że kolumny SaleDate w każdej tabeli są niezależne, podczas gdy tak naprawdę są identyczne. Próbowałem więc dodać porównanie równości dat sprzedaży do warunku przyłączenia lub klauzuli WHERE, np

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

lub

WHERE vt.SaleDate = vs.SaleDate

To nie zadziałało. To nawet pogorszyło szacunki liczności! Więc albo SQL Server nie używa tej wskazówki równości, albo coś innego jest główną przyczyną problemu.

Masz jakieś pomysły, jak rozwiązać problem i, mam nadzieję, naprawić ten problem z oszacowaniem liczności? Moim celem jest oszacowanie liczności sprzężenia wzorcowego / szczegółowego tak samo, jak oszacowania dla większego wejścia („tabeli szczegółów”) sprzężenia.

Jeśli ma to znaczenie, uruchamiamy SQL Server 2014 Enterprise SP2 CU8 build 12.0.5557.0 na Windows Server. Nie są włączone flagi śledzenia. Poziom zgodności bazy danych to SQL Server 2014. Widzimy to samo zachowanie na wielu różnych serwerach SQL, więc wydaje się, że nie jest to problem specyficzny dla serwera.

W SQL Server 2014 Cardinality Estimator istnieje optymalizacja, której właśnie szukam:

Nowy CE wykorzystuje jednak prostszy algorytm, który zakłada, że ​​istnieje powiązanie jeden do wielu między dużym stołem a małym stołem. Zakłada się, że każdy wiersz w dużej tabeli odpowiada dokładnie jednemu rzędowi w małej tabeli. Ten algorytm zwraca szacunkową wielkość większego wejścia jako liczność sprzężenia.

Idealnie byłoby uzyskać takie zachowanie, w którym oszacowanie liczności dla złączenia byłoby takie samo jak oszacowanie dla dużego stołu, nawet jeśli mój „mały” stół nadal zwróci ponad 100 000 wierszy!

Justin Grant
źródło

Odpowiedzi:

6

Zakładając, że nie można uzyskać poprawy, robiąc coś ze statystyk lub używając starszej wersji CE, najprostszym sposobem rozwiązania problemu jest zmiana INNER JOINna 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'

Jeśli masz klucz obcy między tabelami, zawsze filtrujesz według tego samego SaleDatezakresu dla obu tabel i SaleDatezawsze pasuje między tabelami, wtedy wyniki zapytania nie powinny się zmienić. Używanie takiego łączenia zewnętrznego może wydawać się dziwne, ale pomyśl o tym jako o informowaniu optymalizatora zapytań, że połączenie z VisitTargettabelą nigdy nie zmniejszy liczby wierszy zwróconych przez zapytanie. Niestety klucze obce nie zmieniają oszacowań liczności, więc czasami trzeba uciekać się do takich sztuczek. (Sugestia Microsoft Connect: Zwiększ dokładność oszacowań optymalizatora za pomocą metadanych ).

Możliwe, że napisanie zapytania w tym formularzu nie będzie działać dobrze, w zależności od tego, co stanie się w zapytaniu po dołączeniu. Możesz spróbować użyć tabeli temp do przechowywania wyników pośrednich zestawu wyników z najważniejszym oszacowaniem liczności.

Joe Obbish
źródło