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: VisitTarget
ma jeden wiersz dla każdej transakcji sprzedaży i VisitSale
ma 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:
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!