Czy dla osiągnięcia absolutnej wydajności SUM jest szybszy, czy COUNT?

31

Odnosi się to do zliczania liczby rekordów pasujących do określonego warunku, np invoice amount > $100.

Wolę

COUNT(CASE WHEN invoice_amount > 100 THEN 1 END)

Jest to jednak równie ważne

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

Myślałem, że COUNT jest lepszy z 2 powodów:

  1. Przekazuje intencję, którą jest COUNT
  2. COUNT prawdopodobnie wiąże się i += 1gdzieś z prostą operacją, podczas gdy SUM nie może liczyć na to, że jego wyrażenie będzie prostą liczbą całkowitą.

Czy ktoś ma konkretne fakty na temat różnicy w konkretnych RDBMS?

孔夫子
źródło

Odpowiedzi:

32

Przeważnie już odpowiedziałeś na to pytanie. Muszę dodać kilka kęsów:

W PostgreSQL (i innych RDBMS, które obsługują ten booleantyp) możesz bezpośrednio użyć booleanwyniku testu. Prześlij do integeri SUM():

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

Lub użyj go w NULLIF()wyrażeniu i COUNT():

COUNT(NULLIF(amount > 100, FALSE))

Lub za pomocą prostego OR NULL:

COUNT(amount > 100 OR NULL)

Lub różne inne wyrażenia. Wydajność jest prawie identyczna . COUNT()jest zwykle bardzo nieznacznie szybszy niż SUM(). W przeciwieństwie do SUM()i jak Paul już zauważył , COUNT()nigdy nie powraca NULL, co może być wygodniejsze. Związane z:

Od Postgres 9.4 istnieje również FILTERklauzula . Detale:

Jest szybszy niż wszystkie powyższe o około 5-10%:

COUNT(*) FILTER (WHERE amount > 100)

Jeśli zapytanie jest tak proste jak przypadek testowy, zawiera tylko jedną liczbę i nic więcej, możesz przepisać:

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

Który jest prawdziwym królem wydajności, nawet bez indeksu.
Z odpowiednim indeksem może być szybszy o rzędy wielkości, szczególnie w przypadku skanów tylko z indeksem.

Benchmarki

Postgres 10

Przeprowadziłem nową serię testów dla Postgres 10, w tym FILTERklauzulę zagregowaną i wykazując rolę indeksu dla małych i dużych liczb.

Prosta konfiguracja:

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

Rzeczywiste czasy różnią się nieco ze względu na hałas tła i specyfikę stanowiska testowego. Pokazuje typowe najlepsze czasy z większego zestawu testów. Te dwa przypadki powinny uchwycić istotę:

Test 1 zlicza ~ 1% wszystkich rzędów

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 <> skrzypce tutaj

Test 2 zlicza ~ 33% wszystkich rzędów

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 <> skrzypce tutaj

Ostatni test w każdym zestawie wykorzystał skanowanie tylko indeksu , dlatego pomógł zliczyć jedną trzecią wszystkich wierszy. Skanowanie indeksu zwykłego lub mapy bitowej nie może konkurować ze skanowaniem sekwencyjnym, jeśli obejmuje około 5% lub więcej wszystkich wierszy.

Stary test na Postgres 9.1

Aby to sprawdzić, uruchomiłem szybki test EXPLAIN ANALYZEna prawdziwej tabeli w PostgreSQL 9.1.6.

74208 z 184568 wierszy zakwalifikowanych z warunkiem kat_id > 50. Wszystkie zapytania zwracają ten sam wynik. Uruchomiłem każdy z nich 10 razy po kolei, aby wykluczyć efekty buforowania i dołączyłem najlepszy wynik jako notatkę:

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

Prawie żadna prawdziwa różnica w wydajności.

Erwin Brandstetter
źródło
1
Czy rozwiązanie FILTER pokonuje jakąkolwiek odmianę z grupy „wolniejszej”?
Andriy M
@AndriyM: Widzę nieco szybsze czasy dla agregacji FILTERniż z powyższymi wyrażeniami (testowanie z pg 9.5). Czy dostajesz to samo? ( WHEREwciąż jest królem wydajności - tam, gdzie to możliwe).
Erwin Brandstetter,
Nie mam przy sobie PG, więc nie mogę powiedzieć. W każdym razie, miałem tylko nadzieję, że zaktualizujesz swoją odpowiedź o dane czasowe dla ostatniego rozwiązania, tylko dla kompletności :)
Andriy M
@AndriyM: W końcu udało mi się dodać nowe testy porównawcze. FILTERRozwiązanie jest zazwyczaj szybciej w moich testów.
Erwin Brandstetter,
11

To jest mój test na 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

Patrząc na poszczególne przebiegi i partie oddzielnie

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

Wyniki po uruchomieniu 5 razy (i powtórzeniu) są dość niejednoznaczne.

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

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

Pokazuje, że istnieje znacznie większa zmienność warunków pracy niż różnica między implementacją, mierzoną za pomocą ziarnistości timera programu SQL Server. Każda wersja może być na topie, a maksymalna wariancja, jaką kiedykolwiek miałem, wynosi 2,5%.

Jednak przyjmując inne podejście:

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

Z mojego czytania wynika, że ​​wersja SUM robi trochę więcej. Oprócz SUMU wykonuje COUNT . To powiedziawszy, COUNT(*)jest inne i powinno być szybsze niż COUNT([Expr1004])(pomiń NULL, więcej logiki). Rozsądny optymalizator zda sobie sprawę, że [Expr1004]w SUM([Expr1004])wersji SUM jest typem „int”, a zatem wykorzystuje rejestr liczb całkowitych.

W każdym razie, mimo że nadal uważam, że COUNTwersja będzie szybsza w większości RDBMS, wyciągam wnioski z testów, że zamierzam iść SUM(.. 1.. 0..)w przyszłości, przynajmniej dla SQL Server bez żadnego innego powodu niż podniesienie OSTRZEŻENIA ANSI podczas używania COUNT.

孔夫子
źródło
1

Z mojego doświadczenia Wykonywanie śledzenia, dla obu metod w zapytaniu około 10 000 000 zauważyłem, że Count (*) zużywa około dwa razy procesora i działa nieco szybciej. ale moje zapytania są bez filtra.

Liczyć(*)

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

Suma (1)

CPU...........: 3859  
Execution time:  681 ms  
Marco Antonio Avila Arcos
źródło
Należy określić, którego RDBMS użyto do wykonania tego testu.
EAmez