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


14

我有一个查询,目前平均需要2500毫秒才能完成。我的表很窄,但是有4400万行。我必须选择哪些选项来提高性能,或者说它达到了它的理想水平?

查询

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31'; 

桌子

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]

索引

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]

添加其他索引会有帮助吗?如果是这样,他们会是什么样子?当前的性能是可以接受的,因为该查询仅偶尔运行,但是我想知道作为学习练习,我可以做些什么来使其更快吗?

更新

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

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

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

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' AND DeviceID = 4;

如果我添加ORDER BY [DateEntered], [DeviceID]到原始查询中,则我处于50毫秒范围内:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' 
ORDER BY [DateEntered], [DeviceID];

这些都使用了我期望的索引(CommonQueryIndex),所以,我想我的问题是,有没有办法强制将此索引用于这样的查询?还是表的大小超出了优化程序的范围,我必须只使用ORDER BY或提示?


我猜您可以在“ DateEntered”上添加一个非聚集索引,这将在某种程度上提高性能
Praveen

@Praveen基本上与我现有的索引相同吗?由于同一字段上会有两个索引,我是否需要做任何特殊的事情?
Nate 2012年

@Nate,由于该表称为心跳,并且涉及到4,400万条记录,我认为您在此表上插入了很多内容?使用索引,您只能添加覆盖索引以加快速度。但是正如您提到的,您偶尔仅使用此查询,如果您执行大量插入操作,强烈建议您不要这样做。它基本上使您的插入负载增加了一倍。您正在运行企业版吗?
爱德华·多特兰

我注意到您在NC索引中有deviceID。是否可以在您的where子句中包含它?这样会使结果集降低到阈值以下吗?<35k条记录(不包含前1000条)。
爱德华·多特兰

1
最后一个问题,您是否总是按dateEntered的顺序插入?否则这些设备可能会混乱,因为设备可能会彼此插入异步信号。您可能尝试将聚簇索引更改为DateEntered列。现在,聚集索引的请假页面为445页。如果从int转换为datetime,那将翻倍。但是在这种情况下,这可能并不坏。
爱德华·多特兰

Answers:


13

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

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]

与[DateEntered]列的选择性有关。

您告诉我们您的表有4,400万行。行大小为:

ID为4字节,设备ID为4字节,日期为8字节,4位列为1字节。(标记,Null位图,var col偏移量,col计数)的17字节+ 7字节开销总计每行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%。


扫描阈值通常远低于该阈值(10%甚至更低),但是由于范围是一年前的一天,因此甚至不应达到该阈值。而且,由于添加了覆盖索引,因此没有给出聚集索引扫描。由于该索引使WHERE子句SARG-able,因此应首选它。
RBarryYoung

@RBarryYoung我试图解释为什么首先不使用[EnteredDate],[DeviceID]上的非聚集索引。关于阈值,我认为我们都同意,我只是从页面角度进行讨论。我将更改答案以使其更加清晰。
爱德华·多特兰

更改了答案以使其更清楚我在回答什么。我无法解释为什么未使用@RBarryYoung建议的覆盖率索引。我在这里对一百万行进行了测试,并使用覆盖索引对其进行了优化。
爱德华·多特兰

感谢您做出非常全面的回应,这很有意义。关于工作量,该表每5分钟插入150-300次插入,并且每天出于报告目的进行几次读取。
Nate 2012年

考虑到覆盖索引的表很窄,因此其开销并不十分重要,而“覆盖”只是对已包含大部分行的现有索引的补充。
RBarryYoung

8

PK集群是否有特定原因?许多人这样做是因为它默认方式,否则他们认为PK必须成簇。不对 聚集索引通常最适合范围查询(例如这一查询)或子表的外键。

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

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

那么该怎么办?

我的建议是删除NC索引,将聚簇PK更改为非聚簇,然后在[DateEntered]上创建聚簇索引。除非另有证明,否则越简单越好。


假设以递增的顺序插入行,这是最简单的答案-但以非线性顺序插入将导致碎片。
柯克·布罗德赫斯特

将数据添加到任何b树结构将导致其失去平衡。即使您按集群顺序添加行,索引也会失去平衡。重新索引表可以消除碎片,任何DBA都会告诉您,在将“足够”的数据添加到表之后,需要重新索引表。(“足够”的定义可能会引起争议,或者“何时”可能是个讨论。)我在这个问题中看不到任何说明由于某种原因而无法进行重新索引的问题。
达林海峡2012年

4

只要您在那里有“ *”,那么我可以想象的唯一会大不同的就是将索引定义更改为:

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)INCLUDE (ID, IsWebUp, IsPingUp, IsPUp)
 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]

正如我在评论中指出的那样,它应该使用该索引,但是如果没有,您可以使用ORDER BY或索引提示来说服它。


我只是尝试了一下,我仍然在同一地点,等待服务器响应的时间为2500ms,客户端处理时间为10ms。
Nate 2012年

发布查询计划。
RBarryYoung 2012年

看起来它正在使用聚簇索引。(SELECT成本:0%< -顶部成本:20%< -聚集索引扫描PK_Heartbeats成本:80%)
内特

是的,那是不对的,有些东西使统计数据/优化器无法正常工作。添加提示以强制其使用新索引。
RBarryYoung

@Max Vernon:也许可以,但是应该在查询计划中进行标记。
RBarryYoung

3

我会对此有所不同。

  • 是的,我知道这是一个旧线程,但我对此很感兴趣。

我将转储datetime列-将其更改为int。有一个查询表或为您的日期进行转换。

转储聚集索引-将其保留为堆,并在表示日期的新INT列上创建一个非聚集索引。即今天是20121015。该顺序很重要。根据加载表的频率,以DESC顺序创建该索引。维护成本会更高,您将需要引入填充因子或分区。分区也将有助于减少运行时间。

最后,如果可以使用SQL 2012,请尝试使用SEQUENCE-对于插入,它的性能将超过identity()。


有趣的解决方案。虽然从我的问题来看并不明显,但DateTime的时间部分非常重要。通常,我根据日期进行查询,以查看该时间段内的特定时间。您将如何调整此解决方案以解决此问题?
Nate 2012年

在这种情况下,请保留datetime列,并为日期添加int列(因为您的范围基于date元素而不是time元素)。您还可以考虑使用TIME数据类型,然后有效地将时间与日期分开。这样,您的数据占用空间较小,并且您仍然具有该列的“时间”元素。
杰里米·洛厄尔

1
我不确定为什么我早些错过了,但是在聚集索引和非聚集索引上都使用行压缩。我只是对您的表进行了快速测试,发现的结果是:我在上面定义的表中创建了一组数据(580万行)。我压缩(行)了集群索引和非集群索引。根据您的确切查询,逻辑读取数从2,074减少到1,433。这是一个显着的下降,我相信只有这样才能帮助您-而且风险非常低。
杰里米·洛厄尔
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.