Perché il mio indice non viene utilizzato in SELECT TOP?


15

Ecco il fatiscente: sto facendo una query di selezione. Ogni colonna nelle clausole WHEREe ORDER BYsi trova in un singolo indice non cluster IX_MachineryId_DateRecorded, come parte della chiave o come INCLUDEcolonne. Sto selezionando tutte le colonne, in modo che si traduca in una ricerca di segnalibri, ma sto solo prendendoTOP (1) , quindi sicuramente il server può dire che la ricerca deve essere eseguita solo una volta, alla fine.

Soprattutto, quando impongo alla query di utilizzare l'indice IX_MachineryId_DateRecorded, viene eseguita in meno di un secondo. Se lascio che il server decida quale indice utilizzare, prende IX_MachineryIde impiega fino a un minuto. Questo mi suggerisce davvero di aver corretto l'indice e che il server sta solo prendendo una decisione sbagliata. Perché?

CREATE TABLE [dbo].[MachineryReading] (
    [Id]                 INT              IDENTITY (1, 1) NOT NULL,
    [Location]           [sys].[geometry] NULL,
    [Latitude]           FLOAT (53)       NOT NULL,
    [Longitude]          FLOAT (53)       NOT NULL,
    [Altitude]           FLOAT (53)       NULL,
    [Odometer]           INT              NULL,
    [Speed]              FLOAT (53)       NULL,
    [BatteryLevel]       INT              NULL,
    [PinFlags]           BIGINT           NOT NULL,
    [DateRecorded]       DATETIME         NOT NULL,
    [DateReceived]       DATETIME         NOT NULL,
    [Satellites]         INT              NOT NULL,
    [HDOP]               FLOAT (53)       NOT NULL,
    [MachineryId]        INT              NOT NULL,
    [TrackerId]          INT              NOT NULL,
    [ReportType]         NVARCHAR (1)     NULL,
    [FixStatus]          INT              DEFAULT ((0)) NOT NULL,
    [AlarmStatus]        INT              DEFAULT ((0)) NOT NULL,
    [OperationalSeconds] INT              DEFAULT ((0)) NOT NULL,
    CONSTRAINT [PK_dbo.MachineryReading] PRIMARY KEY CLUSTERED ([Id] ASC),
    CONSTRAINT [FK_dbo.MachineryReading_dbo.Machinery_MachineryId] FOREIGN KEY ([MachineryId]) REFERENCES [dbo].[Machinery] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_dbo.MachineryReading_dbo.Tracker_TrackerId] FOREIGN KEY ([TrackerId]) REFERENCES [dbo].[Tracker] ([Id]) ON DELETE CASCADE
);

GO
CREATE NONCLUSTERED INDEX [IX_MachineryId]
    ON [dbo].[MachineryReading]([MachineryId] ASC);

GO
CREATE NONCLUSTERED INDEX [IX_TrackerId]
    ON [dbo].[MachineryReading]([TrackerId] ASC);

GO
CREATE NONCLUSTERED INDEX [IX_MachineryId_DateRecorded]
    ON [dbo].[MachineryReading]([MachineryId] ASC, [DateRecorded] ASC)
    INCLUDE([OperationalSeconds], [FixStatus]);

La tabella è suddivisa in intervalli di mesi (anche se ancora non capisco davvero cosa sta succedendo lì).

ALTER PARTITION SCHEME PartitionSchemeMonthRange NEXT USED [Primary]
ALTER PARTITION FUNCTION [PartitionFunctionMonthRange]() SPLIT RANGE(N'2016-01-01T00:00:00.000') 

ALTER PARTITION SCHEME PartitionSchemeMonthRange NEXT USED [Primary]
ALTER PARTITION FUNCTION [PartitionFunctionMonthRange]() SPLIT RANGE(N'2016-02-01T00:00:00.000') 
...

CREATE UNIQUE CLUSTERED INDEX [PK_dbo.MachineryReadingPs] ON MachineryReading(DateRecorded, Id) ON PartitionSchemeMonthRange(DateRecorded)

La query che normalmente vorrei eseguire:

SELECT TOP (1) [Id], [Location], [Latitude], [Longitude], [Altitude], [Odometer], [ReportType], [FixStatus], [AlarmStatus], [Speed], [BatteryLevel], [PinFlags], [DateRecorded], [DateReceived], [Satellites], [HDOP], [OperationalSeconds], [MachineryId], [TrackerId]
    FROM [dbo].[MachineryReading]
    --WITH(INDEX(IX_MachineryId_DateRecorded)) --This makes all the difference
    WHERE ([MachineryId] = @p__linq__0) AND ([DateRecorded] >= @p__linq__1) AND ([DateRecorded] < @p__linq__2) AND ([OperationalSeconds] > 0)
    ORDER BY [DateRecorded] ASC

Piano di query: https://www.brentozar.com/pastetheplan/?id=r1c-RpxNx

Piano di query con indice forzato: https://www.brentozar.com/pastetheplan/?id=SywwTagVe

I piani inclusi sono i piani di esecuzione effettivi, ma sul database di gestione temporanea (circa 1/100 della dimensione del live). Sono riluttante ad armeggiare con il database live perché ho iniziato in questa azienda solo un mese fa.

Ho la sensazione che sia a causa del partizionamento e la mia query in genere si estende su ogni singola partizione (ad esempio quando voglio ottenere il primo o l'ultimo OperationalSecondsmai registrato per una macchina). Tuttavia, le query che ho scritto a mano sono in esecuzione 10 - 100 volte più veloci di quanto generato da EntityFramework , quindi eseguirò una procedura memorizzata.


1
Ciao @AndrewWilliamson, potrebbe essere un problema di statistiche. Se vedi il piano effettivo dal piano non forzato, il numero stimato di righe è 1,22 e l'attuale è 19039. Questo a sua volta porta alla ricerca chiave che vedrai più avanti nel piano. hai provato ad aggiornare le statistiche? In caso contrario, provare con la scansione completa sul database di gestione temporanea.
jesijesi,

Risposte:


21

Se lascio che il server decida quale indice utilizzare, prende IX_MachineryIde impiega fino a un minuto.

Tale indice non è partizionato, quindi l'ottimizzatore riconosce che può essere utilizzato per fornire l'ordinamento specificato nella query senza ordinamento. Come indice non cluster non univoco, ha anche le chiavi dell'indice cluster come sottochiavi, quindi l'indice può essere utilizzato per cercare MachineryIde l' DateRecordedintervallo:

Ricerca indice

L'indice non include OperationalSeconds, quindi il piano deve cercare quel valore per riga nell'indice cluster (partizionato) per testare OperationalSeconds > 0:

Consultare

L'ottimizzatore stima che una riga dovrà essere letta dall'indice non cluster e cercata per soddisfare il TOP (1) . Questo calcolo si basa sull'obiettivo della riga (trova rapidamente una riga) e presuppone una distribuzione uniforme dei valori.

Dal piano reale, possiamo vedere che la stima di 1 riga è imprecisa. In effetti, 19.039 righe devono essere elaborate per scoprire che nessuna riga soddisfa le condizioni della query. Questo è il caso peggiore per l'ottimizzazione dell'obiettivo di una riga (1 riga stimata, tutte le righe effettivamente necessarie):

Actual / preventivo

È possibile disabilitare gli obiettivi di riga con il flag di traccia 4138 . Ciò determinerebbe molto probabilmente la scelta da parte di SQL Server di un piano diverso, possibilmente quello che hai forzato. In ogni caso, l'indice IX_MachineryIdpotrebbe essere reso più ottimale includendo OperationalSeconds.

È abbastanza insolito avere indici non cluster non allineati (indici partizionati in modo diverso dalla tabella di base, incluso per niente).

Questo mi suggerisce davvero di aver corretto l'indice e che il server sta solo prendendo una decisione sbagliata. Perché?

Come al solito, l'ottimizzatore sta selezionando il piano più economico che considera.

Il costo stimato del IX_MachineryId piano è di 0,01 unità di costo, basato sul presupposto (errato) dell'obiettivo di riga che verrà testata e restituita una riga.

Il costo stimato del IX_MachineryId_DateRecordedpiano è molto più alto, a 0,27 unità, principalmente perché prevede di leggere 5.515 righe dall'indice, ordinarle e restituire quella che ordina più in basso (per DateRecorded):

Top N Sort

Questo indice è partizionato e non può restituire le righe DateRecordeddirettamente nell'ordine (vedere più avanti). Può cercare MachineryIde l' DateRecordedintervallo all'interno di ciascuna partizione , ma è necessario un ordinamento:

Ricerca partizionata

Se questo indice non fosse partizionato, non sarebbe necessario un ordinamento e sarebbe molto simile all'altro indice (non partizionato) con la colonna inclusa extra. Un indice filtrato non partizionato sarebbe ancora leggermente più efficiente.


È necessario aggiornare la query di origine in modo che i tipi di dati dei parametri @Frome corrispondano alla colonna ( ). Al momento, SQL Server sta calcolando un intervallo dinamico a causa della mancata corrispondenza del tipo in fase di esecuzione (utilizzando l'operatore Merge Interval e la sua sottostruttura):@ToDateRecordeddatetime

<ScalarOperator ScalarString="GetRangeWithMismatchedTypes([@From],NULL,(22))">
<ScalarOperator ScalarString="GetRangeWithMismatchedTypes([@To],NULL,(22))">

Questa conversione impedisce all'ottimizzatore di ragionare correttamente sulla relazione tra gli ID di partizione ascendenti (che coprono un intervallo di DateRecordedvalori in ordine crescente) e i predicati di disuguaglianza su DateRecorded.

L'ID della partizione è una chiave iniziale implicita per un indice partizionato. Normalmente, l'ottimizzatore può vedere che l'ordinamento in base all'ID della partizione (dove gli ID ascendenti si associano ai valori ascendenti e disgiunti di DateRecorded) DateRecordedè lo stesso dell'ordinamento da DateRecordedsolo (dato cheMachineryID è costante). Questa catena di ragionamento è interrotta dalla conversione del tipo.

dimostrazione

Una semplice tabella e indice partizionati:

CREATE PARTITION FUNCTION PF (datetime)
AS RANGE LEFT FOR VALUES ('20160101', '20160201', '20160301');

CREATE PARTITION SCHEME PS AS PARTITION PF ALL TO ([PRIMARY]);

CREATE TABLE dbo.T (c1 integer NOT NULL, c2 datetime NOT NULL) ON PS (c2);

CREATE INDEX i ON dbo.T (c1, c2) ON PS (c2);

INSERT dbo.T (c1, c2) 
VALUES (1, '20160101'), (1, '20160201'), (1, '20160301');

Interrogazione con tipi corrispondenti

-- Types match (datetime)
DECLARE 
    @From datetime = '20010101',
    @To datetime = '20090101';

-- Seek with no sort
SELECT T2.c2 
FROM dbo.T AS T2 
WHERE T2.c1 = 1 
AND T2.c2 >= @From
AND T2.c2 < @To
ORDER BY 
    T2.c2;

Non cercare alcun tipo

Interrogazione con tipi non corrispondenti

-- Mismatched types (datetime2 vs datetime)
DECLARE 
    @From datetime2 = '20010101',
    @To datetime2 = '20090101';

-- Merge Interval and Sort
SELECT T2.c2 
FROM dbo.T AS T2 
WHERE T2.c1 = 1 
AND T2.c2 >= @From
AND T2.c2 < @To
ORDER BY 
    T2.c2;

Unisci intervallo e ordina


5

L'indice sembra abbastanza buono per la query e non sono sicuro del motivo per cui non è stato scelto dall'ottimizzatore (statistiche? Il partizionamento? Limitazione dell'azzurro ?, nessuna idea davvero.)

Ma un indice filtrato sarebbe ancora migliore per la query specifica, se si > 0tratta di un valore fisso e non cambia da un'esecuzione di query a un'altra:

CREATE NONCLUSTERED INDEX IX_MachineryId_DateRecorded_filtered
    ON dbo.MachineryReading
        (MachineryId, DateRecorded) 
    WHERE (OperationalSeconds > 0) ;

Esistono due differenze tra l'indice che hai dove OperationalSecondsè la terza colonna e l'indice filtrato:

  • Innanzitutto l'indice filtrato è più piccolo, sia in larghezza (più stretto) che in numero di righe.
    Questo rende l'indice filtrato più efficiente in generale poiché SQL Server ha bisogno di meno spazio per tenerlo in memoria.

  • In secondo luogo, questo è più sottile e importante per la query è che ha solo righe che corrispondono al filtro utilizzato nella query. Questo potrebbe essere estremamente importante, a seconda dei valori di questa terza colonna.
    Ad esempio un insieme specifico di parametri per MachineryIde DateRecordedpuò produrre 1000 righe. Se tutte o quasi tutte queste righe corrispondono al (OperationalSeconds > 0)filtro, entrambi gli indici si comporteranno bene. Ma se le righe corrispondenti al filtro sono molto poche (o solo l'ultima o nessuna), il primo indice dovrà attraversare molte o tutte quelle 1000 righe fino a quando non trova una corrispondenza. L'indice filtrato richiede invece solo una ricerca per trovare una riga corrispondente (o per restituire 0 righe) poiché vengono memorizzate solo le righe corrispondenti al filtro.


1
L'aggiunta dell'indice ha reso la query più efficiente?
ypercubeᵀᴹ

Non per il database di gestione temporanea (ha davvero bisogno di più dati per testarlo correttamente), non l'ho ancora provato dal vivo, i nuovi indici impiegano più di un'ora per basarsi su quello. Sono anche piuttosto riluttante a fare qualsiasi cosa nel nostro database live, poiché è già in esecuzione lentamente. Abbiamo bisogno di un sistema migliore per clonare la nostra vita in scena.
Andrew Williamson,
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.