最近の行の累計をより速く取得するにはどうすればよいですか?


8

現在、トランザクションテーブルを設計しています。各行の現在までの合計を計算する必要があり、パフォーマンスが低下する可能性があることに気付きました。そこで、テスト用に100万行のテーブルを作成しました。

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.

2回目の実行計画

私の質問は:

  • 1回目の試行のクエリが2回目の試行よりも遅いのはなぜですか?
  • パフォーマンスをさらに向上させるにはどうすればよいですか?スキーマを変更することもできます。

明確にするために、両方のクエリは以下と同じ結果を返します。

結果


1
私は通常、ウィンドウ関数を使用しませんが、いくつかの有用な記事を読んだことを覚えています。T-SQLウィンドウ関数の概要の 1つ、特に2012年のウィンドウ集計の機能強化の部分をご覧ください。おそらくそれはあなたにいくつかの答えを与えます。...そして同じ優れた著者T-SQLウィンドウ関数とパフォーマンスの
Denis Rubashkin

インデックスを付けてみましたvalueか?
Jacob H

回答:


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千万行が読み込まれます。

こんにちは

正しい結果を得るには、テーブル全体の行を合計する必要があります。理想的には、この合計を1回だけ実行します。問題への取り組み方を変えると、これを行うことができます。テーブル全体の合計を計算し、結果セットの行から現在の合計を差し引くことができます。これにより、N番目の行の合計を見つけることができます。これを行う1つの方法:

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ミリ秒で実行されます。テーブルが1回スキャンされて完全な合計が取得され、結果セットの各行について追加の行が読み取られます。並べ替えはなく、ほぼすべての時間は計画の並行部分で合計を計算するために費やされます。

かなり良い

このクエリをさらに高速に実行したい場合は、完全な合計を計算する部分を最適化する必要があります。上記のクエリは、クラスター化インデックススキャンを実行します。クラスタ化インデックスにはすべての列が含まれますが、必要なのは[value]列だけです。1つのオプションは、その列に非クラスター化インデックスを作成することです。別のオプションは、その列に非クラスター化列ストアインデックスを作成することです。どちらもパフォーマンスが向上します。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

最初の2つのアプローチの違い

最初の計画では、ウィンドウのスプール演算子で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行を取得するだけで、ネストされたループが別のクラスター化インデックススキャンからの集計(合計)に結合されます。内側はまだテーブル全体を読み取ることになりますが、テーブルは非常に密集しているため、100万行の場合はかなり効率的です。

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.

あなたはおそらくそれを打ち負かすつもりはありません(あなたが本当に賢いのでない限り-素敵なジョー、ジョー)。列ストアは、大量のデータのスキャンと集計に非常に優れています。

ウィンドウ関数オプションROWではなく使用RANGE

このアプローチを使用すると、2番目のクエリと非常に似たパフォーマンスを得ることができます。このアプローチは、別の回答で述べられており、上記の列ストアの例(実行計画)で使用しました。

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

ウィンドウスプールがメモリ内で発生するため、2番目のアプローチよりも読み取りが少なく、最初のアプローチと比較してtempdbアクティビティはありません。

... RANGEはディスク上のスプールを使用し、ROWSはメモリ内のスプールを使用します

残念ながら、ランタイムは2番目のアプローチとほぼ同じです。

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のスキーマのサンプル実装を提供してくれたPaul Whiteの功績です。

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。試しましたが、2番目のクエリよりも速いとは言えません。結果でしたCPU time = 1438 ms, elapsed time = 1537 ms.
user2652379

しかし、これはこのオプションのみです。2番目のクエリは適切にスケーリングされません。より多くの行を返すと、違いがかなり明らかになります。
Luis Cazares

多分t-sqlの外ですか?スキーマを変更できます。
user2652379
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.