连续处理时索引碎片


10

SQL Server 2005

我需要能够连续处理900M记录表中的约350M记录。在处理过程中,我用来选择要处理的记录的查询变得非常分散,我需要停止处理以重建索引。伪数据模型和查询...

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

数据内容...
将[DataType]列键入为CHAR(1)时,所有记录中约35%等于'X',其余等于'A'。
仅在[DataType]等于'X'的记录中,大约10%的NOT NULL [DataStatus]值。

[ProcessDate]和[ProcessThreadId]列将针对每个处理的记录进行更新。
[DataType]列大约有10%的时间更新(“ X”更改为“ A”)。
[DataStatus]列的更新时间少于1%。

现在,我的解决方案是选择所有记录的主键,以处理到单独的处理表中。我在处理键时将其删除,以便作为索引碎片处理更少的记录。

但是,这不适合我想要的工作流程,因此这些数据可以连续处理,而无需人工干预和大量停机。我确实会每季度因为一次家务而停机。但是现在,如果没有单独的处理表,我什至无法处理一半的数据集,而碎片变得如此糟糕,以至于必须停止并重建索引。

对索引或其他数据模型有什么建议吗?我需要研究一种模式吗?
我对数据模型和过程软件拥有完全的控制权,因此没有任何问题。


也有一个想法:您的索引似乎是错误的顺序:应该是最有选择性的到最无选择性的。那么,ProcessThreadId,ProcessDate,DataStatus,DataType可能呢?
gbn 2012年

我们已经在聊天中做广告了。很好的问题。chat.stackexchange.com/rooms/179/the-heap
GBN

我将查询更新为更准确的选择表示。我有多个并发线程正在运行。我注意到了选择性订购的建议。谢谢。
克里斯·加卢奇

@ChrisGallucci如果可以的话,聊天...
JNK

Answers:


4

您正在做的是将表格用作队列。您的更新是出队方法。但是,表上的聚集索引对于队列来说是一个糟糕的选择。使用表作为队列实际上对表设计提出了非常严格的要求。您的聚集索引必须是出队顺序,在这种情况下可能是([DataType], [DataStatus], [ProcessDate])。您可以将主键实现为非集群约束。删除非聚集索引Idx,因为聚集键将发挥作用。

难题的另一个重要方面是在处理过程中保持行大小不变。您已将a声明ProcessThreadId为a VARCHAR(100),这意味着行将随着“处理”而增长和收缩,因为字段值从NULL变为非null。行上的这种增长和收缩模式会导致页面拆分和碎片化。我无法想象线程ID为'VARCHAR(100)'。使用固定长度的类型,也许是INT

附带说明,您无需分两个步骤出队(先执行UPDATE,再执行SELECT)。您可以使用OUTPUT子句,如上面链接的文章中所述:

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

另外,我会考虑将成功处理的项目移到另一个存档表中。您希望队列表的大小保持在零附近,因为它们保留了不需要的旧条目的“历史记录”,因此不希望它们增长。您也可以考虑通过分区来[ProcessDate]替代分区(例如,一个当前的活动分区充当队列并使用NULL ProcessDate存储条目,另一个分区用于所有非空的分区。或者,如果要实现高效,则对非空的多个分区进行分区删除(转出)已超过规定保留期限的数据。[DataType] 如果它具有足够的选择性,但是该设计将非常复杂,因为它需要按持久化的计算列(将[DataType]和[ProcessingDate]粘合在一起的复合列)进行分区。


3

我将从将ProcessDateand Processthreadid字段移到另一个表开始。

现在,您从此相当宽的索引中选择的每一行都需要更新。

如果将这两个字段移动到另一个表,则主表上的更新量将减少90%,这应该处理大部分碎片。

在NEW表中仍然会有碎片,但是在具有更少数据的狭窄表上管理起来会更容易。


这并根据[DataType]物理拆分数据应该可以让我到达需要的位置。我目前正处于设计(实际上是重新设计)阶段,因此需要一段时间才能有机会测试驱动此更改。
克里斯·加卢奇
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.