使用MAX分组,而不是MAX


8

我是一名程序员,正在处理一个具有以下方案的大表:

UpdateTime, PK, datetime, notnull
Name, PK, char(14), notnull
TheData, float

上有一个聚集索引 Name, UpdateTime

我想知道什么应该更快:

SELECT MAX(UpdateTime)
FROM [MyTable]

要么

SELECT MAX([UpdateTime]) AS value
from
   (
    SELECT [UpdateTime]
    FROM [MyTable]
    group by [UpdateTime]
   ) as t

此表的插入是50,000行中具有相同日期的数据块。因此,我认为分组依据可能会简化MAX计算。

与其尝试查找最多150,000行,不如将其分组为3行,然后计算MAX会更快?我的假设是正确的还是分组依据也代价高昂?

Answers:


12

我根据您的模式创建了表big_table

create table big_table
(
    updatetime datetime not null,
    name char(14) not null,
    TheData float,
    primary key(Name,updatetime)
)

然后,我用该代码填充了50,000行的表:

DECLARE @ROWNUM as bigint = 1
WHILE(1=1)
BEGIN
    set @rownum  = @ROWNUM + 1
    insert into big_table values(getdate(),'name' + cast(@rownum as CHAR), cast(@rownum as float))
    if @ROWNUM > 50000
        BREAK;  
END

然后,我使用SSMS测试了这两个查询,并意识到在第一个查询中您正在寻找TheData的最大值,在第二个查询中正在寻找updatetime的最大值。

因此,我修改了第一个查询,以获取更新时间的最大值

set statistics time on -- execution time
set statistics io on -- io stats (how many pages read, temp tables)

-- query 1
SELECT MAX([UpdateTime])
FROM big_table

-- query 2
SELECT MAX([UpdateTime]) AS value
from
   (
    SELECT [UpdateTime]
    FROM big_table
    group by [UpdateTime]
   ) as t


set statistics time off
set statistics io off

使用统计时间,我获得了解析,编译和执行每个语句所需的毫秒数

使用Statistics IO,我可以获得有关磁盘活动的信息

统计时间和统计IO提供有用的信息。例如使用的临时表(由工作表指示)。还读取了多少逻辑页,这些逻辑页指示从缓存读取的数据库页数。

然后,我使用CTRL + M激活执行计划(激活显示实际的执行计划),然后使用F5执行。

这将提供两个查询的比较。

这是“ 消息”选项卡的输出

-查询1

表'big_table'。扫描计数1,逻辑读543,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。

SQL Server执行时间: CPU时间= 16毫秒,经过的时间= 6毫秒

-查询2

表“ 工作台 ”。扫描计数0,逻辑读0,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。

表'big_table'。扫描计数1,逻辑读543,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。

SQL Server执行时间: CPU时间= 0毫秒,经过的时间= 35毫秒

这两个查询都进行543次逻辑读取,但是第二个查询的经过时间为35ms,其中第一个查询只有6ms。您还将注意到,第二个查询导致使用tempdb中的临时表,由单词worktable指示。即使工作表的所有值都为0,工作仍在tempdb中完成。

然后是“消息”选项卡旁边的实际执行计划选项卡的输出

在此处输入图片说明

根据MSSQL提供的执行计划,您提供的第二个查询的总批处理成本为64%,而第一个查询仅占总批处理成本的36%,因此第一个查询所需的工作较少。

使用SSMS,您可以测试和比较查询,并准确了解MSSQL如何解析查询以及哪些对象:表,索引和/或统计信息(如果有的话)用于满足这些查询。

测试时要记住的另一点说明是,如果可能的话,在测试之前清除缓存。这有助于确保比较准确,这在考虑磁盘活动时很重要。我首先使用DBCC DROPCLEANBUFFERSDBCC FREEPROCCACHE清除所有缓存。请注意不要在实际使用的生产服务器上使用这些命令,因为您将有效地迫使服务器将所有内容从磁盘读取到内存中。

这是相关的文档。

  1. 使用DBCC FREEPROCCACHE清除计划缓存
  2. 使用DBCC DROPCLEANBUFFERS清除缓冲池中的所有内容

根据使用环境的方式,可能无法使用这些命令。

更新10/28 12:46 pm

对执行计划图像和统计信息输出进行了更正。


感谢您的深刻回答,请注意我在代码中的秃头,每组50,000行具有相同的日期,该日期不同于其他块。所以我应该getdate()跳出循环
Ofiris

1
您好@Ofiris。我给出的答案实际上只是为了帮助您自己进行比较。我创建了随机的垃圾数据只是为了说明各种命令和工具的使用,您可以使用这些命令和工具得出自己的结论。
Craig Efrein

1
tempdb中未完成任何工作。该工作表用于管理分区,以防散列聚合由于预留了足够的内存而不得不溢出到tempdb。请强调,即使在“实际”计划中,费用也始终是估算值。它们是优化器的估计,可能与实际性能没有太大关系。不要将批处理的百分比用作主要调整指标。仅当您要测试冷缓存性能时,清除缓冲区才重要。
保罗·怀特9

1
您好@PaulWhite。感谢您提供其他信息,对于由衷的建议,我深表谢意。当您说出句子“不要使用”时,这难道不会被误解为发出命令而不是提供专业建议吗?最好的祝福。
Craig Efrein

@CraigEfrein可能是。我很简短,以适应允许的评论空间。
保罗·怀特9

6

该表的插入是具有相同日期的50,000行的块。因此,我认为分组依据可能会简化MAX计算。

如果SQL Server实现了索引跳过扫描,则重写可能会有所帮助,但事实并非如此。

索引跳过扫描允许数据库引擎查找下一个不同的索引值,而不是扫描之间的所有重复项(或不相关的子项)。在您的情况下,跳过扫描将使引擎找到MAX(UpdateTime)第一个Name,然后跳到MAX(UpdateTime)第二个Name……等等。最后一步是MAX(UpdateTime)从每个名字的候选人中查找。

您可以使用递归CTE在某种程度上对此进行模拟,但是它有点混乱,并且效率不如内置的跳过扫描:

WITH RecursiveCTE
AS
(
    -- Anchor: MAX UpdateTime for
    -- highest-sorting Name
    SELECT TOP (1)
        BT.Name,
        BT.UpdateTime
    FROM dbo.BigTable AS BT
    ORDER BY
        BT.Name DESC,
        BT.UpdateTime DESC

    UNION ALL

    -- Recursive part
    -- MAX UpdateTime for Name
    -- that sorts immediately lower
    SELECT
        SubQuery.Name,
        SubQuery.UpdateTime
    FROM 
    (
        SELECT
            BT.Name,
            BT.UpdateTime,
            rn = ROW_NUMBER() OVER (
                ORDER BY BT.Name DESC, BT.UpdateTime DESC)
        FROM RecursiveCTE AS R
        JOIN dbo.BigTable AS BT
            ON BT.Name < R.Name
    ) AS SubQuery
    WHERE
        SubQuery.rn = 1
)
-- Final MAX aggregate over
-- MAX(UpdateTime) per Name
SELECT MAX(UpdateTime) 
FROM RecursiveCTE
OPTION (MAXRECURSION 0);

递归CTE计划

该计划对每个不同的对象执行单例查找Name,然后UpdateTime从候选者中找到最高者。相对于对表进行简单的完整扫描而言,它的性能取决于每个表有多少个重复项Name,以及单例查找所触及的页面是否在内存中。

替代解决方案

如果您能够在此表上创建新索引,则此查询的一个不错选择是UpdateTime单独使用索引:

CREATE INDEX IX__BigTable_UpdateTime 
ON dbo.BigTable (UpdateTime);

该索引将使执行引擎以UpdateTime单例查找的方式找到最高的索引b-tree:

新指数计划

该计划仅消耗一些逻辑IO(用于导航b树级别),并立即完成。请注意,计划中的“索引扫描”不是对新索引的完整扫描-它仅从索引的一个“末端”返回一行。

如果您不想在表上创建一个完整的新索引,则可以考虑仅包含唯一UpdateTime值的索引视图:

CREATE VIEW dbo.BigTableUpdateTimes
WITH SCHEMABINDING AS
SELECT 
    UpdateTime, 
    NumRows = COUNT_BIG(*)
FROM dbo.BigTable AS BT
GROUP BY
    UpdateTime;
GO
CREATE UNIQUE CLUSTERED INDEX cuq
ON dbo.BigTableUpdateTimes (UpdateTime);

这样做的好处是UpdateTime,虽然创建的行的数量与唯一值一样多,但每个更改基表中数据的查询都会在其执行计划中添加额外的运算符,以维护索引视图。查找最大值的查询为UpdateTime

SELECT MAX(BTUT.UpdateTime)
FROM dbo.BigTableUpdateTimes AS BTUT
    WITH (NOEXPAND);

索引视图计划

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.