Fragmentação de índice enquanto processa continuamente

10

SQL Server 2005

Eu preciso ser capaz de processar continuamente cerca de 350 milhões de registros em uma tabela de 900 milhões. A consulta que estou usando para selecionar os registros a serem processados ​​se torna muito fragmentada à medida que eu processo e preciso interromper o processamento para reconstruir o índice. Modelo e consulta de pseudo dados ...

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

Conteúdo dos dados ...
Enquanto a coluna [DataType] é digitada como CHAR (1), cerca de 35% de todos os registros são iguais a 'X' e o restante é igual a 'A'.
Apenas dos registros em que [DataType] é igual a 'X', cerca de 10% terão um valor NOT NULL [DataStatus].

As colunas [ProcessDate] e [ProcessThreadId] serão atualizadas para cada registro processado.
A coluna [DataType] é atualizada ('X' é alterado para 'A') cerca de 10% do tempo.
A coluna [DataStatus] é atualizada menos de 1% do tempo.

Por enquanto, minha solução é selecionar a chave primária de todos os registros para processar em uma tabela de processamento separada. Excluo as chaves ao processá-las para que, como fragmentos do índice, eu esteja lidando com menos registros.

No entanto, isso não se encaixa no fluxo de trabalho que desejo ter, para que esses dados sejam processados ​​continuamente, sem intervenção manual e tempo de inatividade significativo. Antecipo o tempo de inatividade trimestralmente para as tarefas domésticas. Mas agora, sem a tabela de processamento separada, não consigo processar nem metade do conjunto de dados sem que a fragmentação se torne tão ruim que exija a interrupção e a reconstrução do índice.

Alguma recomendação para indexação ou um modelo de dados diferente? Existe um padrão que eu preciso pesquisar?
Eu tenho controle total do modelo de dados e do software de processo, para que nada saia da mesa.

Chris Gallucci
fonte
Um pensamento também: seu índice parece estar na ordem errada: deve ser mais seletivo ou menos seletivo. Então ProcessThreadId, ProcessDate, DataStatus, DataType, talvez?
GBN
Anunciamos em nosso bate-papo. Muito boa pergunta. chat.stackexchange.com/rooms/179/the-heap
GBN
Atualizei a consulta para ser uma representação mais precisa da seleção. Eu vários threads simultâneos executando isso. Observei a recomendação de ordem seletiva. Obrigado.
21412 Chris Gallucci
@ChrisGallucci Venha para conversar se você pode ...
JNK

Respostas:

4

O que você está fazendo é usar uma tabela como uma fila. Sua atualização é o método de remoção da fila. Mas o índice clusterizado na tabela é uma má escolha para uma fila. Usar tabelas como filas, na verdade, impõe requisitos bastante rigorosos ao design da tabela. Seu índice de cluster deve estar na ordem de desenfileiramento, provavelmente nesse caso ([DataType], [DataStatus], [ProcessDate]). Você pode implementar a chave primária como uma restrição não clusterizada . Solte o índice não agrupado Idx, pois a chave agrupada assume sua função.

Outra peça importante do quebra-cabeça é manter o tamanho da linha constante durante o processamento. Você declarou ProcessThreadIdcomo o VARCHAR(100)que implica que a linha cresce e diminui conforme está sendo 'processada' porque o valor do campo muda de NULL para não nulo. Esse padrão de aumento e redução na linha causa divisões e fragmentação da página. Não posso imaginar um ID de segmento que seja 'VARCHAR (100)'. Use um tipo de comprimento fixo, talvez um INT.

Como observação lateral, você não precisa desenfileirar em duas etapas (UPDATE seguido de SELECT). Você pode usar a cláusula OUTPUT, conforme explicado no artigo vinculado acima:

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

Além disso, eu consideraria mover itens processados ​​com êxito para uma tabela diferente de arquivamento. Você deseja que suas tabelas de filas fiquem próximas ao tamanho zero, não deseja que elas cresçam, pois elas retêm o 'histórico' de entradas antigas desnecessárias. Você também pode considerar o particionamento [ProcessDate]como uma alternativa ( por exemplo, uma partição ativa atual que atua como a fila e armazena entradas com NULL ProcessDate e outra partição para tudo que não seja nulo. exclui (alterna) os dados que passaram pelo período de retenção obrigatório.Se as coisas esquentarem, você poderá particionar[DataType] se tiver seletividade suficiente, mas esse design seria realmente complicado, pois exige particionamento por coluna computada persistente (uma coluna composta que cola [DataType] e [ProcessingDate]).

Remus Rusanu
fonte
3

Eu começaria movendo os campos ProcessDatee Processthreadidpara outra tabela.

No momento, todas as linhas selecionadas nesse índice bastante amplo também precisam ser atualizadas.

Se você mover esses dois campos para outra tabela, seu volume de atualização na tabela principal será reduzido em 90%, o que deve cuidar da maior parte da fragmentação.

Você ainda terá fragmentação na tabela NEW, mas será mais fácil gerenciar em uma tabela mais estreita com muito menos dados.

JNK
fonte
Isso e a divisão física dos dados com base em [DataType] devem me levar para onde eu preciso estar. Atualmente, estou na fase de design (na verdade, re-design), portanto levará algum tempo até que eu tenha a chance de testar essa mudança.
31812 Chris Gallucci