So che fare COALESCE
alcune 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_1
e 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 IIF
apparisse nel linguaggio Transact-SQL). I modelli CE IIF
come a UNION
con 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.
ISNULL
d'altra parte, è intrinseco al motore. Non viene creato utilizzando altri componenti di base. Applicare l'effetto di ISNULL
un istogramma, ad esempio, è semplice come sostituire il passaggio per i NULL
valori (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 CASE
sostituto 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 COALESCE
al 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+009
e 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_TABLE
e 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 COALESCE
dell'albero espanso opaco (in termini di UNION
e 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%):
-- 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 COALESCE
predicato 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 ISNULL
suggerito 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 COALESCE
o ISNULL
, idealmente, chiavi esterne e altri vincoli utili per la compilazione di query.
bigint
invece didecimal(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.