La regola WHERE-JOIN-ORDER- (SELECT) per l'ordine delle colonne dell'indice è errata?


9

Sto cercando di migliorare questa (sotto-) query facente parte di una query più grande:

select SUM(isnull(IP.Q, 0)) as Q, 
        IP.OPID 
    from IP
        inner join I
        on I.ID = IP.IID
    where 
        IP.Deleted=0 and
        (I.Status > 0 AND I.Status <= 19) 
    group by IP.OPID

Sentry Plan Explorer ha indicato alcune ricerche chiave relativamente costose per la tabella dbo. [I] eseguite dalla query sopra.

Tabella dbo.I

    CREATE TABLE [dbo].[I] (
  [ID]  UNIQUEIDENTIFIER NOT NULL,
  [OID]  UNIQUEIDENTIFIER NOT NULL,
  []  UNIQUEIDENTIFIER NOT NULL,
  []  UNIQUEIDENTIFIER NOT NULL,
  [] UNIQUEIDENTIFIER NULL,
  []  UNIQUEIDENTIFIER NOT NULL,
  []  CHAR (3) NOT NULL,
  []  CHAR (3)  DEFAULT ('EUR') NOT NULL,
  []  DECIMAL (18, 8) DEFAULT ((1)) NOT NULL,
  [] CHAR (10)  NOT NULL,
  []  DECIMAL (18, 8) DEFAULT ((1)) NOT NULL,
  []  DATETIME  DEFAULT (getdate()) NOT NULL,
  []  VARCHAR (35) NULL,
  [] NVARCHAR (100) NOT NULL,
  []  NVARCHAR (100) NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  [Status]  INT DEFAULT ((0)) NOT NULL,
  []  DECIMAL (18, 2)  NOT NULL,
  [] DECIMAL (18, 2)  NOT NULL,
  [] DECIMAL (18, 2)  NOT NULL,
  [] DATETIME DEFAULT (getdate()) NULL,
  []  DATETIME NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  [] TINYINT  DEFAULT ((0)) NOT NULL,
  []  DATETIME NULL,
  []  VARCHAR (50) NULL,
  []  DATETIME  DEFAULT (getdate()) NOT NULL,
  []  VARCHAR (50) NOT NULL,
  []  DATETIME NULL,
  []  VARCHAR (50) NULL,
  []  ROWVERSION NOT NULL,
  []  DATETIME NULL,
  []  INT  NULL,
  [] TINYINT DEFAULT ((0)) NOT NULL,
  [] UNIQUEIDENTIFIER NULL,
  []  TINYINT DEFAULT ((0)) NOT NULL,
  []  TINYINT  DEFAULT ((0)) NOT NULL,
  [] NVARCHAR (50)  NULL,
  [] TINYINT DEFAULT ((0)) NOT NULL,
  []  UNIQUEIDENTIFIER NULL,
  [] UNIQUEIDENTIFIER NULL,
  []  TINYINT  DEFAULT ((0)) NOT NULL,
  []  TINYINT DEFAULT ((0)) NOT NULL,
  []  UNIQUEIDENTIFIER NULL,
  []  DECIMAL (18, 2)  NULL,
  []  DECIMAL (18, 2)  NULL,
  [] DECIMAL (18, 2)  DEFAULT ((0)) NOT NULL,
  [] UNIQUEIDENTIFIER NULL,
  [] DATETIME NULL,
  [] DATETIME NULL,
  []  VARCHAR (35) NULL,
  [] DECIMAL (18, 2)  DEFAULT ((0)) NOT NULL,
  CONSTRAINT [PK_I] PRIMARY KEY NONCLUSTERED ([ID] ASC) WITH (FILLFACTOR = 90),
  CONSTRAINT [FK_I_O] FOREIGN KEY ([OID]) REFERENCES [dbo].[O] ([ID]),
  CONSTRAINT [FK_I_Status] FOREIGN KEY ([Status]) REFERENCES [dbo].[T_Status] ([Status])
);                  


GO
CREATE CLUSTERED INDEX [CIX_Invoice]
  ON [dbo].[I]([OID] ASC) WITH (FILLFACTOR = 90);

Tabella dbo.IP

CREATE TABLE [dbo].[IP] (
 [ID] UNIQUEIDENTIFIER DEFAULT (newid()) NOT NULL,
 [IID] UNIQUEIDENTIFIER NOT NULL,
 [OID] UNIQUEIDENTIFIER NOT NULL,
 [Deleted] TINYINT DEFAULT ((0)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 []UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] INT NOT NULL,
 [] VARCHAR (35) NULL,
 [] NVARCHAR (100) NOT NULL,
 [] NTEXT NULL,
 [] DECIMAL (18, 4) DEFAULT ((0)) NOT NULL,
 [] NTEXT NULL,
 [] NTEXT NULL,
 [] DECIMAL (18, 4) DEFAULT ((0)) NOT NULL,
 [] DECIMAL (18, 4) DEFAULT ((0)) NOT NULL,
 [] DECIMAL (4, 2) NOT NULL,
 [] INT DEFAULT ((1)) NOT NULL,
 [] DATETIME DEFAULT (getdate()) NOT NULL,
 [] VARCHAR (50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
 [] DATETIME NULL,
 [] VARCHAR (50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
 [] ROWVERSION NOT NULL,
 [] INT DEFAULT ((1)) NOT NULL,
 [] DATETIME NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] DECIMAL (18, 4) DEFAULT ((1)) NOT NULL,
 [] DECIMAL (18, 4) DEFAULT ((1)) NOT NULL,
 [] INT DEFAULT ((0)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 []UNIQUEIDENTIFIER NULL,
 []NVARCHAR (35) NULL,
 [] VARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] VARCHAR (12) NULL,
 [] VARCHAR (4) NULL,
 [] NVARCHAR (50) NULL,
 [] NVARCHAR (50) NULL,
 [] VARCHAR (35) NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] NVARCHAR (50) NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] DECIMAL (18, 2) NULL,
 []TINYINT DEFAULT ((1)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] TINYINT DEFAULT ((1)) NOT NULL,
 CONSTRAINT [PK_IP] PRIMARY KEY NONCLUSTERED ([ID] ASC) WITH (FILLFACTOR = 90),
 CONSTRAINT [FK_IP_I] FOREIGN KEY ([IID]) REFERENCES [dbo].[I] ([ID]) ON DELETE CASCADE NOT FOR REPLICATION,
 CONSTRAINT [FK_IP_XType] FOREIGN KEY ([XType]) REFERENCES [dbo].[xTYPE] ([Value]) NOT FOR REPLICATION
);

GO
CREATE CLUSTERED INDEX [IX_IP_CLUST]
 ON [dbo].[IP]([IID] ASC) WITH (FILLFACTOR = 90);

La tabella "I" ha circa 100.000 righe, l'indice cluster ha 9.386 pagine.
La tabella IP è la "figlia" - tabella di I e ha circa 175.000 righe.

Ho provato ad aggiungere un nuovo indice seguendo la regola dell'ordine della colonna dell'indice: "WHERE-JOIN-ORDER- (SELECT)"

https://www.mssqltips.com/sqlservertutorial/3208/use-where-join-orderby-select-column-order-when-creating-indexes/

per indirizzare le ricerche chiave e creare una ricerca indice:

CREATE NONCLUSTERED INDEX [IX_I_Status_1]
    ON [dbo].[Invoice]([Status], [ID])

La query estratta ha immediatamente utilizzato questo indice. Ma la query più grande originale di cui fa parte, non l'ha fatto. Non lo ha nemmeno usato quando l'ho costretto ad usare WITH (INDEX (IX_I_Status_1)).

Dopo un po 'ho deciso di provare un altro nuovo indice e sono passato all'ordine delle colonne indicizzate:

CREATE NONCLUSTERED INDEX [IX_I_Status_2]
    ON [dbo].[Invoice]([ID], [Status])

WOHA! Questo indice è stato utilizzato dalla query estratta e anche dalla query più grande!

Quindi ho confrontato le statistiche IO delle query estratte costringendole a utilizzare [IX_I_Status_1] e [IX_I_Status_2]:

Risultati [IX_I_Status_1]:

Table 'I'. Scan count 5, logical reads 636, physical reads 16, read-ahead reads 574
Table 'IP'. Scan count 5, logical reads 1134, physical reads 11, read-ahead reads 1040
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0

Risultati [IX_I_Status_2]:

Table 'I'. Scan count 1, logical reads 615, physical reads 6, read-ahead reads 631
Table 'IP'. Scan count 1, logical reads 1024, physical reads 5, read-ahead reads 1040
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0,  read-ahead reads 0

OK, potrei capire che la query mega-large-monster potrebbe essere troppo complessa per fare in modo che il server SQL prenda il piano di esecuzione ideale e potrebbe mancare il mio nuovo indice. Ma non capisco perché l'indice [IX_I_Status_2] sembra essere più adatto e più efficiente per la query.

Poiché la query filtra innanzitutto la tabella I per colonna STATUS e quindi si unisce alla tabella IP, non capisco perché [IX_I_Status_2] sia migliore e utilizzato da SQL Server anziché [IX_I_Status_1]?


Sì, utilizza questo indice nel caso in cui i criteri di filtro soddisfino. Esegue una scansione dell'indice (uguale a IX_I_Status_2) e rispetto a questo salva 1 lettura fisica. ma ho dovuto "includere (stato)" in questo indice perché lo stato è nell'output ed è stato nuovamente cercato prima.
Magier,

Nota a margine divertente: dopo aver applicato il miglior indice che ho potuto capire ([IX_I_Status_2]) ed eseguire nuovamente la query, ora ricevo un suggerimento sull'indice mancante: CREATE INDICE NONCLUSTERED [<Nome dell'indice mancante, sysname,>] ON [ dbo]. [I] ([Status]) INCLUDE ([ID]) Questo è un suggerimento scadente e riduce le prestazioni della query. TY Sql server :)
Magier,

Risposte:


19

La regola WHERE-JOIN-ORDER- (SELECT) per l'ordine delle colonne dell'indice è errata?

Almeno è un consiglio incompleto e potenzialmente fuorviante (non mi sono preoccupato di leggere l'intero articolo). Se hai intenzione di leggere cose su Internet (incluso questo), dovresti regolare la tua quantità di fiducia in base a quanto conosci e credi già nell'autore, ma poi verifica sempre per te.

Esistono una serie di "regole empiriche" per la creazione di indici, a seconda dello scenario esatto, ma nessuna di esse è davvero un buon sostituto per comprendere i problemi fondamentali per te stesso. Leggi l'implementazione degli indici e degli operatori del piano di esecuzione in SQL Server, passa attraverso alcuni esercizi e arriva a una buona conoscenza di come gli indici possono essere utilizzati per rendere i piani di esecuzione più efficienti. Non esiste una scorciatoia efficace per raggiungere questa conoscenza ed esperienza.

In generale, posso dire che il più delle volte gli indici dovrebbero avere le colonne utilizzate per i test di uguaglianza prima, con eventuali disuguaglianze ultime e / o fornite da un filtro sull'indice. Questa non è un'affermazione completa, perché gli indici possono anche fornire ordine, il che può essere più utile che cercare direttamente una o più chiavi in ​​alcune situazioni. Ad esempio, l'ordinamento può essere utilizzato per evitare un ordinamento, per ridurre il costo di un'opzione di unione fisica come unisci unione, per abilitare un aggregato di flussi, trovare rapidamente le prime righe qualificanti ... e così via.

Sto diventando un po 'vago qui, perché la selezione dell'indice o degli indici ideali per una query dipende da così tanti fattori: questo è un argomento molto ampio.

Comunque, non è insolito trovare segnali contrastanti per gli "migliori" indici in una query. Ad esempio, il predicato di join vorrebbe che le righe fossero ordinate in un modo per un join di unione, il gruppo vorrebbe che le righe fossero ordinate in un altro modo per un aggregato di stream e che trovando le righe qualificanti utilizzando i predicati della clausola where suggerissero altri indici.

La ragione per cui l'indicizzazione è un'arte e la scienza è che una combinazione ideale non è sempre logicamente possibile. La scelta dei migliori indici di compromesso per il carico di lavoro (non solo una singola query) richiede capacità analitiche, esperienza e conoscenze specifiche del sistema. Se fosse facile , gli strumenti automatizzati sarebbero perfetti e i consulenti di ottimizzazione delle prestazioni sarebbero molto meno richiesti.

Per quanto riguarda i suggerimenti sugli indici mancanti: questi sono opportunistici. L'ottimizzatore li porta alla tua attenzione quando cerca di far corrispondere i predicati e l'ordinamento richiesto a un indice che non esiste. I suggerimenti si basano quindi su particolari tentativi di abbinamento nel contesto specifico della particolare variante del sotto-piano che stava prendendo in considerazione in quel momento.

Nel contesto, i suggerimenti hanno sempre senso, in termini di riduzione del costo stimato dell'accesso ai dati, secondo il modello dell'ottimizzatore. Essa non fa una più ampia analisi della query nel suo complesso (e tanto meno il carico di lavoro più ampio), così si dovrebbe pensare a queste proposte come un suggerimento delicato che un abile persona ha bisogno di guardare gli indici disponibili, con i suggerimenti come un punto di partenza punto (e di solito non più di così).

Nel tuo caso, il (Status) INCLUDE (ID)suggerimento probabilmente è nato quando stava esaminando la possibilità di un hash o unire l'unione (esempio più avanti). In quel contesto ristretto, il suggerimento ha un senso. Per la query nel suo insieme, forse no. L'indice (ID, Status)abilita un nodo nidificato unito IDcome riferimento esterno: ricerca dell'uguaglianza IDe disuguaglianza Statusper iterazione.

Una possibile selezione di indici è:

CREATE INDEX i1 ON dbo.I (ID, [Status]);
CREATE INDEX i1 ON dbo.IP (Deleted, OPID, IID) INCLUDE (Q);

... che produce un piano come:

Possibile piano

Non sto dicendo che questi indici sono ottimali per te; loro lavorano per produrre un piano dall'aspetto ragionevole per me, senza essere in grado di vedere le statistiche per le tabelle coinvolte, o le definizioni complete e l'indicizzazione esistente. Inoltre, non so nulla del più ampio carico di lavoro o query reale.

In alternativa (solo per mostrare una delle innumerevoli possibilità aggiuntive):

CREATE INDEX i1 ON dbo.I ([Status]) INCLUDE (ID);
CREATE INDEX i1 ON dbo.IP (Deleted, IID, OPID) INCLUDE (Q);

dà:

Piano alternativo

I piani di esecuzione sono stati generati utilizzando SQL Sentry Plan Explorer .

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.