絶対的なパフォーマンスのために、SUMは高速ですか、それともCOUNTですか?


31

これは、特定の条件に一致するレコードの数のカウントに関連していますinvoice amount > $100

私は好む傾向がある

COUNT(CASE WHEN invoice_amount > 100 THEN 1 END)

ただし、これは有効です

SUM(CASE WHEN invoice_amount > 100 THEN 1 ELSE 0 END)

次の2つの理由から、COUNTの方が望ましいと考えていました。

  1. 意図を伝えます COUNT
  2. COUNT おそらくi += 1どこかで単純な操作が行われますが、SUMはその式が単純な整数値であると期待することはできません。

特定のRDBMSの違いに関する特定の事実はありますか?

回答:


32

ほとんどの質問はすでに自分で答えています。追加するいくつかの小片があります:

PostgreSQL(およびサポートする他のRDBMS booleanタイプ)を使用できboolean直接テストの結果を。キャストintegerしてSUM()

SUM((amount > 100)::int))

またはNULLIF()式でそれを使用し、COUNT()

COUNT(NULLIF(amount > 100, FALSE))

またはシンプルなOR NULL

COUNT(amount > 100 OR NULL)

または他のさまざまな表現。パフォーマンスはほぼ同じですCOUNT()は、通常、非常にわずかに高速ですSUM()ポールが既にコメントしたSUM()ように、とは異なり、決して戻らないため、便利かもしれません。関連:COUNT()NULL

Postgres 9.4FILTER以降には、条項もあります。詳細:

それはだより速く、上記のすべてよりも約5によって- 10%:

COUNT(*) FILTER (WHERE amount > 100)

場合は、クエリは単一の数と何もないと、あなたのテストケースとして、単純なようである、あなたは書き換えることができます。

SELECT count(*) FROM tbl WHERE amount > 100;

これは、インデックスがなくてもパフォーマンスの真の王様です。
適用可能なインデックスを使用すると、特にインデックスのみのスキャンで、桁違いに高速になります。

ベンチマーク

Postgres 10

集計FILTER句を含むPostgres 10の新しい一連のテストを実行し、少数および多数のカウントに対するインデックスの役割を示しました。

簡単なセットアップ:

CREATE TABLE tbl (
   tbl_id int
 , amount int NOT NULL
);

INSERT INTO tbl
SELECT g, (random() * 150)::int
FROM   generate_series (1, 1000000) g;

-- only relevant for the last test
CREATE INDEX ON tbl (amount);

実際の時間は、バックグラウンドノイズとテストベッドの仕様により、かなり異なります。表示典型的なテストの大きなセットから最高の時間を。これらの2つのケースでは、本質を把握する必要があります。

すべての行の〜1%をカウントするテスト1

SELECT COUNT(NULLIF(amount > 148, FALSE))            FROM tbl; -- 140 ms
SELECT SUM((amount > 148)::int)                      FROM tbl; -- 136 ms
SELECT SUM(CASE WHEN amount > 148 THEN 1 ELSE 0 END) FROM tbl; -- 133 ms
SELECT COUNT(CASE WHEN amount > 148 THEN 1 END)      FROM tbl; -- 130 ms
SELECT COUNT((amount > 148) OR NULL)                 FROM tbl; -- 130 ms
SELECT COUNT(*) FILTER (WHERE amount > 148)          FROM tbl; -- 118 ms -- !

SELECT count(*) FROM tbl WHERE amount > 148; -- without index  --  75 ms -- !!
SELECT count(*) FROM tbl WHERE amount > 148; -- with index     --   1.4 ms -- !!!

db <> fiddle here

すべての行の〜33%をカウントするテスト2

SELECT COUNT(NULLIF(amount > 100, FALSE))            FROM tbl; -- 140 ms
SELECT SUM((amount > 100)::int)                      FROM tbl; -- 138 ms
SELECT SUM(CASE WHEN amount > 100 THEN 1 ELSE 0 END) FROM tbl; -- 139 ms
SELECT COUNT(CASE WHEN amount > 100 THEN 1 END)      FROM tbl; -- 138 ms
SELECT COUNT(amount > 100 OR NULL)                   FROM tbl; -- 137 ms
SELECT COUNT(*) FILTER (WHERE amount > 100)          FROM tbl; -- 132 ms -- !

SELECT count(*) FROM tbl WHERE amount > 100; -- without index  -- 102 ms -- !!
SELECT count(*) FROM tbl WHERE amount > 100; -- with index     --  55 ms -- !!!

db <> fiddle here

各セットの最後のテストではインデックスのみのスキャンを使用したため、すべての行の3分の1をカウントするのに役立ちました。プレーンインデックスまたはビットマップインデックススキャンは、すべての行の約5%以上を含む場合、順次スキャンと競合できません。

Postgres 9.1の古いテスト

確認するためにEXPLAIN ANALYZE、PostgreSQL 9.1.6の実際のテーブルで簡単なテストを実行しました。

184568行のうち74208行が条件で修飾されていますkat_id > 50。すべてのクエリは同じ結果を返します。キャッシング効果を除外するためにそれぞれ10回ずつ順番に実行し、注として最良の結果を追加しました。

SELECT SUM((kat_id > 50)::int)                      FROM log_kat; -- 438 ms
SELECT COUNT(NULLIF(kat_id > 50, FALSE))            FROM log_kat; -- 437 ms
SELECT COUNT(CASE WHEN kat_id > 50 THEN 1 END)      FROM log_kat; -- 437 ms
SELECT COUNT((kat_id > 50) OR NULL)                 FROM log_kat; -- 436 ms
SELECT SUM(CASE WHEN kat_id > 50 THEN 1 ELSE 0 END) FROM log_kat; -- 432 ms

パフォーマンスにほとんど違いはありません。


1
FILTERソリューションは、「遅い」グループのバリエーションのどれよりも優れていますか?
アンドリーM

@AndriyM:FILTER上記の式(pg 9.5でのテスト)よりも、集計の時間がわずかに速くなります。同じようになりますか?(WHEREまだパフォーマンスの王様-可能な場合)。
アーウィンブランドステッター

PGが手元にないので、わかりません。とにかく、完全性のために、最後のソリューションのタイミング図で答えを更新することを望んでいただけです:)
Andriy M

@AndriyM:ようやく新しいベンチマークを追加しました。FILTER解決策はある私のテストでは、通常より速いです。
アーウィンブランドステッター

11

これは、SQL Server 2012 RTMでの私のテストです。

if object_id('tempdb..#temp1') is not null drop table #temp1;
if object_id('tempdb..#timer') is not null drop table #timer;
if object_id('tempdb..#bigtimer') is not null drop table #bigtimer;
GO

select a.*
into #temp1
from master..spt_values a
join master..spt_values b on b.type='p' and b.number < 1000;

alter table #temp1 add id int identity(10,20) primary key clustered;

create table #timer (
    id int identity primary key,
    which bit not null,
    started datetime2 not null,
    completed datetime2 not null,
);
create table #bigtimer (
    id int identity primary key,
    which bit not null,
    started datetime2 not null,
    completed datetime2 not null,
);
GO

--set ansi_warnings on;
set nocount on;
dbcc dropcleanbuffers with NO_INFOMSGS;
dbcc freeproccache with NO_INFOMSGS;
declare @bigstart datetime2;
declare @start datetime2, @dump bigint, @counter int;

set @bigstart = sysdatetime();
set @counter = 1;
while @counter <= 100
begin
    set @start = sysdatetime();
    select @dump = count(case when number < 100 then 1 end) from #temp1;
    insert #timer values (0, @start, sysdatetime());
    set @counter += 1;
end;
insert #bigtimer values (0, @bigstart, sysdatetime());
set nocount off;
GO

set nocount on;
dbcc dropcleanbuffers with NO_INFOMSGS;
dbcc freeproccache with NO_INFOMSGS;
declare @bigstart datetime2;
declare @start datetime2, @dump bigint, @counter int;

set @bigstart = sysdatetime();
set @counter = 1;
while @counter <= 100
begin
    set @start = sysdatetime();
    select @dump = SUM(case when number < 100 then 1 else 0 end) from #temp1;
    insert #timer values (1, @start, sysdatetime());
    set @counter += 1;
end;
insert #bigtimer values (1, @bigstart, sysdatetime());
set nocount off;
GO

個別の実行とバッチを別々に見る

select which, min(datediff(mcs, started, completed)), max(datediff(mcs, started, completed)),
            avg(datediff(mcs, started, completed))
from #timer group by which
select which, min(datediff(mcs, started, completed)), max(datediff(mcs, started, completed)),
            avg(datediff(mcs, started, completed))
from #bigtimer group by which

5回(および繰り返し)実行した後の結果は非常に決定的ではありません。

which                                       ** Individual
----- ----------- ----------- -----------
0     93600       187201      103927
1     93600       187201      103864

which                                       ** Batch
----- ----------- ----------- -----------
0     10108817    10545619    10398978
1     10327219    10498818    10386498

SQL Serverタイマーの粒度で測定した場合、実行条件の違いは実装の違いよりもはるかに大きいことを示しています。どちらのバージョンもトップになり、私がこれまでに得た最大の変動は2.5%です。

ただし、別のアプローチをとる:

set showplan_text on;
GO
select SUM(case when number < 100 then 1 else 0 end) from #temp1;
select count(case when number < 100 then 1 end) from #temp1;

StmtText(SUM)

  |--Compute Scalar(DEFINE:([Expr1003]=CASE WHEN [Expr1011]=(0) THEN NULL ELSE [Expr1012] END))
       |--Stream Aggregate(DEFINE:([Expr1011]=Count(*), [Expr1012]=SUM([Expr1004])))
            |--Compute Scalar(DEFINE:([Expr1004]=CASE WHEN [tempdb].[dbo].[#temp1].[number]<(100) THEN (1) ELSE (0) END))
                 |--Clustered Index Scan(OBJECT:([tempdb].[dbo].[#temp1]))

StmtText(COUNT)

  |--Compute Scalar(DEFINE:([Expr1003]=CONVERT_IMPLICIT(int,[Expr1008],0)))
       |--Stream Aggregate(DEFINE:([Expr1008]=COUNT([Expr1004])))
            |--Compute Scalar(DEFINE:([Expr1004]=CASE WHEN [tempdb].[dbo].[#temp1].[number]<(100) THEN (1) ELSE NULL END))
                 |--Clustered Index Scan(OBJECT:([tempdb].[dbo].[#temp1]))

私の読書から、SUMバージョンがもう少しするように見えます。SUM に加えて COUNT 実行しています。とは言っても、COUNT(*)は異なりますCOUNT([Expr1004])(NULLをスキップし、ロジックを増やす)。合理的なオプティマイザーは[Expr1004]SUM([Expr1004]) SUMバージョンで「INT」タイプであるので、整数レジスタを利用します。

いずれにせよ、COUNTほとんどのRDBMSではバージョンの方が高速であるとまだ信じていますが、テストからの結論は、SUM(.. 1.. 0..)少なくともSQL Serverの場合、ANSI WARNINGSが使用中に発生する以外の理由がない限り、今後も対応することですCOUNT


1

私の経験では、トレースの作成で、約10,000,000のクエリの両方のメソッドで、Count(*)がCPUの約2倍を使用し、少し高速になっていることに気付きました。しかし、私のクエリはフィルターなしです。

カウント(*)

CPU...........: 1828   
Execution time:  470 ms  

合計(1)

CPU...........: 3859  
Execution time:  681 ms  

Yoは、このテストの作成に使用したRDBMSを指定する必要があります。
EAmez
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.