GROUP BY 与 MAX 与仅 MAX

Ofi*_*ris 8 performance sql-server group-by query-performance

我是一名程序员,正在处理一个大表,其方案如下:

UpdateTime, PK, datetime, notnull
Name, PK, char(14), notnull
TheData, float
Run Code Online (Sandbox Code Playgroud)

有一个聚集索引 Name, UpdateTime

我想知道什么应该更快:

SELECT MAX(UpdateTime)
FROM [MyTable]
Run Code Online (Sandbox Code Playgroud)

或者

SELECT MAX([UpdateTime]) AS value
from
   (
    SELECT [UpdateTime]
    FROM [MyTable]
    group by [UpdateTime]
   ) as t
Run Code Online (Sandbox Code Playgroud)

对该表的插入以具有相同日期的 50,000 行的块为单位。所以我认为分组可能会简化MAX计算。

而不是试图找到最多 150,000 行,按 3 行分组,然后计算MAX会更快?我的假设是否正确或 group by 也很昂贵?

Cra*_*ein 12

我根据您的架构创建了表 big_table

create table big_table
(
    updatetime datetime not null,
    name char(14) not null,
    TheData float,
    primary key(Name,updatetime)
)
Run Code Online (Sandbox Code Playgroud)

然后我用以下代码在表格中填充了 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
Run Code Online (Sandbox Code Playgroud)

使用 SSMS,然后我测试了这两个查询并意识到在第一个查询中您正在寻找 TheData 的 MAX,在第二个查询中寻找更新时间的 MAX

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

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
Run Code Online (Sandbox Code Playgroud)

使用Statistics Time我得到解析、编译和执行每个语句所需的毫秒数

使用Statistics IO获取有关磁盘活动的信息

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

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

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

这是消息选项卡的输出

-- 查询 1

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

SQL Server 执行时间: CPU 时间 = 16 ms, elapsed time = 6 ms

-- 查询 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 次逻辑读取,但第二个查询的经过时间为 35 毫秒,而第一个查询只有 6 毫秒。您还会注意到第二个查询导致在 tempdb 中使用临时表,由单词worktable指示。即使 worktable 的所有值都为 0,工作仍然在 tempdb 中完成。

然后是Messages 选项卡旁边的实际执行计划选项卡的输出

在此处输入图片说明

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

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

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

这是相关文档。

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

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

10/28 下午 12:46 更新

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


Pau*_*ite 6

The inserts to this table are in chunks of 50,000 rows with the same date. So I thought grouping by might ease the MAX calculation.

The rewrite might have helped if SQL Server implemented index skip-scan, but it does not.

Index skip-scan allows a database engine to seek to the next different index value instead of scanning all the duplicates (or irrelevant sub-keys) in between. In your case, skip-scan would allow the engine to find the MAX(UpdateTime) for the first Name, skip to the MAX(UpdateTime) for the second Name...and so on. The final step would be to find the MAX(UpdateTime) from the one-per-name candidates.

You can simulate this to some extent using a recursive CTE, but it is a bit messy, and not as efficient as built-in skip-scan would be:

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);
Run Code Online (Sandbox Code Playgroud)

递归 CTE 计划

That plan performs a singleton seek for each distinct Name, then finds the highest UpdateTime from the candidates. It's performance relative to a simple full scan of the table depends on how many duplicates there are per Name, and whether the pages touched by the singleton seeks are in memory or not.

Alternative solutions

If you are able to create a new index on this table, a good choice for this query would be an index on UpdateTime alone:

CREATE INDEX IX__BigTable_UpdateTime 
ON dbo.BigTable (UpdateTime);
Run Code Online (Sandbox Code Playgroud)

This index will allow the execution engine to find the highest UpdateTime with a singleton seek to the end of the index 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);
Run Code Online (Sandbox Code Playgroud)

这样做的优点是仅创建具有与唯一UpdateTime值一样多的行的结构,尽管每个更改基表中数据的查询都会在其执行计划中添加额外的运算符以维护索引视图。查找最大值的查询UpdateTime将是:

SELECT MAX(BTUT.UpdateTime)
FROM dbo.BigTableUpdateTimes AS BTUT
    WITH (NOEXPAND);
Run Code Online (Sandbox Code Playgroud)

索引视图计划