我有 3 个表:Room
、Conference
和Participant
。 Room
有很多Conference
s,Conference
有很多Participant
s。我需要我的查询来显示来自的字段Room
,以及它具有的关联数量,以及每个具有Conferences
的关联数量的总和。这是我为获取此信息而编写的查询的简化版本;首先,我刚刚选择了房间 ID:Participant
Conference
SELECT
SELECT TOP(1000)
rm.[Id]
FROM
[Room] rm
LEFT JOIN (
SELECT
conf.[Id] AS [ConferenceId],
MIN(conf.[Name]) AS [ConferenceName],
MIN(conf.[RoomId]) AS [RoomId],
COUNT(part.[Id]) AS CalcConferenceParticipantCount
FROM
[Conference] conf
LEFT JOIN
[Participant] part on part.[ConferenceId] = conf.[Id]
GROUP BY
conf.[Id]
) confData ON confData.[RoomId] = rm.[Id]
GROUP BY
rm.[Id]
这是非常快的,因为 SQL Server 能够仅从子查询中提取数据Room
并且几乎忽略了子查询(请参见下图中的试验 1 - 试验 4)。然后我在ConferenceName
子查询的字段中添加,以及每个房间的会议数量计数:
SELECT TOP(1000)
rm.[Id],
COUNT(confData.[ConferenceId]) AS CalcRoomConferenceCount,
MIN(confData.[ConferenceName])
FROM
[Room] rm
LEFT JOIN (
SELECT
conf.[Id] AS [ConferenceId],
MIN(conf.[Name]) AS [ConferenceName],
MIN(conf.[RoomId]) AS [RoomId],
COUNT(part.[Id]) AS CalcConferenceParticipantCount
FROM
[Conference] conf
LEFT JOIN
[Participant] part on part.[ConferenceId] = conf.[Id]
GROUP BY
conf.[Id]
) confData ON confData.[RoomId] = rm.[Id]
GROUP BY
rm.[Id]
这大大降低了查询速度,大约降低了 100 倍(请参见下图中的试验 5 - 试验 7)。然后我从子查询中添加了参与者计数,这意味着使用了 2 个级别的聚合函数:
SELECT TOP(1000)
rm.[Id],
COUNT(confData.[ConferenceId]) AS CalcRoomConferenceCount,
MIN(confData.[ConferenceName]),
SUM(confData.[CalcConferenceParticipantCount]) AS CalcRoomParticipantCount
FROM
[Room] rm
LEFT JOIN (
SELECT
conf.[Id] AS [ConferenceId],
MIN(conf.[Name]) AS [ConferenceName],
MIN(conf.[RoomId]) AS [RoomId],
COUNT(part.[Id]) AS CalcConferenceParticipantCount
FROM
[Conference] conf
LEFT JOIN
[Participant] part on part.[ConferenceId] = conf.[Id]
GROUP BY
conf.[Id]
) confData ON confData.[RoomId] = rm.[Id]
GROUP BY
rm.[Id]
这进一步将查询速度减慢了大约 4 倍(参见下图中的试验 8 - 试验 10)。以下是包含 10 次试验数据的客户统计数据:
下面是慢查询的查询计划:https ://www.brentozar.com/pastetheplan/?id=SJpyeec5Q
有没有一种方法可以使这种查询——我计算子查询聚合的聚合——更有效?
我通过查看表中的行数模拟数据,为它们提供均匀的数据分布,并对模式进行猜测:
我对架构所做的最重要的假设是
Id
列是[Conference]
表的主键。考虑到查询计划和涉及的索引名称,这似乎是合理的。在我的机器上,我获得了与您相同的查询计划,但我的起始查询仅占用 163 毫秒的 CPU。我假设差异归结为硬件、数据分布的差异,以及我没有将数据返回给客户端这一事实。
我首先想到的是派生表
GROUP BY
中的不必要项。是表的主键,因此您不需要所有聚合。有了正确的索引(对于这种特殊情况,您已经有了),子查询不一定是坏事。重写你必须删除的内容:confData
Id
GROUP BY
这导致流聚合被进一步下推到计划中:
上传的计划占用 113 毫秒的 CPU。存在相同的运算符,但其中一些运算符处理的行数较少,从而节省了时间。您可以通过在
[Conference]
with上定义覆盖索引Id
作为索引键来提高此查询的效率。这似乎是一件奇怪的事情,但您的聚簇索引扫描占用了总查询时间的 10%,并且可能包括您不需要的列。如果你想让查询更快,你也可以考虑索引视图。当您可以定义一个简单的索引视图来为您执行聚合时,为什么每次都执行聚合?
这将导致在表上执行 DML 时会多一点空间和一点点开销。总的来说,我认为这是索引视图的一个很好的用例。再次重写查询:
SQL Server 同意我的评估,这是一个好主意,CPU 时间下降到 78 ms。
在我的机器上,我能够使查询更快,但是这开始进入有点冒险的优化,因为它可能需要
LOOP JOIN
提示。当您的查询或表中的数据发生变化时,该提示可能不是一个好主意。它也可能不适合您的硬件。这种方法背后的想法是创建一个合适的索引[Conference]
并充分利用TOP
只执行嵌套循环的计划。这是我添加的索引:运行与之前相同的查询并
LOOP JOIN
提示我得到以下计划:该查询只占用了 58 毫秒的 CPU 时间。值得一提的是,我注意到在这个阶段请求实际计划会增加相当多的相对开销。我想到的所有其他可能的优化对于生产来说都是不安全的,所以我会在这里停下来。
最后想一想,您真的要返回 1000 行任意行和最小会议名称吗?这些信息对您的最终用户有用吗?