Для абсолютной производительности 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)

Я бы подумал, что COUNT предпочтительнее по двум причинам:

  1. Передает намерение, которое заключается в COUNT
  2. COUNT вероятно,i += 1 где-то подразумевается простая операция, тогда как SUM не может рассчитывать на то, что ее выражение будет простым целочисленным значением.

У кого-нибудь есть конкретные факты о разнице в конкретных СУБД?

孔夫子
источник

Ответы:

32

Вы в основном уже ответили на вопрос сами. У меня есть несколько кусочков, чтобы добавить:

В PostgreSQL (и других СУБД, поддерживающих booleanтип) вы можете booleanнапрямую использовать результат теста. Cast его 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.4 есть также FILTERпункт . Детали:

Это быстрее, чем все вышеперечисленное, примерно на 5-10%:

COUNT(*) FILTER (WHERE amount > 100)

Если запрос такой же простой, как ваш тестовый пример, с одним счетчиком и ничем иным, вы можете переписать:

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

Который является истинным королем производительности, даже без индекса.
При использовании соответствующего индекса он может быть на порядок быстрее, особенно при сканировании только по индексу.

Ориентиры

Postgres 10

Я запустил новую серию тестов для Postgres 10, в том числе агрегатное FILTERпредложение и демонстрацию роли индекса для малых и больших количеств.

Простая настройка:

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

Фактические времена меняются немного из-за фонового шума и особенностей испытательного стенда. Показаны типичные лучшие времена из большого набора тестов. Эти два случая должны охватывать суть:

Тест 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 -- !!!

дБ <> скрипка здесь

Тест 2 с подсчетом ~ 33% всех строк

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 -- !!!

дБ <> скрипка здесь

В последнем тесте в каждом наборе использовалось сканирование только по индексу , поэтому оно помогло подсчитать треть всех строк. Сканирования с обычным индексом или растровым индексом не могут конкурировать с последовательным сканированием, когда задействовано примерно 5% или более всех строк.

Старый тест для Postgres 9.1

Чтобы убедиться, что я провел быстрый тест EXPLAIN ANALYZEна реальной таблице в PostgreSQL 9.1.6.

74208 из 184568 строк квалифицированы с условием 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
Решение «ФИЛЬТР» побеждает какие-либо изменения из «более медленной» группы?
Андрей М
@AndriyM: я вижу немного более быстрые времена для агрегата, FILTERчем с выражениями выше (тестирование с помощью pg 9.5). Вы получаете то же самое? ( WHEREпо-прежнему царь производительности - где это возможно).
Эрвин Брандштеттер
У вас нет под рукой PG, так что не могу сказать. Во всяком случае, я просто надеялся, что вы обновите свой ответ с указанием сроков последнего решения, просто для полноты :)
Андрей М
@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 делает немного больше. Он выполняет COUNT в дополнение к SUM. Сказав это, COUNT(*)он отличается и должен быть быстрее, чем COUNT([Expr1004])(пропустите NULL, больше логики). Разумный оптимизатор поймет , что [Expr1004]в SUM([Expr1004])в версии SUM является типом «INT» и поэтому использовать целочисленный регистр.

В любом случае, хотя я все еще верю, что COUNTв большинстве СУБД версия будет быстрее, мой вывод из тестирования таков, что я собираюсь продолжить SUM(.. 1.. 0..)в будущем, по крайней мере для SQL Server, не по другой причине, кроме предупреждений ANSI, возникающих при использовании COUNT,

孔夫子
источник
1

По моему опыту Как сделать трассировку, для обоих методов в запросе около 10 000 000 я заметил, что Count (*) использует примерно вдвое больше ЦП и работает немного быстрее. но мои Запросы без фильтра.

Count (*)

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

Сумма (1)

CPU...........: 3859  
Execution time:  681 ms  
Марко Антонио Авила Аркос
источник
Вам следует указать, какую СУБД вы использовали для этого теста.
EAmez