为什么 SQL 服务器突然决定使用这么糟糕的执行计划?

Why did SQL Server suddenly decide to use such a terrible execution plan?

背景

我们最近遇到了查询计划问题 sql 服务器正在我们较大的 table 之一(大约 175,000,000 行)上使用。 table 的列和索引结构已经 5 年多没有改变了。

table 和索引如下所示:

create table responses (
    response_uuid uniqueidentifier not null,
    session_uuid uniqueidentifier not null,
    create_datetime datetime not null,
    create_user_uuid uniqueidentifier not null,
    update_datetime datetime not null,
    update_user_uuid uniqueidentifier not null,
    question_id int not null,
    response_data varchar(4096) null,
    question_type_id varchar(3) not null,
    question_length tinyint null,
    constraint pk_responses primary key clustered (response_uuid),
    constraint idx_responses__session_uuid__question_id unique nonclustered (session_uuid asc, question_id asc) with (fillfactor=80),
    constraint fk_responses_sessions__session_uuid foreign key(session_uuid) references dbo.sessions (session_uuid),
    constraint fk_responses_users__create_user_uuid foreign key(create_user_uuid) references dbo.users (user_uuid),
    constraint fk_responses_users__update_user_uuid foreign key(update_user_uuid) references dbo.users (user_uuid)
)

create nonclustered index idx_responses__session_uuid_fk on responses(session_uuid) with (fillfactor=80)

性能不佳的查询(约 2.5 分钟,而不是正常的 <1 秒性能)如下所示:

SELECT 
[Extent1].[response_uuid] AS [response_uuid], 
[Extent1].[session_uuid] AS [session_uuid], 
[Extent1].[create_datetime] AS [create_datetime], 
[Extent1].[create_user_uuid] AS [create_user_uuid], 
[Extent1].[update_datetime] AS [update_datetime], 
[Extent1].[update_user_uuid] AS [update_user_uuid], 
[Extent1].[question_id] AS [question_id], 
[Extent1].[response_data] AS [response_data], 
[Extent1].[question_type_id] AS [question_type_id], 
[Extent1].[question_length] AS [question_length]
FROM [dbo].[responses] AS [Extent1]
WHERE [Extent1].[session_uuid] = @f6_p__linq__0;

(查询由entity framework生成并使用sp_executesql执行)

性能不佳期间的执行计划如下所示:

一些数据背景- 运行上面的查询绝不会 return 超过 400 行。换句话说,过滤 session_uuid 确实减少了结果集。

定期维护的一些背景知识 - 每周计划作业 运行 重建数据库的统计信息并重建 table 的索引。作业 运行 是一个如下所示的脚本:

alter index all on responses rebuild with (fillfactor=80)

性能问题的解决方案是 运行 在此 table 上重建索引脚本(以上)。

其他可能相关的信息花絮...自上次重建索引以来,数据分布根本没有改变。查询中没有连接。我们是一家 SAAS 商店,我们有 50 - 100 个具有完全相同模式的实时生产数据库,一些数据更多,一些数据更少,所有这些数据库都在几个 sql 服务器上执行相同的查询。

问题:

可能发生什么事情会让 sql 服务器开始在这个特定的数据库中使用这个糟糕的执行计划?

请记住,只需在 table 上重建索引即可解决问题。

也许更好的问题是 "what are the circumstances where sql server would stop using an index?"

另一种看待它的方式是"why would the optimizer not use an index that was rebuilt a few days ago and then start using it again after doing an emergency rebuild of the index once we noticed the bad query plan?"

原因很简单:优化器改变了对最佳计划的看法。这可能是由于数据分布的细微变化(或其他原因,例如 join 键中的类型不兼容)。我希望有一种工具不仅可以给出查询的执行计划,还可以显示您与另一个执行计划的接近程度的阈值。或者是一种工具,可以让您隐藏执行计划并在同一查询开始使用不同的计划时发出警报。

我不止一次问过自己这个完全相同的问题。您有一个连续几个月 运行 每晚都在运行的系统。它使用非常复杂的查询处理大量数据。然后,有一天,你早上进来,通常在 11:00 p.m 之前完成的工作。还是运行。啊啊啊!

我们提出的解决方案是对失败的连接使用显式 join 提示。 (option (merge join, hash join))。我们还开始保存所有复杂查询的执行计划,因此我们可以比较一晚与下一晚的变化。最后,这是学术兴趣多于实际兴趣——当计划改变时,我们已经遭受了糟糕的执行计划的折磨。

这是我最讨厌的 SQL 问题之一 - 由于这个问题我遇到过不止一次失败 - 一旦一个已经工作了几个月的查询从 ~250 毫秒变为超过超时阈值当然,导致制造系统在凌晨 3 点崩溃。花了一些时间来隔离查询并将其粘贴到 SSMS 中,然后开始将其分解 - 但我所做的一切只是 "worked"。最后,我只是将短语“AND 1=1”添加到查询中,这让事情再次运行了几周 - 最后的补丁是 "blind" 优化器 - 基本上将所有传递的参数复制到本地参数中。如果查询立即有效,它似乎会继续有效。

对我来说,来自 MS 的一个相当简单的修复是:如果这个查询已经被分析过并且 运行 上次就好了,并且相关的统计数据没有显着变化(例如想出一些tables 或新索引等的各种变化因素),并且 "optimizer" 决定用新的执行计划来增加趣味性,如果新的和改进的计划花费的时间超过 X 倍数呢?的旧计划,我中止并再次切换回来。我可以理解 table 是否从 100 行变为 100,000,000 行,或者是否删除了一个键索引,但是对于 stable 生产环境,要使查询持续时间跳到慢 100 倍到 1000 倍之间,它不能很难发现这一点,标记计划,然后返回上一个计划。

较新的 SQL 服务器版本有一个很棒的新功能,称为 "Query Store",您可以在其中分析最近的查询性能。

如果您发现查询有时使用 "fast" 计划,有时使用 "slow" 计划 - 您可以强制执行快速计划。请参阅屏幕截图。 "yellow circle" 计划是最快的,但 "blue square" 计划不是(它在 "duration" 图表上更高)