Database
 sql >> Database >  >> RDS >> Database

Indici filtrati e colonne INCLUSI

Gli indici filtrati sono incredibilmente potenti, ma vedo ancora un po' di confusione al riguardo, in particolare sulle colonne utilizzate nei filtri e su cosa succede quando si desidera restringere i filtri.

Una recente domanda su dba.stackexchange ha chiesto aiuto sul motivo per cui le colonne utilizzate nel filtro di un indice filtrato dovrebbero essere incluse nelle colonne "incluse" dell'indice. Ottima domanda, tranne per il fatto che mi sembrava che fosse iniziato con una premessa scadente, perché quelle colonne non dovrebbero essere incluse nell'indice . Sì, aiutano, ma non nel modo in cui sembrava suggerire la domanda.

Per evitare di guardare la domanda stessa, ecco un breve riassunto:

Per soddisfare questa domanda...

SELECT Id, DisplayName 
FROM Users 
WHERE Reputation > 400000;

…il seguente indice filtrato è abbastanza buono:

CREATE UNIQUE NONCLUSTERED INDEX Users_400k_Club
ON dbo.Users ( DisplayName, Id )
INCLUDE ( Reputation )
WHERE Reputation > 400000;

Ma nonostante disponga di questo indice, Query Optimizer consiglia il seguente indice se il valore filtrato viene ridotto, ad esempio, a 450000.

CREATE NONCLUSTERED INDEX IndexThatWasMissing
ON dbo.Users ( Reputation )
INCLUDE ( DisplayName, Id );

Sto qui parafrasando un po' la domanda, che inizia facendo riferimento a questa situazione e poi costruisce un esempio diverso, ma l'idea è la stessa. Semplicemente non volevo complicare le cose coinvolgendo una tabella separata.

Il punto è:l'indice suggerito dal QO è l'indice originale ma capovolto. L'indice originale aveva Reputazione nell'elenco INCLUDE e DisplayName e Id come colonne chiave, mentre il nuovo indice consigliato è il contrario con Reputation come colonna chiave e DisplayName &ID in INCLUDE. Vediamo il perché.

La domanda si riferisce a un post di Erik Darling, in cui spiega di aver ottimizzato la query "450.000" sopra inserendo Reputazione nella colonna INCLUDE. Erik mostra che senza Reputazione nell'elenco INCLUDE, una query che filtra su un valore più alto di Reputazione deve eseguire ricerche (non valide!), o forse anche rinunciare completamente all'indice filtrato (potenzialmente anche peggio). Conclude che avere la colonna Reputazione nell'elenco INCLUDE consente a SQL di avere statistiche, in modo che possa fare scelte migliori e mostra che con Reputation in INCLUDE una varietà di query che filtrano tutte su valori di Reputazione più alti scansionano tutti il ​​suo indice filtrato.

In una risposta alla domanda dba.stackexchange, Brent Ozar sottolinea che i miglioramenti di Erik non sono particolarmente grandi perché causano scansioni. Tornerò su quello, perché è un punto interessante in sé e in qualche modo errato.

Per prima cosa pensiamo un po' agli indici in generale.

Un indice fornisce una struttura ordinata a un insieme di dati. (Potrei essere pedante e sottolineare che leggere i dati in un indice dall'inizio alla fine potrebbe saltare da una pagina all'altra in modo apparentemente casuale, ma comunque mentre leggi le pagine, seguendo i puntatori da una pagina a la prossima volta puoi essere certo che i dati sono ordinati. All'interno di ogni pagina potresti anche saltare per leggere i dati in ordine, ma c'è un elenco che ti mostra quali parti (slot) della pagina dovrebbero essere lette in quale ordine. C'è davvero non ha senso nella mia pedanteria se non rispondere a quei altrettanto pedanti che commenteranno se non lo faccio.)

E questo ordine è in base alle colonne chiave:questa è la parte facile che ottengono tutti. È utile non solo per evitare di riordinare i dati in un secondo momento, ma anche per individuare rapidamente una riga o un intervallo di righe particolare in base a tali colonne.

I livelli foglia dell'indice contengono i valori in tutte le colonne dell'elenco INCLUDE o, nel caso di un indice cluster, i valori in tutte le colonne della tabella (tranne le colonne calcolate non persistenti). Gli altri livelli nell'indice contengono solo le colonne chiave e (se l'indice non è univoco) l'indirizzo univoco della riga, che è o le chiavi dell'indice cluster (con l'unificatore della riga se l'indice cluster non è nemmeno univoco ) o il valore RowID per un heap, sufficiente per consentire un facile accesso a tutti gli altri valori di colonna per la riga. I livelli foglia includono anche tutte le informazioni sull'"indirizzo".

Ma non è questa la parte interessante per questo post. La parte interessante per questo post è cosa intendo con "a un insieme di dati". Ricorda che ho detto "Un indice fornisce una struttura ordinata a un insieme di dati ".

In un indice cluster, quel set di dati è l'intera tabella, ma potrebbe essere qualcos'altro. Probabilmente puoi già immaginare come la maggior parte degli indici non cluster non coinvolga tutte le colonne della tabella. Questa è una delle cose che rende così utili gli indici non cluster, perché in genere sono molto più piccoli della tabella sottostante.

Nel caso di una vista indicizzata, il nostro set di dati potrebbe essere il risultato di un'intera query, inclusi i join su molte tabelle! Questo è per un altro post.

Ma in un indice filtrato, non è solo una copia di un sottoinsieme di colonne, ma anche un sottoinsieme di righe. Quindi, nell'esempio qui, l'indice riguarda solo gli utenti con più di 400.000 di reputazione.

CREATE UNIQUE NONCLUSTERED INDEX Users_400k_Club_NoInclude
ON dbo.Users ( DisplayName, Id )
WHERE Reputation > 400000;

Questo indice prende gli utenti che hanno più di 400.000 di reputazione e li ordina per DisplayName e Id. Può essere univoco perché (presumibilmente) la colonna Id è già univoca. Se provi qualcosa di simile sul tuo tavolo, potresti dover stare attento a questo.

Ma a questo punto, all'indice non importa quale sia la reputazione per ciascun utente, importa solo se la reputazione è sufficientemente alta da essere nell'indice o meno. Se la reputazione di un utente viene aggiornata e supera la soglia, il DisplayName e l'ID dell'utente verranno inseriti nell'indice. Se scende al di sotto, verrà eliminato dall'indice. È proprio come avere una tabella separata per gli high roller, tranne per il fatto che inseriamo le persone in quella tabella aumentando il loro valore di Reputazione oltre la soglia di 400.000 nella tabella sottostante. Può farlo senza dover effettivamente memorizzare il valore di Reputazione stesso.

Quindi ora, se vogliamo trovare persone con una soglia superiore a 450.000, in quell'indice mancano alcune informazioni.

Certo, possiamo affermare con sicurezza che tutti quelli che troveremo sono in quell'indice, ma l'indice non contiene informazioni sufficienti in sé per filtrare ulteriormente la reputazione. Se ti dicessi che avevo un elenco alfabetico dei film vincitori dell'Oscar per il miglior film degli anni '90 (American Beauty, Braveheart, Balla coi lupi, Paziente inglese, Forrest Gump, Schindler's List, Shakespeare in Love, Il silenzio degli innocenti, Titanic, Unforgiven) , quindi posso assicurarti che i vincitori per il 1994-1996 sarebbero un sottoinsieme di quelli, ma non posso rispondere alla domanda senza prima ottenere qualche informazione in più.

Ovviamente il mio indice filtrato sarebbe più utile se avessi incluso l'anno, e potenzialmente ancora di più se l'anno fosse una colonna chiave, poiché la mia nuova query vuole trovare quelle per il 1994-1996. Ma probabilmente ho progettato questo indice attorno a una query per elencare tutti i film degli anni '90 in ordine alfabetico. A quella query non interessa quale sia l'anno effettivo, solo se è negli anni '90 o meno, e non ho nemmeno bisogno di restituire l'anno – solo il titolo – quindi posso scansionare il mio indice filtrato per ottenere i risultati. Per quella query non ho nemmeno bisogno di riordinare i risultati o trovare il punto di partenza:il mio indice è davvero perfetto.

Un esempio più pratico di non preoccuparsi del valore della colonna nel filtro è sullo stato, ad esempio:

WHERE IsActive = 1

Vedo spesso codice che sposta i dati da una tabella all'altra quando le righe smettono di essere "attive". Le persone non vogliono che le vecchie righe ingombrano la loro tabella e riconoscono che i loro dati "caldi" sono solo un piccolo sottoinsieme di tutti i loro dati. Quindi spostano i loro dati di raffreddamento in una tabella di archivio, mantenendo piccola la loro tabella attiva.

Un indice filtrato può farlo per te. Dietro le quinte. Non appena aggiorni la riga e modifichi la colonna IsActive in qualcosa di diverso da 1. Se ti interessa solo avere dati attivi nella maggior parte dei tuoi indici, gli indici filtrati sono l'ideale. Riporterà anche le righe negli indici se il valore IsActive torna a 1.

Ma non è necessario inserire IsActive nell'elenco INCLUDE per ottenere ciò. Perché dovresti voler memorizzare il valore - sai già qual è il valore - è 1! A meno che tu non stia chiedendo di restituire il valore, non dovresti averne bisogno. E perché dovresti restituire il valore quando sai già che la risposta è 1, giusto?! Tranne che in modo frustrante, le statistiche a cui fa riferimento Erik nel suo post trarranno vantaggio dall'essere nell'elenco INCLUDE. Non ti serve per la query, ma dovresti includerlo per le statistiche.

Pensiamo a cosa deve fare Query Optimizer per capire l'utilità di un indice.

Prima che possa fare molto, deve considerare se l'indice è un candidato. Non ha senso usare un indice se non ha tutte le righe che potrebbero essere necessarie, a meno che non disponiamo di un modo efficace per ottenere il resto. Se voglio film dal 1985 al 1995, il mio indice dei film degli anni '90 è piuttosto inutile. Ma per il 1994-1996 forse non è male.

A questo punto, proprio come qualsiasi considerazione sull'indice, devo pensare se aiuterà abbastanza per trovare i dati e metterli in un ordine che aiuterà a eseguire il resto della query (possibilmente per un Merge Join, Stream Aggregate, soddisfacendo un ORDINE BY, o vari altri motivi). Se il mio filtro di query corrisponde esattamente al filtro dell'indice, non è necessario filtrare ulteriormente:è sufficiente utilizzare l'indice. Sembra fantastico, ma se non corrisponde esattamente, se il mio filtro di query è più stretto del filtro dell'indice (come il mio esempio del 1994-1996 o 450.000 di Erik), avrò bisogno di quei valori Anno o Valori di reputazione per controllare - si spera di ottenerli da INCLUDEd a livello di foglia o da qualche parte nelle mie colonne chiave. Se non sono nell'indice, dovrò eseguire una ricerca per ogni riga del mio indice filtrato (e idealmente, avere un'idea di quante volte verrà chiamata la mia ricerca, quali sono le statistiche che Erik vuole la colonna inclusa per).

Idealmente, qualsiasi indice che intendo utilizzare è ordinato correttamente (tramite le chiavi), INCLUDE tutte le colonne che devo restituire ed è prefiltrato solo per le righe di cui ho bisogno. Sarebbe l'indice perfetto e il mio piano di esecuzione sarà una scansione.

Esatto, uno SCAN. Non una ricerca, ma una scansione. Inizierà sulla prima pagina del mio indice e continuerà a darmi righe finché non avrò tutte le righe di cui ho bisogno o finché non ci saranno più righe da restituire. Non saltarne nessuno, non ordinarli, solo dandomi le righe in ordine.

Un Seek suggerirebbe che non ho bisogno dell'intero indice, il che significa che sto sprecando risorse per mantenere quella parte dell'indice e per interrogarlo devo trovare il punto di partenza e continuare a controllare le righe per vedere se ho raggiungere la fine o no. Se la mia scansione ha un predicato, allora dovrò esaminare (e testare) più dati del necessario, ma se i miei filtri di indice sono perfetti, Query Optimizer dovrebbe riconoscerlo e non dover eseguire quei controlli .

Pensieri finali

INCLUDEs non sono fondamentali per gli indici filtrati. Sono utili per fornire un facile accesso alle colonne che potrebbero essere utili per la tua query e se ti capita di restringere ciò che è nel tuo indice filtrato in base a qualsiasi colonna, indipendentemente dal fatto che sia menzionato nel filtro o meno, dovresti considerare di avere quella colonna in il mix. Ma a quel punto dovresti chiederti se il filtro del tuo indice è quello giusto, cos'altro dovresti avere nell'elenco INCLUDE e persino quali dovrebbero essere le colonne chiave. Le query di Erik non funzionavano bene perché aveva bisogno di informazioni che non erano nell'indice, anche se aveva menzionato la colonna nel filtro. Ha trovato un buon uso anche per le statistiche e per questo motivo ti incoraggerei comunque a includere le colonne dei filtri. Ma inserirli in un INCLUDE non consente loro di iniziare improvvisamente a fare una ricerca, perché non è così che funziona qualsiasi indice, filtrato o meno.

Voglio che tu, lettore, comprendi molto bene gli indici filtrati. Sono incredibilmente utili e, quando inizi a immaginarli come tabelle a sé stanti, possono diventare parte della progettazione generale del database. Sono anche un motivo per utilizzare sempre le impostazioni ANSI_NULLs e QUOTED_IDENTIFIER, perché riceverai errori dall'indice filtrato a meno che tali impostazioni non siano attive, ma si spera che tu ti assicuri già che siano sempre attive comunque.

Oh, e quei film erano Forrest Gump, Braveheart e The English Patient.

@rob_farley