Indexfragmentierung bei kontinuierlicher Verarbeitung

10

SQL Server 2005

Ich muss in der Lage sein, ungefähr 350 Millionen Datensätze in einer 900 Millionen Datensatztabelle kontinuierlich zu verarbeiten. Die Abfrage, mit der ich die zu verarbeitenden Datensätze auswähle, wird während der Verarbeitung stark fragmentiert, und ich muss die Verarbeitung stoppen, um den Index neu zu erstellen. Pseudodatenmodell & Abfrage ...

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

Dateninhalt ...
Während die Spalte [Datentyp] als CHAR (1) eingegeben wird, entsprechen ungefähr 35% aller Datensätze 'X', während der Rest 'A' entspricht.
Von nur den Datensätzen, bei denen [DataType] gleich 'X' ist, haben etwa 10% den Wert NOT NULL [DataStatus].

Die Spalten [ProcessDate] und [ProcessThreadId] werden für jeden verarbeiteten Datensatz aktualisiert.
Die Spalte [Datentyp] wird in etwa 10% der Fälle aktualisiert ('X' wird in 'A' geändert).
Die Spalte [DataStatus] wird in weniger als 1% der Fälle aktualisiert.

Im Moment besteht meine Lösung darin, den Primärschlüssel aller zu verarbeitenden Datensätze in einer separaten Verarbeitungstabelle auszuwählen. Ich lösche die Schlüssel, während ich sie verarbeite, so dass ich als Indexfragmente weniger Datensätze habe.

Dies passt jedoch nicht zu dem Workflow, den ich haben möchte, sodass diese Daten kontinuierlich verarbeitet werden, ohne manuelle Eingriffe und erhebliche Ausfallzeiten. Ich rechne vierteljährlich mit Ausfallzeiten für die Hausarbeit. Aber jetzt, ohne die separate Verarbeitungstabelle, kann ich nicht einmal die Hälfte des Datensatzes verarbeiten, ohne dass die Fragmentierung so schlecht wird, dass der Index gestoppt und neu erstellt werden muss.

Empfehlungen für die Indizierung oder ein anderes Datenmodell? Gibt es ein Muster, das ich erforschen muss?
Ich habe die volle Kontrolle über das Datenmodell und die Prozesssoftware, sodass nichts vom Tisch ist.

Chris Gallucci
quelle
Ein Gedanke auch: Ihr Index scheint in der falschen Reihenfolge zu sein: Er sollte am selektivsten bis am wenigsten selektiv sein. Also ProcessThreadId, ProcessDate, DataStatus, DataType vielleicht?
Gbn
Wir haben es in unserem Chat beworben. Sehr gute Frage. chat.stackexchange.com/rooms/179/the-heap
gbn
Ich habe die Abfrage aktualisiert, um eine genauere Darstellung der Auswahl zu erhalten. Ich habe mehrere gleichzeitige Threads, die dies ausführen. Ich habe die Empfehlung zur selektiven Bestellung zur Kenntnis genommen. Vielen Dank.
Chris Gallucci
@ChrisGallucci Kommen Sie zum Chatten, wenn Sie können ...
JNK

Antworten:

4

Sie verwenden eine Tabelle als Warteschlange. Ihr Update ist die Dequeue-Methode. Der Clustered-Index für die Tabelle ist jedoch eine schlechte Wahl für eine Warteschlange. Die Verwendung von Tabellen als Warteschlangen stellt tatsächlich sehr hohe Anforderungen an das Tabellendesign. Ihr Clustered-Index muss in diesem Fall wahrscheinlich die Reihenfolge der Warteschlangen sein ([DataType], [DataStatus], [ProcessDate]). Sie können den Primärschlüssel als nicht gruppierte Einschränkung implementieren . Löschen Sie den nicht gruppierten Index Idx, da der gruppierte Schlüssel seine Rolle übernimmt.

Ein weiteres wichtiges Puzzleteil ist es, die Zeilengröße während der Verarbeitung konstant zu halten. Sie haben das ProcessThreadIdals a deklariert, VARCHAR(100)was bedeutet, dass die Zeile während der Verarbeitung wächst und schrumpft, da sich der Feldwert von NULL auf ungleich Null ändert. Dieses Vergrößerungs- und Verkleinerungsmuster in der Zeile führt zu Seitenteilen und Fragmentierung. Ich kann mir unmöglich eine Thread-ID vorstellen, die 'VARCHAR (100)' ist. Verwenden Sie einen Typ mit fester Länge, z INT.

Als Randnotiz müssen Sie nicht in zwei Schritten aus der Warteschlange entfernen (UPDATE gefolgt von SELECT). Sie können die OUTPUT-Klausel verwenden, wie im oben verlinkten Artikel erläutert:

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

Außerdem würde ich in Betracht ziehen, erfolgreich verarbeitete Elemente in eine andere Archivtabelle zu verschieben. Sie möchten, dass Ihre Warteschlangentabellen nahe der Größe Null schweben. Sie möchten nicht, dass sie wachsen, da sie den Verlauf nicht benötigter alter Einträge beibehalten. Sie können auch eine Partitionierung nach [ProcessDate]als Alternative in Betracht ziehen (dh eine aktuell aktive Partition, die als Warteschlange fungiert und Einträge mit NULL ProcessDate speichert, und eine andere Partition für alles, was nicht null ist. Oder mehrere Partitionen für nicht null, wenn Sie effizient implementieren möchten löscht (ausschaltet) Daten, die die vorgeschriebene Aufbewahrungsfrist überschritten haben. Wenn es heiß hergeht, können Sie zusätzlich durch partitionieren[DataType] Wenn es genügend Selektivität hat, aber dieses Design wirklich kompliziert wäre, da es eine Partitionierung durch eine persistierte berechnete Spalte erfordert (eine zusammengesetzte Spalte, die [DataType] und [ProcessingDate] zusammenklebt).

Remus Rusanu
quelle
3

Ich würde damit beginnen, die Felder ProcessDateund Processthreadidin eine andere Tabelle zu verschieben.

Im Moment muss jede Zeile, die Sie aus diesem ziemlich breiten Index auswählen, ebenfalls aktualisiert werden.

Wenn Sie diese beiden Felder in eine andere Tabelle verschieben, wird Ihr Aktualisierungsvolumen in der Haupttabelle um 90% reduziert, wodurch der größte Teil der Fragmentierung behoben werden sollte.

Die NEUE Tabelle ist weiterhin fragmentiert, die Verwaltung in einer schmaleren Tabelle mit viel weniger Daten ist jedoch einfacher.

JNK
quelle
Dies und die physische Aufteilung der Daten basierend auf [DataType] sollten mich dahin bringen, wo ich sein muss. Ich bin derzeit in der Entwurfsphase (eigentlich Neugestaltung), daher wird es einige Zeit dauern, bis ich die Gelegenheit bekomme, diese Änderung zu testen.
Chris Gallucci