Pour des performances absolues, SUM est-il plus rapide ou COUNT?

31

Cela concerne le comptage du nombre d'enregistrements qui correspondent à une certaine condition, par exemple invoice amount > $100.

J'ai tendance à préférer

COUNT(CASE WHEN invoice_amount > 100 THEN 1 END)

Cependant, c'est tout aussi valable

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

J'aurais pensé que COUNT est préférable pour 2 raisons:

  1. Transmet l'intention, qui est de COUNT
  2. COUNT implique probablement une i += 1opération simple quelque part, alors que SUM ne peut pas compter sur son expression pour être une simple valeur entière.

Quelqu'un a-t-il des faits spécifiques concernant la différence sur un SGBDR spécifique?

孔夫子
la source

Réponses:

32

Vous avez déjà répondu à la question vous-même. J'ai quelques morceaux à ajouter:

Dans PostgreSQL (et les autres SGBDR qui prennent en charge le booleantype), vous pouvez utiliser booleandirectement le résultat du test. Castez-le integeret SUM():

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

Ou utilisez-le dans une NULLIF()expression et COUNT():

COUNT(NULLIF(amount > 100, FALSE))

Ou avec un simple OR NULL:

COUNT(amount > 100 OR NULL)

Ou diverses autres expressions. Les performances sont presque identiques . COUNT()est généralement très légèrement plus rapide que SUM(). Contrairement SUM()et comme Paul l'a déjà commenté , COUNT()ne revient jamais NULL, ce qui peut être pratique. En relation:

Depuis Postgres 9.4, il y a aussi la FILTERclause . Détails:

C'est plus rapide que tout ce qui précède d'environ 5 à 10%:

COUNT(*) FILTER (WHERE amount > 100)

Si la requête est aussi simple que votre cas de test, avec un seul décompte et rien d'autre, vous pouvez réécrire:

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

Qui est le vrai roi de la performance, même sans indice.
Avec un index applicable, il peut être plus rapide de plusieurs ordres de grandeur, en particulier avec les analyses d'index uniquement.

Repères

Postgres 10

J'ai exécuté une nouvelle série de tests pour Postgres 10, y compris la FILTERclause d' agrégation et démontrant le rôle d'un indice pour les petits et les grands comptes.

Configuration simple:

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

Les temps réels varient beaucoup en raison du bruit de fond et des spécificités du banc d'essai. Affichage des meilleurs temps typiques à partir d'un plus grand ensemble de tests. Ces deux cas devraient saisir l'essence:

Test 1 comptant ~ 1% de toutes les lignes

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 <> violon ici

Test 2 comptant ~ 33% de toutes les lignes

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 <> violon ici

Le dernier test de chaque ensemble a utilisé un index uniquement analyse d' , c'est pourquoi il a aidé à compter un tiers de toutes les lignes. Les analyses d'index simple ou d'index bitmap ne peuvent pas rivaliser avec une analyse séquentielle lorsqu'elles impliquent environ 5% ou plus de toutes les lignes.

Ancien test pour Postgres 9.1

Pour vérifier, j'ai effectué un test rapide avec EXPLAIN ANALYZE une table réelle dans PostgreSQL 9.1.6.

74208 des 184568 lignes qualifiées avec la condition kat_id > 50. Toutes les requêtes renvoient le même résultat. J'ai couru chacun 10 fois à tour de rôle pour exclure les effets de mise en cache et j'ai ajouté le meilleur résultat comme note:

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

Presque aucune réelle différence de performances.

Erwin Brandstetter
la source
1
La solution FILTER bat-elle l'une des variations du groupe "plus lent"?
Andriy M
@AndriyM: Je vois des temps légèrement plus rapides pour l'agrégat FILTERqu'avec les expressions ci-dessus (test avec pg 9.5). Obtenez-vous la même chose? ( WHEREest toujours roi de la performance - si possible).
Erwin Brandstetter
Je n'ai pas de PG à portée de main, donc je ne peux pas le dire. Quoi qu'il en soit, j'espérais simplement que vous mettriez à jour votre réponse avec les chiffres de synchronisation pour la dernière solution, juste pour être complet :)
Andriy M
@AndriyM: J'ai finalement réussi à ajouter de nouveaux repères. La FILTERsolution est généralement plus rapide dans mes tests.
Erwin Brandstetter
11

Ceci est mon test sur 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

Examen des analyses et des lots individuels séparément

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

Les résultats après avoir exécuté 5 fois (et répété) ne sont pas concluants.

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

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

Cela montre qu'il y a beaucoup plus de variabilité dans les conditions d'exécution qu'il n'y a de différence entre l'implémentation, mesurée avec la granularité du temporisateur SQL Server. L'une ou l'autre version peut venir en haut, et l'écart maximum que j'ai jamais obtenu est de 2,5%.

Cependant, en adoptant une approche différente:

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

D'après ma lecture, il semblerait que la version SUM fasse un peu plus. Il exécute un COUNT en plus d' un SUM. Cela dit, COUNT(*)c'est différent et devrait être plus rapide que COUNT([Expr1004])(sauter les NULL, plus de logique). Un optimiseur raisonnable se rendra compte que [Expr1004]dansSUM([Expr1004]) la version SUM est un type "int" et utilisera donc un registre entier.

Dans tous les cas, même si je crois toujours que la COUNTversion sera plus rapide dans la plupart des SGBDR, ma conclusion des tests est que je vais continuer à SUM(.. 1.. 0..)l'avenir, au moins pour SQL Server pour aucune autre raison que les avertissements ANSI générés lors de l'utilisation COUNT.

孔夫子
la source
1

Dans Mon expérience Faire une trace, pour les deux méthodes dans une requête d'environ 10 000 000, j'ai remarqué que Count (*) utilise environ deux fois le processeur et s'exécute un peu plus rapidement. mais mes requêtes sont sans filtre.

Compter(*)

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

Somme (1)

CPU...........: 3859  
Execution time:  681 ms  
Marco Antonio Avila Arcos
la source
Vous devez spécifier le SGBDR que vous avez utilisé pour effectuer ce test.
EAmez