Enorme rallentamento della query di SQL Server durante l'aggiunta di caratteri jolly (o superiore)


52

Ho uno zoo di 20 milioni di animali che seguo sul mio database di SQL Server 2005. Circa l'1% è nero e circa l'1% è costituito da cigni. Volevo ottenere i dettagli di tutti i cigni neri e quindi, non volendo inondare la pagina dei risultati che ho fatto:

select top 10 * 
from animal 
where colour like 'black'  
and species like 'swan'

(Sì, inavvertitamente quei campi sono in formato libero ma sono entrambi indicizzati). Si scopre che non abbiamo animali come la query restituisce un set vuoto in circa 300 millisecondi. Sarebbe stato circa due volte più veloce se avessi usato '=' piuttosto che 'mi piace' ma ho una premonizione che mi sta per salvare un po '.

Si scopre che il capo guardiano dello zoo pensa che potrebbe aver inserito alcuni dei cigni come "nerastri", quindi modifico la query di conseguenza:

select top 10 * 
from animal  
where colour like 'black%' 
and species like 'swan'

Si scopre che non ci sono nemmeno quelli (e in realtà non ci sono animali "neri%" tranne quelli "neri") ma la query ora impiega circa 30 secondi per tornare vuota.

Sembra che solo la combinazione di "top" e "like%" causi problemi

select count(*) 
from animal  
where colour like 'black%' 
and species like 'swan'

restituisce 0 molto rapidamente e anche

select * 
from animal 
where colour like 'black%' 
and species like 'swan'

ritorna vuoto in una frazione di secondo.

Qualcuno ha idea del perché 'top' e '%' dovrebbero cospirare per causare una perdita così drammatica delle prestazioni, specialmente in un set di risultati vuoto?

EDIT: Solo per chiarire, non sto usando alcun indice FreeText, intendevo solo che i campi sono liberi nel punto di entrata, cioè non normalizzati nel database. Ci scusiamo per la confusione, scarsa espressione da parte mia.

Risposte:


76

Questa è una decisione dell'ottimizzatore basato sui costi.

I costi stimati utilizzati in questa scelta non sono corretti in quanto presuppone l'indipendenza statistica tra valori in colonne diverse.

È simile al problema descritto in Row Goals Gone Rogue in cui i numeri pari e dispari sono negativamente correlati.

È facile da riprodurre.

CREATE TABLE dbo.animal(
    id int IDENTITY(1,1) NOT NULL PRIMARY KEY,
    colour varchar(50) NOT NULL,
    species varchar(50) NOT NULL,
    Filler char(10) NULL
);

/*Insert 20 million rows with 1% black and 1% swan but no black swans*/
WITH T
     AS (SELECT TOP 20000000 ROW_NUMBER() OVER (ORDER BY @@SPID) AS RN
         FROM   master..spt_values v1,
                master..spt_values v2,
                master..spt_values v3)
INSERT INTO dbo.animal
            (colour,
             species)
SELECT CASE
         WHEN RN % 100 = 1 THEN 'black'
         ELSE CAST(RN % 100 AS VARCHAR(3))
       END,
       CASE
         WHEN RN % 100 = 2 THEN 'swan'
         ELSE CAST(RN % 100 AS VARCHAR(3))
       END
FROM   T 

/*Create some indexes*/
CREATE NONCLUSTERED INDEX ix_species ON  dbo.animal(species);
CREATE NONCLUSTERED INDEX ix_colour ON  dbo.animal(colour);

Adesso prova

SELECT TOP 10 *
FROM   animal
WHERE  colour LIKE 'black'
       AND species LIKE 'swan' 

Questo dà il piano di seguito al quale è costato 0.0563167.

inserisci qui la descrizione dell'immagine

Il piano è in grado di eseguire un'unione di unione tra i risultati dei due indici sulla idcolonna. ( Maggiori dettagli sull'algoritmo merge join qui ).

Unisci join richiede che entrambi gli input siano ordinati dalla chiave di join.

Gli indici non cluster sono ordinati per (species, id)e (colour, id)rispettivamente (gli indici non cluster non cluster hanno sempre il localizzatore di riga aggiunto implicitamente alla fine della chiave se non aggiunto esplicitamente). La query senza caratteri jolly sta eseguendo una ricerca di uguaglianza in species = 'swan'e colour ='black'. Poiché ogni ricerca sta recuperando solo un valore esatto dalla colonna principale, le righe corrispondenti verranno ordinate in base a idquesto piano è possibile.

Gli operatori del piano di query vengono eseguiti da sinistra a destra . Con l'operatore di sinistra che richiede le righe dai propri figli, che a loro volta richiedono le righe dai propri figli (e così via fino al raggiungimento dei nodi foglia). L' TOPiteratore smetterà di richiedere altre righe dal proprio figlio una volta che sono state ricevute 10.

SQL Server ha statistiche sugli indici che indicano che l'1% delle righe corrisponde a ciascun predicato. Presuppone che queste statistiche siano indipendenti (cioè non correlate né positivamente né negativamente) in modo tale che in media una volta che ha elaborato 1.000 righe corrispondenti al primo predicato ne troverà 10 corrispondenti al secondo e possa uscire. (il piano sopra mostra in realtà 987 anziché 1.000 ma abbastanza vicino).

Infatti, poiché i predicati sono negativamente correlati, il piano effettivo mostra che tutte le 200.000 righe corrispondenti dovevano essere elaborate da ciascun indice, ma questo è mitigato in una certa misura perché le righe unite a zero significano che erano effettivamente necessarie zero ricerche.

Confrontare con

SELECT TOP 10 *
FROM   animal
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan' 

Che dà il piano di seguito al quale è costato 0.567943

inserisci qui la descrizione dell'immagine

L'aggiunta del carattere jolly finale ha ora causato una scansione dell'indice. Il costo del piano è comunque piuttosto basso per una scansione su una tabella di 20 milioni di righe.

L'aggiunta querytraceon 9130mostra alcune ulteriori informazioni

SELECT TOP 10 *
FROM   animal
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan'       
OPTION (QUERYTRACEON 9130)  

inserisci qui la descrizione dell'immagine

Si può vedere che SQL Server calcola che dovrà solo scansionare circa 100.000 righe prima di trovare 10 corrispondenti al predicato e che TOPpuò interrompere la richiesta di righe.

Ancora una volta questo ha senso con il presupposto di indipendenza come 10 * 100 * 100 = 100,000

Infine, proviamo a forzare un piano di intersezione dell'indice

SELECT TOP 10 *
FROM   animal WITH (INDEX(ix_species), INDEX(ix_colour))
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan' 

Questo mi dà un piano parallelo con un costo stimato di 3,4625

inserisci qui la descrizione dell'immagine

La differenza principale qui è che il colour like 'black%'predicato ora può abbinare più colori diversi. Ciò significa che le righe dell'indice corrispondenti per quel predicato non sono più garantite per essere ordinate in ordine di id.

Ad esempio, la ricerca dell'indice like 'black%'potrebbe restituire le seguenti righe

+------------+----+
|   Colour   | id |
+------------+----+
| black      | 12 |
| black      | 20 |
| black      | 23 |
| black      | 25 |
| blackberry |  1 |
| blackberry | 50 |
+------------+----+

All'interno di ciascun colore gli ID sono ordinati ma gli ID su colori diversi potrebbero non esserlo.

Di conseguenza, SQL Server non è più in grado di eseguire un'intersezione dell'indice di unione di unione (senza aggiungere un operatore di ordinamento di blocco) e opta invece per eseguire un'unione hash. Hash Join sta bloccando l'input di compilazione, quindi ora il costo riflette il fatto che tutte le righe corrispondenti dovranno essere elaborate dall'input di compilazione anziché supporre che dovrà solo scansionare 1.000 come nel primo piano.

L'ingresso della sonda non è tuttavia bloccante e stima ancora erroneamente che sarà in grado di interrompere il sondaggio dopo aver elaborato 987 righe da quello.

(Ulteriori informazioni sugli iteratori non bloccanti e bloccanti qui)

Dato l'aumento dei costi delle righe extra stimate e dell'hash join, la scansione parziale dell'indice cluster sembra più economica.

In pratica, naturalmente, la scansione dell'indice cluster "parziale" non è affatto parziale e deve esaminare tutte le 20 milioni di righe anziché le 100 mila ipotizzate nel confrontare i piani.

L'aumento del valore del TOP(o la sua rimozione totale) alla fine incontra un punto di non ritorno in cui il numero di righe che stima che la scansione CI dovrà coprire rende quel piano più costoso e ritorna al piano di intersezione dell'indice. Per me il cut-off point tra i due piani è TOP (89)contro TOP (90).

Per te potrebbe differire in quanto dipende dalla larghezza dell'indice cluster.

Rimozione TOPe forzatura della scansione CI

SELECT *
FROM   animal WITH (INDEX = 1)
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan' 

È costato 88.0586sulla mia macchina per la mia tabella di esempio.

Se SQL Server fosse consapevole del fatto che lo zoo non aveva cigni neri e che avrebbe dovuto eseguire una scansione completa piuttosto che leggere solo 100.000 righe, questo piano non sarebbe stato scelto.

Ho provato le statistiche multipli di colonna animal(species,colour)ed animal(colour,species)e statistiche filtrata su animal (colour) where species = 'swan', ma nessuno di questi aiuto convincerlo che non esistono cigni neri e la TOP 10volontà di scansione necessità di processo di più di 100.000 righe.

Ciò è dovuto al "presupposto di inclusione" in cui SQL Server presuppone essenzialmente che se stai cercando qualcosa che probabilmente esiste.

Nel 2008+ c'è un flag di traccia documentato 4138 che disattiva gli obiettivi delle file. L'effetto di ciò è che il piano è costato senza il presupposto che TOPciò consentirà agli operatori figlio di terminare in anticipo senza leggere tutte le righe corrispondenti. Con questo flag di traccia in atto ottengo naturalmente il piano di intersezione dell'indice più ottimale.

SELECT TOP 10 *
FROM   animal
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan'
OPTION (QUERYTRACEON 4138)       

inserisci qui la descrizione dell'immagine

Questo piano ora costa correttamente per la lettura di tutte le 200 mila righe in entrambe le ricerche dell'indice, ma oltre i costi delle ricerche chiave (stimate 2 mila rispetto allo 0 effettivo. TOP 10Ciò limiterebbe questo a un massimo di 10 ma il flag di traccia impedisce che questo venga preso in considerazione) . Tuttavia, il piano ha un costo significativamente inferiore rispetto alla scansione CI completa, quindi viene selezionato.

Naturalmente questo piano potrebbe non essere ottimale per le combinazioni che sono comuni. Come i cigni bianchi.

Un indice composito su animal (colour, species)o idealmente animal (species, colour)consentirebbe alla query di essere molto più efficiente per entrambi gli scenari.

Per utilizzare in modo più efficiente l'indice composito LIKE 'swan', è necessario modificare anche = 'swan'.

La tabella seguente mostra i predicati di ricerca e i predicati residui mostrati nei piani di esecuzione per tutte e quattro le permutazioni.

+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
|                 WHERE clause                 |       Index       |                         Seek Predicate                         |              Residual Predicate              |
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
| colour LIKE 'black%' AND species LIKE 'swan' | ix_colour_species | colour >= 'black' AND colour < 'blacL'                         | colour like 'black%' AND species like 'swan' |
| colour LIKE 'black%' AND species LIKE 'swan' | ix_species_colour | species >= 'swan' AND species <= 'swan'                        | colour like 'black%' AND species like 'swan' |
| colour LIKE 'black%' AND species = 'swan'    | ix_colour_species | (colour,species) >= ('black', 'swan')) AND colour < 'blacL'    | colour LIKE 'black%' AND species = 'swan'    |
| colour LIKE 'black%' AND species = 'swan'    | ix_species_colour | species = 'swan' AND (colour >= 'black' and colour <  'blacL') | colour like 'black%'                         |
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+

15

Fondando questo intrigante, ho fatto qualche ricerca e mi sono imbattuto in questo Q / A In che modo (e perché) TOP influisce su un piano di esecuzione?

Fondamentalmente, l'utilizzo di TOP modifica il costo degli operatori che lo seguono (in modo non banale), il che provoca anche la modifica del piano generale (sarebbe fantastico se si includesse ExecPlans con e senza TOP 10), che praticamente modifica l'esecuzione generale di la query.

Spero che sia di aiuto.

Per esempio, l'ho provato su un database e: -quando non viene invocato alcun top, viene utilizzato il parallelismo -con TOP, non viene utilizzato il parallelismo

Quindi, ancora una volta, mostrare i tuoi piani di esecuzione fornirebbe maggiori informazioni.

Buona giornata


-1

Credo che ciò possa essere dovuto alla natura sottostante di MSSQL 2005 e al modo in cui Query Optimizer decide quale piano di esecuzione sia il più efficiente.

Se si utilizza una variabile SQL, dovrebbe "ingannare" l'ottimizzatore delle query in modo da utilizzare le corrispondenze hash anziché i cicli nidificati, il che comporterà un grado di parallelismo molto più elevato.

Provare:

DECLARE @topn INT = 10
SELECT TOP (@topn) *
FROM    animal
WHERE   colour LIKE 'black%' 
AND species LIKE 'swan'

5
Offuscare il TOPvalore in una variabile significa che assumerà TOP 100piuttosto che TOP 10. Questo può o meno aiutare a seconda di quale sia il punto critico tra i due piani.
Martin Smith,
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.