Frammentazione dell'indice durante l'elaborazione continua


10

SQL Server 2005

Devo essere in grado di elaborare continuamente circa 350 milioni di record in una tabella di record di 900 milioni. La query che sto usando per selezionare i record da elaborare diventa fortemente frammentata mentre elaboro e ho bisogno di interrompere l'elaborazione per ricostruire l'indice. Modello di dati pseudo e query ...

/**************************************/
CREATE TABLE [Table] 
(
    [PrimaryKeyId] [INT] IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
    [ForeignKeyId] [INT] NOT NULL,
    /* more columns ... */
    [DataType] [CHAR](1) NOT NULL,
    [DataStatus] [DATETIME] NULL,
    [ProcessDate] [DATETIME] NOT NULL,
    [ProcessThreadId] VARCHAR (100) NULL
);

CREATE NONCLUSTERED INDEX [Idx] ON [Table] 
(
    [DataType],
    [DataStatus],
    [ProcessDate],
    [ProcessThreadId]
);
/**************************************/

/**************************************/
WITH cte AS (
    SELECT TOP (@BatchSize) [PrimaryKeyId], [ProcessThreadId]
    FROM [Table] WITH ( ROWLOCK, UPDLOCK, READPAST )
    WHERE [DataType] = 'X'
    AND [DataStatus] IS NULL
    AND [ProcessDate] < DATEADD(m, -2, GETDATE()) -- older than 2 months
    AND [ProcessThreadId] IS NULL
)
UPDATE cte
SET [ProcessThreadId] = @ProcessThreadId;

SELECT * FROM [Table] WITH ( NOLOCK )
WHERE [ProcessThreadId] = @ProcessThreadId;
/**************************************/

Contenuto dei dati ...
Mentre la colonna [DataType] è digitata come CHAR (1), circa il 35% di tutti i record è uguale a "X" mentre il resto è uguale a "A".
Solo dei record in cui [DataType] è uguale a 'X', circa il 10% avrà un valore NOT NULL [DataStatus].

Le colonne [ProcessDate] e [ProcessThreadId] verranno aggiornate per ogni record elaborato.
La colonna [DataType] viene aggiornata ('X' viene cambiata in 'A') circa il 10% delle volte.
La colonna [DataStatus] viene aggiornata meno dell'1% delle volte.

Per ora la mia soluzione è selezionare la chiave primaria di tutti i record da elaborare in una tabella di elaborazione separata. Elimina le chiavi mentre le elaboro in modo che, come frammenti di indice, ho a che fare con meno record.

Tuttavia, questo non si adatta al flusso di lavoro che desidero avere in modo che questi dati vengano elaborati continuamente, senza intervento manuale e significativi tempi di inattività. Prevedo i tempi di inattività su base trimestrale per le faccende domestiche. Ma ora, senza la tabella di elaborazione separata, non riesco a superare nemmeno la metà del set di dati senza che la frammentazione diventi così grave da richiedere l'interruzione e la ricostruzione dell'indice.

Qualche consiglio per l'indicizzazione o un modello di dati diverso? C'è un modello che devo ricercare?
Ho il pieno controllo del modello di dati e del software di processo, quindi nulla è fuori dal tavolo.


Un pensiero anche: il tuo indice sembra essere nell'ordine sbagliato: dovrebbe essere più selettivo a meno selettivo. Quindi ProcessThreadId, ProcessDate, DataStatus, DataType forse?
gbn,

L'abbiamo pubblicizzato nella nostra chat. Ottima domanda chat.stackexchange.com/rooms/179/the-heap
gbn

Ho aggiornato la query per essere una rappresentazione più accurata della selezione. Ho più thread simultanei che eseguono questo. Ho notato la raccomandazione di ordine selettivo. Grazie.
Chris Gallucci,

@ChrisGallucci Vieni a chattare se puoi ...
JNK,

Risposte:


4

Quello che stai facendo è usare una tabella come coda. Il tuo aggiornamento è il metodo dequeue. Ma l'indice cluster sulla tabella non è una buona scelta per una coda. L'uso delle tabelle come code in realtà impone requisiti piuttosto rigorosi sulla progettazione della tabella. L'indice cluster deve essere l'ordine di dequeue, in questo caso probabilmente ([DataType], [DataStatus], [ProcessDate]). È possibile implementare la chiave primaria come vincolo non cluster . Rilasciare l'indice non cluster Idx, poiché la chiave cluster assume il suo ruolo.

Un altro pezzo importante del puzzle è quello di mantenere costanti le dimensioni della riga durante l'elaborazione. Hai dichiarato ProcessThreadIdcome un VARCHAR(100)che implica che la riga cresce e si restringe mentre viene "elaborata" perché il valore del campo cambia da NULL a non null. Questo modello di ingrandimento sulla riga provoca suddivisioni e frammentazione della pagina. Non riesco a immaginare un ID thread che sia "VARCHAR (100)". Utilizzare un tipo di lunghezza fissa, forse un INT.

Come nota a margine, non è necessario accodare in due passaggi (AGGIORNAMENTO seguito da SELEZIONA). È possibile utilizzare la clausola OUTPUT, come spiegato nell'articolo collegato sopra:

/**************************************/
CREATE TABLE [Table] 
(
    [PrimaryKeyId] [INT] IDENTITY(1,1) NOT NULL PRIMARY KEY NONCLUSTERED,
    [ForeignKeyId] [INT] NOT NULL,
    /* more columns ... */
    [DataType] [CHAR](1) NOT NULL,
    [DataStatus] [DATETIME] NULL,
    [ProcessDate] [DATETIME] NOT NULL,
    [ProcessThreadId] INT NULL
);

CREATE CLUSTERED INDEX [Cdx] ON [Table] 
(
    [DataType],
    [DataStatus],
    [ProcessDate]
);
/**************************************/

declare @BatchSize int, @ProcessThreadId int;

/**************************************/
WITH cte AS (
    SELECT TOP (@BatchSize) [PrimaryKeyId], [ProcessThreadId] , ... more columns 
    FROM [Table] WITH ( ROWLOCK, UPDLOCK, READPAST )
    WHERE [DataType] = 'X'
    AND [DataStatus] IS NULL
    AND [ProcessDate] < DATEADD(m, -2, GETDATE()) -- older than 2 months
    AND [ProcessThreadId] IS NULL
)
UPDATE cte
SET [ProcessThreadId] = @ProcessThreadId
OUTPUT DELETED.[PrimaryKeyId] , ... more columns ;
/**************************************/

Inoltre, prenderei in considerazione lo spostamento degli elementi elaborati con successo in una tabella di archiviazione diversa. Volete che le vostre tabelle delle code siano posizionate vicino allo zero, non volete che crescano poiché conservano la "cronologia" delle vecchie voci non necessarie. Puoi anche considerare il partizionamento [ProcessDate]come un'alternativa (cioè una partizione attiva corrente che funge da coda e archivia le voci con NULL ProcessDate e un'altra partizione per tutto ciò che non è nullo. O più partizioni per non null se vuoi implementare in modo efficiente elimina (cambia) per i dati che hanno superato il periodo di conservazione obbligatorio. Se le cose si surriscaldano, puoi partizionare inoltre[DataType] se ha abbastanza selettività, ma quella progettazione sarebbe davvero complicata in quanto richiede il partizionamento per colonna calcolata persistente (una colonna composita che incolla [DataType] e [ProcessingDate]).


3

Vorrei iniziare spostando i campi ProcessDatee Processthreadidsu un altro tavolo.

In questo momento, anche ogni riga selezionata da questo indice piuttosto ampio deve essere aggiornata.

Se si spostano questi due campi in un'altra tabella, il volume di aggiornamento sulla tabella principale viene ridotto del 90%, il che dovrebbe occuparsi della maggior parte della frammentazione.

Avrai comunque frammentazione nella NUOVA tabella, ma sarà più facile gestirla su una tabella più ristretta con molti meno dati.


Questo e la divisione fisica dei dati in base a [DataType] dovrebbe portarmi dove devo essere. Sono attualmente nella fase di progettazione (riprogettazione in realtà) di questo, quindi ci vorrà del tempo prima che abbia la possibilità di testare questo cambiamento.
Chris Gallucci,
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.