È possibile aumentare le prestazioni della query su una tabella ristretta con milioni di righe?


14

Ho una query che attualmente richiede in media 2500 ms per il completamento. Il mio tavolo è molto stretto, ma ci sono 44 milioni di righe. Quali opzioni ho per migliorare le prestazioni o è buono come può?

The Query

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31'; 

La tavola

CREATE TABLE [dbo].[Heartbeats](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [DeviceID] [int] NOT NULL,
    [IsPUp] [bit] NOT NULL,
    [IsWebUp] [bit] NOT NULL,
    [IsPingUp] [bit] NOT NULL,
    [DateEntered] [datetime] NOT NULL,
 CONSTRAINT [PK_Heartbeats] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

L'indice

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

L'aggiunta di ulteriori indici sarebbe di aiuto? In tal caso, come sarebbero? Le prestazioni attuali sono accettabili, poiché la query viene eseguita solo occasionalmente, ma mi chiedo come esercizio di apprendimento, c'è qualcosa che posso fare per renderlo più veloce?

AGGIORNARE

Quando cambio la query per utilizzare un suggerimento sull'indice di forza, la query viene eseguita in 50ms:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats] WITH(INDEX(CommonQueryIndex))
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' 

L'aggiunta di una clausola DeviceID correttamente selettiva colpisce anche l'intervallo di 50ms:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' AND DeviceID = 4;

Se aggiungo ORDER BY [DateEntered], [DeviceID]alla query originale, mi trovo nell'intervallo di 50 ms:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' 
ORDER BY [DateEntered], [DeviceID];

Tutti usano l'indice che mi aspettavo (CommonQueryIndex), quindi, suppongo che la mia domanda sia ora, c'è un modo per forzare questo indice ad essere utilizzato su query come questa? O la dimensione del mio tavolo sta gettando troppo l'ottimizzatore e devo solo usare un ORDER BYo un suggerimento?


Suppongo che potresti aggiungere un altro indice non cluster su "DateEntered" che aumenterebbe ulteriormente le prestazioni
Praveen,

@Praveen Sarebbe sostanzialmente lo stesso del mio indice esistente? Devo fare qualcosa di speciale poiché ci saranno due indici sullo stesso campo?
Nate,

@Nate, dato che la tabella si chiama heartbeat e ci sono 44 milioni di record coinvolti, suppongo che tu abbia degli inserti pesanti su questa tabella? Con l'indicizzazione, puoi solo aggiungere un indice di copertura per accelerare. Ma come hai detto, usi questa query solo occasionalmente, sconsiglio vivamente che se fai inserti pesanti. In pratica raddoppia il carico di inserimento. Stai eseguendo un'edizione enterprise?
Edward Dortland,

Ho notato che hai deviceID nel tuo indice NC. È possibile includerlo nella clausola where? E ciò porterebbe il risultato al di sotto della soglia? <35k record (senza la prima clausola 1000).
Edward Dortland,

1
ultima domanda, inserisci sempre in ordine di data inserito? Oppure possono essere fuori servizio poiché i dispositivi potrebbero inserirsi in modo asincrono l'uno dall'altro. È possibile provare a modificare l'indice cluster nella colonna DateEntered. Le pagine di congedo dell'indice cluster sono ora 445 pagine. Ciò raddoppierebbe se passassi da un int a un datetime. Ma in questo caso, potrebbe non essere male.
Edward Dortland,

Risposte:


13

Perché l'ottimizzatore non va per il tuo primo indice:

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

È una questione di selettività della colonna [DateEntered].

Ci hai detto che il tuo tavolo ha 44 milioni di righe. la dimensione della riga è:

4 byte, per l'ID, 4 byte per l'ID dispositivo, 8 byte per la data e 1 byte per le colonne a 4 bit. sono 17 byte + 7 byte di overhead per (tag, Null bitmap, var col offset, col count) per un totale di 24 byte per riga.

Ciò si tradurrebbe approssimativamente in 140k pagine. Per memorizzare quei 44 milioni di righe.

Ora l'ottimizzatore può fare due cose:

  1. Potrebbe eseguire la scansione della tabella (scansione indice cluster)
  2. O potrebbe usare il tuo indice. Per ogni riga dell'indice, dovrebbe quindi effettuare una ricerca nei segnalibri nell'indice cluster.

Ora a un certo punto diventa solo più costoso eseguire tutte queste singole ricerche nell'indice cluster per ogni voce di indice trovata nell'indice non cluster. La soglia per questo è generalmente il conteggio totale delle ricerche dovrebbe superare il 25% e il 33% del conteggio totale delle pagine della tabella.

Quindi in questo caso: 140k / 25% = 35000 righe 140k / 33% = 46666 righe.

(@RBarryYoung, 35k è lo 0,08% delle righe totali e 46666 è lo 0,10%, quindi penso che sia qui la confusione)

Quindi, se la tua clausola where risulterà in un punto compreso tra 35000 e 46666 righe (questo è sotto la clausola in alto!) È molto probabile che il tuo non cluster non verrà utilizzato e che verrà utilizzata la scansione dell'indice cluster.

Gli unici due modi per cambiare questo sono:

  1. Rendi la tua clausola where più selettiva. (se possibile)
  2. Rilascia il * e seleziona solo alcune colonne in modo da poter utilizzare un indice di copertura.

ora sei sicuro di poter creare un indice di copertura anche quando usi select *. Hoever che crea un enorme sovraccarico per i tuoi inserti / aggiornamenti / eliminazioni. Dovremmo sapere di più sul tuo carico di lavoro (leggi vs scrivi) per assicurarci che sia la soluzione migliore.

Passare da datetime a smalldatetime è una riduzione delle dimensioni del 16% sull'indice cluster e una riduzione delle dimensioni del 24% sull'indice non cluster.


la soglia di scansione è normalmente molto più bassa di quella (10% o addirittura inferiore), tuttavia poiché l'intervallo è di un solo giorno da oltre un anno fa non dovrebbe raggiungere nemmeno quella soglia. E una scansione dell'indice cluster non è scontata, poiché è stato aggiunto un indice di copertura. Poiché tale indice rende la clausola WHERE in grado di SARG, dovrebbe essere preferito.
RBarryYoung,

@RBarryYoung Stavo cercando di spiegare perché l'indice non cluster su [EnteredDate], [DeviceID] non fosse utilizzato in primo luogo. Per quanto riguarda la soglia, penso che siamo entrambi d'accordo, sto parlando solo dal punto di vista della pagina. Modificherò la mia risposta per renderlo più chiaro.
Edward Dortland,

Modificata la risposta per rendere più chiaro ciò a cui stavo rispondendo. Non riesco a spiegare perché l'indice di copertura suggerito da @RBarryYoung non sia utilizzato. L'ho testato su un milione di righe proprio qui, e l'ottimizzatore lo utilizzava l'indice di copertura.
Edward Dortland,

Grazie per una risposta molto completa, ha molto senso. Per quanto riguarda il carico di lavoro, la tabella ha 150-300 inserti per un periodo di 5 minuti e alcune letture al giorno a fini di reportistica.
Nate,

Il sovraccarico per l'indice di copertura non è davvero significativo dato che è una tabella stretta e la "copertura" è solo un'aggiunta all'indice preesistente che già includeva la maggior parte della riga.
RBarryYoung,

8

C'è un motivo particolare per cui il tuo PK è raggruppato? Molte persone lo fanno perché di default in questo modo, o pensano che i PK debbano essere raggruppati. No Gli indici cluster sono in genere i migliori per le query di intervallo (come questo) o sulla chiave esterna di una tabella figlio.

Un effetto di un indice di clustering è che raggruppa tutti i dati insieme perché i dati sono memorizzati sui nodi foglia dell'albero b del cluster. Quindi, supponendo che non si stia chiedendo "troppo ampio" di un intervallo, l'ottimizzatore saprà esattamente quale parte dell'albero b contiene i dati e non dovrà trovare un identificatore di riga e quindi passare a dove i dati è (come succede quando si ha a che fare con un indice NC). Cosa è "troppo ampio" di un intervallo? Un esempio ridicolo potrebbe essere la richiesta di 11 mesi di dati da una tabella che ha solo un record di un anno. L'estrazione di un giorno di dati non dovrebbe essere un problema, supponendo che le tue statistiche siano aggiornate. (Tuttavia, l'ottimizzatore potrebbe avere problemi se stai cercando i dati di ieri e non hai aggiornato le statistiche per tre giorni.)

Poiché stai eseguendo una query "SELECT *", il motore dovrà restituire tutte le colonne nella tabella (anche se qualcuno ne aggiunge una nuova che la tua app non ha bisogno in quel momento), quindi un indice di copertura o un indice con le colonne incluse non sarà di grande aiuto, se non del tutto. (Se si include ogni colonna della tabella in un indice, si sta facendo qualcosa di sbagliato.) L'ottimizzatore probabilmente ignorerà quegli indici NC.

Quindi che si fa?

Il mio suggerimento sarebbe di eliminare l'indice NC, modificare il PK cluster in non cluster e creare un indice cluster su [DateEntered]. Semplice è meglio, fino a prova contraria.


Supponendo che le righe vengano inserite in ordine crescente, questa è la risposta più semplice, ma l'inserimento in ordine non lineare provocherà la frammentazione.
Kirk Broadhurst,

L'aggiunta di dati a qualsiasi struttura b-tree farà perdere l'equilibrio. Anche se si aggiungono righe nell'ordine del cluster, gli indici perderanno l'equilibrio. La reindicizzazione delle tabelle rimuove la frammentazione e qualsiasi DBA ti dirà che le tabelle devono essere reindicizzate dopo che "una quantità" di dati è stata aggiunta a una tabella. (La definizione di "abbastanza" potrebbe essere discussa, o "quando" potrebbe essere una discussione.) Non vedo nulla nella domanda che dice che la reindicizzazione non può essere fatta per qualche motivo.
darin strait

4

Finché hai quel "*" lì dentro, l'unica cosa che potrei immaginare che farebbe molta differenza sarebbe cambiare la definizione del tuo indice in questo:

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)INCLUDE (ID, IsWebUp, IsPingUp, IsPUp)
 WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

Come ho notato nei commenti, dovrebbe usare quell'indice, ma in caso contrario puoi convincerlo con un ORDER BY o un suggerimento sull'indice.


Ho appena provato questo e sono ancora praticamente nello stesso punto, 2500ms attendono la risposta del server e 10ms il tempo di elaborazione del client.
Nate,

Pubblica il piano di query.
RBarryYoung,

Sembra che stia usando l'indice cluster. (SELEZIONA costo: 0% <- Costo massimo: 20% <- Scansione indice cluster Costo PK_Heartbeats: 80%)
Nate

Sì, non è vero, qualcosa che sta gettando via le statistiche / l'ottimizzatore. Aggiungi un suggerimento per forzarlo a utilizzare il nuovo indice.
RBarryYoung,

@Max Vernon: Forse, ma avrebbe dovuto essere contrassegnato sul piano di query.
RBarryYoung,

3

Lo guarderei un po 'diversamente.

  • Sì, lo so che è un vecchio thread ma sono incuriosito.

Dump della colonna data / ora - la cambierei in un int. Avere una tabella di ricerca o fare una conversione per la tua data.

Dump dell'indice cluster: lasciarlo come un heap e creare un indice non cluster sulla nuova colonna INT che rappresenta la data. cioè oggi sarebbe 20121015. Quell'ordine è importante. A seconda della frequenza con cui carichi la tabella, osserva come creare quell'indice in ordine DESC. Il costo di manutenzione sarà più alto e vorrai introdurre un fattore di riempimento o partizionamento. Il partizionamento contribuirebbe anche a ridurre il tempo di esecuzione.

Infine, se è possibile utilizzare SQL 2012, provare a utilizzare SEQUENCE: supererà l'identità () per gli inserti.


Soluzione interessante. Sebbene non sia ovvio dalla mia domanda, la parte dell'ora del DateTime è molto importante. Generalmente eseguo una query in base alla data, per rivedere orari specifici durante quel periodo. Come regoleresti questa soluzione per tenerne conto?
Nate,

In tal caso, mantieni la colonna datetime, aggiungi la colonna int per date (poiché il tuo intervallo si basa sull'elemento date e non sull'elemento time). Si potrebbe anche prendere in considerazione l'utilizzo del tipo di dati TIME e quindi, dividere efficacemente l'ora a parte la data. In questo modo, il tuo footprint dei dati è più piccolo e hai ancora l'elemento Time della colonna.
Jeremy Lowell,

1
Non sono sicuro del motivo per cui mi sono perso in precedenza, ma uso anche la compressione delle righe sull'indice cluster e anche sull'indice non cluster. Ho appena fatto un rapido test con la tua tabella ed ecco cosa ho trovato: ho creato un set di dati (5,8 milioni di righe) nella tabella sopra definita. Ho compresso (riga) l'indice cluster e non cluster. le letture logiche, basate sulla query esatta, sono diminuite da 2.074 a 1.433. Si tratta di una riduzione significativa e sono fiducioso che da solo ti potrebbe aiutare - ed è a rischio molto basso.
Jeremy Lowell,
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.