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

Un approccio all'accordatura dell'indice - Parte 2

Nel mio ultimo post, ho iniziato a delineare il processo che affronto durante l'ottimizzazione delle query, in particolare quando scopro che devo aggiungere un nuovo indice o modificarne uno esistente. Fino a questo punto abbiamo identificato la query problematica, l'indice di cui ho bisogno, quali indici esistono attualmente sul tavolo e se tali indici vengono utilizzati o meno. Una volta che abbiamo questi dati, possiamo passare ai passaggi successivi del processo.

Fase 5:cosa utilizza un indice

Oltre a vedere la frequenza con cui un indice viene utilizzato (o meno), è utile sapere quali query utilizzare un indice, in particolare se sto cercando di unirlo con un altro indice. Fortunatamente, Jonathan Kehayias ha già scritto una query per aiutare a identificare quali piani utilizzano un indice specifico. La sua versione può essere utilizzata per la cache del piano:l'unica sfida è che le informazioni sono transitorie, quindi potresti non acquisire tutte le query che utilizzano un particolare indice. Query Store può aiutare in questo:ho modificato la sua query per ottenere le stesse informazioni dai piani in Query Store:

  SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
  DECLARE @IndexName AS NVARCHAR(128) = N'[IX_Sales_OrderLines_AllocatedStockItems]',
          @lb AS nchar(1) = N'[', @rb AS nchar(1) = N']';
 
  -- Make sure the name passed is appropriately quoted
  IF (LEFT(@IndexName, 1) <> @lb AND RIGHT(@IndexName, 1) <> @rb) SET @IndexName = QUOTENAME(@IndexName);
 
  --Handle the case where the left or right was quoted manually but not the opposite side
  IF LEFT(@IndexName, 1)  <> @lb SET @IndexName = @rb + @IndexName;
  IF RIGHT(@IndexName, 1) <> @rb SET @IndexName = @IndexName + @rb;
 
  ;WITH XMLNAMESPACES (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan')   
  SELECT
    stmt.value('(@StatementText)[1]', 'varchar(max)') AS SQL_Text,
    obj.value('(@Database)[1]', 'varchar(128)') AS DatabaseName,
    obj.value('(@Schema)[1]', 'varchar(128)') AS SchemaName,
    obj.value('(@Table)[1]', 'varchar(128)') AS TableName,
    obj.value('(@Index)[1]', 'varchar(128)') AS IndexName,
    obj.value('(@IndexKind)[1]', 'varchar(128)') AS IndexKind,
    query_plan
  FROM 	
  (
    SELECT query_plan
    FROM
    (
      SELECT TRY_CONVERT(XML, [qsp].[query_plan]) AS [query_plan]
      FROM sys.query_store_plan [qsp]
    ) tp
  ) AS tab (query_plan)
  CROSS APPLY query_plan.nodes('/ShowPlanXML/BatchSequence/Batch/Statements/StmtSimple') AS batch(stmt)
  CROSS APPLY stmt.nodes('.//IndexScan/Object[@Index=sql:variable("@IndexName")]') AS idx(obj)
  OPTION(MAXDOP 1, RECOMPILE);

Vale la pena notare che questo è un altro punto in cui posso trovarmi molto in profondità in una tana del coniglio, a seconda del numero di indici che sto esaminando e del numero di query che li utilizzano. Se possibile, prenderò in considerazione anche i conteggi delle esecuzioni (da Query Store o dalla cache del piano) non solo per capire cosa query utilizza un indice, ma la frequenza con cui viene eseguita la query. È qui che l'accordatura dell'indice diventa un'arte. Posso raccogliere una quantità ridicola di dati... ma non ho tempo infinito per l'analisi, quindi devo esprimere un giudizio su quante domande esaminerò.

Fase 6:test

Nella sua forma più semplice, testare un indice significa prendere la query problematica e acquisire i dati del piano e delle prestazioni (durata, IO, CPU, ecc.), quindi creare l'indice, rieseguire la query e acquisire le stesse informazioni. Se le prestazioni migliorano, sei a posto!

Raramente è così semplice.

Per cominciare, ho spesso almeno due varianti di un indice che voglio testare, a volte di più. Inizio con la mia linea di base, quindi creo tutte le variazioni dell'indice, svuoto la cache del piano e vedo cosa sceglie SQL Server. Quindi scorro e forzo ogni indice con un suggerimento, catturando il piano e le metriche delle prestazioni per ogni esecuzione. Nota:questo presuppone che io abbia spazio su disco sufficiente per tutti gli indici... in caso contrario, li creo uno alla volta e provo. Infine, confronto i numeri. Se sto solo aggiungendo un nuovo indice, ho quasi finito. Ma se sto modificando un indice o unendone una coppia, può diventare complicato.

In un mondo ideale, se modifico un indice esistente, trovo le query più frequenti/importanti che utilizzano l'indice corrente e ottengo i loro piani e le metriche delle prestazioni (questo è facile con Query Store). Quindi cambio l'indice, rieseguo tutte quelle query e vedo se ottengo cambiamenti significativi nella forma del piano e/o nelle prestazioni.

Se unisco due indici, faccio la stessa cosa, ma con tutte le query che utilizzano uno dei due indici, quindi riprovo con l'indice unito.

Se sto aggiungendo/modificando/unendo più indici per una tabella, ho bisogno di ottenere tutte le query pertinenti e i loro piani e metriche, modificare gli indici, quindi recuperare tutte le informazioni e confrontare. Questo può richiedere molto tempo, a seconda di quante query diverse ci sono. È qui che è una forma d'arte e devi determinare quante query hai davvero bisogno di testare. È una funzione della frequenza di esecuzione, dell'importanza/rilevanza della query e del tempo che ho a disposizione/allocato.

Infine, se aggiungo un indice a una tabella e non ne rimuovo quelli esistenti, ho aggiunto un sovraccarico per INSERT, DELETE e potenzialmente UPDATE. È possibile testare le prestazioni di questa modifica, ma è necessario un ambiente di test e la possibilità di eseguire un test di carico e acquisire metriche pre e post modifica relative a durata, IO e CPU.

Sono molti amici, motivo per cui è ironico che inizialmente pensassi di affermare che l'accordatura dell'indice era facile. Potrebbe non essere sempre semplice, ma è possibile. È una questione di diligenza e di tenere traccia di tutto.

Fase 7:implementazione

Dopo aver controllato il più possibile i nuovi indici, siamo pronti per la produzione. Ammetto che considero i cambiamenti dell'indice come a basso rischio, in particolare quelli nuovi. Se si tratta di un problema, puoi eliminarlo immediatamente e ripristinare lo stato originale. Con uno scenario di modifica/unione/rilascio, si desidera avere tutto sotto script, in modo da poter modificare e ricreare gli indici secondo necessità per reimpostare gli indici. Raccomando sempre di disabilitare inizialmente gli indici invece di eliminarli, poiché in questo modo non devi preoccuparti della definizione:se devi aggiungere nuovamente l'indice, devi semplicemente ricostruirlo.

Riepilogo

Il tuo metodo per aggiungere e/o consolidare gli indici potrebbe essere diverso! Proprio come l'ottimizzazione delle query, non esiste un processo perfetto. Per chiunque sia nuovo all'ottimizzazione dell'indice, si spera che questo fornisca un antipasto di elementi da rivedere e considerazioni importanti. È impossibile aggiungere indici senza aggiungere una certa quantità di sovraccarico, ed è ancora qui che entra in gioco l'arte:devi determinare se il vantaggio dell'indice supera il suo costo per le modifiche.

L'ottimizzazione dell'indice è un processo perpetuo e iterativo:non credo che tu abbia mai finito, perché vengono apportate modifiche al codice, vengono aggiunte nuove tabelle o funzionalità e i dati nelle tabelle cambiano. Kimberly ha due post (https://www.sqlskills.com/blogs/kimberly/spring-cleaning-your-indexes-part-i/ e https://www.sqlskills.com/blogs/kimberly/spring-cleaning- your-indexes-part-ii/) che parlano di ripulire i tuoi indici - ora è il momento giusto per iniziare! E infine, ogni volta che qualcuno chiede "quanti indici dovrebbero esserci per una tabella?" Rispondo con qualcosa del tipo "il minor numero di cui hai bisogno per soddisfare il maggior numero possibile di domande". Non esiste un numero magico:ho visto tabelle con zero indici e ho visto tabelle con oltre 100 (sono sicuro che alcuni di voi hanno visto conteggi più alti). Né zero né 100 vanno bene, ma il numero "giusto" è quello che devi capire usando i dati disponibili e la tua esperienza.