L'unione hash tra le tabelle principale / di dettaglio produce una stima di cardinalità troppo bassa


9

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: VisitTargetha una riga per ogni transazione di vendita e VisitSaleuna 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:

inserisci qui la descrizione dell'immagine

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!

Risposte:


6

Supponendo che non si possa ottenere alcun miglioramento facendo qualcosa per le statistiche o usando il CE legacy, quindi il modo più semplice per aggirare il problema è cambiare il tuo INNER JOINa 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'

Se si dispone di una chiave esterna tra le tabelle, si filtra sempre sullo stesso SaleDateintervallo per entrambe le tabelle e SaleDatecorrisponde sempre tra le tabelle, quindi i risultati della query non devono cambiare. Può sembrare strano usare un join esterno come questo, ma pensalo come un informatore di Query Optimizer che il join alla VisitTargettabella non ridurrà mai il numero di righe restituite dalla query. Sfortunatamente, le chiavi esterne non cambiano le stime della cardinalità, quindi a volte è necessario ricorrere a trucchi come questo. (Suggerimento di Microsoft Connect: rendere le stime dell'ottimizzatore più accurate utilizzando i metadati ).

È possibile che la scrittura della query in questo modulo non funzioni bene a seconda di cosa succede nella query dopo il join. È possibile provare a utilizzare una tabella temporanea per contenere i risultati intermedi del set di risultati con la stima cardinalità più importante.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.