Для абсолютної продуктивності, 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 є кращим з 2 причин:

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

Хтось має конкретні факти про різницю щодо конкретних RDBMS?

Відповіді:


32

Ви здебільшого вже відповідали на питання. Я маю додати декілька заквасок:

У PostgreSQL (та інших RDBMS, які підтримують booleanтип) ви можете використовувати booleanрезультат тесту безпосередньо. Передайте це 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;

Який справжній король продуктивності, навіть без індексу.
За допомогою застосовного індексу він може бути швидшим на порядок, особливо при скануванні лише з індексом.

Орієнтири

Постгрес 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 -- !!!

db <> скрипка тут

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

db <> скрипка тут

Останній тест у кожному наборі використовував сканування лише для покажчиків , тому воно допомогло підрахувати третину всіх рядків. Просте сканування індексу чи растрових зображень не може конкурувати з послідовним скануванням, якщо залучається приблизно 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
Чи перемагає рішення FILTER будь-яку з варіацій групи "повільніше"?
Андрій М

@AndriyM: Я бачу трохи швидші часи для сукупності, FILTERніж для виразів вище (тестування з pg 9.5). Ви отримуєте те саме? ( WHEREвсе-таки король продуктивності - де це можливо).
Ервін Брандстеттер

Не знайшли PG, тому не можу сказати. У всякому разі, я просто сподівався, що ви оновите свою відповідь часовими цифрами для останнього рішення, просто для повноти :)
Андрій М

@AndriyM: Нарешті я почав додавати нові орієнтири. FILTERРішення є , як правило , швидше в моїх тестах.
Ервін Брандстеттер

11

Це мій тест на RTM SQL Server 2012.

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версія буде швидшою у більшості RDBMS, моє висновок із тестування полягає в тому, що я збираюся піти з цим SUM(.. 1.. 0..)у майбутньому, принаймні для SQL Server ні з якої іншої причини, ніж ANSI ПОПЕРЕДЖЕННЯ, які піднімаються під час використання COUNT.


1

На моєму досвіді Слід відстежувати, що для обох методів у запиті близько 10 000 000 я помітив, що Count (*) використовує близько два рази процесор і працює трохи швидше. але мої запити без фільтра.

Рахувати(*)

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

Сума (1)

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

Йо слід вказати, які RDBMS ви використовували для цього тесту.
EAmez
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.