100% CPU 执行计划错误

Ste*_*old 8 performance execution-plan sql-server-2016 query-performance

由于特定查询使用了错误的执行计划,我遇到了 100% CPU 峰值的大问题。我现在花了几个星期自己解决。

我的数据库

我的示例数据库包含 3 个简化表。

[数据记录仪]

CREATE TABLE [model].[DataLogger](
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [ProjectID] [bigint] NULL,
CONSTRAINT [PK_DataLogger] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
Run Code Online (Sandbox Code Playgroud)

【逆变器】

CREATE TABLE [model].[Inverter](
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [SerialNumber] [nvarchar](50) NOT NULL,
 CONSTRAINT [PK_Inverter] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY],
 CONSTRAINT [UK_Inverter] UNIQUE NONCLUSTERED 
(
    [DataLoggerID] ASC,
    [SerialNumber] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]

ALTER TABLE [model].[Inverter] WITH CHECK
ADD CONSTRAINT [FK_Inverter_DataLogger]
FOREIGN KEY([DataLoggerID])
REFERENCES [model].[DataLogger] ([ID])
Run Code Online (Sandbox Code Playgroud)

[逆变器数据]

CREATE TABLE [data].[InverterData](
    [InverterID] [bigint] NOT NULL,
    [Timestamp] [datetime] NOT NULL,
    [DayYield] [decimal](18, 2) NULL,
 CONSTRAINT [PK_InverterData] PRIMARY KEY CLUSTERED 
(
    [InverterID] ASC,
    [Timestamp] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF)
)
Run Code Online (Sandbox Code Playgroud)

统计和维护

[InverterData]表包含按月垃圾分区的数百万行(在多个 PaaS 实例中不同)。

所有索引器都进行了碎片整理,并且每天/每周都根据需要重建/重新组织所有统计数据。

我的查询

查询是实体框架生成的,也很简单。但我每分钟符文 1,000 次,性能至关重要。

SELECT
[Extent1].[InverterID] AS [InverterID],
[Extent1].[DayYield] AS [DayYield]
FROM [data].[InverterDayData] AS [Extent1]
INNER JOIN [model].[Inverter] AS [Extent2] ON [Extent1].[InverterID] = [Extent2].[ID]
INNER JOIN [model].[DataLogger] AS [Extent3] ON [Extent2].[DataLoggerID] = [Extent3].[ID]
WHERE ([Extent3].[ProjectID] = @p__linq__0)
AND ([Extent1].[Date] = @p__linq__1) OPTION (MAXDOP 1)
Run Code Online (Sandbox Code Playgroud)

MAXDOP 1提示是一个缓慢的计划相同常另一个问题。

“好”计划

在 90% 的时间里,使用的计划是闪电般快速的,看起来像这样:

快速计划

问题

一天之内,好的计划随机变成了一个糟糕而缓慢的计划。

“坏”计划使用10-60分钟,然后改回“好”计划。“坏”计划使 CPU 永久使用 100%。

这是它的外观:

慢计划

到目前为止我尝试过的

我的第一个想法是这Hash Match是坏男孩。所以我用一个新的提示修改了查询。

...Extent1].[Date] = @p__linq__1) OPTION (MAXDOP 1, LOOP JOIN)
Run Code Online (Sandbox Code Playgroud)

LOOP JOIN应强制使用Nested Loop的瞬间Hash Match

结果是 90% 的计划看起来像以前一样。但是这个计划也随机地变成了一个糟糕的计划。

“坏”计划现在看起来像这样(表循环顺序已更改):

计划也很慢

在“新坏”计划期间,CPU 也会达到 100%。

解决方案?

我想到要强制执行“好”计划。但我不知道这是否是个好主意。

计划内是推荐的索引,其中包括所有列。但这将使整个表加倍并减慢高频插入。

请帮我!


更新 1 - 与@James 评论相关

这是两个计划(计划中显示了一些额外的字段,因为它来自真实表):

好计划

错误的计划 1(哈希匹配)

糟糕的计划 2(嵌套循环)

更新 2 - 与@David Fowler 相关的回答

糟糕的计划是随机参数值。所以正常情况下,我@p__linq__1 ='2016-11-26 00:00:00.0000000' @p__linq__0 =20825的洞天和糟糕的计划都以相同的价值出现。

我知道存储过程中的参数嗅探问题以及如何在 SP 中避免它们。您是否有提示我如何避免我的查询出现此问题?

创建推荐的索引将包括所有列。这将使整个表加倍并减慢插入频率,这是非常频繁的。建立一个简单地克隆表的索引不是“感觉”正确的。另外我的意思是将这个大表的数据大小加倍。

更新 3 - 与 @David Fowler 评论相关

它也不起作用,我认为它不能。为了更好地理解,我将向您解释如何调用查询。

假设我在[DataLogger]表中有 3 个实体。在一天中,我一遍又一遍地在往返中调用相同的 3 个查询:

基本查询: ...WHERE ([Extent3].[ProjectID] = @p__linq__0) AND ([Extent1].[Date] = @p__linq__1)

范围:

  1. @p__linq__0 = 1; @p__linq__1 = '2018-01-05 00:00:00.0000000'
  2. @p__linq__0 = 2; @p__linq__1 = '2018-01-05 00:00:00.0000000'
  3. @p__linq__0 = 3; @p__linq__1 = '2018-01-05 00:00:00.0000000'

该参数@p__linq__1始终是相同的日期。但是它会随机选择一个错误的计划,该查询之前使用一个好的计划运行了无数次。同一个参数!

更新 4 - 与 @Nic 评论相关

维护每晚运行,看起来像这样。

指数

如果一个索引碎片超过 5%,它将被重新组织......

ALTER INDEX [{index}] ON [{table}] REORGANIZE

如果索引碎片超过 30%,它将被重建......

ALTER INDEX [{index}] ON [{table}] REBUILD WITH (ONLINE=ON, MAXDOP=1)

如果索引已分区,它将证明碎片并更改每个分区...

ALTER INDEX [{index}] ON [{table}] REBUILD PARTITION = {partitionNr} WITH (ONLINE=ON, MAXDOP=1)

统计数据

如果modification_counter高于 0,所有统计数据都将更新...

UPDATE STATISTICS [{schema}].[{object}] ([{stats}]) WITH FULLSCAN

或在分区..

UPDATE STATISTICS [{schema}].[{object}] ([{stats}]) WITH RESAMPLE ON PARTITIONS({partitionNr})

维护包括所有统计数据,也包括自动生成的统计数据。

例子

小智 3

看看计划,好的和坏的还是有一些区别的。首先要注意的是,好的计划在 InverterDayData 上执行查找,而坏的计划则执行扫描。这是为什么,如果您检查估计的行数,您会发现好的计划预计 1 行,而坏计划预计 6661 行和大约 7000 行。

现在看一下编译后的参数值,

好计划 @p__linq__1 ='2016-11-26 00:00:00.0000000'@p__linq__0 =20825

糟糕的计划 @p__linq__1 ='2018-01-03 00:00:00.0000000' @p__linq__0 = 20686

所以在我看来,这是一个参数嗅探问题,当查询性能不佳时,您将哪些参数值传递到该查询中?

InverterDayData 上的坏计划中有一个索引建议看起来很合理,我会尝试运行它并看看它是否对您有帮助。它可能允许 SQL 对表执行扫描。