为啥 SQL Server 执行计划取决于比较顺序

Posted

技术标签:

【中文标题】为啥 SQL Server 执行计划取决于比较顺序【英文标题】:Why SQL Server execution plan depends on comparison order为什么 SQL Server 执行计划取决于比较顺序 【发布时间】:2020-03-28 00:21:37 【问题描述】:

我正在优化 SQL Server 上的查询并遇到了一些我没有预料到的事情。数据库中有一个表tblEvent,在其他列中它有IntegrationEventStateIdModifiedDateUtc。这些列有一个索引:

create index IX_tblEvent_IntegrationEventStateId_ModifiedDateUtc
on dbo.tblEvent (
    IntegrationEventStateId,
    ModifiedDateUtc
)

当我执行以下语句时:

select *
from dbo.tblEvent e
where
    e.IntegrationEventStateId = 1
    or e.IntegrationEventStateId = 2
    or e.IntegrationEventStateId = 5
    or (e.IntegrationEventStateId = 4 and e.ModifiedDateUtc >= dateadd(minute, -5, getutcdate()))

我得到了这个执行计划(注意索引没有被使用):

但是当我执行这个语句时:

select *
from dbo.tblEvent e
where
    1 = e.IntegrationEventStateId
    or 2 = e.IntegrationEventStateId
    or 5 = e.IntegrationEventStateId
    or (4 = e.IntegrationEventStateId and e.ModifiedDateUtc >= dateadd(minute, -5, getutcdate()))

我得到了这个执行计划(注意索引确实被使用了):

这两个语句之间的唯一区别是where 子句中的比较顺序。谁能解释一下为什么我得到不同的执行计划?

更新 1 - 下面是完整的重现脚本

CREATE TABLE dbo.tblEvent
(
   EventId                 INT IDENTITY PRIMARY KEY,
   IntegrationEventStateId INT,
   ModifiedDateUtc         DATETIME,
   OtherCol                CHAR(1),
   index IX_tblEvent_IntegrationEventStateId_ModifiedDateUtc(IntegrationEventStateId, ModifiedDateUtc)
);

INSERT INTO dbo.tblEvent
SELECT TOP 356525 3,
                  DATEADD(SECOND, ROW_NUMBER() OVER (ORDER BY @@SPID)%63424, GETUTCDATE()),
                  'A'
FROM   sys.all_objects o1,
       sys.all_objects o2;

UPDATE STATISTICS dbo.tblEvent WITH FULLSCAN


select *
from dbo.tblEvent e 
where
    e.IntegrationEventStateId = 1
    or e.IntegrationEventStateId = 2
    or e.IntegrationEventStateId = 5
    or (e.IntegrationEventStateId = 4 and e.ModifiedDateUtc >= dateadd(minute, -5, getutcdate()))


select *
from dbo.tblEvent e
where
    1 = e.IntegrationEventStateId
    or 2 = e.IntegrationEventStateId
    or 5 = e.IntegrationEventStateId
    or (4 = e.IntegrationEventStateId and e.ModifiedDateUtc >= dateadd(minute, -5, getutcdate()))

更新 2 - 原始表的 DDL

CREATE TABLE [dbo].[tblEvent]
(
[EventId] [int] NOT NULL IDENTITY(1, 1),
[EventTypeId] [int] NOT NULL,
[ScorecardId] [int] NULL,
[ScorecardAreaId] [int] NULL,
[AreaId] [int] NULL,
[ScorecardTopicId] [int] NULL,
[TopicId] [int] NULL,
[ScorecardRequirementId] [int] NULL,
[RequirementId] [int] NULL,
[DocumentId] [int] NULL,
[FileId] [int] NULL,
[TopicTitle] [nvarchar] (100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[ScorecardTopicStatus] [nvarchar] (255) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[RequirementText] [nvarchar] (500) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[ScorecardRequirementStatus] [nvarchar] (255) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[DocumentName] [nvarchar] (260) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[CreatedByUserId] [int] NOT NULL,
[CreatedByUserSessionId] [int] NOT NULL,
[CreatedDateUtc] [datetime2] (4) NOT NULL CONSTRAINT [DF__tblEvent__Create__0737E4A2] DEFAULT (sysutcdatetime()),
[CreatedDateLocal] [datetime2] (4) NOT NULL CONSTRAINT [DF__tblEvent__Create__082C08DB] DEFAULT (sysdatetime()),
[ModifiedByUserId] [int] NOT NULL,
[ModifiedByUserSessionId] [int] NOT NULL,
[ModifiedDateUtc] [datetime2] (4) NOT NULL CONSTRAINT [DF__tblEvent__Modifi__09202D14] DEFAULT (sysutcdatetime()),
[ModifiedDateLocal] [datetime2] (4) NOT NULL CONSTRAINT [DF__tblEvent__Modifi__0A14514D] DEFAULT (sysdatetime()),
[IsDeleted] [bit] NOT NULL,
[RowVersion] [timestamp] NOT NULL,
[ScorecardRequirementPriority] [nvarchar] (255) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[AffectedUserId] [int] NULL,
[UserId] [int] NULL,
[CorrelationId] [nvarchar] (255) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[IntegrationEventStateId] [int] NULL,
[IntegrationEventId] [nvarchar] (255) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[IntegrationEventContent] [nvarchar] (max) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[IntegrationEventType] [nvarchar] (255) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[IntegrationEventTryCount] [int] NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
ALTER TABLE [dbo].[tblEvent] ADD CONSTRAINT [PK_dbo.tblEvent] PRIMARY KEY CLUSTERED ([EventId]) ON [PRIMARY]
GO
CREATE NONCLUSTERED INDEX [IX_tblEvent_IntegrationEventStateId_ModifiedDateUtc] ON [dbo].[tblEvent] ([IntegrationEventStateId], [ModifiedDateUtc]) ON [PRIMARY]
GO
ALTER TABLE [dbo].[tblEvent] ADD CONSTRAINT [FK_dbo.tblEvent_dbo.tblEventType_EventTypeId] FOREIGN KEY ([EventTypeId]) REFERENCES [dbo].[tblEventType] ([EventTypeId])
GO
ALTER TABLE [dbo].[tblEvent] ADD CONSTRAINT [FK_dbo.tblEvent_dbo.tblIntegrationEventState_IntegrationEventStateId] FOREIGN KEY ([IntegrationEventStateId]) REFERENCES [dbo].[tblIntegrationEventState] ([IntegrationEventStateId])
GO

【问题讨论】:

哪个版本的 SQL Server 和什么数据库兼容级别?不同的执行计划程序用于不同的版本(和兼容性级别),因此这种行为差异将非常有趣。它是可重复的,还是规划者只是采取了不同的策略,因为索引的统计数据发生了变化,从而使其在成本评估方面具有更好的基数? 嗨@AlwaysLearning。 SQL Server 版本:Microsoft SQL Server 2016 (SP1-CU5) (KB4040714) - 13.0.4451.0 (X64) Sep 5 2017 16:12:34 Copyright (c) Microsoft Corporation Standard Edition (64-bit) on Windows Server 2012 R2 Standard 6.3 <X64> (Build 9600: ) (Hypervisor)。数据库兼容级别:SQL Server 2016 (130)。该行为是可重复的,我多次运行查询并始终获得上述执行计划。 你能提供两个执行计划的 XML 吗?例如到brentozar.com/pastetheplan @MartinSmith 完成,这里是链接:brentozar.com/pastetheplan/?id=ry40ir3I8, brentozar.com/pastetheplan/?id=SJcShB2LI 它可能刚刚缓存了一个错误的计划,当您更改语法时,它决定了一个新的(更好的)计划。如果将where 1=1 添加到第一个计划会发生什么? 【参考方案1】:

这里有很多问题,但最重要的是基数估计 (CE)。

较新的(“默认”)CE 模型在尝试针对没有匹配步骤的直方图计算选择性时,很难使用谓词。

例如,初始基数估计返回的选择性为 1:

select *
from dbo.tblEvent e
where
    1 = e.IntegrationEventStateId
    or 2 = e.IntegrationEventStateId
    or 5 = e.IntegrationEventStateId
    or (4 = e.IntegrationEventStateId and e.ModifiedDateUtc >= dateadd(minute, -5, getutcdate()))

如使用跟踪标志 3604 和 2363 所示:

Begin selectivity computation

Input tree:

  LogOp_Select
      CStCollBaseTable(ID=1, CARD=356525 TBL: dbo.tblEvent AS TBL: e)
      ScaOp_Logical x_lopOr
          ScaOp_Comp x_cmpEq
              ScaOp_Identifier QCOL: [e].IntegrationEventStateId
              ScaOp_Const TI(int,ML=4) XVAR(int,Not Owned,Value=1)
          ScaOp_Comp x_cmpEq
              ScaOp_Identifier QCOL: [e].IntegrationEventStateId
              ScaOp_Const TI(int,ML=4) XVAR(int,Not Owned,Value=2)
          ScaOp_Comp x_cmpEq
              ScaOp_Identifier QCOL: [e].IntegrationEventStateId
              ScaOp_Const TI(int,ML=4) XVAR(int,Not Owned,Value=5)
          ScaOp_Logical x_lopAnd
              ScaOp_Comp x_cmpGe
                  ScaOp_Identifier QCOL: [e].ModifiedDateUtc
                  ScaOp_Identifier COL: ConstExpr1001 
              ScaOp_Comp x_cmpEq
                  ScaOp_Identifier QCOL: [e].IntegrationEventStateId
                  ScaOp_Const TI(int,ML=4) XVAR(int,Not Owned,Value=4)

Plan for computation:

  CSelCalcCombineFilters_ExponentialBackoff (OR)
      CSelCalcCombineFilters_ExponentialBackoff (AND)
          CSelCalcColumnInInterval
              Column: QCOL: [e].ModifiedDateUtc
          CSelCalcColumnInInterval
              Column: QCOL: [e].IntegrationEventStateId
      CSelCalcColumnInInterval
          Column: QCOL: [e].IntegrationEventStateId

Loaded histogram for column QCOL: [e].ModifiedDateUtc from stats with id 3
Loaded histogram for column QCOL: [e].IntegrationEventStateId from stats with id 2

Selectivity: 1

Stats collection generated: 

  CStCollFilter(ID=2, CARD=356525)
      CStCollBaseTable(ID=1, CARD=356525 TBL: dbo.tblEvent AS TBL: e)

End selectivity computation

当基于成本的优化开始时,输入树的形式略有不同,要求 CE 计算更简单谓词的选择性:

Begin selectivity computation

Input tree:

  LogOp_Select
      CStCollBaseTable(ID=1, CARD=356525 TBL: dbo.tblEvent AS TBL: e)
      ScaOp_Logical x_lopOr
          ScaOp_Comp x_cmpEq
              ScaOp_Identifier QCOL: [e].IntegrationEventStateId
              ScaOp_Const TI(int,ML=4) XVAR(int,Not Owned,Value=1)
          ScaOp_Comp x_cmpEq
              ScaOp_Identifier QCOL: [e].IntegrationEventStateId
              ScaOp_Const TI(int,ML=4) XVAR(int,Not Owned,Value=2)
          ScaOp_Comp x_cmpEq
              ScaOp_Identifier QCOL: [e].IntegrationEventStateId
              ScaOp_Const TI(int,ML=4) XVAR(int,Not Owned,Value=5)

Plan for computation:

  CSelCalcColumnInInterval
      Column: QCOL: [e].IntegrationEventStateId

Selectivity: 1

Stats collection generated: 

  CStCollFilter(ID=3, CARD=356525)
      CStCollBaseTable(ID=1, CARD=356525 TBL: dbo.tblEvent AS TBL: e)

End selectivity computation

这相当于:

SELECT *
FROM dbo.tblEvent AS TE 
WHERE TE.IntegrationEventStateId IN (1, 2, 5);

在这两种情况下,CE 都会评估 100% 的行将匹配,尽管值 1、2 或 5 没有直方图步骤(样本数据只有值 3)。很容易将此归咎于 CSelCalcColumnInInterval 计算器,因为它似乎将 1, 2, 5 视为单个区间 1:5。

通常情况下,“旧版”CE 在这里做得更好(更详细),因此您应该会发现以下提示会产生更好的计划:

OPTION (USE HINT ('FORCE_LEGACY_CARDINALITY_ESTIMATION'));

使用重现数据,这会产生一个希望的单一搜索和键查找。

请注意,seek 执行四个搜索操作,每个不相交谓词一个。

[1] Seek Keys[1]: Prefix: IntegrationEventStateId = 1
[2] Seek Keys[1]: Prefix: IntegrationEventStateId = 2
[3] Seek Keys[1]: Prefix: IntegrationEventStateId = 4, Start: ModifiedDateUtc >= dateadd(minute,(-5),getutcdate())
[4] Seek Keys[1]: Prefix: IntegrationEventStateId = 5

与原来的 CE 相比,新 CE 的设计更易于预测,并且更易于维护/扩展。 “旧版”有一些螺栓固定在上面,并在很长一段时间内进行了改进。这种复杂性有好处也有陷阱。较新的 CE 在某种程度上预计会出现回归和较低质量的估计。这应该会随着时间的推移而改善,但我们还没有做到。我会将此处显示的行为视为计算器的限制。也许他们会解决它。

见Optimizing Your Query Plans with the SQL Server 2014 Cardinality Estimator。


为什么计划形状取决于文本表示的问题更多的是一个次要问题。编译过程确实包含将谓词重写为规范化形式的逻辑(例如规则SelPredNorm),并且两个 repro 查询都成功地重写为同一棵树。这样做是为了各种内部目的,包括索引和计算列匹配,并使逻辑简化更容易处理。

很遗憾,重写后的表单仅在基于成本的优化之前使用。基于成本的优化器的输入保留了原始查询中存在的文本顺序差异。我相信这是故意的,这样做是为了防止意外的计划更改。人们有时会以略微不同且不寻常的方式编写查询来实现特定的计划形状。如果优化器突然开始挫败这些逻辑上多余的尝试,人们会感到不安。对于查询存储和更有效的计划强制等问题,这可以说不是问题,但这些都是相对较新的创新。

换句话说,计划是不同的,因为人们过去依靠不同的文本产生不同的计划,而现在改变它会造成太大的破坏。

【讨论】:

【参考方案2】:

这很有趣且不寻常,我不知道为什么 QO 认为搜索索引 4 次会比仅在其中一个查询中进行扫描便宜。

有时当您得到奇怪的计划时,最好的解决方案是更改索引和查询,以便更容易获得好的计划。

可能是这样的

CREATE TABLE dbo.tblEvent
(
   EventId                 INT IDENTITY PRIMARY KEY,
   IntegrationEventStateId INT,
   ModifiedDateUtc         DATETIME,
   OtherCol                CHAR(1),
   index IX_tblEvent_IntegrationEventStateId_ModifiedDateUtc(IntegrationEventStateId, ModifiedDateUtc) 
     include (OtherCol) 
     where IntegrationEventStateId in (1,2,4,5)
);

然后

select *
from dbo.tblEvent e 
where
    e.IntegrationEventStateId in (1,2,4,5)
    and (e.IntegrationEventStateId <> 4 or e.ModifiedDateUtc >= dateadd(minute, -5, getutcdate()))

【讨论】:

感谢@DavidBrowne-Microsoft,为了消除“脆弱”,我将查询从多个ors 重写为多个union alls。

以上是关于为啥 SQL Server 执行计划取决于比较顺序的主要内容,如果未能解决你的问题,请参考以下文章

sql server 2008同一个语句查询,为啥时快时慢

sql server 执行计划(execution plan)介绍

sql server 执行计划(execution plan)介绍

SQL Server 执行计划利用统计信息对数据行的预估原理二(为什么复合索引列顺序会影响到执行计划对数据行的预估)

SQL Server 执行计划利用统计信息对数据行的预估原理二(为什么复合索引列顺序会影响到执行计划对数据行的预估)

SQL*Plus BREAK 的行为如何/为啥取决于列顺序?