Fragmentation de l'index lors du traitement en continu

10

SQL Server 2005

J'ai besoin de pouvoir traiter en continu environ 350 millions d'enregistrements dans une table d'enregistrement de 900 millions. La requête que j'utilise pour sélectionner les enregistrements à traiter devient très fragmentée au fur et à mesure que je traite et j'ai besoin d'arrêter le traitement pour reconstruire l'index. Pseudo modèle de données et requête ...

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

Contenu des données ...
Alors que la colonne [DataType] est tapée en tant que CHAR (1), environ 35% de tous les enregistrements sont égaux à «X», le reste égal à «A».
Seuls les enregistrements où [DataType] est égal à «X», environ 10% auront une valeur NOT NULL [DataStatus].

Les colonnes [ProcessDate] et [ProcessThreadId] seront mises à jour pour chaque enregistrement traité.
La colonne [DataType] est mise à jour («X» est remplacé par «A») environ 10% du temps.
La colonne [DataStatus] est mise à jour moins de 1% du temps.

Pour l'instant, ma solution consiste à sélectionner la clé primaire de tous les enregistrements à traiter dans une table de traitement distincte. Je supprime les clés au fur et à mesure que je les traite afin qu'en tant que fragments d'index, je traite moins d'enregistrements.

Cependant, cela ne correspond pas au flux de travail que je souhaite avoir afin que ces données soient traitées en continu, sans intervention manuelle et temps d'arrêt important. Je prévois des temps d'arrêt tous les trimestres pour les tâches ménagères. Mais maintenant, sans la table de traitement séparée, je ne peux pas passer à travers le traitement de la moitié de l'ensemble de données sans que la fragmentation ne devienne si mauvaise qu'elle nécessite l'arrêt et la reconstruction de l'index.

Des recommandations pour l'indexation ou un modèle de données différent? Existe-t-il un modèle que je dois rechercher?
J'ai un contrôle total sur le modèle de données et le logiciel de processus, donc rien n'est hors de table.

Chris Gallucci
la source
Une pensée aussi: votre index semble être dans le mauvais ordre: il doit être le plus sélectif au moins sélectif. Donc ProcessThreadId, ProcessDate, DataStatus, DataType peut-être?
gbn
Nous l'avons annoncé dans notre chat. Très bonne question. chat.stackexchange.com/rooms/179/the-heap
GBN
J'ai mis à jour la requête pour être une représentation plus précise de la sélection. J'ai plusieurs threads simultanés exécutant cela. J'ai noté la recommandation de commande sélective. Merci.
Chris Gallucci
@ChrisGallucci Venez discuter si vous le pouvez ...
JNK

Réponses:

4

Vous utilisez une table comme file d'attente. Votre mise à jour est la méthode de retrait. Mais l'index cluster sur la table est un mauvais choix pour une file d'attente. L'utilisation de tables comme files d'attente impose en fait des exigences assez strictes sur la conception des tables. Votre index cluster doit être l'ordre de retrait, dans ce cas probable ([DataType], [DataStatus], [ProcessDate]). Vous pouvez implémenter la clé primaire en tant que contrainte non clusterisée . Supprimez l'index non clusterisé Idx, car la clé clusterisée joue son rôle.

Une autre pièce importante du puzzle consiste à maintenir la taille de la ligne constante pendant le traitement. Vous avez déclaré le en ProcessThreadIdtant que, VARCHAR(100)ce qui implique que la ligne grandit et se rétrécit au fur et à mesure qu'elle est «traitée», car la valeur du champ passe de NULL à non nulle. Ce modèle d'agrandissement et de réduction sur la ligne provoque des fractionnements de page et une fragmentation. Je ne peux pas imaginer un ID de thread qui soit 'VARCHAR (100)'. Utilisez un type de longueur fixe, peut-être un INT.

En guise de remarque, vous n'avez pas besoin de retirer la file d'attente en deux étapes (UPDATE suivi de SELECT). Vous pouvez utiliser la clause OUTPUT, comme expliqué dans l'article lié ci-dessus:

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

De plus, j'envisagerais de déplacer des éléments traités avec succès dans une autre table d'archive. Vous voulez que vos tables de files d'attente planent près de la taille zéro, vous ne voulez pas qu'elles grandissent car elles conservent «l'historique» des anciennes entrées inutiles. Vous pouvez également envisager le partitionnement par [ProcessDate]comme alternative (c'est-à-dire une partition active actuelle qui fait office de file d'attente et stocke les entrées avec NULL ProcessDate, et une autre partition pour tout ce qui n'est pas nul. Ou plusieurs partitions pour non-nul si vous voulez implémenter un système efficace supprime (basculer) pour les données qui ont passé la période de rétention obligatoire. Si les choses deviennent chaudes, vous pouvez partitionner en plus en[DataType] s'il a suffisamment de sélectivité, mais cette conception serait vraiment compliquée car elle nécessite un partitionnement par colonne calculée persistante (une colonne composite qui colle ensemble [DataType] et [ProcessingDate]).

Remus Rusanu
la source
3

Je commencerais par déplacer les champs ProcessDateet Processthreadidvers une autre table.

À l'heure actuelle, chaque ligne que vous sélectionnez dans cet index assez large doit également être mise à jour.

Si vous déplacez ces deux champs vers une autre table, votre volume de mise à jour sur la table principale est réduit de 90%, ce qui devrait prendre en charge la majeure partie de la fragmentation.

Vous aurez toujours une fragmentation dans la nouvelle table, mais ce sera plus facile à gérer sur une table plus étroite avec beaucoup moins de données.

JNK
la source
Cela et le fractionnement physique des données sur la base de [DataType] devrait m'amener là où je dois être. Je suis actuellement dans la phase de conception (re-conception en fait) de cela, il faudra donc un certain temps avant que je puisse tester ce changement.
Chris Gallucci