通过对包含列的索引进行大量更新来节省性能

Dir*_*oer 2 performance index sql-server clustered-index index-tuning

Points
------------------
PK  QuestionId  int (+30.000.000 distinct values)
PK  EventId     int (large batches where 80.000 rows have the same EventId)
    Value       smallint
Run Code Online (Sandbox Code Playgroud)

该表大约有 4000 万行并且存在性能问题。

有两个主要查询:

关于QuestionId

  • 大约 3000 万个不同的QuestionId值(很多变化)
  • 繁忙时段的大量查询(每分钟数千次)

EventId 上

  • 将有 +150.000 行的批量更新where EventId=X来设置Value=NULL非常繁忙的时刻。

所以我第一个想获得最佳性能的想法是我制作EventId,QuestionIdClusteredIndex 以便批量更新可以轻松找到彼此接近的所有 EventId 并直接更新值。

我的第二个想法是添加一个包含QuestionId包含列 的索引,Value以便它可以直接从索引中读取值(EventId在这种情况下无关紧要)。

但后来我想:聚集索引会重要吗?由于索引中包含的列值也需要在批量更新期间更新。

  • 虽然不牺牲查询性能 - 是否可以快速获得批量更新(几秒钟),或者我必须接受这个过程在不升级硬件的情况下总是很慢。
  • 还有其他想法什么是设置 ClusteredIndex / Indexes 的最佳方法?

我知道理论上我应该测试所有内容并对其进行测量,但是该站点是实时的并且被大量使用。

我是一名独立开发人员,我没有资源聘请某人。对此的任何估计猜测和想法都将非常有帮助,因为这已经为我提供了正确的方向!

bba*_*ird 7

因此,如果您的主要访问路径有问题,那么最有意义的唯一聚集索引将是(QuestionId, EventId).

添加第二个索引EventId可能没有用,因为索引可能没有足够的选择性,查询引擎将决定读取整个表的速度更快,而不是做大量工作来读取大部分。

或者,如果您总是完全或部分基于 进行查询EventId,则 的聚集索引(EventId,Questionid)更合适,并且具有使您的更新基于EventId需要更少的 I/O 来完成的额外好处。

不会包括Value额外的索引,因为这基本上会复制整个表(只是聚集在不同的列上),并且您的更新将花费更长的时间,因为Value必须在聚集索引和额外索引之间保持同步。

到了某个时候没有免费的午餐,正确的解决方案可能是选择支持最多用例的前导列的聚簇索引,然后添加RAM/CPU/更快的存储来处理整个表(或大它的块)必须被读取或写入。4000万行,这么窄的桌子,我不能想象这是东西,更多的内存就不会解决。

根据您的 SQL Server 版本,您还可以查看页面压缩是否会显着减少表大小,因为这会减少对磁盘的读/写次数(额外的 CPU 开销被更少的磁盘操作抵消)。我的猜测是你的情况不会,但它的工作正在寻找。

因此,如果我理解正确(以目前的知识),您的直觉将是聚集索引QuestionId,EventId,然后是批量更新的 正常索引EventId

仅当主要用途是返回特定QuestionIds而不考虑EventId. 您可以尝试 中的附加索引EventId,但您可能会发现它并不经常(或根本没有)用于更新(或更新所需的时间仍然比您希望的要长),这取决于 EventIds 在您的数据中的分布方式到QuestionId

您还必须确定对您整体而言更重要的是什么 - 选择性能还是更新性能。如果更新是痛点,(EventId,QuestionId)无疑是更好的选择。给定 的唯一值数量QuestionId,在该列上添加索引可能有助于提高SELECT性能,但这将取决于QuestionId分布方式以及您一次搜索的数量。

在这两种情况下,保持最新的统计数据都是至关重要的。

一个非常简单的例子(为了完整起见):

假设我们有一个 DBMS,它维护一个聚集索引并每页存储 4 行。我们有一个主键为(QuestionId, EventId)和一个附加列的表Value

如果我们将聚集索引创建为(QuestionId, EventId),那么我们想象中的 DBMS 中的数据(粗略地说)存储如下:

Page | QuestionId | EventId | Value
-----------------------------------
A    | 1          | 2       | ...
A    | 1          | 3       | ...
A    | 1          | 6       | ...
A    | 1          | 7       | ...
B    | 1          | 8       | ...
B    | 1          | 10      | ...
B    | 1          | 11      | ...
B    | 2          | 2       | ...
C    | 3          | 2       | ...
C    | 4          | 1       | ...
C    | 5          | 6       | ...
C    | 5          | 7       | ...
D    | 6          | 1       | ...
D    | 7          | 2       | ...
D    | 7          | 6       | ...
D    | 7          | 8       | ...
Run Code Online (Sandbox Code Playgroud)

因此,如果我需要执行基于 的操作QuestionId,引擎将不必读取不必要的页面。

但是,如果我需要执行基于 的操作EventId,我将不得不读取整个表(聚集索引扫描),除非我添加一个额外的索引,它看起来像这样(并且需要四页):

EventId | QuestionId
--------------------
1       | 4
1       | 6
2       | 1
2       | 2
2       | 3
2       | 7
3       | 1
6       | 1
6       | 5
6       | 7
7       | 1
7       | 5
8       | 1
8       | 7
10      | 1
11      | 1
Run Code Online (Sandbox Code Playgroud)

这个索引对某些人是有选择性的EventIds,但在极端情况下 ( EventId = 2) 仍然需要读取整个表,对于某些情况下 ( EventId = 6) 我们的优化器可能会决定搜索索引和读取表比仅仅读取整个表更昂贵桌子。

如果我们改为在EventId, QuestionId我们的表上进行集群,则如下所示:

Page | EventId | QuestionId | Value
-----------------------------------
A    | 1        | 4         | ...
A    | 1        | 6         | ...
A    | 2        | 1         | ...
A    | 2        | 2         | ...
B    | 2        | 3         | ...
B    | 2        | 7         | ...
B    | 3        | 1         | ...
B    | 6        | 1         | ...
C    | 6        | 5         | ...
C    | 6        | 7         | ...
C    | 7        | 1         | ...
C    | 7        | 5         | ...
D    | 8        | 1         | ...
D    | 8        | 7         | ...
D    | 10       | 1         | ...
D    | 11       | 1         | ...
Run Code Online (Sandbox Code Playgroud)

任何基于 的操作EventId都只会读取表的必要部分,就像我们的第一个实例一样,任何基于 的操作QuestionId都需要在没有额外索引的情况下进行扫描。如果我们在 上创建索引QuestionId,则索引将是:

QuestionId | EventId
--------------------
1          | 2
1          | 3
1          | 6
1          | 7
1          | 8
1          | 10
1          | 11
2          | 2
3          | 2
4          | 1
5          | 6
5          | 7
6          | 1
7          | 2
7          | 6
7          | 8
Run Code Online (Sandbox Code Playgroud)

因此,在第一个示例中,该索引对某些问题更有用,而对其他问题则不太有用。因为QuestionId = 1优化器可能会说读取一半索引然后查找一半表的成本是不值得的,只会读取整个表而不是使用索引。

如果我们包含Value在索引上,我们现在必须在同一事务中更改表和索引。最好的情况是,这会使任何操作的工作量加倍。最坏的情况是需要读取整个表或索引(它只是表的副本)并可能锁定。

现在可以对您的实际数据添加额外的索引QuestionIdEventId将提供很多好处。但它不会解决所有问题,并且可能不值得对插入/更新/删除施加的开销。