我在 MS SQL Server 2014(以及 2014SP3)上运行以下查询
SET NOCOUNT ON
CREATE TABLE #GUIDs(
PartyNames_GUID UNIQUEIDENTIFIER NULL,
Party_GUID UNIQUEIDENTIFIER NULL,
FirstName NVARCHAR(255) NULL
)
insert #GUIDs(Party_GUID)
select top 1 Party_GUID
FROM Party a
join PartyNames b on a.Party_ID = b.Party_ID
--Give the optimizer all kinds of choices.
create index i1 on #GUIDs(PartyNames_GUID)
create index i2 on #GUIDs(Party_GUID)
create index i3 on #GUIDs(Party_GUID, PartyNames_GUID)
create index i4 on #GUIDs(PartyNames_GUID,Party_GUID)
update statistics #GUIDs
SELECT PartyNames.PartyNames_ID, PartyNames.LastName, PartyNames.FirstName
FROM Party INNER JOIN PartyNames ON PartyNames.Party_ID = Party.Party_ID
INNER JOIN #GUIDs ON Party.Party_GUID = #GUIDs.Party_GUID --Hard Match on Party_GUID
AND
(#GUIDs.PartyNames_GUID IS NULL OR PartyNames.PartyNames_GUID = #GUIDs.PartyNames_GUID ) --Optional Match
AND
(#GUIDs.FirstName IS NULL OR PartyNames.FirstName = #GUIDs.FirstName ) --Optional Match
drop table #GUIDs
Party 和 PartyNames 表在它们的主 _ID 上都有一个聚集索引,在它们各自的 GUID 上有一个非聚集唯一索引,而 PartyNames 在来自 Party 的外键上有一个索引。这些表的 DDL 包含在末尾,以免混淆问题的描述。Party 有大约 1.9M 行,PartyNames 有 1.3M。PartyNames 每个 Party 最多可以有 3 条记录。上面的查询需要几百毫秒才能运行。但相同的查询在 SQL Server 2012、2016 和 2019 上运行时间为 15 毫秒或更短。所有版本的架构都相同,完全相同的数据在 BCP 完成后和运行查询之前更新。这是 2014 年执行计划的外观 这是其他 SQL Server 版本的执行计划,2012、2016、2019
为什么 2014 年会产生如此糟糕的计划,它扫描 PartyNames 的主键而不是寻找
[PartyNames].[ix_PartyNames_Party_ID?或者更确切地说,高于和低于 2014 的版本如何设法提出一个好的计划,而 2014 没有?其他版本生成的计划显然更好,因为它消耗更少的 CPU 和更少的 IO。是 2014 年的某些服务器设置导致了这种情况吗?SQL Server 2014 优化器的成本计算与之前或之后的版本有很大不同吗?任何帮助或指针表示赞赏。我不愿意认为这是 SQL Server 2014 中的一个缺点。有趣的是,即使记录数减少到几千条,SQL Server 2014 仍会继续生成相同的错误计划。但是删除最后一个 AND 子句 (#GUIDs.FirstName IS NULL OR PartyNames.FirstName = #GUIDs.FirstName ) 会导致 SQL Server 2014 生成与其他版本相同的良好计划。这当然是人为的复制。在 MS SQL Server 2014 上运行我们产品的客户抱怨性能不佳,而在其他版本上则没有。对问题进行故障排除会导致这种简单的再现。
这是用于模式生成的 DDL
CREATE TABLE [dbo].[Party](
[Party_ID] [int] IDENTITY(1,1) NOT NULL,
[Party_GUID] [uniqueidentifier] ROWGUIDCOL NOT NULL,
CONSTRAINT [pk_Party_Party_ID] PRIMARY KEY CLUSTERED ( [Party_ID] ASC),
CONSTRAINT [uq_Party_Party_GUID] UNIQUE NONCLUSTERED ( [Party_GUID] ASC)
)
GO
ALTER TABLE [dbo].[Party] ADD CONSTRAINT [df_Party_Party_GUID] DEFAULT (newid()) FOR [Party_GUID]
GO
CREATE TABLE [dbo].[PartyNames](
[PartyNames_ID] [int] IDENTITY(1,1) NOT NULL,
[PartyNames_GUID] [uniqueidentifier] ROWGUIDCOL NOT NULL,
[Party_ID] [int] NOT NULL,
[FirstName] [nvarchar](255) NULL,
[MiddleName] [nvarchar](255) NULL,
[LastName] [nvarchar](255) NULL,
CONSTRAINT [pk_PartyNames_PartyNames_ID] PRIMARY KEY CLUSTERED ([PartyNames_ID] ASC),
CONSTRAINT [uq_PartyNames_PartyNames_GUID] UNIQUE NONCLUSTERED ([PartyNames_GUID] ASC)
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[PartyNames] ADD CONSTRAINT [df_PartyNames_PartyNames_GUID] DEFAULT (newid()) FOR [PartyNames_GUID]
GO
ALTER TABLE [dbo].[PartyNames] WITH NOCHECK ADD CONSTRAINT [fk_PartyNames_Party_ID] FOREIGN KEY([Party_ID])
REFERENCES [dbo].[Party] ([Party_ID])
GO
ALTER TABLE [dbo].[PartyNames] CHECK CONSTRAINT [fk_PartyNames_Party_ID]
GO
CREATE NONCLUSTERED INDEX [ix_PartyNames_Party_ID] ON [dbo].[PartyNames]
(
[Party_ID] ASC
)
GO
您需要启用记录和支持的跟踪标志 4199(启用查询优化器修复)来纠正此问题。
您可以在查询级别启用它来测试它,但这需要系统管理员权限(希望您的应用程序没有)。可以提出一个非常有说服力的论点来全局打开它(作为启动跟踪标志),您可以在此处阅读一些讨论:跟踪标志 4199 - 全局启用?
我能够使用以下测试数据重现您的问题:
获取测试查询的估计计划会产生错误的计划:
优化器选择扫描聚簇索引,
dbo.PartyNames
因为出于某种原因,它认为其中的每一行都dbo.PartyNames
将与从中检索的一行匹配dbo.Party
。添加
OPTION (QUERYTRACEON 4199)
到查询的末尾可以纠正这个错误的推理并产生好的计划:有关 TF 4199 的详细信息,请参阅此 KB:SQL Server 查询优化器修补程序跟踪标志 4199 服务模型
另一种解决方案是使用带有跟踪标志 9481 的遗留基数估计器,这也产生了良好的计划。