web-dev-qa-db-ja.com

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

現在、トランザクションテーブルを設計しています。各行の積算合計を計算する必要があり、パフォーマンスが低下する可能性があることに気付きました。そこで、テスト用に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.

1st attempt execution plan

計画のパフォーマンスが遅いために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.

2nd attempt execution plan

私の質問は:

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

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

results

8
user2652379

何が起こっているのかをよりよく理解し、さまざまなアプローチがどのように実行されるかを確認するために、もう少し多くのデータでテストすることをお勧めします。同じ構造のテーブルに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千万行が読み取られます。

hello

正しい結果を得るには、テーブル全体の行を合計する必要があります。理想的には、この合計を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回スキャンされて完全な合計が取得され、結果セットの各行について追加の行が読み取られます。並べ替えはなく、プランの並行部分で合計の計算にほとんどすべての時間が費やされます。

pretty_good

このクエリをさらに高速に実行したい場合は、完全な合計を計算する部分を最適化する必要があります。上記のクエリは、クラスター化インデックススキャンを実行します。クラスタ化インデックスにはすべての列が含まれますが、必要なのは[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ミリ秒かかります。

enter image description here

実際の計画 ここ 。このアプローチの最も良い点は、テーブルのサイズによってランタイムが変更されないことです。重要なのは、返される行の数だけです。たとえば、最初の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]);
5
Joe Obbish

最初の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.

2番目の計画 はスプールを回避できるため、作業テーブル全体を回避できます。クラスター化インデックスから上位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)
        )
);
4
Josh Darnell

返される行のこのような小さなサブセットを処理する場合、三角結合が適切なオプションです。ただし、ウィンドウ関数を使用する場合は、パフォーマンスを向上できるオプションが増えます。ウィンドウオプションのデフォルトオプションはRANGEですが、最適なオプションはROWSです。違いはパフォーマンスだけでなく、関係が関係しているときの結果にもあることに注意してください。

次のコードは、提示したコードよりもわずかに高速です。

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