Perché è più veloce ed è sicuro da usare? (DOVE la prima lettera è in alfabeto)


10

Per farla breve, stiamo aggiornando piccole tabelle di persone con valori da una tabella di persone molto grande. In un test recente, l'esecuzione di questo aggiornamento richiede circa 5 minuti.

Ci siamo imbattuti in quella che sembra la più stupida ottimizzazione possibile, che sembra funzionare perfettamente! La stessa query ora viene eseguita in meno di 2 minuti e produce gli stessi risultati, perfettamente.

Ecco la domanda. L'ultima riga viene aggiunta come "l'ottimizzazione". Perché l'intensa riduzione dei tempi di interrogazione? Ci stiamo perdendo qualcosa? Questo potrebbe portare a problemi in futuro?

UPDATE smallTbl
SET smallTbl.importantValue = largeTbl.importantValue
FROM smallTableOfPeople smallTbl
JOIN largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(TRIM(smallTbl.last_name),TRIM(largeTbl.last_name)) = 4
    AND DIFFERENCE(TRIM(smallTbl.first_name),TRIM(largeTbl.first_name)) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(TRIM(largeTbl.last_name), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å')

Note tecniche: siamo consapevoli che l'elenco di lettere da testare potrebbe richiedere qualche altra lettera. Siamo anche consapevoli dell'ovvio margine di errore quando si utilizza "DIFFERENCE".

Piano di query (regolare): https://www.brentozar.com/pastetheplan/?id=rypV84y7V
Piano di query (con "ottimizzazione"): https://www.brentozar.com/pastetheplan/?id=r1aC2my7E


4
Piccola risposta alla tua nota tecnica: AND LEFT(TRIM(largeTbl.last_name), 1) BETWEEN 'a' AND 'z' COLLATE LATIN1_GENERAL_CI_AIdovresti fare quello che vuoi lì senza richiedere di elencare tutti i caratteri e avere un codice che è difficile da leggere
Erik A

Hai righe in cui la condizione finale in WHEREè falsa? In particolare si noti che il confronto potrebbe essere case sensitive.
jpmc26,

@ErikvonAsmuth fa un punto eccellente. Ma solo una piccola nota tecnica: per SQL Server 2008 e 2008 R2, è meglio usare le collation versione "100" (se disponibili per la cultura / locale in uso). Così sarebbe Latin1_General_100_CI_AI. E per SQL Server 2012 e versioni successive (almeno tramite SQL Server 2019), è preferibile utilizzare le regole di confronto abilitate per i caratteri supplementari nella versione più alta per le impostazioni internazionali utilizzate. Quindi sarebbe Latin1_General_100_CI_AI_SCin questo caso. Le versioni> 100 (finora solo in giapponese) non hanno (o necessitano) _SC(ad es Japanese_XJIS_140_CI_AI.).
Solomon Rutzky,

Risposte:


9

Dipende dai dati nelle tue tabelle, dai tuoi indici, .... Difficile dirlo senza poter confrontare i piani di esecuzione / le statistiche io + time.

La differenza che mi aspetterei è il filtraggio aggiuntivo che si verifica prima del JOIN tra le due tabelle. Nel mio esempio, ho modificato gli aggiornamenti in modo da riutilizzare le mie tabelle.

Il piano di esecuzione con "l'ottimizzazione" inserisci qui la descrizione dell'immagine

Progetto esecutivo

Si vede chiaramente un'operazione di filtro in corso, nei miei dati di test nessun record è stato filtrato e di conseguenza non sono stati apportati miglioramenti.

Il piano di esecuzione, senza "l'ottimizzazione" inserisci qui la descrizione dell'immagine

Progetto esecutivo

Il filtro è sparito, il che significa che dovremo fare affidamento sul join per filtrare i record non necessari.

Altri motivi Un altro motivo / conseguenza della modifica della query potrebbe essere la creazione di un nuovo piano di esecuzione durante la modifica della query, che risulta essere più rapido. Un esempio di ciò è il motore che sceglie un altro operatore Join, ma a questo punto è solo un'ipotesi.

MODIFICARE:

Chiarire dopo aver ottenuto i due piani di query:

La query sta leggendo 550M righe dalla tabella grande e le sta filtrando. inserisci qui la descrizione dell'immagine

Ciò significa che il predicato è quello che esegue la maggior parte del filtro, non il predicato di ricerca. Il risultato è la lettura dei dati, ma molto meno la restituzione.

Far sì che il server sql utilizzi un indice diverso (piano di query) / l'aggiunta di un indice potrebbe risolverlo.

Quindi perché la query di ottimizzazione non presenta questo stesso problema?

Perché viene utilizzato un piano di query diverso, con una scansione anziché una ricerca.

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine

Senza fare alcuna ricerca, ma restituendo solo 4M righe con cui lavorare.

La prossima differenza

Ignorando la differenza di aggiornamento (nulla viene aggiornato sulla query ottimizzata) viene utilizzata una corrispondenza hash sulla query ottimizzata:

inserisci qui la descrizione dell'immagine

Invece di un loop nidificato join sul non ottimizzato:

inserisci qui la descrizione dell'immagine

Un ciclo nidificato è preferibile quando una tabella è piccola e l'altra grande. Dato che entrambi sono vicini alla stessa dimensione, direi che la corrispondenza hash è la scelta migliore in questo caso.

Panoramica

La query ottimizzata inserisci qui la descrizione dell'immagine

Il piano della query ottimizzata ha parallelismo, utilizza un join di corrispondenza hash e deve eseguire un filtro I / O residuo meno. Utilizza inoltre una bitmap per eliminare i valori chiave che non possono produrre righe di join. (Inoltre non viene aggiornato nulla)

La query inserisci qui la descrizione dell'immagine non ottimizzata Il piano della query non ottimizzata non ha parallelismo, utilizza un join loop nidificato e deve eseguire il filtro IO residuo sui record 550M. (Anche l'aggiornamento sta avvenendo)

Cosa potresti fare per migliorare la query non ottimizzata?

  • Modifica dell'indice per avere first_name e last_name nell'elenco delle colonne chiave:

    CREATE INDEX IX_largeTableOfPeople_birth_date_first_name_last_name su dbo.largeTableOfPeople (birth_date, first_name, last_name) include (id)

Ma a causa dell'uso di funzioni e della grande tabella, questa potrebbe non essere la soluzione ottimale.

  • Aggiornamento delle statistiche, utilizzando la ricompilazione per provare a ottenere il piano migliore.
  • Aggiunta di OPZIONE (HASH JOIN, MERGE JOIN)alla query
  • ...

Dati di prova + query utilizzate

CREATE TABLE #smallTableOfPeople(importantValue int, birthDate datetime2, first_name varchar(50),last_name varchar(50));
CREATE TABLE #largeTableOfPeople(importantValue int, birth_date datetime2, first_name varchar(50),last_name varchar(50));


set nocount on;
DECLARE @i int = 1
WHILE @i <= 1000
BEGIN
insert into #smallTableOfPeople (importantValue,birthDate,first_name,last_name)
VALUES(NULL, dateadd(mi,@i,'2018-01-18 11:05:29.067'),'Frodo','Baggins');

set @i += 1;
END


set nocount on;
DECLARE @j int = 1
WHILE @j <= 20000
BEGIN
insert into #largeTableOfPeople (importantValue,birth_Date,first_name,last_name)
VALUES(@j, dateadd(mi,@j,'2018-01-18 11:05:29.067'),'Frodo','Baggins');

set @j += 1;
END


SET STATISTICS IO, TIME ON;

SELECT  smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(RTRIM(LTRIM(largeTbl.last_name)), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å');

SELECT  smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
--AND LEFT(RTRIM(LTRIM(largeTbl.last_name)), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å')




drop table #largeTableOfPeople;
drop table #smallTableOfPeople;

8

Non è chiaro che la seconda query sia in realtà un miglioramento.

I piani di esecuzione contengono QueryTimeStats che mostrano una differenza molto meno drammatica di quanto indicato nella domanda.

Il piano lento aveva un tempo trascorso di 257,556 ms(4 minuti 17 secondi). Il piano veloce aveva un tempo trascorso di 190,992 ms(3 minuti e 11 secondi) nonostante l'esecuzione con un grado di parallelismo di 3.

Inoltre, il secondo piano era in esecuzione in un database in cui non c'era lavoro da fare dopo il join.

Primo piano

inserisci qui la descrizione dell'immagine

Secondo piano

inserisci qui la descrizione dell'immagine

In questo modo il tempo extra potrebbe essere spiegato dal lavoro necessario per aggiornare 3,5 milioni di righe (il lavoro richiesto nell'operatore di aggiornamento per individuare queste righe, bloccare la pagina, scrivere l'aggiornamento nella pagina e il registro delle transazioni non è trascurabile)

Se questo è effettivamente riproducibile quando si confronta come con simile, allora la spiegazione è che sei stato fortunato in questo caso.

Il filtro con le 37 INcondizioni ha eliminato solo 51 righe dalle 4.008.334 della tabella, ma l'ottimizzatore ha ritenuto che avrebbe eliminato molto di più

inserisci qui la descrizione dell'immagine

   LEFT(TRIM(largeTbl.last_name), 1) IN ( 'a', 'à', 'á', 'b',
                                          'c', 'd', 'e', 'è',
                                          'é', 'f', 'g', 'h',
                                          'i', 'j', 'k', 'l',
                                          'm', 'n', 'o', 'ô',
                                          'ö', 'p', 'q', 'r',
                                          's', 't', 'u', 'ü',
                                          'v', 'w', 'x', 'y',
                                          'z', 'æ', 'ä', 'ø', 'å' ) 

Tali stime errate della cardinalità sono generalmente negative. In questo caso ha prodotto un piano di forma diversa (e parallela) che apparentemente (?) Ha funzionato meglio per te nonostante gli sversamenti di hash causati dall'enorme sottovalutazione.

Senza TRIMSQL Server è in grado di convertirlo in un intervallo di intervallo nell'istogramma della colonna di base e fornire stime molto più accurate, ma con il TRIMsolo ricorso a ipotesi.

La natura dell'ipotesi può variare ma la stima per un singolo predicato LEFT(TRIM(largeTbl.last_name), 1)è in alcune circostanze * appena stimata table_cardinality/estimated_number_of_distinct_column_values.

Non sono sicuro di quali circostanze - la dimensione dei dati sembra svolgere un ruolo. Sono stato in grado di riprodurre questo con larghi tipi di dati a lunghezza fissa come qui, ma ho avuto un'ipotesi diversa, più alta, con varchar(che ha appena usato un'ipotesi piatta al 10% e stimato 100.000 righe). @Solomon Rutzky sottolinea che se varchar(100)viene riempito con spazi finali come accade per charla stima inferiore

L' INelenco viene espanso ORe SQL Server utilizza il backoff esponenziale con un massimo di 4 predicati considerati. Quindi la 219.707stima è arrivata come segue.

DECLARE @TableCardinality FLOAT = 4008334, 
        @DistinctColumnValueEstimate FLOAT = 34207

DECLARE @NotSelectivity float = 1 - (1/@DistinctColumnValueEstimate)

SELECT @TableCardinality * ( 1 - (
@NotSelectivity * 
SQRT(@NotSelectivity) * 
SQRT(SQRT(@NotSelectivity)) * 
SQRT(SQRT(SQRT(@NotSelectivity)))
))
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.