Per prestazioni assolute, SUM è più veloce o COUNT?


31

Ciò si riferisce al conteggio del numero di record che corrispondono a una determinata condizione, ad es invoice amount > $100.

Tendo a preferire

COUNT(CASE WHEN invoice_amount > 100 THEN 1 END)

Tuttavia, questo è altrettanto valido

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

Avrei pensato che COUNT fosse preferibile per 2 motivi:

  1. Trasmette l'intenzione, che è di COUNT
  2. COUNT probabilmente comporta una semplice i += 1operazione da qualche parte, mentre SUM non può contare sulla sua espressione come un semplice valore intero.

Qualcuno ha fatti specifici sulla differenza su RDBMS specifici?

Risposte:


32

Per lo più hai già risposto alla domanda da solo. Ho alcuni bocconcini da aggiungere:

In PostgreSQL (e altri RDBMS che supportano il booleantipo) è possibile utilizzare direttamente il booleanrisultato del test. Trasmettilo su integere SUM():

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

Oppure usalo in NULLIF()un'espressione e COUNT():

COUNT(NULLIF(amount > 100, FALSE))

O con un semplice OR NULL:

COUNT(amount > 100 OR NULL)

O varie altre espressioni. Le prestazioni sono quasi identiche . COUNT()è in genere leggermente più veloce di SUM(). A differenza SUM()e come già commentato da Paolo , COUNT()non ritorna mai NULL, il che può essere conveniente. Relazionato:

Da Postgres 9.4 esiste anche la FILTERclausola . Dettagli:

È più veloce di tutto quanto sopra di circa il 5-10%:

COUNT(*) FILTER (WHERE amount > 100)

Se la query è semplice come il tuo test case, con un solo conteggio e nient'altro, puoi riscrivere:

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

Qual è il vero re delle prestazioni, anche senza indice.
Con un indice applicabile può essere più veloce per ordini di grandezza, specialmente con scansioni solo indice.

benchmark

Postgres 10

Ho eseguito una nuova serie di test per Postgres 10, inclusa la FILTERclausola aggregata e dimostrando il ruolo di un indice per conteggi piccoli e grandi.

Installazione semplice:

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

I tempi effettivi variano un po 'a causa del rumore di fondo e delle specifiche del banco di prova. Mostra i tempi migliori tipici di una serie più ampia di test. Questi due casi dovrebbero catturare l'essenza:

Test 1 contando ~ 1% di tutte le righe

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 <> violino qui

Test 2 conteggio ~ 33% di tutte le righe

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 <> violino qui

L'ultimo test in ogni set ha utilizzato una scansione di solo indice , motivo per cui ha contribuito a contare un terzo di tutte le righe. Le scansioni dell'indice semplice o dell'indice bitmap non possono competere con una scansione sequenziale se coinvolgono circa il 5% o più di tutte le righe.

Vecchio test per Postgres 9.1

Per verificare ho eseguito un test rapido con EXPLAIN ANALYZEsu una tabella di vita reale in PostgreSQL 9.1.6.

74208 di 184568 file qualificate con la condizione kat_id > 50. Tutte le query restituiscono lo stesso risultato. Ho eseguito ciascuno come 10 volte a turno per escludere effetti di cache e ho aggiunto il risultato migliore come nota:

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

Quasi nessuna vera differenza nelle prestazioni.


1
La soluzione FILTER supera alcune delle variazioni rispetto al gruppo "più lento"?
Andriy M,

@AndriyM: vedo tempi leggermente più veloci per l'aggregato FILTERrispetto alle espressioni sopra (test con pag. 9.5). Ottieni lo stesso? ( WHEREè ancora il re delle prestazioni - ove possibile).
Erwin Brandstetter,

Non ho un PG a portata di mano, quindi non posso dirlo. Ad ogni modo, speravo solo che avresti aggiornato la tua risposta con le cifre dei tempi per l'ultima soluzione, solo per completezza :)
Andriy M,

@AndriyM: sono finalmente riuscito ad aggiungere nuovi parametri di riferimento. La FILTERsoluzione è in genere più veloce nei miei test.
Erwin Brandstetter,

11

Questo è il mio test su 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

Esame di singole serie e lotti separatamente

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

I risultati dopo aver eseguito 5 volte (e ripetendo) sono abbastanza inconcludenti.

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

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

Mostra che esiste una variabilità molto maggiore nelle condizioni di funzionamento rispetto alla differenza tra l'implementazione, se misurata con la granularità del timer di SQL Server. Entrambe le versioni possono arrivare al top e la varianza massima che io abbia mai ottenuto è del 2,5%.

Tuttavia, adottando un approccio diverso:

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

Dalla mia lettura, sembrerebbe che la versione SUM faccia un po 'di più. Sta eseguendo un COUNT in aggiunta a un SUM. Detto questo, COUNT(*)è diverso e dovrebbe essere più veloce di COUNT([Expr1004])(salta i NULL, più logica). Un ragionevole ottimizzatore si renderà conto che [Expr1004]nella SUM([Expr1004])versione SUM è un tipo "int" e quindi utilizza un registro intero.

In ogni caso, mentre credo ancora che la COUNTversione sarà più veloce nella maggior parte dei RDBMS, la mia conclusione dai test è che andrò avanti SUM(.. 1.. 0..)in futuro, almeno per SQL Server per nessun altro motivo se non le AVVISI ANSI sollevate durante l'utilizzo COUNT.


1

Nella mia esperienza Creazione di una traccia, per entrambi i metodi in una query di circa 10.000.000 ho notato che Count (*) utilizza circa il doppio della CPU ed esegue un po 'più veloce. ma le mie query sono senza filtro.

Contare(*)

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

Sum (1)

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

Yo dovrebbe specificare quale RDBMS hai usato per fare questo test.
EAmez,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.