Come suggerire un join molti-a-molti in SQL Server?


9

Ho 3 tabelle "grandi" che si uniscono su una coppia di colonne (entrambe int).

  • Table1 ha ~ 200 milioni di righe
  • Table2 ha ~ 1,5 milioni di righe
  • Table3 ha ~ 6 milioni di righe

Ogni tabella ha un indice cluster su Key1, Key2quindi un'altra colonna. Key1ha una cardinalità bassa ed è molto distorto. È sempre indicato nella WHEREclausola. Key2non è mai menzionato nella WHEREclausola. Ogni join è molti-a-molti.

Il problema è con la stima della cardinalità. La stima dell'output di ciascun join diventa più piccola anziché più grande . Ciò si traduce in stime finali di centinaia basse quando il risultato effettivo è ben in milioni.

C'è un modo per me di indurre la CE a fare stime migliori?

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;

Soluzioni che ho provato:

  • Creazione di statistiche multi-colonna su Key1,Key2
  • Creare tonnellate di statistiche filtrate su Key1(Questo aiuta parecchio, ma finisco con migliaia di statistiche create dall'utente nel database.)

Piano di esecuzione mascherato (scusate il cattivo mascheramento)

Nel caso che sto guardando, il risultato ha 9 milioni di righe. Il nuovo CE stima 180 righe; l'eredità CE stima 6100 righe.

Ecco un esempio riproducibile:

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;

Risposte:


5

Giusto per essere chiari, l'ottimizzatore sa già che è un join molti-a-molti. Se si forzano join di unione e si osserva un piano stimato, è possibile visualizzare una proprietà per l'operatore di join che indica se il join potrebbe essere molti-a-molti. Il problema che devi risolvere qui sta aumentando le stime di cardinalità, presumibilmente in modo da ottenere un piano di query più efficiente per la parte della query che hai lasciato fuori.

La prima cosa che vorrei provare è inserire i risultati del join da Object3e Object5in una tabella temporanea. Per il piano che hai pubblicato è solo una singola colonna su 51393 righe, quindi non dovrebbe occupare spazio in tempdb. Puoi raccogliere statistiche complete sulla tabella delle temp e questo da solo potrebbe essere sufficiente per ottenere una stima della cardinalità finale sufficientemente accurata. Anche raccogliere statistiche complete Object1può essere d'aiuto. Le stime della cardinalità spesso peggiorano quando si passa da un piano da destra a sinistra.

Se il ENABLE_QUERY_OPTIMIZER_HOTFIXESproblema persiste, puoi provare il suggerimento per la query se non lo hai già abilitato a livello di database o server. Microsoft blocca le correzioni delle prestazioni che incidono sul piano per SQL Server 2016 dietro tale impostazione. Alcuni si riferiscono alle stime della cardinalità, quindi forse sarai fortunato e una delle correzioni ti aiuterà con la tua query. Puoi anche provare a utilizzare lo stimatore di cardinalità legacy con un FORCE_LEGACY_CARDINALITY_ESTIMATIONsuggerimento per la query. Alcuni set di dati possono ottenere stime migliori con il CE legacy.

Come ultima risorsa puoi aumentare manualmente la stima della cardinalità di qualunque fattore ti piaccia usare la MANY()funzione di Adam Machanic . Ne parlo in un'altra risposta, ma sembra che il link sia morto. Se sei interessato, posso provare a scavare qualcosa.


La make_parallelfunzione di Adam viene utilizzata per aiutare a mitigare il problema. Darò un'occhiata many. Sembra un cerotto piuttosto grossolano.
Steven Hibble,

2

Le statistiche di SQL Server contengono solo un istogramma per la colonna iniziale dell'oggetto statistico. Pertanto, è possibile creare statistiche filtrate che forniscono un istogramma di valori per Key2, ma solo tra le righe con Key1 = 1. La creazione di queste statistiche filtrate su ogni tabella corregge le stime e porta al comportamento previsto per la query di test: ogni nuovo join non influisce sulla stima di cardinalità finale (confermata in SQL 2016 SP1 e 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

Senza queste statistiche filtrate, SQL Server adotterà un approccio più euristico per stimare la cardinalità del join. Il seguente white paper contiene buone descrizioni di alto livello di alcune euristiche utilizzate da SQL Server: Ottimizzazione dei piani di query con lo stimatore di cardinalità di SQL Server 2014 .

Ad esempio, l'aggiunta del USE HINT('ASSUME_JOIN_PREDICATE_DEPENDS_ON_FILTERS')suggerimento alla query cambierà l'euristica di contenimento del join in modo da assumere una correlazione (anziché indipendenza) tra il Key1predicato e il Key2predicato del join, il che potrebbe essere utile per la query. Per la query di test finale, questo suggerimento aumenta la stima della cardinalità da 1,175a 7,551, ma è ancora un po 'timida della 20,000stima di riga corretta prodotta con le statistiche filtrate.

Un altro approccio che abbiamo usato in situazioni simili è quello di estrarre il sottoinsieme rilevante dei dati nelle tabelle #temp. Soprattutto ora che le versioni più recenti di SQL Server non scrivono più avidamente le tabelle #temp sul disco , abbiamo ottenuto buoni risultati con questo approccio. La tua descrizione del join molti-a-molti implica che ogni singola tabella #temp nel tuo caso sarebbe relativamente piccola (o almeno più piccola della serie di risultati finali), quindi vale la pena provare questo approccio.

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

Usiamo ampiamente le statistiche filtrate, ma le rendiamo una per Key1valore su ogni tabella. Ne abbiamo ora migliaia.
Steven Hibble,

2
@StevenHibble Aspetto positivo che migliaia di statistiche filtrate potrebbero rendere difficile la gestione. (Abbiamo anche visto che influisce negativamente sul tempo di compilazione del piano.) Potrebbe non adattarsi al tuo caso d'uso, ma ho anche aggiunto un altro approccio di tabella #temp che abbiamo usato con successo diverse volte.
Geoff Patterson,

-1

Una portata Nessuna vera base se non quella di provare.

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