Quando si unisce una tabella principale a una tabella dei dettagli, come è possibile incoraggiare SQL Server 2014 a utilizzare la stima della cardinalità della tabella (di dettaglio) più grande come stima della cardinalità dell'output del join?
Ad esempio, quando si uniscono righe master 10K a righe di dettaglio 100K, voglio che SQL Server valuti l'unione su righe di 100K, lo stesso del numero stimato di righe di dettaglio. Come devo strutturare le mie query e / o tabelle e / o indici per aiutare lo stimatore di SQL Server a sfruttare il fatto che ogni riga di dettaglio ha sempre una riga principale corrispondente? (Ciò significa che un'unione tra loro non dovrebbe mai ridurre la stima della cardinalità.)
Ecco maggiori dettagli. Il nostro database ha una coppia di tabelle principale / dettaglio: VisitTarget
ha una riga per ogni transazione di vendita e VisitSale
una riga per ogni prodotto in ciascuna transazione. È una relazione uno-a-molti: una riga VisitTarget per una media di 10 righe VisitSale.
Le tabelle sono simili alle seguenti: (Sto semplificando solo le colonne pertinenti per questa domanda)
-- "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;
Per motivi di prestazioni, abbiamo parzialmente denormalizzato copiando le colonne di filtro più comuni (ad es. SaleDate
) Dalla tabella principale nelle righe di ciascuna tabella dei dettagli, quindi abbiamo aggiunto gli indici di copertura su entrambe le tabelle per supportare meglio le query filtrate per data. Funziona alla grande per ridurre l'I / O quando si eseguono query con filtro data, ma penso che questo approccio stia causando problemi di stima della cardinalità quando si uniscono le tabelle principale e di dettaglio.
Quando ci uniamo a queste due tabelle, le query si presentano così:
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.
Il filtro data nella tabella dei dettagli ( VisitSale
) è ridondante. È lì per abilitare l'I / O sequenziale (noto anche come operatore di ricerca indice) nella tabella dei dettagli per le query filtrate in base a un intervallo di date.
Il piano per questo tipo di query è simile al seguente:
Un piano reale di una query con lo stesso problema è disponibile qui .
Come puoi vedere, la stima della cardinalità per il join (la descrizione in basso a sinistra nella foto) è oltre 4 volte troppo bassa: 2,1 milioni effettivi rispetto a 0,5 milioni stimati. Ciò causa problemi di prestazioni (ad es. Fuoriuscita in tempdb), specialmente quando questa query è una sottoquery utilizzata in una query più complessa.
Ma le stime del conteggio delle righe per ciascun ramo del join sono vicine ai conteggi delle righe effettivi. La metà superiore del join è effettiva 100.000 rispetto a 164.000 stimati. La metà inferiore del join è pari a 2,1 milioni di righe rispetto a 3,7 milioni stimati. Anche la distribuzione dell'hash bucket sembra buona. Queste osservazioni mi suggeriscono che le statistiche sono OK per ogni tabella e che il problema è la stima della cardinalità del join.
All'inizio ho pensato che il problema fosse SQL Server, prevedendo che le colonne SaleDate in ogni tabella fossero indipendenti, mentre in realtà erano identiche. Quindi ho provato ad aggiungere un confronto di uguaglianza per le date di vendita alla condizione di join o alla clausola WHERE, ad es
ON vt.VisitTargetId = vs.VisitTargetId and vt.SaleDate = vs.SaleDate
o
WHERE vt.SaleDate = vs.SaleDate
Questo non ha funzionato. Ha persino peggiorato le stime sulla cardinalità! Quindi o SQL Server non utilizza quel suggerimento sull'uguaglianza o qualcos'altro è la causa principale del problema.
Hai qualche idea su come risolvere e spero di risolvere questo problema di stima della cardinalità? Il mio obiettivo è di stimare la cardinalità del master / dettaglio in modo analogo alla stima per l'input più grande ("tabella dettagli") del join.
Se è importante, stiamo eseguendo SQL Server 2014 Enterprise SP2 CU8 build 12.0.5557.0 su Windows Server. Non ci sono flag di traccia abilitati. Il livello di compatibilità del database è SQL Server 2014. Vediamo lo stesso comportamento su più server SQL diversi, quindi sembra improbabile che si tratti di un problema specifico del server.
C'è un'ottimizzazione nello stimatore della cardinalità di SQL Server 2014 che è esattamente il comportamento che sto cercando:
Il nuovo CE, tuttavia, utilizza un algoritmo più semplice che presuppone l'esistenza di un'associazione di join uno-a-molti tra una tabella grande e una tabella piccola. Ciò presuppone che ogni riga nella tabella grande corrisponda esattamente a una riga nella tabella piccola. Questo algoritmo restituisce la dimensione stimata dell'input più grande come cardinalità di join.
Idealmente potrei ottenere questo comportamento, in cui la stima della cardinalità per il join sarebbe la stessa della stima per la tabella grande, anche se la mia tabella "piccola" restituirà comunque oltre 100.000 righe!