web-dev-qa-db-fra.com

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

Cela concerne le comptage du nombre d'enregistrements qui correspondent à une certaine condition, par ex. 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. COUNTprobablement implique un simple i += 1 quelque part, alors que SUM ne peut pas compter sur son expression pour être un simple entier.

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

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 type boolean), vous pouvez utiliser directement le résultat boolean du test . Castez-le dans integer et SUM():

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

Ou utilisez-le dans une expression NULLIF() 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 a déjà commenté , COUNT() ne renvoie jamais NULL, ce qui peut être pratique. En relation:

Depuis Postgres 9.4, il y a aussi la clause FILTER. 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 par ordre de grandeur, en particulier avec les scans indexés uniquement.

Repères

Postgres 10

J'ai exécuté une nouvelle série de tests pour Postgres 10, y compris la clause d'agrégat FILTER et démontrant le rôle d'un index 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 typique meilleurs temps à 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 balayage d'index uniquement , 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 exécuté un test rapide avec EXPLAIN ANALYZE Sur 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.

33
Erwin Brandstetter

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 effectue un COUNT en plus de un SUM. Cela dit, COUNT(*) est différent et devrait être plus rapide que COUNT([Expr1004]) (sauter NULLs, plus de logique). Un optimiseur raisonnable se rendra compte que [Expr1004] Dans SUM([Expr1004]) dans la version SUM est un type "int" et utilisera donc un registre entier.

Dans tous les cas, même si je pense toujours que la version COUNT sera plus rapide dans la plupart des SGBDR, ma conclusion des tests est que je vais continuer avec 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 de COUNT.

11
孔夫子

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.

nombre (*)

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

Somme (1)

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