为啥 SQL Server 查询优化器有时会忽略明显的聚集主键?

Posted

技术标签:

【中文标题】为啥 SQL Server 查询优化器有时会忽略明显的聚集主键?【英文标题】:Why does SQL Server query optimizer sometimes overlook obvious clustered primary key?为什么 SQL Server 查询优化器有时会忽略明显的聚集主键? 【发布时间】:2016-06-05 01:54:06 【问题描述】:

我一直在摸索这个问题。

我在具有id 作为聚集整数主键 的表上运行简单的select count(id),SQL 优化器完全忽略其查询执行计划中的主键,支持在日期字段.... ???

实际表格:

CREATE TABLE [dbo].[msgr](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [dt] [datetime2](3) NOT NULL CONSTRAINT [DF_msgr_dt]  DEFAULT (sysdatetime()),
    [uid] [int] NOT NULL,
    [msg] [varchar](7000) NOT NULL CONSTRAINT [DF_msgr_msg]  DEFAULT (''),
    [type] [tinyint] NOT NULL,
    [cid] [int] NOT NULL CONSTRAINT [DF_msgr_cid]  DEFAULT ((0)),
    [via] [tinyint] NOT NULL,
    [msg_id] [bigint] NOT NULL,
 CONSTRAINT [PK_msgr] PRIMARY KEY CLUSTERED 
(
    [id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

请问这是什么原因?

【问题讨论】:

SQL Server 认为二级索引在性能方面更好用。可能是因为聚集索引需要在叶级读取数据页。 @GordonLinoff 所以不用担心? 任何非聚集索引(不包括过滤的)中的记录数将与聚集索引中的相同。然后可以使用 smallest 索引(按分配的页数)计算并得到正确的答案,花费更少的 IO。发生这种情况是因为 1) 没有 WHERE 子句和 2) id 是聚集索引键(可能是主键)。 count(*) 也可以利用相同的优化。 关于 2) 的重要之处在于它 id 不可为空,因此 count(id) 与计数记录相同 (count(*))。 @RemusRusanu 请确认您是说 count(*) 应该比 count(id) 更有效 【参考方案1】:

1) 在我看来,这里的关键点是对于聚集表(具有聚集索引的表=主要数据结构=是存储表数据的数据结构=聚集索引是表本身)每个非聚集索引还包括聚集索引的键。这意味着

CREATE [UNIQUE] NONCLUSTERED INDEX bla 
ON [dbo].[msgr] (uid) 

基本一样
CREATE [UNIQUE] NONCLUSTERED INDEX bla 
ON [dbo].[msgr] (uid) 
INCLUDE (id) -- id = key of clustered index

因此,对于这样的表,叶子页上非聚集索引的每条记录还包括聚集索引的键。这样,在每个非聚集索引和每个叶记录中,SQL Server 还存储了某种指向主数据结构的指针。

2) 这意味着SELECT COUNT(id) FROM dbo.msgr 可以使用 CI 执行,也可以使用 NCI 执行,因为两个索引都包含 id(聚集索引的键)列。

作为本主题中的辅助说明,因为IDENTITY 属性(对于id 列)表示必填列(NOT NULL),所以COUNT(id)COUNT(*) 相同。此外,这意味着COUNT(msg_id)(也是强制性/NOT NULL)列与COUNT(*) 相同。因此,SELECT COUNT(msg_id) FROM dbo.msgr 的执行计划很可能会使用相同的 NCI(例如 bla)。

3) 非聚集索引的大小比聚集索引小。这也意味着更少的 IO => 从性能的角度来看,使用 NCI 比使用 CI 更好。

我会做以下简单的测试:

SET STATISTICS IO ON;
GO

SELECT COUNT(id)
FROM dbo.[dbo].[msgr] WITH(INDEX=[bla]) -- It forces usage of NCI
GO

SELECT COUNT(id)
FROM dbo.[dbo].[msgr] WITH(INDEX=[PK_msgr]) -- It forces usage of CI
GO

SET STATISTICS IO OFF;
GO

如果msgr 表中有大量数据,则STATISTICS IO 将显示不同的LIO(逻辑 IO),用于 NCI 查询的 LIO 较少

【讨论】:

我现在将检查这些STATISTICS IO 输出。您提出了一个有趣的观点,所以我们需要随着数据量的增长而改变我们的选择查询?我从来没有想过这个 没有。我的观点是,当行数很少时,no diff |就 LIO 而言,CI 和 NCI 执行计划之间只有一个小差异。【参考方案2】:

SQL Server 有一个非常好的优化器。如果它选择一个非聚集索引,那么这可能是最好的。这是我的解释。

索引将键列表和中断值存储在辅助树状数据结构中。对于非聚集索引,索引的叶子是记录标识符(指向记录的指针)。

聚集索引没有叶子。数据页本身就是叶子。因此,使用聚集索引计算记录数需要读取所有数据页。老实说,我可能认为有一种方法可以避免读取数据页读取,但读取数据页可能是必要的。

在任何情况下,非聚集索引都不需要读取原始数据页,因为所有用于计数的信息都在索引中。

【讨论】:

感谢您的解释,但我不遵循“所有用于计数的信息都在非聚集索引中”的部分。怎么样? @CharlesOkwuagwu 。 . .因为索引包含指向有效记录的指针。无需在实际数据中达到峰值即可进行计数。 对于非聚集索引,索引的叶子是记录标识符(指向记录的指针)。知道了!谢谢!【参考方案3】:

SQL Server 只是遍历索引的叶子进行计数。它不必去数据。 SQL Server 已选择该索引来计算所有指针以获取计数。

【讨论】:

【参考方案4】:

我在一个以 id 作为聚集整数主键的表上运行简单的 select count(id),SQL 优化器完全忽略了它的查询执行计划中的主键,有利于在日期字段上建立索引...... ???

SQL server 是一个基于成本的优化器,当它选择一个计划时,它会考虑两件事

1.查询的总成本 2.在合理的时间内选择计划。

SQL Server 查询优化器不知道的一件事是缓存中有多少页。所以它总是假设它必须从磁盘读取它们..

现在考虑到上述情况..

SQL 可能已经考虑扫描可能的最窄索引,因为 id 是主键并且它不会为空(count (id) 不包括空值)加上扫描这需要扫描所有大的索引,所以改为选择扫描另一个可能不为空的缩小索引。

【讨论】:

以上是关于为啥 SQL Server 查询优化器有时会忽略明显的聚集主键?的主要内容,如果未能解决你的问题,请参考以下文章

如何帮助 SQL Server 制定更好的计划?

为啥 SQL Server 在 equals 语句中忽略表情符号?

SQL Server的优化器会缓存标量子查询结果集吗

从 SQL Server 查询优化器生成多个脚本

Sql Server 优化 SQL 查询:如何写出高性能SQL语句

SQL Server 查询优化器运行方式