简单条件分解查询优化器及其性能

Posted

技术标签:

【中文标题】简单条件分解查询优化器及其性能【英文标题】:Simple condition break down query optimizer and its performance 【发布时间】:2015-10-06 11:39:58 【问题描述】:

我有一个简单的查询:

select top 10 * 
FROM Revision2UploadLocations r2l
inner join Revisions r on r2l.RevisionId = r.Id
INNER JOIN [Databases] [D] on [R].[DatabaseId] = [D].[Id]
INNER JOIN [SqlServers] [S] on [D].[InstanceId] = [S].[Id]
where --r.ValidationStatus in (2, 3) and 
r2l.[ChecksumWasSent] = 0 AND r2l.Status = 2

这个查询通常执行0.5s:

但是相同的未注释条件的查询被执行了 5s (!!!) 并且有一个非常奇怪的执行计划 (Revisions 和 SqlServers 虽然没有链接列和最具选择性的条件 "r2l.[ChecksumWasSent] = 0 AND r2l.Status = 2" 在查询处理结束时执行:

ValidationStatus 是普通的 int 非空列。 列 Revision2UploadLocations.RevisionId、Revisions.DatabaseId、Databases.InstanceId 已编入索引。 以下是表格说明:

CREATE TABLE [SqlServers]
(
    [Id] int identity(1,1) NOT NULL CONSTRAINT PK_SqlServers PRIMARY KEY,
...
)

CREATE TABLE [Databases](
    [Id] int identity(1,1) NOT NULL CONSTRAINT PK_Databases PRIMARY KEY,
    [InstanceId] int NOT NULL,
    [Name] nvarchar(128) NOT NULL,
...
    CONSTRAINT FK_Databases_SqlServers FOREIGN KEY ([InstanceId]) REFERENCES [SqlServers]([Id])
)

CREATE INDEX [IX_Databases_DatabaseId] ON [Databases] ([InstanceId] ASC)

CREATE TABLE [Revisions]
(
    [Id] int identity(1, 1) NOT NULL,
    [DatabaseId] int NOT NULL,
    [BackupStatus] tinyint NOT NULL,
    [ValidationStatus] tinyint NOT NULL,
...
    CONSTRAINT PK_Revisions PRIMARY KEY([Id]),
    CONSTRAINT FK_Revisions_Databases FOREIGN KEY ([DatabaseId]) REFERENCES [Databases]([Id])
)

CREATE INDEX [IX_Revisions_DatabaseId] ON [Revisions] ([DatabaseId] ASC)

CREATE TABLE [Revision2UploadLocations]
(
    [Id] int NOT NULL IDENTITY (1, 1) CONSTRAINT PK_Revision2UploadLocations PRIMARY KEY,
    [Status] int NOT NULL,
    RevisionId int NOT NULL,
    [ChecksumWasSent] bit NOT NULL,
    CONSTRAINT FK_r2l_Revisions FOREIGN KEY ([RevisionId]) REFERENCES [Revisions]([Id])
)

CREATE INDEX [IX_Revision2UploadLocations_RevisionId] ON [Revision2UploadLocations] ([RevisionId] ASC)

如何提高此查询的性能?

编辑现在我有更多细节: 一些表(SqlServers 和 Databases)有 1-10 条记录,但 Revisions 和 Revision2UploadLocations)有 500K+ 记录,因此查询优化决定使用全扫描而不是索引搜索小表并优先使用。 Query Performance Tuning (SQL Server Compact):

小表格是一种内容适合一个或几个数据页的表格。避免索引非常小的表,因为执行表扫描通常更有效。

作为临时解决方案,我尝试使用查询提示 FORCE ORDER:Query Hint (SQL Server Compact) 响应时间从 5 秒减少到 0.5 秒。

但我认为这不是一个好的解决方案。

【问题讨论】:

【参考方案1】:

Geoffrey 的解决方案没有给您预期的结果。 第一条语句选择 10 行,不保证它们的 r.ValidationStatus 为 2 或 3。所以最后,您可以获得少于 10 行(甚至根本没有行)。 我认为您可以将查询重写为:

SELECT top 10 *     
FROM Revisions r
  INNER JOIN Revision2UploadLocations r2l 
    ON r2l.RevisionId = r.Id
      AND r2l.[ChecksumWasSent] = 0 
      AND r2l.Status = 2
  INNER JOIN [Databases] [D] on [D].[Id] = [R].[DatabaseId]
  INNER JOIN [SqlServers] [S] on [S].[Id] = [D].[InstanceId]
WHERE r.ValidationStatus in (2, 3)

如果 r2l.[ChecksumWasSent] 数据类型是位(布尔值):

0大于1,可以在RevisionId + Status上创建索引 1 比 0 多得多,您可以创建和定义 RevisionId + ChecksumWasSent + Status

【讨论】:

我已经尝试过这个变体和其他一些类似的变体,但是查询优化器足够聪明,可以检测这些查询的身份,但不幸的是,它没有那么聪明,无法构建最佳计划。我会尝试推荐的索引,谢谢! 你有Revision.ValidationStatus 的索引吗?它也可以提供帮助。你有多少种不同的状态?不同状态之间的行分布是什么? 我已经尝试过索引 (RevisionId, Status) 和 (RevisionId, ChecksumWasSent, Status)。这很奇怪,但是 QA 仍然使用完整扫描来进行修订,然后才索引搜索 Revision2UploadLocations。 Revision2UploadLocations 的条件更具选择性。 ValidationStatus 具有以下值密度:1 (811)、2(441252)、4 (4)。 RevisionsRevision2UploadLocations中有多少行?你需要所有表的所有列 (*) 吗?如果没有,您可以在表别名前面加上星号 如果 Revision2UploadLocations 中的行数很多,而 ChecksumWasSent=0 的行数比 ChecksumWasSent=1 的行数少,并且如果您的 SqlServer发布/版本支持它,您可以在 RevisionId + Status 上创建过滤索引,其中 ChecksumWasSent=0【参考方案2】:

我过去发现,如果我首先将查询的第一部分插入到临时表中,并带有要进一步过滤的字段(“ValidationStatus”],然后查询你的临时表,性能/速度会很大更好的。 所以最初的查询是这样的:

select * 
into #tmp
FROM Revision2UploadLocations r2l
inner join Revisions r on r2l.RevisionId = r.Id
INNER JOIN [Databases] [D] on [R].[DatabaseId] = [D].[Id]
INNER JOIN [SqlServers] [S] on [D].[InstanceId] = [S].[Id]
where --r.ValidationStatus in (2, 3) and 
r2l.[ChecksumWasSent] = 0 AND r2l.Status = 2

那么最终的选择是:

select * from #tmp 
where ValidationStatus in (2,3)

不需要索引,而且我知道优化器并不总是有效的奇怪之处,但这种方法在过去曾多次对我有用。

【讨论】:

我编辑了代码,因为我最初是从给定的 OPs 代码中复制和粘贴的。 “选择前 10 个 *”应该刚刚选择 *。但无论如何,关键是在将 (2,3) 中的 ValidationStatus 插入临时表之前,不要对其进行过滤。 对不起,但我认为这种方式类似于 FORCE ORDER 选项。我们严格设置表格处理的顺序。对我来说,正确的方法是向查询优化器提供更多信息并让他决定如何处理表。

以上是关于简单条件分解查询优化器及其性能的主要内容,如果未能解决你的问题,请参考以下文章

MySql性能优化查询优化

高性能mysql 第6章 查询性能优化

Oracle查询速度优化问题

数据库查询优化器的艺术:原理解析与SQL性能优化

提高sql server查询优化器结果的方法

使用多个连接和条件优化 SQL 查询