Fragmentacja indeksu podczas ciągłego przetwarzania

10

SQL Server 2005

Muszę być w stanie nieprzerwanie przetwarzać około 350 milionów rekordów w tabeli rekordów 900 milionów. Kwerenda, której używam do wybrania rekordów do przetworzenia, ulega znacznej fragmentacji podczas przetwarzania i muszę przerwać przetwarzanie w celu odbudowania indeksu. Pseudo model danych i zapytanie ...

/**************************************/
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;
/**************************************/

Treść danych ...
Podczas gdy kolumna [DataType] jest wpisana jako CHAR (1), około 35% wszystkich rekordów jest równych „X”, a reszta równa się „A”.
Tylko z rekordów, w których [DataType] równa się „X”, około 10% będzie miało wartość NOT NULL [DataStatus].

Kolumny [ProcessDate] i [ProcessThreadId] będą aktualizowane dla każdego przetwarzanego rekordu.
Kolumna [DataType] jest aktualizowana („X” zmienia się na „A”) przez około 10% czasu.
Kolumna [DataStatus] jest aktualizowana mniej niż 1% czasu.

Na razie moim rozwiązaniem jest wybranie klucza podstawowego wszystkich rekordów do przetworzenia na osobną tabelę przetwarzania. Usuwam klucze podczas ich przetwarzania, tak że jako fragmenty indeksu mam do czynienia z mniejszą liczbą rekordów.

Jednak to nie pasuje do przepływu pracy, który chcę mieć, aby te dane były przetwarzane w sposób ciągły, bez ręcznej interwencji i znacznego przestoju. Przewiduję przestoje co kwartał w przypadku prac domowych. Ale teraz, bez osobnej tabeli przetwarzania, nie mogę przejść nawet przez przetwarzanie nawet połowy zbioru danych, ponieważ fragmentacja nie jest tak poważna, że ​​konieczne jest zatrzymanie i przebudowanie indeksu.

Wszelkie zalecenia dotyczące indeksowania lub innego modelu danych? Czy jest jakiś wzór, który muszę zbadać?
Mam pełną kontrolę nad modelem danych i oprogramowaniem procesowym, więc nic nie stoi na przeszkodzie.

Chris Gallucci
źródło
Jedna myśl także: twój indeks wydaje się być w niewłaściwej kolejności: powinien być najbardziej selektywny do najmniej selektywnego. Może więc ProcessThreadId, ProcessDate, DataStatus, DataType?
gbn
Reklamowaliśmy to na naszym czacie. Bardzo dobre pytanie. chat.stackexchange.com/rooms/179/the-heap
gbn
Zaktualizowałem zapytanie, aby było dokładniejszym odwzorowaniem zaznaczenia. Uruchomiłem wiele wątków jednocześnie. Zanotowałem zalecenie dotyczące selektywnego zamówienia. Dzięki.
Chris Gallucci,
@ChrisGallucci Przyjdź porozmawiać, jeśli możesz ...
JNK

Odpowiedzi:

4

To, co robisz, to używanie tabeli jako kolejki. Twoja aktualizacja to metoda dequeue. Ale indeks klastrowany w tabeli jest złym wyborem dla kolejki. Używanie tabel jako kolejek w rzeczywistości nakłada dość rygorystyczne wymagania na projekt tabeli. W tym przypadku indeks klastrowany musi być kolejnością odwrotną do kolejki ([DataType], [DataStatus], [ProcessDate]). Klucz główny można zaimplementować jako ograniczenie nieklastrowane . Upuść indeks nieklastrowany Idx, ponieważ klucz klastrowany przyjmuje swoją rolę.

Innym ważnym elementem układanki jest utrzymywanie stałego rozmiaru rzędu podczas przetwarzania. Zadeklarowałeś ProcessThreadIdjako, VARCHAR(100)co oznacza, że ​​rząd rośnie i kurczy się, ponieważ jest „przetwarzany”, ponieważ wartość pola zmienia się z NULL na inną niż null. Ten rosnący i kurczący się wzór w wierszu powoduje podział strony i fragmentację. Nie mogę sobie wyobrazić identyfikatora wątku, który jest „VARCHAR (100)”. Użyj typu o stałej długości, być może INT.

Na marginesie, nie trzeba usuwać z kolejki w dwóch krokach (AKTUALIZACJA, a następnie WYBIERZ). Możesz użyć klauzuli OUTPUT, jak wyjaśniono w powyższym artykule:

/**************************************/
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 ;
/**************************************/

Ponadto rozważyłbym przeniesienie pomyślnie przetworzonych elementów do innej, zarchiwizowanej tabeli. Chcesz, aby tabele kolejek unosiły się w pobliżu zera, nie chcesz, aby rosły, ponieważ zachowują „historię” niepotrzebnych starych wpisów. Możesz również rozważyć podział na partycje [ProcessDate]jako alternatywę (tj. Jedną bieżącą aktywną partycję, która działa jako kolejka i przechowuje wpisy z NULL ProcessDate, oraz inną partycję na wszystko inne niż null. Lub wiele partycji na wartość inną niż null, jeśli chcesz zaimplementować wydajne usuwa (wyłącza) dane, które minęły obowiązkowy okres przechowywania. Jeśli robi się gorąco, możesz dodatkowo podzielić na partycje[DataType] jeśli ma wystarczającą selektywność, ale ten projekt byłby naprawdę skomplikowany, ponieważ wymaga partycjonowania przez utrwaloną kolumnę obliczeniową (kolumna złożona, która łączy się ze sobą [DataType] i [ProcessingDate]).

Remus Rusanu
źródło
3

Zacznę od przeniesienia pól ProcessDatei Processthreadiddo innego stołu.

W tej chwili każdy wiersz wybrany z tego dość szerokiego indeksu musi również zostać zaktualizowany.

Jeśli przeniesiesz te dwa pola do innej tabeli, wolumin aktualizacji na głównej tabeli zostanie obcięty o 90%, co powinno zająć się większą fragmentacją.

Nadal będziesz mieć fragmentację w tabeli NEW, ale łatwiej będzie zarządzać w węższej tabeli z dużo mniejszą ilością danych.

JNK
źródło
To i fizyczne podzielenie danych w oparciu o [DataType] powinno doprowadzić mnie tam, gdzie powinienem być. Obecnie jestem w fazie projektowania (właściwie przeprojektowania), więc minie trochę czasu, zanim będę mógł przetestować tę zmianę.
Chris Gallucci