Indice la fragmentación mientras procesa continuamente

10

SQL Server 2005

Necesito poder procesar continuamente unos 350 millones de registros en una tabla de registros de 900 millones. La consulta que estoy usando para seleccionar los registros a procesar se fragmenta mucho a medida que proceso y tengo que detener el procesamiento para reconstruir el índice. Modelo de pseudodatos y consulta ...

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

Contenido de datos ...
Mientras que la columna [DataType] se escribe como CHAR (1), aproximadamente el 35% de todos los registros equivalen a 'X' con el resto igual a 'A'.
De solo los registros donde [DataType] es igual a 'X', aproximadamente el 10% tendrá un valor NOT NULL [DataStatus].

Las columnas [ProcessDate] y [ProcessThreadId] se actualizarán para cada registro procesado.
La columna [DataType] se actualiza ('X' se cambia a 'A') aproximadamente el 10% del tiempo.
La columna [DataStatus] se actualiza menos del 1% del tiempo.

Por ahora, mi solución es seleccionar la clave principal de todos los registros para procesar en una tabla de procesamiento separada. Borro las claves a medida que las proceso para que, a medida que el índice se fragmente, esté lidiando con menos registros.

Sin embargo, esto no se ajusta al flujo de trabajo que quiero tener para que estos datos se procesen continuamente, sin intervención manual y tiempo de inactividad significativo. Preveo el tiempo de inactividad trimestralmente para las tareas domésticas. Pero ahora, sin la tabla de procesamiento separada, no puedo procesar ni siquiera la mitad del conjunto de datos sin que la fragmentación se vuelva tan grave que sea necesario detener y reconstruir el índice.

¿Alguna recomendación para indexar o un modelo de datos diferente? ¿Hay algún patrón que deba investigar?
Tengo el control total del modelo de datos y el software del proceso, por lo que no hay nada fuera de la mesa.

Chris Gallucci
fuente
Un pensamiento también: su índice parece estar en un orden incorrecto: debe ser más selectivo a menos selectivo. Entonces, ¿ProcessThreadId, ProcessDate, DataStatus, DataType quizás?
gbn
Lo hemos anunciado en nuestro chat. Muy buena pregunta chat.stackexchange.com/rooms/179/the-heap
gbn
Actualicé la consulta para que sea una representación más precisa de la selección. Múltiples hilos concurrentes que ejecutan esto. He tomado nota de la recomendación de pedido selectivo. Gracias.
Chris Gallucci
@ChrisGallucci Ven a chatear si puedes ...
JNK

Respuestas:

4

Lo que estás haciendo es usar una tabla como cola. Su actualización es el método de eliminación de cola. Pero el índice agrupado en la tabla es una mala elección para una cola. El uso de tablas como colas en realidad impone requisitos bastante estrictos en el diseño de la tabla. Su índice agrupado debe ser el orden de la cola, en este caso probable ([DataType], [DataStatus], [ProcessDate]). Puede implementar la clave primaria como una restricción no agrupada. Descarte el índice no agrupado Idx, ya que la clave agrupada toma su papel.

Otra pieza importante del rompecabezas es mantener constante el tamaño de la fila durante el procesamiento. Ha declarado el ProcessThreadIdcomo un VARCHAR(100)que implica que la fila crece y se reduce a medida que se 'procesa' porque el valor del campo cambia de NULL a no nulo. Este patrón de crecimiento y reducción en la fila provoca divisiones y fragmentación de la página. No puedo imaginar una ID de hilo que sea 'VARCHAR (100)'. Use un tipo de longitud fija, quizás un INT.

Como nota al margen, no es necesario retirar la cola en dos pasos (ACTUALIZACIÓN seguido de SELECCIONAR). Puede usar la cláusula OUTPUT, como se explica en el artículo vinculado anteriormente:

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

Además, consideraría mover elementos procesados ​​con éxito a una tabla diferente de archivo. Desea que sus tablas de cola se mantengan cerca del tamaño cero, no desea que crezcan ya que retienen el "historial" de las entradas antiguas innecesarias. También puede considerar la partición [ProcessDate]como una alternativa (es decir, una partición activa actual que actúa como la cola y almacena entradas con NULL ProcessDate, y otra partición para todo lo que no es nulo. O particiones múltiples para no nulo si desea implementar eficientemente elimina (cambia) los datos que han pasado el período de retención obligatorio. Si las cosas se ponen calientes, puede particionar además[DataType] si tiene suficiente selectividad, pero ese diseño sería realmente complicado ya que requiere la partición por una columna computada persistente (una columna compuesta que une [DataType] y [ProcessingDate]).

Remus Rusanu
fuente
3

Comenzaría moviendo los campos ProcessDatey Processthreadida otra tabla.

En este momento, cada fila que seleccione de este índice bastante amplio también debe actualizarse.

Si mueve esos dos campos a otra tabla, su volumen de actualización en la tabla principal se reduce en un 90%, lo que debería encargarse de la mayor parte de la fragmentación.

Aún tendrá fragmentación en la NUEVA tabla, pero será más fácil de administrar en una tabla más estrecha con muchos menos datos.

JNK
fuente
Esto y dividir físicamente los datos basados ​​en [DataType] debería llevarme a donde necesito estar. Actualmente estoy en la fase de diseño (rediseño en realidad) de esto, así que pasará algún tiempo antes de que tenga la oportunidad de probar este cambio.
Chris Gallucci