是否可以提高具有数百万行的窄表的查询性能?

Nat*_*ate 16 performance sql-server optimization query-performance

我有一个查询目前平均需要 2500 毫秒才能完成。我的表很窄,但有 4400 万行。我有什么选择可以提高性能,或者这是否已经达到了最好的效果?

查询

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31'; 
Run Code Online (Sandbox Code Playgroud)

桌子

CREATE TABLE [dbo].[Heartbeats](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [DeviceID] [int] NOT NULL,
    [IsPUp] [bit] NOT NULL,
    [IsWebUp] [bit] NOT NULL,
    [IsPingUp] [bit] NOT NULL,
    [DateEntered] [datetime] NOT NULL,
 CONSTRAINT [PK_Heartbeats] 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]
Run Code Online (Sandbox Code Playgroud)

指数

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
Run Code Online (Sandbox Code Playgroud)

添加额外的索引会有帮助吗?如果是这样,它们会是什么样子?当前的性能是可以接受的,因为查询只是偶尔运行,但我想知道作为一个学习练习,有什么我可以做的更快吗?

更新

当我将查询更改为使用强制索引提示时,查询将在 50 毫秒内执行:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats] WITH(INDEX(CommonQueryIndex))
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' 
Run Code Online (Sandbox Code Playgroud)

添加正确选择的 DeviceID 子句也会达到 50ms 范围:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' AND DeviceID = 4;
Run Code Online (Sandbox Code Playgroud)

如果我添加ORDER BY [DateEntered], [DeviceID]到原始查询,我在 50ms 范围内:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' 
ORDER BY [DateEntered], [DeviceID];
Run Code Online (Sandbox Code Playgroud)

这些都使用我期望的索引 (CommonQueryIndex) 所以,我想我现在的问题是,有没有办法强制在这样的查询中使用这个索引?或者我的表的大小是否超出了优化器,我必须只使用一个ORDER BY或一个提示?

Edw*_*and 15

为什么优化器不适合您的第一个索引:

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
Run Code Online (Sandbox Code Playgroud)

是 [DateEntered] 列的选择性问题。

你告诉我们你的表有 4400 万行。行大小是:

4 个字节,用于 ID,4 个字节用于设备 ID,8 个字节用于日期,1 个字节用于 4 位列。这是 17 字节 + 7 字节开销(标签、空位图、var col 偏移量、col 计数),每行总共 24 字节。

这将粗略地转换为 140k 页。存储这 4400 万行。

现在优化器可以做两件事:

  1. 它可以扫描表(聚集索引扫描)
  2. 或者它可以使用您的索引。对于索引中的每一行,它都需要在聚集索引中进行书签查找。

现在,在某个时刻,为在非聚集索引中找到的每个索引条目在聚集索引中执行所有这些单一查找变得更加昂贵。阈值通常是查找总数应超过总表页数的 25% 到 33%。

所以在这种情况下:140k/25%=35000 行 140k/33%=46666 行。

(@RBarryYoung,35k 是总行数的 0.08%,46666 是 0.10%,所以我认为这就是混乱所在)

因此,如果您的 where 子句将导致 35000 到 46666 行之间的某处。(这是在 top 子句下方!)很可能不会使用您的非聚集索引扫描,而将使用聚集索引扫描。

改变这种情况的唯一方法有两种:

  1. 使您的 where 子句更具选择性。(如果可能的话)
  2. 删除 * 并仅选择几列,以便您可以使用覆盖索引。

现在确定即使使用 select * 也可以创建覆盖索引。然而,这只会为您的插入/更新/删除带来巨大的开销。我们必须更多地了解您的工作负载(读取与写入),以确保这是否是最佳解决方案。

从 datetime 更改为 smalldatetime 会使聚集索引的大小减少 16%,非聚集索引的大小减少 24%。


Dar*_*ait 8

您的 PK 是否有特殊原因?许多人这样做是因为它默认采用这种方式,或者他们认为 PK 必须是集群的。没有。聚簇索引通常最适合范围查询(如这个)或子表的外键。

集群索引的一个作用是它将所有数据聚集在一起,因为数据存储在集群 b 树的叶节点上。因此,假设您不要求“太宽”的范围,优化器将确切知道 b 树的哪个部分包含数据,并且不必找到行标识符然后跳到数据的位置是(就像处理 NC 索引时那样)。什么是“太宽”的范围?一个荒谬的例子是从一个只有一年记录的表中请求 11 个月的数据。假设您的统计数据是最新的,提取一天的数据应该不是问题。(不过,如果您正在查找昨天的数据并且您已经三天没有更新统计信息,优化器可能会遇到麻烦。)

由于您正在运行“SELECT *”查询,引擎将需要返回表中的所有列(即使有人添加了您的应用程序不需要的新列),因此覆盖索引或索引包含列不会有太大帮助,如果有的话。(如果您将表中的每一列都包含在索引中,那么您就做错了。)优化器可能会忽略那些 NC 索引。

那么该怎么办?

我的建议是删除 NC 索引,将聚集 PK 更改为非聚集并在 [DateEntered] 上创建聚集索引。越简单越好,直到证明不是这样。