为什么在列大小增加后创建索引需要更长的时间?

Dan*_*ler 18 sql-server database-internals

我们的供应商更改了整个数据库中几乎每一列的列宽。数据库大约 7TB,9000 多个表。我们正在尝试在具有 55 亿行的表上创建索引。在供应商升级之前,我们可以在 2 小时内创建索引。现在需要几天时间。他们所做的是将任何 varchar(xx) 大小增加到 varchar(256)。所以大多数列曾经是 varchar(18) 或 varchar(75) 等。

无论如何,主键由 6 列组成,组合宽度为 126 个字符。现在升级后,主键是 1283 个字符,这违反了 SQL Server 900 个字符的限制。整个表列宽度从 1049 的总组合 varchar 计数变为 4009 的总组合 varchar 计数。

数据没有增加,表不会占用比所有列宽增加之前更多的“空间”,但是创建像索引这样简单的东西的性能现在花费了不合理的时间。

任何人都可以解释为什么当唯一要做的就是增加列的大小时,为什么创建和索引需要这么长时间?

我们尝试创建的索引是非聚集的,因为 pk 是聚集索引。在多次尝试创建索引后,我们放弃了。我认为它运行了 4 或 5 天而没有完成。

我在非生产环境中尝试了这一点,方法是拍摄文件系统快照并将数据库放在更安静的服务器上。

Geo*_*son 12

Remus 有用地指出,VARCHAR列的最大长度会影响估计的行大小,因此会影响 SQL Server 提供的内存授权。

我试图做更多的研究来扩展他的答案中“从这件事上级联”部分。我没有完整或简洁的解释,但这是我发现的。

复制脚本

我创建了一个完整的脚本,它生成一个假数据集,在该VARCHAR(256)版本的机器上创建索引所需的时间大约是我机器上的 10 倍。所使用的数据是完全一样的,但在第一表使用的实际最大长度1875915123,和5,而所有列使用的最大长度256在所述第二表中。


键控原始表

在这里我们看到原始查询在大约 20 秒内完成,逻辑读取等于表大小~1.5GB(195K 页,每页 8K)。

-- CPU time = 37674 ms,  elapsed time = 19206 ms.
-- Table 'testVarchar'. Scan count 9, logical reads 194490, physical reads 0
CREATE CLUSTERED INDEX IX_testVarchar
ON dbo.testVarchar (s1, s2, s3, s4)
WITH (MAXDOP = 8) -- Same as my global MAXDOP, but just being explicit
GO
Run Code Online (Sandbox Code Playgroud)


键入 VARCHAR(256) 表

对于VARCHAR(256)表格,我们看到经过的时间急剧增加。

有趣的是,CPU 时间和逻辑读取都没有增加。鉴于该表具有完全相同的数据,这是有道理的,但它并不能解释为什么经过的时间如此之慢。

-- CPU time = 33212 ms,  elapsed time = 263134 ms.
-- Table 'testVarchar256'. Scan count 9, logical reads 194491
CREATE CLUSTERED INDEX IX_testVarchar256
ON dbo.testVarchar256 (s1, s2, s3, s4)
WITH (MAXDOP = 8) -- Same as my global MAXDOP, but just being explicit
GO
Run Code Online (Sandbox Code Playgroud)


I/O 和等待统计:原始

如果我们捕获更多细节(使用p_perfMon,我编写的一个过程),我们可以看到绝大多数 I/O 是在LOG文件上执行的。我们看到实际ROWS(主数据文件)上的 I/O 数量相对较少,主要等待类型是LATCH_EX,表明内存页面争用。

根据 Paul Randal 的说法,我们还可以看到我的旋转磁盘介于“坏”和“令人震惊的坏”之间:)

在此处输入图片说明


I/O 和等待统计:VARCHAR(256)

对于VARCHAR(256)版本,I/O 和等待统计看起来完全不同!在这里,我们看到数据文件 ( ROWS)上的 I/O 大幅增加,现在停顿时间让 Paul Randal 简单地说“哇!”。

#1 等待类型现在是IO_COMPLETION. 但为什么会产生如此多的 I/O?

在此处输入图片说明


实际查询计划:VARCHAR(256)

从查询计划中,我们可以看到Sort运算符VARCHAR(256)在查询版本中存在递归溢出(5 级深!)。(原始版本中根本没有溢出。)

在此处输入图片说明


实时查询进度:VARCHAR(256)

我们可以使用 sys.dm_exec_query_profiles 在 SQL 2014+ 中查看实时查询进度。在原始版本中,整个Table ScanSort都经过处理,没有任何溢出(spill_page_count始终保持0)。

VARCHAR(256)然而,在版本中,我们可以看到页面溢出为Sort操作员迅速积累。这是查询完成之前查询进度的快照。这里的数据跨所有线程聚合。

在此处输入图片说明

如果我单独深入研究每个线程,我会看到 2 个线程在大约 5 秒内完成排序(总共 20 秒,在表扫描上花费 15 秒后)。如果所有线程都以这种速度进行,VARCHAR(256)索引创建将在与原始表大致相同的时间内完成。

但是,其余 6 个线程的进度要慢得多。这可能是由于内存分配方式以及线程在溢出数据时被 I/O 阻塞的方式。不过我不确定。

在此处输入图片说明


你能做什么?

您可能会考虑尝试以下几种方法:

  • 与供应商合作以回滚到以前的版本。如果这是不可能的,请告知供应商您对此更改不满意,以便他们可以考虑在将来的版本中恢复它。
  • 当添加索引,可以考虑使用OPTION (MAXDOP X)其中X一个较小的数字比目前的服务器级别设置。当我OPTION (MAXDOP 2)在我的机器上使用这个特定的数据集时,VARCHAR(256)版本完成25 seconds(与 8 个线程的 3-4 分钟相比!)。更高的并行度可能会加剧溢出行为。
  • 如果有可能进行额外的硬件投资,请分析系统上的 I/O(可能的瓶颈)并考虑使用 SSD 来减少溢出引起的 I/O 延迟。


进一步阅读

Paul White 有一篇关于SQL Server 内部结构的不错的博客文章,您可能会感兴趣。它确实谈到了并行排序的溢出、线程倾斜和内存分配。


Rem*_*anu 11

中间排序表将在两种情况下进行不同的估计。VARCHAR(256)与“理想”请求相比,这将导致不同的内存授予请求(将更大),并且在百分比方面可能会导致实际授予要小得多。我想这会导致排序过程中发生溢出。

从 Geoff 测试脚本(仅在 100k 行上)我可以清楚地看到排序估计行大小的差异(141B 与 789B)。从此事情级联。

  • 我相信 Paul 会提供一个更彻底和更完整的答案,也许包括调用堆栈,这将被开发团队用作学习材料。再次... (8认同)