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
.
Il piano è in grado di eseguire un'unione di unione tra i risultati dei due indici sulla id
colonna. ( 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 id
questo 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' TOP
iteratore 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
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 9130
mostra alcune ulteriori informazioni
SELECT TOP 10 *
FROM animal
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
OPTION (QUERYTRACEON 9130)
Si può vedere che SQL Server calcola che dovrà solo scansionare circa 100.000 righe prima di trovare 10 corrispondenti al predicato e che TOP
può 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
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 TOP
e forzatura della scansione CI
SELECT *
FROM animal WITH (INDEX = 1)
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
È costato 88.0586
sulla 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 10
volontà 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 TOP
ciò 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)
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 10
Ciò 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%' |
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
TOP
valore in una variabile significa che assumeràTOP 100
piuttosto cheTOP 10
. Questo può o meno aiutare a seconda di quale sia il punto critico tra i due piani.