如何更快地获得最近行的总数?


8

我目前正在设计交易表。我意识到将需要计算每一行的运行总计,这可能会降低性能。因此,出于测试目的,我创建了一个包含一百万行的表。

CREATE TABLE [dbo].[Table_1](
    [seq] [int] IDENTITY(1,1) NOT NULL,
    [value] [bigint] NOT NULL,
 CONSTRAINT [PK_Table_1] PRIMARY KEY CLUSTERED 
(
    [seq] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

我尝试获取10个最近的行及其运行总计,但大约花了10秒钟。

--1st attempt
SELECT TOP 10 seq
    ,value
    ,sum(value) OVER (ORDER BY seq) total
FROM Table_1
ORDER BY seq DESC

--(10 rows affected)
--Table 'Worktable'. Scan count 1000001, logical reads 8461526, physical reads 2, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--Table 'Table_1'. Scan count 1, logical reads 2608, physical reads 516, read-ahead reads 2617, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--
--(1 row affected)
--
-- SQL Server Execution Times:
--   CPU time = 8483 ms,  elapsed time = 9786 ms.

第一次尝试执行计划

我怀疑TOP是由于该计划的性能下降所致,因此我更改了查询,大约花了1到2秒。但是我认为这对于生产来说仍然很慢,并且想知道是否可以进一步改善。

--2nd attempt
SELECT *
    ,(
        SELECT SUM(value)
        FROM Table_1
        WHERE seq <= t.seq
        ) total
FROM (
    SELECT TOP 10 seq
        ,value
    FROM Table_1
    ORDER BY seq DESC
    ) t
ORDER BY seq DESC

--(10 rows affected)
--Table 'Table_1'. Scan count 11, logical reads 26083, physical reads 1, read-ahead reads 443, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--
--(1 row affected)
--
-- SQL Server Execution Times:
--   CPU time = 1422 ms,  elapsed time = 1621 ms.

第二次尝试执行计划

我的问题是:

  • 为什么第一次尝试的查询要比第二次慢?
  • 如何进一步提高性能?我也可以更改架构。

只是为了清楚起见,两个查询都返回相同的结果,如下所示。

结果


1
我通常不使用窗口函数,但是我记得我读了一些关于它们的有用文章。看一看《T-SQL窗口函数简介》,尤其是2012年的“窗口聚合增强 ”部分。也许它给您一些答案。...以及同一位杰出作者T-SQL窗口函数和性能的
Denis Rubashkin

您是否尝试过建立索引value
Jacob H

Answers:


5

我建议使用更多数据进行测试,以更好地了解正在发生的事情并了解不同方法的执行情况。我将1600万行加载到具有相同结构的表中。您可以在此答案的底部找到用于填充表格的代码。

以下方法在我的计算机上花费19秒:

SELECT TOP (10) seq
    ,value
    ,sum(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING) total
FROM dbo.[Table_1_BIG]
ORDER BY seq DESC;

实际计划在这里。大部分时间都花在计算总和并进行排序上。令人担忧的是,查询计划几乎会为整个结果集完成所有工作,并最终过滤您所要求的10行。该查询的运行时根据表的大小而不是结果集的大小进行缩放。

此选项在我的计算机上需要23秒:

SELECT *
    ,(
        SELECT SUM(value)
        FROM dbo.[Table_1_BIG]
        WHERE seq <= t.seq
        ) total
FROM (
    SELECT TOP (10) seq
        ,value
    FROM dbo.[Table_1_BIG]
    ORDER BY seq DESC
    ) t
ORDER BY seq DESC;

实际计划在这里。此方法可根据请求的行数和表的大小进行缩放。从表中读取了近1.6亿行:

你好

为了获得正确的结果,您必须对整个表的行进行求和。理想情况下,您只执行一次此求和。如果您更改解决问题的方式,则可以执行此操作。您可以计算整个表的总和,然后从结果集中的行中减去运行总计。这样就可以找到第N行的总和。一种方法是:

SELECT TOP (10) seq
,value
, [value]
    - SUM([value]) OVER (ORDER BY seq DESC ROWS UNBOUNDED PRECEDING)
    + (SELECT SUM([value]) FROM dbo.[Table_1_BIG]) AS total
FROM dbo.[Table_1_BIG]
ORDER BY seq DESC;

实际计划在这里。新查询在我的计算机上运行644毫秒。对该表进行一次扫描以获取完整的总计,然后为结果集中的每一行读取另一行。没有排序,几乎所有时间都花在计划的并行部分中来计算总和:

非常好

如果您希望该查询更快,则只需要优化计算总和的部分即可。上面的查询执行聚集索引扫描。聚集索引包括所有列,但您只需要该[value]列。一种选择是在该列上创建非聚集索引。另一个选择是在该列上创建一个非聚集列存储索引。两者都会提高性能。如果您使用的是Enterprise,最好的选择是创建一个索引视图,如下所示:

CREATE OR ALTER VIEW dbo.Table_1_BIG__SUM
WITH SCHEMABINDING
AS
SELECT SUM([value]) SUM_VALUE
, COUNT_BIG(*) FOR_U
FROM dbo.[Table_1_BIG];

GO

CREATE UNIQUE CLUSTERED INDEX CI ON dbo.Table_1_BIG__SUM (SUM_VALUE);

该视图返回一行,因此几乎不占用空间。进行DML时会受到惩罚,但它与索引维护没有太大区别。在使用索引视图的情况下,查询现在需要0毫秒:

在此处输入图片说明

实际计划在这里。关于此方法的最好之处在于,运行时不会因表的大小而改变。唯一重要的是返回多少行。例如,如果您获得前10000行,则查询现在需要18毫秒才能执行。

填充表的代码:

DROP TABLE IF EXISTS dbo.[Table_1_BIG];

CREATE TABLE dbo.[Table_1_BIG] (
    [seq] [int] NOT NULL,
    [value] [bigint] NOT NULL
);

DROP TABLE IF EXISTS #t;
CREATE TABLE #t (ID BIGINT);

INSERT INTO #t WITH (TABLOCK)
SELECT TOP (4000) -1 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

INSERT INTO dbo.[Table_1_BIG] WITH (TABLOCK)
SELECT t1.ID * 4000 + t2.ID, 8 * t2.ID + t1.ID
FROM (SELECT TOP (4000) ID FROM #t) t1
CROSS JOIN #t t2;

ALTER TABLE dbo.[Table_1_BIG]
ADD CONSTRAINT [PK_Table_1] PRIMARY KEY ([seq]);

4

前两种方法的差异

一个计划在Window Spool运算符中花费了大约10秒钟中的7秒钟,因此这是它这么慢的主要原因。它正在tempdb中执行很多I / O来创建它。我的统计数据I / O和时间如下所示:

Table 'Worktable'. Scan count 1000001, logical reads 8461526
Table 'Table_1'. Scan count 1, logical reads 2609
Table 'Worktable'. Scan count 0, logical reads 0

 SQL Server Execution Times:
   CPU time = 8641 ms,  elapsed time = 8537 ms.

第二方案是能够避免卷轴,从而完全在工作台上。它仅从聚集索引中获取前10行,然后将嵌套循环加入到来自单独的聚集索引扫描的聚合(总和)中。内侧仍然可以读取整个表,但是表非常密集,因此对于一百万行而言,这是相当有效的。

Table 'Table_1'. Scan count 11, logical reads 26093
 SQL Server Execution Times:
   CPU time = 1563 ms,  elapsed time = 1671 ms.

改善表现

列存储

如果您确实希望使用“在线报告”方法,那么列存储可能是您的最佳选择。

ALTER TABLE [dbo].[Table_1] DROP CONSTRAINT [PK_Table_1];

CREATE CLUSTERED COLUMNSTORE INDEX [PK_Table_1] ON dbo.Table_1;

然后,这个查询非常快:

SELECT TOP 10
    seq, 
    value, 
    SUM(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING)
FROM dbo.Table_1
ORDER BY seq DESC;

这是我机器上的统计信息:

Table 'Table_1'. Scan count 4, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 3319
Table 'Table_1'. Segment reads 1, segment skipped 0.
Table 'Worktable'. Scan count 0, logical reads 0

 SQL Server Execution Times:
   CPU time = 375 ms,  elapsed time = 205 ms.

您可能不会击败它(除非您真的很聪明 -乔,一个好人)。Columnstore非常擅长扫描和聚合大量数据。

使用ROW而不是RANGE窗口功能选项

通过这种方法,您可以获得与第二个查询非常相似的性能,这是另一个答案中提到的,并且我在上面的列存储示例(执行计划)中使用了该方法:

SELECT TOP 10
    seq, 
    value, 
    SUM(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING)
FROM dbo.Table_1
ORDER BY seq DESC;

与第二种方法相比,它导致的读取次数更少,并且与第一种方法相比,tempdb没有活动,因为窗口假脱机发生在内存中

... RANGE使用磁盘上的后台打印,而ROWS使用内存中的后台打印

不幸的是,运行时与第二种方法大致相同。

Table 'Worktable'. Scan count 0, logical reads 0
Table 'Table_1'. Scan count 1, logical reads 2609
Table 'Worktable'. Scan count 0, logical reads 0

 SQL Server Execution Times:
   CPU time = 1984 ms,  elapsed time = 1474 ms.

基于架构的解决方案:异步运行总计

由于您愿意接受其他想法,因此可以考虑异步更新“运行总额”。您可以定期获取这些查询之一的结果,并将其加载到“总计”表中。因此,您需要执行以下操作:

CREATE TABLE [dbo].[Table_1_Totals]
(
    [seq] [int] NOT NULL,
    [running_total] [bigint] NOT NULL,
    CONSTRAINT [PK_Table_1_Totals] PRIMARY KEY CLUSTERED ([seq])
);

每天/每小时/以任何方式加载它(这在我的1mm行的机器上花费了大约2秒钟,并且可以进行优化):

INSERT INTO dbo.Table_1_Totals
SELECT
    seq, 
    SUM(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING) as total
FROM dbo.Table_1 t
WHERE NOT EXISTS (
            SELECT NULL 
            FROM dbo.Table_1_Totals t2
            WHERE t.seq = t2.seq)
ORDER BY seq DESC;

然后,您的报告查询非常有效:

SELECT TOP 10
    t.seq, 
    t.value, 
    t2.running_total
FROM dbo.Table_1 t
    INNER JOIN dbo.Table_1_Totals t2
        ON t.seq = t2.seq
ORDER BY seq DESC;

以下是读取的统计信息:

Table 'Table_1'. Scan count 0, logical reads 35
Table 'Table_1_Totals'. Scan count 1, logical reads 3

基于模式的解决方案:行内总数受约束

这个问题的答案详细介绍了一个非常有趣的解决方案:写一个简单的银行模式:如何使我的余额与他们的交易记录保持同步?

基本方法是跟踪当前行的总计行以及先前的行总计和序号。然后,您可以使用约束条件来验证运行总计始终正确并且是最新的。

感谢保罗·怀特提供对该Q&A架构的实现:

CREATE TABLE dbo.Table_1
(
    seq integer IDENTITY(1,1) NOT NULL,
    val bigint NOT NULL,
    total bigint NOT NULL,

    prev_seq integer NULL,
    prev_total bigint NULL,

    CONSTRAINT [PK_Table_1] 
        PRIMARY KEY CLUSTERED (seq ASC),

    CONSTRAINT [UQ dbo.Table_1 seq, total]
        UNIQUE (seq, total),

    CONSTRAINT [UQ dbo.Table_1 prev_seq]
        UNIQUE (prev_seq),

    CONSTRAINT [FK dbo.Table_1 previous seq and total]
        FOREIGN KEY (prev_seq, prev_total) 
        REFERENCES dbo.Table_1 (seq, total),

    CONSTRAINT [CK dbo.Table_1 total = prev_total + val]
        CHECK (total = ISNULL(prev_total, 0) + val),

    CONSTRAINT [CK dbo.Table_1 denormalized columns all null or all not null]
        CHECK 
        (
            (prev_seq IS NOT NULL AND prev_total IS NOT NULL)
            OR
            (prev_seq IS NULL AND prev_total IS NULL)
        )
);

2

当处理返回的行的一小部分时,三角连接是一个不错的选择。但是,使用窗口功能时,您有更多可以提高其性能的选项。窗口选项的默认选项是RANGE,但最佳选项是ROWS。请注意,涉及关系时,差异不仅在于性能,还在于结果。

以下代码比您提供的代码要快一些。

SELECT TOP 10 seq
    ,value
    ,sum(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING) total
FROM Table_1
ORDER BY seq DESC

谢谢告诉你ROWS。我试过了,但不能说它比第二次查询快。结果是CPU time = 1438 ms, elapsed time = 1537 ms.
user2652379'4

但这仅在此选项上。您的第二个查询无法很好地扩展。尝试返回更多的行,区别就很明显了。
路易斯·卡扎雷斯

也许在T-SQL之外?我可以更改架构。
user2652379 '19
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.