Sottoquery con prestazioni scadenti con confronti di date


15

Quando si utilizza una sottoquery per trovare il conteggio totale di tutti i record precedenti con un campo corrispondente, le prestazioni sono terribili su una tabella con un minimo di 50.000 record. Senza la subquery, la query viene eseguita in pochi millisecondi. Con la subquery, il tempo di esecuzione è superiore a un minuto.

Per questa query, il risultato deve:

  • Includi solo quei record in un determinato intervallo di date.
  • Includi un conteggio di tutti i record precedenti, escluso il record corrente, indipendentemente dall'intervallo di date.

Schema di tabella di base

Activity
======================
Id int Identifier
Address varchar(25)
ActionDate datetime2
Process varchar(50)
-- 7 other columns

Dati di esempio

Id  Address     ActionDate (Time part excluded for simplicity)
===========================
99  000         2017-05-30
98  111         2017-05-30
97  000         2017-05-29
96  000         2017-05-28
95  111         2017-05-19
94  222         2017-05-30

risultati aspettati

Per l'intervallo di date di 2017-05-29a2017-05-30

Id  Address     ActionDate    PriorCount
=========================================
99  000         2017-05-30    2  (3 total, 2 prior to ActionDate)
98  111         2017-05-30    1  (2 total, 1 prior to ActionDate)
94  222         2017-05-30    0  (1 total, 0 prior to ActionDate)
97  000         2017-05-29    1  (3 total, 1 prior to ActionDate)

I record 96 e 95 sono esclusi dal risultato, ma sono inclusi nella PriorCountsottoquery

Query corrente

select 
    *.a
    , ( select count(*) 
        from Activity
        where 
            Activity.Address = a.Address
            and Activity.ActionDate < a.ActionDate
    ) as PriorCount
from Activity a
where a.ActionDate between '2017-05-29' and '2017-05-30'
order by a.ActionDate desc

Indice attuale

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON [dbo].[Activity]
(
    [ActionDate] ASC
)
INCLUDE ([Address]) WITH (
    PAD_INDEX = OFF, 
    STATISTICS_NORECOMPUTE = OFF, 
    SORT_IN_TEMPDB = OFF, 
    DROP_EXISTING = OFF, 
    ONLINE = OFF, 
    ALLOW_ROW_LOCKS = ON, 
    ALLOW_PAGE_LOCKS = ON
)

Domanda

  • Quali strategie potrebbero essere utilizzate per migliorare le prestazioni di questa query?

Modifica 1
In risposta alla domanda su cosa posso modificare sul DB: posso modificare gli indici, ma non la struttura della tabella.

Modifica 2
Ora ho aggiunto un indice di base sulla Addresscolonna, ma questo non sembra migliorare molto. Attualmente sto trovando prestazioni molto migliori con la creazione di una tabella temporanea e l'inserimento dei valori senza PriorCounte quindi l'aggiornamento di ogni riga con i loro conteggi specifici.

Modifica 3
Il problema è stato riscontrato dall'indice Joe Obbish (risposta accettata). Dopo aver aggiunto una nuova nonclustered index [xyz] on [Activity] (Address) include (ActionDate), i tempi delle query sono scesi da un minuto verso l'alto a meno di un secondo senza utilizzare una tabella temporanea (vedi modifica 2).

Risposte:


17

Con la definizione dell'indice che hai per IDX_my_nme, SQL Server sarà in grado di cercare utilizzando la ActionDatecolonna ma non con la Addresscolonna. L'indice contiene tutte le colonne necessarie per coprire la sottoquery ma probabilmente non è molto selettivo per quella sottoquery. Supponiamo che quasi tutti i dati nella tabella abbiano un ActionDatevalore precedente a '2017-05-30'. Una ricerca di ActionDate < '2017-05-30'restituirà quasi tutte le righe dall'indice, che vengono ulteriormente filtrate dopo il recupero della riga dall'indice. Se la tua query restituisce 200 righe, probabilmente eseguirai quasi 200 scansioni dell'indice complete IDX_my_nme, il che significa che leggerai circa 50000 * 200 = 10 milioni di righe dall'indice.

È probabile che la ricerca Addresssia molto più selettiva per la tua sottoquery, anche se non ci hai fornito informazioni statistiche complete sulla query, quindi è un presupposto da parte mia. Tuttavia, supponi di creare un indice su just Addresse che la tua tabella abbia 10k valori univoci per Address. Con il nuovo indice, SQL Server dovrà solo cercare 5 righe dall'indice per ogni esecuzione della sottoquery, quindi leggerai circa 200 * 5 = 1000 righe dall'indice.

Sto testando con SQL Server 2016, quindi potrebbero esserci alcune piccole differenze di sintassi. Di seguito sono riportati alcuni dati di esempio in cui ho formulato ipotesi simili alle precedenti per la distribuzione dei dati:

CREATE TABLE #Activity (
    Id int NOT NULL,
    [Address] varchar(25) NULL,
    ActionDate datetime2 NULL,
    FILLER varchar(100),
    PRIMARY KEY (Id)
);

INSERT INTO #Activity WITH (TABLOCK)
SELECT TOP (50000) -- 50k total rows
x.RN
, x.RN % 10000 -- 10k unique addresses
, DATEADD(DAY, x.RN / 100, '20160201') -- 100 rows per day
, REPLICATE('Z', 100)
FROM
(
    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) x;

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON #Activity
([ActionDate] ASC) INCLUDE ([Address]);

Ho creato il tuo indice come descritto nella domanda. Sto testando questa query che restituisce gli stessi dati di quella nella domanda:

select 
    a.*
    , ( select count(*) 
        from #Activity Activity
        where 
            Activity.[Address] = a.[Address]
            and Activity.ActionDate < a.ActionDate
    ) as PriorCount
from #Activity a
where a.ActionDate between '2017-05-29' and '2017-05-30'
order by a.ActionDate desc;

Ricevo una bobina di indice. Ciò significa a livello di base che Query Optimizer crea al volo un indice temporaneo perché nessuno degli indici esistenti rispetto alla tabella era adatto.

bobina di indice

La query finisce ancora rapidamente per me. Forse non stai ottenendo l'ottimizzazione dello spool dell'indice sul tuo sistema o c'è qualcosa di diverso nella definizione della tabella o nella query. Per scopi didattici posso usare una funzione non documentata OPTION (QUERYRULEOFF BuildSpool)per disabilitare lo spool dell'indice. Ecco come appare il piano:

ricerca indice errata

Non lasciarti ingannare dall'aspetto di una semplice ricerca di indici. SQL Server legge quasi 10 milioni di righe dall'indice:

10 M righe dall'indice

Se eseguirò la query più di una volta, probabilmente non ha senso che Query Optimizer crei un indice ogni volta che viene eseguito. Potrei creare un indice in anticipo che sarebbe più selettivo per questa query:

CREATE NONCLUSTERED INDEX [IDX_my_nme_2] ON #Activity
([Address] ASC) INCLUDE (ActionDate);

Il piano è simile a prima:

ricerca indice

Tuttavia, con il nuovo indice SQL Server legge solo 1000 righe dall'indice. 800 delle righe vengono restituite per essere conteggiate. L'indice potrebbe essere definito come più selettivo, ma potrebbe essere abbastanza buono a seconda della distribuzione dei dati.

buona ricerca

Se non riesci a definire alcun indice aggiuntivo sulla tabella, prenderei in considerazione l'utilizzo delle funzioni della finestra. Il seguente sembra funzionare:

SELECT t.*
FROM
(
    select 
        a.*
        , -1 + ROW_NUMBER() OVER (PARTITION BY [Address] ORDER BY ActionDate) PriorCount
    from #Activity a
) t
where t.ActionDate between '2017-05-29' and '2017-05-30'
order by t.ActionDate desc;

Quella query esegue una singola scansione dei dati ma esegue un ordinamento costoso e calcola la ROW_NUMBER()funzione per ogni riga della tabella, quindi sembra che ci sia del lavoro extra fatto qui:

brutta specie

Tuttavia, se ti piace davvero quel modello di codice, potresti definire un indice per renderlo più efficiente:

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON #Activity
([Address], [ActionDate]) INCLUDE (FILLER);

Questo sposta il tipo verso la fine, che sarà molto meno costoso:

buona specie

Se nulla di tutto ciò aiuta, dovrai aggiungere ulteriori informazioni alla domanda, preferibilmente includendo piani di esecuzione effettivi.


1
La bobina dell'indice che hai trovato era il problema. Una volta aggiunto un nuovo nonclustered index [xyz] on [Activity] (Address) include (ActionDate), i tempi di interrogazione sono scesi da un minuto a meno di un secondo. +10 se potessi. Grazie!
Metro Puffo
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.