Perché questa stima della cardinalità di join è così ampia?


18

Sto sperimentando quella che penso sia una stima di cardinalità incredibilmente alta per la seguente query:

SELECT dm.PRIMARY_ID
FROM
(
    SELECT COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID) PRIMARY_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID
    LEFT OUTER JOIN X_DETAIL_2 d2 ON dt.ID = d2.ID
    LEFT OUTER JOIN X_DETAIL_3 d3 ON dt.ID = d3.ID
) dm
INNER JOIN X_LAST_TABLE lst ON dm.PRIMARY_ID = lst.JOIN_ID;

Il piano stimato è qui . Sto lavorando su una copia statistica delle tabelle, quindi non posso includere un piano reale. Tuttavia, non penso che sia molto rilevante per questo problema.

SQL Server stima che 481577 righe verranno restituite dalla tabella derivata "dm". Quindi stima che verranno restituite 4528030000 righe dopo l'esecuzione del join su X_LAST_TABLE, ma JOIN_ID è la chiave primaria di X_LAST_TIME. Mi aspetterei una stima della cardinalità di join tra 0 e 481577 righe. Invece, la stima delle righe sembra essere il 10% del numero di righe che otterrei quando si uniscono le tabelle esterna e interna. La matematica per questo funziona con arrotondamento: 481577 * 94025 * 0,1 = 45280277425 che viene arrotondato a 4528030000.

Sto principalmente cercando una causa principale per questo comportamento. Sono anche interessato a soluzioni alternative semplici, ma per favore non suggerire di cambiare il modello di dati o di usare le tabelle temporanee. Questa query è una semplificazione della logica all'interno di una vista. So che fare COALESCE su alcune colonne e unirle non è una buona pratica. Parte dell'obiettivo di questa domanda è capire se devo raccomandare di ridisegnare il modello di dati.

Sto testando su Microsoft SQL Server 2014 con lo stimatore di cardinalità legacy abilitato. TF 4199 e altri sono attivi. Posso fornire un elenco completo di flag di traccia se ciò risulta rilevante.

Ecco la definizione di tabella più pertinente:

CREATE TABLE X_LAST_TABLE (
JOIN_ID NUMERIC(18, 0) NOT NULL
    CONSTRAINT PK_X_LAST_TABLE PRIMARY KEY CLUSTERED (JOIN_ID ASC)
);

Ho anche scritto tutti gli script di creazione della tabella insieme alle loro statistiche se qualcuno vuole riprodurre il problema su uno dei loro server.

Per aggiungere alcune delle mie osservazioni, l'uso del TF 2312 risolve la stima, ma non è un'opzione per me. TF 2301 non corregge la stima. La rimozione di una delle tabelle consente di correggere la stima. Stranamente, cambiando l'ordine di join di X_DETAIL_LINK si corregge anche la stima. Modificando l'ordine di join intendo riscrivere la query e non forzare l'ordine di join con un suggerimento. Ecco un piano di query stimato quando si cambia l'ordine dei join.


PS Se puoi in qualche modo passare a bigintinvece di decimal(18, 0)ottenere vantaggi: 1) usa 8 byte invece di 9 per ogni valore, e 2) usa un tipo di dati comparabile a byte invece di un tipo di dati compresso, che potrebbe avere implicazioni per il tempo della CPU quando si confrontano i valori.
ErikE

@ErikE Grazie per la punta ma lo sapevo già. Sfortunatamente siamo bloccati con NUMERIC (18,0) su BIGINT per motivi legacy.
Joe Obbish

Ne è valsa la pena!
ErikE

Hai bisogno delle tabelle X_DETAIL2e X_DETAIL3se JOIN_IDnon è null in X_DETAIL1?
ErikE

@ErikE Questo è un MCVE, quindi la query non ha esattamente senso a questo punto.
Joe Obbish

Risposte:


14

So che fare COALESCEalcune colonne e unirle non è una buona pratica.

Generare una buona cardinalità e stime di distribuzione è abbastanza difficile quando lo schema è 3NF + (con chiavi e vincoli) e la query è relazionale e principalmente SPJG (selezione-proiezione-join-gruppo di). Il modello CE si basa su questi principi. Più funzionalità insolite o non relazionali ci sono in una query, più ci si avvicina ai confini di ciò che la struttura di cardinalità e selettività può gestire. Andare troppo lontano e CE si arrenderà e indovinerà .

La maggior parte dell'esempio MCVE è semplice SPJ (no G), sebbene con equijoin prevalentemente esterni (modellati come join interno più anti-semijoin) piuttosto che il più semplice equijoin interno (o semijoin). Tutte le relazioni hanno chiavi, anche se nessuna chiave esterna o altri vincoli. Tutti i join tranne uno sono uno-a-molti, il che è positivo.

L'eccezione è il join esterno molti-a-molti tra X_DETAIL_1e X_DETAIL_LINK. L'unica funzione di questo join nel MCVE è quella di duplicare potenzialmente le righe X_DETAIL_1. Questa è una cosa insolita .

Anche i predicati di uguaglianza semplici (selezioni) e gli operatori scalari sono migliori. Ad esempio, l' attributo compare-uguale attributo / costante normalmente funziona bene nel modello. È relativamente "facile" modificare gli istogrammi e le statistiche di frequenza per riflettere l'applicazione di tali predicati.

COALESCEè basato su CASE, che a sua volta è implementato internamente come IIF(e questo era vero ben prima che IIFapparisse nel linguaggio Transact-SQL). I modelli CE IIFcome a UNIONcon due figli reciprocamente esclusivi, ciascuno costituito da un progetto su una selezione sulla relazione di input. Ciascuno dei componenti elencati ha il supporto del modello, quindi combinarli è relativamente semplice. Anche così, più sono le astrazioni di uno strato, meno accurato tende a essere il risultato finale - una ragione per cui i piani di esecuzione più grandi tendono ad essere meno stabili e affidabili.

ISNULLd'altra parte, è intrinseco al motore. Non viene creato utilizzando altri componenti di base. Applicare l'effetto di ISNULLun istogramma, ad esempio, è semplice come sostituire il passaggio per i NULLvalori (e compattarlo se necessario). È ancora relativamente opaco, come fanno gli operatori scalari, e quindi è meglio evitarlo dove possibile. Tuttavia, generalmente parla più ottimizzatore (meno ottimizzatore-ostile) di un CASEsostituto basato.

Il CE (70 e 120+) è molto complesso, anche per gli standard di SQL Server. Non si tratta di applicare una logica semplice (con una formula segreta) a ciascun operatore. Il CE è a conoscenza di chiavi e dipendenze funzionali; sa stimare usando frequenze, statistiche multivariate e istogrammi; e c'è una tonnellata assoluta di casi speciali, perfezionamenti, controlli e bilanci e strutture di supporto. Stima spesso, ad esempio, i join in più modi (frequenza, istogramma) e decide un risultato o una regolazione in base alle differenze tra i due.

Un'ultima cosa di base da trattare: la stima della cardinalità iniziale viene eseguita per ogni operazione nella struttura della query, dal basso verso l'alto. La selettività e la cardinalità derivano innanzitutto dagli operatori fogliari (relazioni di base). Gli istogrammi modificati e le informazioni sulla densità / frequenza sono derivati ​​per gli operatori principali. Più si sale dall'albero, più bassa è la qualità delle stime, poiché gli errori tendono ad accumularsi.

Questa singola stima completa iniziale fornisce un punto di partenza e si verifica molto prima che venga presa in considerazione qualsiasi piano di esecuzione finale (accade molto prima della fase di compilazione del piano banale). L'albero delle query a questo punto tende a riflettere abbastanza attentamente la forma scritta della query (sebbene con le sottoquery rimosse e le semplificazioni applicate ecc.)

Immediatamente dopo la stima iniziale, SQL Server esegue il riordino euristico dei join, che in termini vaghi cerca di riordinare l'albero per posizionare tabelle più piccole e i join ad alta selettività per primi. Cerca inoltre di posizionare i join interni prima dei join esterni e dei prodotti incrociati. Le sue capacità non sono estese; i suoi sforzi non sono esaustivi; e non considera i costi fisici (poiché non esistono ancora - sono presenti solo informazioni statistiche e informazioni sui metadati). Il riordino euristico ha maggior successo su semplici alberi interni equijoin. Esiste per fornire un punto di partenza "migliore" per l'ottimizzazione basata sui costi.

Perché questa stima della cardinalità di join è così ampia?

L'MCVE ha un join molti-a-molti "insolito" per lo più ridondante e un equi si unisce COALESCEal predicato. L'albero dell'operatore ha anche un ultimo join interno , il quale il riordino euristico dei join non è stato in grado di spostare l'albero in una posizione più preferita. Lasciando da parte tutti gli scalari e le proiezioni, l'albero di join è:

LogOp_Join [ Card=4.52803e+009 ]
    LogOp_LeftOuterJoin [ Card=481577 ]
        LogOp_LeftOuterJoin [ Card=481577 ]
            LogOp_LeftOuterJoin [ Card=481577 ]
                LogOp_LeftOuterJoin [ Card=481577 ]
                LogOp_Get TBL: X_DRIVING_TABLE(alias TBL: dt) [ Card=481577 ]
                LogOp_Get TBL: X_DETAIL_1(alias TBL: d1) [ Card=70 ]
                LogOp_Get TBL: X_DETAIL_LINK(alias TBL: lnk) [ Card=47 ]
            LogOp_Get TBL: X_DETAIL_2(alias TBL: d2) X_DETAIL_2 [ Card=119 ]
        LogOp_Get TBL: X_DETAIL_3(alias TBL: d3) X_DETAIL_3 [ Card=281 ]
    LogOp_Get TBL: X_LAST_TABLE(alias TBL: lst) X_LAST_TABLE [ Card=94025 ]

Si noti che la stima finale errata è già in atto. Viene stampato Card=4.52803e+009e memorizzato internamente come valore in virgola mobile a precisione doppia 4.5280277425e + 9 (4528027742,5 in decimale).

La tabella derivata nella query originale è stata rimossa e le proiezioni sono state normalizzate. Una rappresentazione SQL dell'albero su cui è stata eseguita la stima iniziale di cardinalità e selettività è:

SELECT 
    PRIMARY_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)
FROM X_DRIVING_TABLE dt
LEFT OUTER JOIN X_DETAIL_1 d1
    ON dt.ID = d1.ID
LEFT OUTER JOIN X_DETAIL_LINK lnk 
    ON d1.LINK_ID = lnk.LINK_ID
LEFT OUTER JOIN X_DETAIL_2 d2 
    ON dt.ID = d2.ID
LEFT OUTER JOIN X_DETAIL_3 d3 
    ON dt.ID = d3.ID
INNER JOIN X_LAST_TABLE lst 
    ON lst.JOIN_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)

(A parte, il ripetuto COALESCEè presente anche nel piano finale - una volta nello Scalare di calcolo finale e una volta sul lato interno del join interno).

Si noti il ​​join finale. Questo join interno è (per definizione) il prodotto cartesiano X_LAST_TABLEe l'output del join precedente, con una selezione (predicato di join) lst.JOIN_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)applicata. La cardinalità del prodotto cartesiano è semplicemente 481577 * 94025 = 45280277425.

Per questo, dobbiamo determinare e applicare la selettività del predicato. La combinazione COALESCEdell'albero espanso opaco (in termini di UNIONe IIF, ricordate) insieme all'impatto sulle informazioni chiave, sugli istogrammi derivati ​​e sulle frequenze del precedente giunto "molti-molti" esterno "insolito" per lo più ridondante significa che il CE non è in grado di ricavare una stima accettabile in uno dei modi normali.

Di conseguenza, entra nella Guess Logic. La logica di ipotesi è moderatamente complessa, con strati di ipotesi "istruite" e algoritmi di ipotesi "non così istruiti". Se non viene trovata una base migliore per un'ipotesi, il modello utilizza un'ipotesi di ultima istanza, che per un confronto di uguaglianza è: sqllang!x_Selectivity_Equal= selettività 0,1 fissa (ipotesi del 10%):

Call stack

-- the moment of doom
movsd xmm0,mmword ptr [sqllang!x_Selectivity_Equal

Il risultato è una selettività di 0,1 sul prodotto cartesiano: 481577 * 94025 * 0,1 = 4528027742,5 (~ 4,52803e + 009) come menzionato prima.

riscritture

Quando il join problematico viene commentato , viene prodotta una stima migliore perché viene evitata la "ipotesi dell'ultima risorsa" a selettività fissa (le informazioni chiave vengono conservate dai join 1-M). La qualità della stima è ancora poco attendibile, perché un COALESCEpredicato di join non è affatto compatibile con CE. La nuova stima fa almeno aspetto più ragionevole per gli esseri umani, suppongo.

Quando la query viene scritta con il join esterno X_DETAIL_LINK nell'ultima posizione , il riordino euristico è in grado di scambiarlo con il join interno finale X_LAST_TABLE. Mettere il join interno proprio accanto al problema del join esterno offre alle capacità limitate del riordino anticipato l'opportunità di migliorare la stima finale, poiché gli effetti del join esterno "insolito" per lo più ridondante derivano dopo la delicata stima della selettività per COALESCE. Ancora una volta, le stime sono leggermente migliori delle ipotesi fisse e probabilmente non reggono un determinato esame incrociato in un tribunale.

Riordinare una miscela di join interni ed esterni è difficile e richiede tempo (anche l'ottimizzazione completa della fase 2 tenta solo un sottoinsieme limitato di mosse teoriche).

Il nidificato ISNULLsuggerito nella risposta di Max Vernon riesce a evitare l'ipotesi fissa del salvataggio, ma la stima finale è uno zero improbabile (elevato a una fila per decenza). Potrebbe anche trattarsi di un'ipotesi fissa di 1 riga, per tutte le basi statistiche del calcolo.

Mi aspetterei una stima della cardinalità di join tra 0 e 481577 righe.

Questa è un'aspettativa ragionevole, anche se si accetta che la stima della cardinalità può avvenire in momenti diversi (durante l'ottimizzazione basata sui costi) su sottotitoli fisicamente diversi, ma logicamente e semanticamente identici - con il piano finale che è una sorta di migliore insieme migliore (per gruppo memo). La mancanza di una garanzia di coerenza a livello di piano non significa che un'unione individuale dovrebbe essere in grado di infrangere la rispettabilità, lo capisco.

D'altra parte, se finiamo per indovinare l' ultima risorsa , la speranza è già persa, quindi perché preoccuparsi. Abbiamo provato tutti i trucchi che conoscevamo e ci siamo arresi. Se non altro, la stima finale selvaggia è un grande segnale di avvertimento che non tutto è andato bene all'interno della CE durante la compilazione e l'ottimizzazione di questa query.

Quando ho provato l'MCVE, la 120+ CE ha prodotto una stima finale di zero (= 1) (come quella nidificata ISNULL) per la query originale, che è altrettanto inaccettabile per il mio modo di pensare.

La vera soluzione probabilmente comporta una modifica del design, per consentire semplici equi-join senza COALESCEo ISNULL, idealmente, chiavi esterne e altri vincoli utili per la compilazione di query.


10

Credo che l' Compute Scalaroperatore risultante COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)dall'adesione X_LAST_TABLE.JOIN_IDsia la causa principale del problema. Storicamente, gli scalari di calcolo sono stati difficili da costare esattamente 1 , 2 .

Dato che hai fornito un esempio verificabile minimamente completo (grazie!) Con statistiche accurate, sono in grado di riscrivere la query in modo tale che il join non richieda più la CASEfunzionalità che COALESCEviene espansa, risultando in stime di riga molto più accurate e apparentemente più costi complessivi accurati Vedi l'addendum alla fine. :

SELECT COALESCE(dm.d1ID, dm.d2ID, dm.d3ID)
FROM
(
    SELECT d1ID = d1.JOIN_ID
        , d2ID = d2.JOIN_ID
        , d3ID = d3.JOIN_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID
    LEFT OUTER JOIN X_DETAIL_2 d2 ON dt.ID = d2.ID
    LEFT OUTER JOIN X_DETAIL_3 d3 ON dt.ID = d3.ID
) dm
INNER JOIN X_LAST_TABLE lst 
    ON (dm.d1ID IS NOT NULL AND dm.d1ID = lst.JOIN_ID)
    OR (dm.d1ID IS NULL AND dm.d2ID IS NOT NULL AND dm.d2ID = lst.JOIN_ID)
    OR (dm.d1ID IS NULL AND dm.d2ID IS NULL AND dm.d3ID IS NOT NULL AND dm.d3ID = lst.JOIN_ID);

Sebbene xID IS NOT NULLtecnicamente non sia richiesto, poiché ID = JOIN_IDnon si uniranno su valori null, li ho inclusi poiché ritrae più chiaramente l'intento.

Piano 1 e Piano 2

Piano 1:

inserisci qui la descrizione dell'immagine

Piano 2:

inserisci qui la descrizione dell'immagine

La nuova query beneficia (?) Della parallelizzazione. Inoltre, la nuova query ha un numero di output stimato di righe di 1, che alla fine della giornata potrebbe essere peggiore rispetto alla stima di 4528030000 per la query originale. Il costo della sottostruttura per l'operatore selezionato sulla nuova query arriva a 243210, mentre l'orologio originale arriva a 536.535, che è chiaramente inferiore. Detto questo, non credo che la prima stima sia vicina alla realtà.


Addendum 1.

Dopo ulteriori consultazioni con varie persone su The Heap ™ stimolate da una discussione con @Lamak, sembra che la mia domanda osservativa sopra riportata si comporti terribilmente, anche con il parallelismo. Una soluzione che consente sia buone prestazioni che buone stime di cardinalità consiste nel sostituire il COALESCE(x,y,z)con un ISNULL(ISNULL(x, y), z), come in:

SELECT dm.PRIMARY_ID
FROM
(
    SELECT ISNULL(ISNULL(d1.JOIN_ID, d2.JOIN_ID), d3.JOIN_ID) PRIMARY_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID
    LEFT OUTER JOIN X_DETAIL_2 d2 ON dt.ID = d2.ID
    LEFT OUTER JOIN X_DETAIL_3 d3 ON dt.ID = d3.ID
) dm
INNER JOIN X_LAST_TABLE lst ON dm.PRIMARY_ID = lst.JOIN_ID;

COALESCEviene trasformato in CASEun'istruzione "sotto le coperte" da Query Optimizer. Pertanto, lo stimatore della cardinalità fa fatica a scoprire statistiche affidabili per le colonne sepolte all'interno COALESCE. ISNULLessere una funzione intrinseca è molto più "aperto" per lo stimatore della cardinalità. Inoltre, non vale nulla che ISNULLpuò essere ottimizzato se il target è noto per essere nulla -annullabile.

Il piano per la ISNULLvariante è simile a:

inserisci qui la descrizione dell'immagine

(Incolla la versione del piano qui ).

Cordiali saluti, propugna Sentry One per il loro grande Plan Explorer, che ho usato per produrre i piani grafici sopra.


-1

Secondo la tua condizione di join, la tabella può essere organizzata in così tanti modi, che "cambiando in un modo particolare" risolve il risultato.

Supponiamo che l'unione di una sola tabella ti dia il risultato corretto.

SELECT COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID) PRIMARY_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID

Qui al posto di X_DETAIL_1, è possibile utilizzare X_DETAIL_2o X_DETAIL_3.

Pertanto lo scopo di riposo 2 tabelle non è chiaro.

È come se avessi diviso il tavolo X_DETAIL_1in altre 2 parti.

Molto probabilmente " non v'è errore in cui si popolano quei tavoli. " Idealmente X_DETAIL_1, X_DETAIL_2e X_DETAIL_3dovrebbe contenere la stessa quantità di righe.

Ma una o più tabelle contengono un numero indesiderato di righe.

Scusa se sbaglio.

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.