Nic*_*ris 1 index sql-server clustered-index fragmentation
我正在使用 Ola Hallengren 的解决方案来优化索引。我每周周日运行它。该指数在过去 6 个月内碎片化程度较低,不需要重组。它的使用方式也没有改变。
在过去的三周里,每次运行它都会报告数据库最大表上的聚集索引的碎片化程度在 5-12% 之间。奇怪的是,这不会发生在工作日,因此它必须在维护作业运行之前的某个时间出现。
我的表的每一行都有一个时间戳,所以我知道流量几个月来一直保持在同一水平。通常每个月的碎片化率低于 1%。
我有两个问题:
该表有 134 列,包含 14gb 的数据,主键不是身份(叹气)
有问题的索引如下所示:
分区 ID:1
CREATE UNIQUE CLUSTERED INDEX [FOOINDEX] ON [dbo].[FOOTABLE]
(
[FOONO] ASC,
[ID] ASC
)
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
Run Code Online (Sandbox Code Playgroud)在开始时要明确:碎片是指下一个逻辑数据页(即 1 行或更多行的字段中的值)不是下一个物理数据页(数据文件中页的位置)。
非碎片页面(按物理页面顺序):
碎片页面(按物理页面顺序):
一般来说,碎片化是由以下原因造成的:
关于索引碎片的其他资源:
对于此特定索引,平均行大小为 1413,因此每页只能获得 5 行(同样,平均而言)。未指定 FILLFACTOR,因此它将采用系统默认值(默认“默认”值为 0,表示填充数据页)。鉴于以下情况,这些值似乎很可能不会一直增加FOONO
是VARCHAR(12)
. 当新行进入时,这会经常给你一些碎片。当然,即使没有指定 FILLFACTOR,也会有少量的自然保留空间,因为行大小是可变的。意思是,在 REINDEX 之后,页面上可能还剩下 2000 个字节,因为按顺序排列的下一行大于 2000 个字节并且无法容纳在页面上。如果一个新行在逻辑上适合这些现有行并且 <= 2000 字节,它应该去那里。因此,对于这些情况,为了了解传入的值与碎片增加之间的关系,您需要识别新行,考虑到时间戳字段,这似乎是可行的。
已经确认这个表是 INSERT-only。这缩小了碎片的来源是新行的进入。碎片的差异应该是 a)FOONO
字段值的分布和 b) 新行的宽度的函数。鉴于有一个 DATETIME 字段指示何时插入行,您应该能够每周查询过去 6 周:
FOONO
场ID
场COALESCE(DATALENGTH(ColumnName), 0)
用于每个字段)。您不必担心固定长度的字段,因为它们不是碎片增加的一个因素(除非它们被标记为 SPARSE 并且现在作为非 NULL 值出现,而在前几周它们通常是 NULL)。您正在寻找的是(将最近的 3 周或现在的 4 周与碎片化程度较低的情况进行比较;并确保ORDER BY
在DATETIME
现场)至少有以下一项:
FOONO
价值分布ID
价值观:
ID
值的分布,AND / OR ORDER BY TimestampField
)这些事情发生的任何组合都应该说明碎片化的增加。
这 4 种可能性的共同主题是它们都具有增加页面拆分率的效果。当一个新行(或比以前更大的更新行,但现在我们只关心新行)在逻辑上应该放在没有足够可用空间来容纳新行的数据页上时,就会发生页面拆分排。发生这种情况时,SQL Server 将(通常)取一些现有数据的后一部分并将其移动到新页面,并将新行添加到它逻辑上属于的两个页面中的任何一个。最终结果是新页面中至少有一半的行,如果不是略多的话(我用一个每页包含 10 行的表进行了多次测试,每个页面拆分后,现有页面只有 5 行, 6 行的新页面)。
不过,典型的页面拆分只是故事的一半。另一半是将数据按顺序添加到这些由拆分操作创建的新页面时会发生什么。随着新行的添加和新页面的填满,无需移动任何行即可创建新页面。当这种情况发生时,与前一页相比,新页面不仅按逻辑顺序排列,而且按物理顺序排列。这不被视为碎片页面,因为从前一个页面到这个新页面,它们都在逻辑和物理顺序中。
意思是,如果我们有完整的页面 A0、B0 和 C0,并添加了一些逻辑上进入 B0 的行,那么页面拆分就会发生,给我们一个新页面 B1,在 B0 和 B1 上都有一定数量的行。但是现在,如果我们填充 B1 并继续按顺序添加行,这些行在逻辑上位于 C0 上的数据之前,它将(或经常)为新行创建一个新页面 B2,而无需将任何内容从 B1 移动到 B2。在物理上,页面将被排序为:
或者
由于逻辑顺序(A0、B0、B1、B2、C0)与物理顺序不匹配,因此从 B0 跳转到 B1,再从 B2 跳转回 C0,被视为碎片。但是,从 B1 到 B2 的跳转不被视为碎片,因为逻辑顺序确实与物理顺序匹配。
现在让我们把所有这些放在一起。请记住,此特定索引的数据页打包得相当紧密(这很好),大多数插入操作很可能会进入已填充的页,因此需要拆分。如果要添加 4 个新行(根据 的值FOONO
和/或根据ID
--if 新值可能位于现有值之间),以便它们应进入 4 个单独的页面,即是 4 个页面拆分,代表 4 个新页面和 8 次碎片化(每个拆分的跳出顺序和返回顺序)。但,如果4个新行是有序的,那么有可能只发生1页分裂,代表1个新页和2次碎片发生。如果将更多行顺序添加到拆分创建的新页面,从而需要新页面,则它们可以滚动到新的下一页,这显然会使页面计数增加 1,但不会注册为碎片的发生。
另一个场景——就一周的新记录而言是连续的,但没有按照由“时间戳”DATETIME 字段排序确定的顺序添加的数据——与刚刚描述的主要场景大致相同。这里的区别是 a) 如果没有按“时间戳”字段排序,它可能看起来好像数据是连续的,并且会被错误地排除为碎片来源,并且 b) 页面拆分将在创建的集合中更频繁地发生由于初始页面拆分而刚刚创建的页面。
希望下面的例子能帮助说明这个问题。假设我们正在查看逻辑页面 A0、B0 和 C0,并且它们在物理上也是有序的。页 B0 已填充并包含行 Ba、Bf 和 Bn。新行是 Bo、Bp、Bq、Br 和 Bt。但它们不是按这个顺序来的。下面列出了每行进入时数据页应该发生的情况。页 ID(例如 A0)表示逻辑页,而顺序表示它们的物理位置。
开始:
A0
B0 = Ba、Bf 和 Bn(页面已满)
C0
添加Br:
B1 = Bn 和 Br(物理上在 A0 之前,但逻辑上在 B0 = 碎片之后)
A0
B0 = Ba 和 Bf(拆分:Bn 移至新页面 B1)
C0
添加BT:
B1 = Bn、Br 和 Bt(页面填充)
A0
B0 = Ba 和 Bf
C0
添加基点:
B1 = Bn 和 Bp(拆分:Br 和 Bt 移到新页面 B2)
B2 = Br 和 Bt(在 B1 = NOT 碎片之后的物理和逻辑上)
A0
B0 = Ba 和 Bf
C0
添加 Bq:
B1 = Bn、Bp 和 Bq(页面填充)
B2 = Br 和 Bt
A0
B0 = Ba 和 Bf
C0
加博:
B1 = Bn 和 Bo(拆分:Bp 和 Bq 移至新页面 B2)
B3 = Br 和 Bt(物理上仍在 B1 之后,但现在逻辑上 B2 = 分片之后)
B2 = Bp 和 Bq(物理上在 B3 之后,但逻辑上在 B1 之后= 碎裂)
A0
B0 = Ba 和 Bf
C0
结果: 3 个页面拆分,3 个新页面,4 个碎片计数,4 个页面部分填充
如果按顺序插入相同的行,它最终看起来像:
开始:
A0
B0 = Ba、Bf 和 Bn(页面已满)
C0
最终的:
B1 = Bn、Bo 和 Bp
B2 = Bq、Br 和 Bt
A0
B0 = Ba 和 Bf(拆分:Bn 移至新页面 B1)
C0
结果: 1 个页面拆分,2 个新页面,2 个碎片计数,1 个页面部分填充
只是为了让那些具有类似设置但确实允许 UPDATE 操作的人注意到它,如果确实发生了 UPATE,那么这个问题将包含一个错误的前提并且最终将是无法回答的。手头的问题是时间戳(无论是使用 TIMESTAMP / ROWVERSION 数据类型还是 DATETIME / DATETIME2 数据类型)只告诉您何时最后一次发生了变化;它没有告诉你 a) 发生了多少变化,也没有 b) 发生的具体变化是什么(即行变小了吗?5 个字段变大了但 20 个字段变小了,所以行大小实际上保持不变一样吗?等)。如果此表确实发生了更新,那么查看导致碎片增加的原因的唯一方法是收集更改的审计数据(无论是自定义的、基于触发器的解决方案还是更改数据捕获)。但是在当前设置中,无法将更改的行数(如时间戳字段所示)与任何数量的碎片(同样,IF 更新正在发生)可靠地相关联。
此外,关于此声明:
该表有 134 列,包含 14gb 的数据,主键不是身份(叹气)
如果这是一个 OLTP 表,那么我会发现 134 列比不基于 IDENTITY 字段的主键更有问题。因为这是一个历史表,所以有 134 列可能是可以的。但即使是历史表,它也不是一定PK 最好基于 IDENTITY 字段,或者该表甚至有一个 PK(我假设 PK 和聚集索引不相同,因为问题中的索引是 UNIQUE CLUSTERED 而不是 PK)。并且即使我们在谈论聚集索引(无论是否为 PK),那么仍然不一定总是最适合作为前导列。这一切都归结为如何访问数据。如果前导字段是 IDENTITY 但该值从未在 JOIN 或 UPDATE 或 DELETE 操作中使用,那么您将牺牲所有查询的性能,即使没有碎片(不可修复),只是为了避免一些碎片(可通过REBUILD
或REORGANIZE
)修复。