向表添加列存储索引是否会影响在同一表上使用行存储索引的查询的读取性能?

J.D*_*.D. 5 index sql-server columnstore nonclustered-index sql-server-2016

我正在对具有大约 5 亿行的单个表上的列存储索引进行一些测试。聚合查询的性能提升非常棒(以前运行大约需要 2 分钟的查询现在可以在 0 秒内运行以聚合整个表)。

但我也注意到另一个测试查询,它利用在同一个表上的现有行存储索引上进行搜索,现在运行速度是创建列存储索引之前的 4 倍。我可以反复演示当删除列存储索引时,行存储查询在 5 秒内运行,通过添加回列存储索引,行存储查询在 20 秒内运行。

我一直关注行存储索引查询的实际执行计划,无论是否存在列存储索引,这两种情况几乎完全相同。(它在两种情况下都使用行存储索引。)

行存储测试查询是:

SELECT *
INTO #TEMP
FROM Table1 WITH (FORCESEEK)
WHERE IntField1 = 571
    AND DateField1 >= '6/01/2020'
Run Code Online (Sandbox Code Playgroud)

此查询中使用的行存储索引是: CREATE NONCLUSTERED INDEX IX_Table1_1 ON Table1 (IntField1, DateField1) INCLUDE (IntField2)

列存储测试查询是:

SELECT COUNT(DISTINCT IntField2) AS IntField2_UniqueCount, COUNT(1) AS RowCount
FROM Table1
WHERE IntField1 = 571 -- Some other test columnstore queries also don't use any WHERE predicates on this table
    AND DateField1 >= '1/1/2019' 
Run Code Online (Sandbox Code Playgroud)

列存储索引是: CREATE NONCLUSTERED COLUMNSTORE INDEX IX_Table1_2 ON Table1 (IntField2, IntField1, DateField1)

这是在我创建列存储索引之前行存储索引查询的执行计划: 执行计划 - 行存储索引 - 列存储前索引创建

这是我创建列存储索引后行存储索引查询的执行计划: 执行计划 - 行存储索引 - 列存储后索引创建

我注意到这两个计划之间的唯一区别是在创建列存储索引后 Sort 操作的警告消失了,并且 Key Lookup 和 Table Insert (#TEMP) 操作需要更长的时间。

这是带有警告的排序操作信息(在创建列存储索引之前): 排序操作 - 警告

这是没有警告的排序操作的信息(在创建列存储索引之后): 排序操作

我原以为在这两种情况下专门利用相同行存储索引和执行计划的读取查询在每次运行时都应该具有大致相同的性能,而不管该表上存在哪些其他索引。这里有什么?

编辑:这是创建索引之前的 TIME 和 IO 统计信息: 统计信息 - 创建列存储索引之前

这是创建列存储索引后的统计信息: 统计信息 - 创建列存储索引之后

Jos*_*ell 6

添加非聚集列存储索引允许在第二个执行计划中进行批处理模式排序。这导致所有处理都在一个线程上完成 - 因此即使查询具有并行计划,它本质上也是串行运行的。您可以通过查看不同运算符的详细信息来了解这一点。

我在本地重现了您的问题,这是带有每个线程计数的排序运算符 - 如您所见,所有内容都在线程 1 上:

显示排序运算符的线程倾斜和执行模式的 SSMS 屏幕截图

注意“实际执行模式”是“批处理”。

排序之后的所有内容(嵌套循环连接、键查找等)本质上都是串行的,这会减慢查询速度。

有关详细信息和可能的解决方案,请参阅此知识库文章:

添加跟踪标志 9358 以禁用 SQL Server 2016 中复杂并行查询中的批处理模式排序操作

批处理模式排序是在兼容级别 130 下在 SQL Server 2016 中引入的。如果查询执行计划包含并行批处理模式排序以及直接上游并行运算符,与行模式排序计划等效项相比,您可能会遇到性能下降。

这是因为并行批处理排序通过单个线程将完全排序的数据输出到上游并行运算符(例如,并行合并连接运算符)。由于传入的单线程批处理模式排序运算符,当上游并行运算符使用单线程处理时,会发生性能下降。

为完整起见,此处列出的选项包括:

  • 启用 TF 9358
  • 启用查询优化器修补程序(通过 TF 4199、QUERY_OPTIMIZER_HOTFIXES数据库选项或ENABLE_QUERY_OPTIMIZER_HOTFIXES查询提示)

摆脱排序是这个问题的另一个解决方案。排序只是为了尝试防止来自嵌套循环连接的太多随机 I/O,它使用无序预取,正如 Craig Freedman 在这篇文章中提到的:

通过排序优化 I/O 性能——第 1 部分

该计划使用非聚集索引来避免不必要地接触多行。然而,执行 64,000 次随机 I/O 仍然相当昂贵,因此 SQL Server 增加了一种排序。 通过对聚集索引键上的行进行排序,SQL Server 将随机 I/O 转换为顺序 I/O。

您可以通过以下方式摆脱排序:

  • 消除键查找的需要(通过选择较少的列,或创建覆盖非聚集索引)
  • 通过向OPTION (QUERYTRACEON 9115)查询添加(未记录、不支持的跟踪标志)来禁用嵌套循环预取