Sql server 在SQL Server中查询最小值比查询所有行要长得多
当我在一个包含大约一亿行的表中查询特定日期的最小ID时,我当前在数据库中遇到了一个奇怪的行为。查询非常简单:Sql server 在SQL Server中查询最小值比查询所有行要长得多,sql-server,performance,aggregate-functions,sql-server-2012,database-partitioning,Sql Server,Performance,Aggregate Functions,Sql Server 2012,Database Partitioning,当我在一个包含大约一亿行的表中查询特定日期的最小ID时,我当前在数据库中遇到了一个奇怪的行为。查询非常简单: SELECT MIN(Id) FROM Connection WITH(NOLOCK) WHERE DateConnection = '2012-06-26' 这个查询永远不会结束,至少我让它运行了几个小时。DateConnection列不是索引,也不包含在索引中。所以我理解这个查询可以持续相当长一段时间。但我尝试了几秒钟内运行的以下查询: SELECT Id FROM Connect
SELECT MIN(Id) FROM Connection WITH(NOLOCK) WHERE DateConnection = '2012-06-26'
这个查询永远不会结束,至少我让它运行了几个小时。DateConnection列不是索引,也不包含在索引中。所以我理解这个查询可以持续相当长一段时间。但我尝试了几秒钟内运行的以下查询:
SELECT Id FROM Connection WITH(NOLOCK) WHERE DateConnection = '2012-06-26'
它返回30万行
我的表格定义如下:
CREATE TABLE [dbo].[Connection](
[Id] [bigint] IDENTITY(1,1) NOT NULL,
[DateConnection] [datetime] NOT NULL,
[TimeConnection] [time](7) NOT NULL,
[Hour] AS (datepart(hour,[TimeConnection])) PERSISTED NOT NULL,
CONSTRAINT [PK_Connection] PRIMARY KEY CLUSTERED
(
[Hour] ASC,
[Id] ASC
)
)
它具有以下索引:
CREATE UNIQUE NONCLUSTERED INDEX [IX_Connection_Id] ON [dbo].[Connection]
(
[Id] ASC
)ON [PRIMARY]
我发现使用这种奇怪行为的一种解决方案是使用以下代码。但在我看来,对于这样一个简单的问题,它似乎有点沉重
create table #TempId
(
[Id] bigint
)
go
insert into #TempId
select id from partitionned_connection with(nolock) where dateconnection = '2012-06-26'
declare @displayId bigint
select @displayId = min(Id) from #CoIdTest
print @displayId
go
drop table #TempId
go
有人遇到过这种行为吗?原因是什么?最小聚合是否扫描整个表?如果这就是为什么简单选择没有的原因?找到最小值比遍历所有记录花费的时间更长是有道理的。查找未排序结构的最小值比遍历一次要长得多(未排序是因为MIN()没有利用标识列)。因为您使用的是标识列,所以可以使用嵌套的select,从具有指定日期的记录集中获取第一条记录。在您的情况下,NC索引扫描是问题所在。它使用唯一的非聚集索引扫描,然后对于每一个数亿行,它将遍历聚集索引,从而导致数百万io(通常说你的索引高度是4,那么它可能会导致对非聚集索引页进行1亿*4 IO+索引扫描)。优化器必须选择此索引以避免strem聚合获得最小值。要找到最小值,有3种主要技术,一种是对我们想要最小值的列使用索引(如果有索引,并且在这种情况下,只要您得到返回的行,就不需要计算),那么它是有效的),第二,它可以使用哈希聚合(但通常在您使用group by时发生)第三个是流聚合,它将扫描所有符合条件的行,并始终保持最小值,在扫描所有行时返回最小值 然而,当不带min的查询使用聚集索引扫描时,速度很快,因为它必须读取更少的页面数,从而减少io 现在的问题是,为什么优化器在非聚集索引上选择索引扫描。我确信使用流聚合查找最小值可以避免流聚合中涉及的计算,但在这种情况下,不使用流聚合的成本要高得多。这取决于估计,所以我猜表中的统计信息不是最新的 所以首先要检查你的数据是否是最新的。上次数据是什么时候更新的 因此,为了避免这个问题,请执行以下操作 1.首先更新表统计信息,我确信它必须删除您的问题。
2.如果您不能使用update stats或update stats没有更改计划,并且仍然使用NC索引扫描,那么您可以强制执行聚集索引扫描,以便它使用更少的IO,然后使用流聚合来获得最小值。尽管以不需要索引提示的方式解决问题可能是明智的,但快速解决方案是:
SELECT MIN(Id) FROM Connection WITH(NOLOCK, INDEX(PK_Connection)) WHERE DateConnection = '2012-06-26'
这将强制进行表扫描
或者,尝试此方法,尽管它可能会产生相同的问题:
select top 1 Id
from Connection
WHERE DateConnection = '2012-06-26'
order by Id
问题的根本原因是未对齐的非聚集索引,加上统计限制Martin Smith(有关详细信息,请参阅另一个问题) 您的表在
[Hour]
上按以下行进行分区:
CREATE PARTITION FUNCTION PF (integer)
AS RANGE RIGHT
FOR VALUES (1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23);
CREATE PARTITION SCHEME PS
AS PARTITION PF ALL TO ([PRIMARY]);
-- Partitioned
CREATE TABLE dbo.Connection
(
Id bigint IDENTITY(1,1) NOT NULL,
DateConnection datetime NOT NULL,
TimeConnection time(7) NOT NULL,
[Hour] AS (DATEPART(HOUR, TimeConnection)) PERSISTED NOT NULL,
CONSTRAINT [PK_Connection]
PRIMARY KEY CLUSTERED
(
[Hour] ASC,
[Id] ASC
)
ON PS ([Hour])
);
-- Not partitioned
CREATE UNIQUE NONCLUSTERED INDEX [IX_Connection_Id]
ON dbo.Connection
(
Id ASC
)ON [PRIMARY];
-- Pretend there are lots of rows
UPDATE STATISTICS dbo.Connection WITH ROWCOUNT = 200000000, PAGECOUNT = 4000000;
查询和执行计划是:
SELECT
MinID = MIN(c.Id)
FROM dbo.Connection AS c WITH (READUNCOMMITTED)
WHERE
c.DateConnection = '2012-06-26';
优化器利用索引(在Id
上排序)将MIN
聚合转换为TOP(1)
——因为根据定义,最小值将是排序流中遇到的第一个值。(如果非聚集索引也被分区,优化器将不会选择此策略,因为所需的排序将丢失)
稍微复杂的是,我们还需要在WHERE
子句中应用谓词,这需要查找基表以获取DateConnection
值。Martin提到的统计限制解释了为什么优化器在查找之前只需检查有序索引中的119行一个具有与WHERE子句
匹配的DateConnection
值。DateConnection
和Id
值之间的隐藏相关性意味着这一估计值还有很长的路要走
如果您感兴趣,Compute Scalar将计算要执行键查找的分区。对于非聚集索引中的每一行,它将计算一个表达式,如[PtnId1000]=Scalar运算符(RangePartitionNew([dbo].[Connection].[Hour])作为[c].[Hour],(1)、(1)、(2)、(3)、(4)、(5)、(6)、(7)、(8)、(9)、(10)、(11)、(12)、(13)、(15)、(16),(17),(18),(19),(20),(21),(22),(23))
,这是用作查找查找的前导键。嵌套循环联接上有预取(预读),但这需要是有序的预取,以保留TOP(1)
优化所需的排序
解决方案
我们可以通过查找每个Hour
值的最小Id
,然后取每小时最小值中的最小值来避免统计限制(不使用查询提示):
-- Global minimum
SELECT
MinID = MIN(PerHour.MinId)
FROM
(
-- Local minimums (for each distinct hour value)
SELECT
MinID = MIN(c.Id)
FROM dbo.Connection AS c WITH(READUNCOMMITTED)
WHERE
c.DateConnection = '2012-06-26'
GROUP BY
c.[Hour]
) AS PerHour;
执行计划是:
如果启用了并行性,您将看到一个更像下面这样的计划,它使用并行索引扫描和多线程流聚合来更快地生成结果:
是否在“显示执行计划”选项打开的情况下运行查询?我怀疑您看到了索引查找和索引扫描/表扫描之间的区别。有关执行计划选项,请参阅。您还可以通过在中添加