Ist SUM schneller oder COUNT für absolute Leistung?

31

Dies bezieht sich auf das Zählen der Anzahl von Datensätzen, die einer bestimmten Bedingung entsprechen, z invoice amount > $100.

Ich neige dazu, zu bevorzugen

COUNT(CASE WHEN invoice_amount > 100 THEN 1 END)

Dies gilt jedoch ebenso

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

Ich hätte gedacht, dass COUNT aus zwei Gründen vorzuziehen ist:

  1. Vermittelt die Absicht, was zu tun ist COUNT
  2. COUNT Vermutlich handelt es sich i += 1irgendwo um eine einfache Operation, während SUM nicht davon ausgehen kann, dass sein Ausdruck ein einfacher ganzzahliger Wert ist.

Hat jemand spezifische Fakten über den Unterschied bei bestimmten RDBMS?

孔夫子
quelle

Antworten:

32

Sie haben die Frage meistens schon selbst beantwortet. Ich habe ein paar Kleinigkeiten hinzuzufügen:

In PostgreSQL (und anderen RDBMS, die den booleanTyp unterstützen) können Sie das booleanErgebnis des Tests direkt verwenden. Cast it to integerund SUM():

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

Oder verwende es in einem NULLIF()Ausdruck und COUNT():

COUNT(NULLIF(amount > 100, FALSE))

Oder mit einem einfachen OR NULL:

COUNT(amount > 100 OR NULL)

Oder verschiedene andere Ausdrücke. Die Leistung ist nahezu identisch . COUNT()ist in der Regel etwas schneller als SUM(). Im Gegensatz zu SUM()und wie Paulus es bereits kommentierte , COUNT()kehrt er niemals zurück NULL, was vielleicht praktisch ist. Verbunden:

Seit Postgres 9.4 gibt es auch die FILTERKlausel . Einzelheiten:

Es ist um 5 - 10% schneller als alle oben genannten:

COUNT(*) FILTER (WHERE amount > 100)

Wenn die Abfrage so einfach wie Ihr Testfall ist und nur eine einzige Anzahl und nichts anderes enthält, können Sie Folgendes umschreiben:

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

Welches ist der wahre König der Leistung, auch ohne Index.
Mit einem anwendbaren Index kann es um Größenordnungen schneller sein, insbesondere bei Nur-Index-Scans.

Benchmarks

Postgres 10

Ich habe eine neue Testreihe für Postgres 10 durchgeführt, einschließlich der Aggregatklausel FILTERund der Darstellung der Rolle eines Index für kleine und große Zahlen.

Einfaches Setup:

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);

Die tatsächlichen Zeiten variieren aufgrund von Hintergrundgeräuschen und Besonderheiten des Prüfstands erheblich. Zeigen Sie typische Bestzeiten aus einer größeren Reihe von Tests. Diese beiden Fälle sollten das Wesentliche erfassen:

Test 1 mit ~ 1% aller Zeilen

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 <> hier fummeln

Test 2 mit ~ 33% aller Zeilen

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 <> hier fummeln

Der letzte Test in jedem Satz verwendete einen Nur-Index- Scan, weshalb er dazu beitrug, ein Drittel aller Zeilen zu zählen. Einfache Index- oder Bitmap-Index-Scans können nicht mit einem sequentiellen Scan konkurrieren, wenn ungefähr 5% oder mehr aller Zeilen betroffen sind.

Alter Test für Postgres 9.1

Zur Verifizierung habe ich EXPLAIN ANALYZEin PostgreSQL 9.1.6 einen Schnelltest mit einer echten Tabelle durchgeführt.

74208 von 184568 Zeilen qualifizierten sich mit der Bedingung kat_id > 50. Alle Abfragen geben das gleiche Ergebnis zurück. Ich habe jeweils 10-mal nacheinander ausgeführt, um Caching-Effekte auszuschließen, und das beste Ergebnis als Anmerkung angehängt:

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

Kaum ein wirklicher Leistungsunterschied.

Erwin Brandstetter
quelle
1
Schlägt die FILTER-Lösung eine der Variationen aus der "langsameren" Gruppe?
Andriy M
@AndriyM: Ich sehe für das Aggregat etwas schnellere Zeiten FILTERals mit den obigen Ausdrücken (Testen mit S. 9.5). Bekommst du das gleiche? ( WHEREist immer noch König der Leistung - wo möglich).
Erwin Brandstetter
Ich habe kein PG zur Hand, kann es also nicht sagen. Ich hatte nur gehofft, Sie würden Ihre Antwort der Vollständigkeit halber mit den Zeitangaben für die letzte Lösung aktualisieren :)
Andriy M
@AndriyM: Ich bin endlich dazu gekommen, neue Benchmarks hinzuzufügen. Die FILTERLösung ist in meinen Tests normalerweise schneller.
Erwin Brandstetter
11

Dies ist mein Test unter 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

Einzelne Läufe und Chargen getrennt betrachten

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

Die Ergebnisse nach 5-maligem Ausführen (und Wiederholen) sind ziemlich nicht schlüssig.

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

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

Es zeigt, dass die Ausführungsbedingungen weitaus variabler sind als die Implementierung, gemessen an der Granularität des SQL Server-Timers. Beide Versionen können die Oberhand gewinnen, und die maximale Varianz, die ich je hatte, beträgt 2,5%.

Ein anderer Ansatz:

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]))

Meiner Lektüre nach scheint die SUM-Version etwas mehr zu leisten. Sie führt zusätzlich zu einer SUMME eine COUNT durch . Having said that, ist COUNT(*)anders und sollte schneller sein als COUNT([Expr1004])(überspringen NULLs, mehr Logik). Ein vernünftiger Optimierer wird erkennen, dass [Expr1004]in SUM([Expr1004])der SUM-Version ein "int" -Typ vorliegt und daher ein Ganzzahlregister verwendet wird.

Auf jeden Fall, obwohl ich immer noch glaube, dass die COUNTVersion in den meisten RDBMS schneller sein wird, ist meine Schlussfolgerung aus den Tests, dass ich SUM(.. 1.. 0..)in Zukunft damit umgehen werde, zumindest für SQL Server aus keinem anderen Grund als den ANSI-WARNUNGEN, die bei der Verwendung ausgegeben werden COUNT.

孔夫子
quelle
1

Nach meiner Erfahrung habe ich festgestellt, dass bei beiden Methoden in einer Abfrage von etwa 10.000.000 die Anzahl (*) etwa das Zweifache der CPU ausmacht und etwas schneller ausgeführt wird. aber meine Abfragen sind ohne Filter.

Anzahl(*)

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

Summe (1)

CPU...........: 3859  
Execution time:  681 ms  
Marco Antonio Avila Arcos
quelle
Sie sollten angeben, welches RDBMS Sie für diesen Test verwendet haben.
EAmez