具有高选择性和低选择性字段的复合索引顺序中的字段顺序


11

我有一个超过30亿行的SQL Server表。我的查询之一花费了很长时间,因此我正在考虑对其进行优化。查询如下所示:

SELECT [Enroll_Date]
      ,Count(*) AS [Record #]
      ,Count(Distinct UserID) AS [User #]
  FROM UserTable
  GROUP BY [Enroll_Date]

[Enroll_Date]是一个低选择性列,可能值少于50个,而UserID列是高选择性列,其中有2亿多个不同的值。根据我的研究,我相信我应该在这两列上创建非聚集复合索引,并且从理论上讲,高选择性列应该是第一列。但是我不确定我的工作是否可行,因为我在group by子句中使用了低选择性列。

该表没有聚簇索引。


您可以发布实际的执行计划xml(使用pastebin并在此处链接)吗?您正在使用哪个版本的sql server?
金莎(Kin Shah)2016年

3
首先具有高选择性列的索引对于特定查询将是无用的。
ypercubeᵀᴹ

最佳实践是将较高选择性的列用作索引中的第一个键列(通常)。如您所料,在这种情况下,它根本无法帮助您。您可能需要两个索引!首先使用enroll_date而第二次使用user_id会发生什么?
paulbarbin '16

Answers:


12

作为@AaronBertrand解决方案的替代方法(如果您不能或不想创建索引视图),建议您在上创建索引(Enroll_Date, UserID)。如果这种类型的问题在您的表上很常见,那么它甚至应该是您的聚集索引。

我一般不建议将高选择性索引作为一般的“最佳实践”,而是要看哪种索引将使您的查询具有最佳性能。

索引打开(Enroll_Date, UserID)将为您的查询提供具有流聚合的高度优化的,非阻塞的查询计划。

流聚合查询计划

在这种情况下,“非阻塞”表示查询不需要缓冲任何大量的数据(例如,排序或哈希聚合),这意味着(a)立即开始返回行,并且( b)实际上不占用任何工作内存。


有趣,相隔4秒,答案相同。
usr

11

Aarons的答案是一个很好的解决方案。假设您不想采用这种方法,我将回答这个问题。

您发布的查询通常将首先分组(Enroll_Date, UserID),然后再分组(Enroll_Date)。此优化是SQL Server 2012的新增功能。只有一个时,该优化才会生效COUNT DISTINCT

按特定顺序在这两个列上建立索引就(Enroll_Date, UserID)足以获得一个有效的计划,该计划将索引扫描集中到两个连续的流聚合中。相反的顺序将不会启用该计划。

因此,请使用order (Enroll_Date, UserID)。您在这里别无选择。


相隔5秒,解决方法相同。长官,打得好。:)
Daniel Hutmacher '16

@DanielHutmacher OMG,我们将第三次与我们的职位保持一致吗?+1给你!我怎么能给予好评相同的答案?
usr

矩阵故障。:)
Daniel Hutmacher '16

非常感谢你。我正在创建索引,并将在完成后发布改进。服务器版本是AWS上的Microsoft SQL Server 2008 R2,但我想无论如何它仍然是唯一的选择。
Thinkinger

@Thinkinger,如果您不接受Aarons方法,您将有一个艰难的选择:)
usr

11

对于索引视图来说,这听起来像是一个理想的方案,它使您可以在写时间而不是查询时间支付计算和聚合费用。

CREATE VIEW dbo.MyIndexedView
WITH SCHEMABINDING
AS 
  SELECT Enroll_Date, UserID, RawCount = COUNT_BIG(*)
  FROM dbo.UserTable
  GROUP BY Enroll_Date, UserID;
GO

CREATE UNIQUE CLUSTERED INDEX CIX_miv ON dbo.MyIndexedView(Enroll_Date, UserID);

这将花费一些时间来创建,并且当然需要在所有DML操作中进行维护,就像在基表上创建索引一样。

现在,针对该视图的查询将非常相似-视图中的每一行现在代表一个不同的用户/日期组合,因此可以通过单个COUNT(*)计算该图,而基表中的总行数为已经为您进行了部分汇总,现在您只需要使用每个日期的SUM进行累加即可:

SELECT Enroll_Date, 
  [Record #] = SUM(RawCount),
  [User #] = COUNT(*)
FROM dbo.MyIndexedView WITH (NOEXPAND)
GROUP BY Enroll_Date; 

在记住了thisthis之后,添加了NOEXPAND提示。

我可以毫无疑问地告诉您,该查询将比您当前的查询快(但不会快多少),除了极少数情况下,您每个日期只有一个用户(在这种情况下,相同数量的数据将具有待读取),我们知道的列是基表索引中唯一的列。我们无法告诉您,在读取时间提高性能是否值得进行会影响工作负载的写入部分的额外工作-您必须测试一下以权衡取舍(没有索引是免费的)。

而且,如果您经常对Enroll_Date使用相同的通用WHERE子句来定义特定的,明确定义的范围(例如,当前的当前季度或当年),则可以添加匹配的过滤索引以进一步减少该I / O(但总会有一个交易)。

您可能还考虑在基表上放置聚簇索引。这似乎并不是从堆中受益的非常罕见的用例之一。


我刚刚证实了我们的IT知识,看来我无法提出这种观点。但是仍然请您采纳您的建议,它将对其他可以使用它的人有所帮助。
Thinkinger

1
您的IT部门是否认为索引视图与基表上的其他索引或不同索引之间存在显着差异?不是好斗,只是好奇,因为很多人对索引视图有误解。我喜欢将它们视为表上附加的,更细的聚簇索引,但行数较少。
亚伦·伯特兰

此外,@ Thinkinger,索引视图也不是仅EE。索引视图匹配仅适用于EE。您可以使用NOEXPAND直接定位它们。
usr
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.