Points
------------------
PK QuestionId int (+30.000.000 distinct values)
PK EventId int (large batches where 80.000 rows have the same EventId)
Value smallint
该表大约有 4000 万行,并且存在性能问题。
主要有两个查询:
在QuestionId上:
- 大约 3000 万个不同
QuestionId
的值(变化很大) - 繁忙时间有很多查询(每分钟数千次)
在EventId上:
- 将有 +150.000 行的批量更新
where EventId=X
来设置Value=NULL
在非常忙碌的时刻。
所以我首先想到的要获得最佳性能是我制作EventId,QuestionId
了ClusteredIndex,以便批量更新可以轻松找到所有靠近彼此的EventId并直接更新值。
我的第二个想法是添加一个包含QuestionId
和包含列 Value
的索引,以便它可以直接从索引中读取值(EventId
在这种情况下无关紧要)。
但后来我想:聚集索引是否重要?由于索引中的包含列值也需要在批量更新期间进行更新。
- 虽然不牺牲查询性能 - 是否可以快速(几秒钟)获得批量更新,或者我是否必须接受这个过程在不升级硬件的情况下总是很慢。
- 任何其他想法设置 ClusteredIndex / 索引的最佳方法是什么?
我知道理论上我应该测试一切并衡量它,但该网站是活跃的并且被大量使用。
我是一名独立开发人员,我没有资源聘请某人。任何估计的猜测和想法都会非常有帮助,因为这已经给了我正确的方向!
因此,如果您的主要访问路径有问题,那么最有意义的唯一聚集索引将是
(QuestionId, EventId)
。添加第二个索引
EventId
可能没有用,因为索引可能没有足够的选择性,并且查询引擎将决定读取整个表而不是做大量工作来读取它的大部分更快。或者,如果您始终完全或部分基于 查询
EventId
,则 的聚集索引(EventId,Questionid)
更合适,并且具有使您的更新基于EventId
需要更少的 I/O 来完成的额外好处。我不会包含
Value
附加索引,因为这实际上会复制整个表(只是聚集在不同的列上),并且您的更新将需要更长的时间,因为Value
必须在聚集索引和附加索引之间保持同步。在某个时候没有免费的午餐,正确的解决方案可能是选择具有支持最多用例的前导列的聚集索引,然后添加 RAM/CPU/更快的存储来处理整个表(或大它的块)必须被读取或写入。有 4000 万行和如此狭窄的表格,我无法想象这是更多 RAM无法解决的问题。
根据您的 SQL Server 版本,您还可以查看页面压缩是否会显着减小表大小,因为这会减少对磁盘的读取/写入次数(额外的 CPU 开销被更少的磁盘操作所抵消)。在你的情况下,我的猜测是它不会,但它正在寻找。
仅当主要用途是返回特定
QuestionIds
而不考虑EventId
. 您可以尝试 中的附加索引EventId
,但您可能会发现它并不经常(或根本不)用于更新(或更新仍然需要比您想要的更长的时间),具体取决于 EventIds 在您的数据中的分布方式到QuestionId
.您还必须确定总体上对您更重要的是什么 - 选择性能或更新性能。如果更新是痛点,
(EventId,QuestionId)
无疑会是更好的选择。鉴于 的唯一值的数量QuestionId
,在该列上添加索引可能对SELECT
性能有用,但这将取决于QuestionId
分布方式以及您一次搜索的数量。在任何一种情况下,保持最新统计数据都是至关重要的。
一个非常简单的例子(为了完整起见):
假设我们有一个 DBMS,它维护一个聚集索引并每页存储 4 行。我们有一个主键为 的表
(QuestionId, EventId)
和一个附加列,Value
。如果我们将聚集索引创建为
(QuestionId, EventId)
,我们想象中的 DBMS 中的数据(粗略地说)存储如下:因此,如果我需要执行基于 的操作
QuestionId
,引擎将不必读取不必要的页面。但是,如果我需要执行基于 的操作
EventId
,我将不得不读取整个表(聚集索引扫描),除非我添加一个额外的索引,它看起来像这样(并且需要四页):这个索引对某些人来说是选择性的
EventIds
,但在极端情况下(EventId = 2
)仍然需要读取整个表,并且对于某些情况(EventId = 6
)我们的优化器可能会决定搜索索引和读取表比读取整个表更昂贵桌子。如果我们改为聚集在
EventId, QuestionId
我们的表上,如下所示:任何基于 的操作
EventId
都只会读取表的必要部分,并且像我们的第一个实例一样,任何基于 的操作QuestionId
都需要扫描而不需要额外的索引。如果我们在 上创建索引QuestionId
,则索引将是:因此,与第一个示例一样,该索引对某些问题更有用,而对其他问题则不太有用。因为
QuestionId = 1
优化器可能会说读取一半索引然后查找一半表的成本不值得,并且只会读取整个表而不是使用索引。如果我们包含
Value
在索引中,我们现在必须在同一个事务中更改表和索引。最好的情况是,这会使任何操作的工作加倍。在最坏的情况下,这需要读取整个表或索引(它只是表的副本)并可能锁定。现在可以使用您的实际数据添加额外的索引
QuestionId
或EventId
将提供很多好处。但这并不能解决所有问题,而且插入/更新/删除的开销可能不值得。