Untuk kinerja absolut, apakah SUM lebih cepat atau COUNT?

31

Ini berkaitan dengan penghitungan jumlah rekaman yang cocok dengan kondisi tertentu, misalnya invoice amount > $100.

Saya cenderung lebih suka

COUNT(CASE WHEN invoice_amount > 100 THEN 1 END)

Namun, ini sama validnya

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

Saya akan berpikir COUNT lebih disukai karena 2 alasan:

  1. Menyampaikan niat, yaitu untuk COUNT
  2. COUNT mungkin melibatkan yang sederhanai += 1 operasi suatu tempat, sedangkan SUM tidak dapat mengandalkan ekspresinya sebagai nilai integer sederhana.

Adakah yang punya fakta spesifik tentang perbedaan RDBMS tertentu?

孔夫子
sumber

Jawaban:

32

Anda kebanyakan sudah menjawab pertanyaan itu sendiri. Saya punya beberapa bagian untuk ditambahkan:

Di PostgreSQL (dan RDBMS lainnya yang mendukung booleantipe), Anda dapat menggunakan booleanhasil tes secara langsung. Keluarkan ke integerdan SUM():

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

Atau gunakan dalam NULLIF()ekspresi dan COUNT():

COUNT(NULLIF(amount > 100, FALSE))

Atau dengan yang sederhana OR NULL:

COUNT(amount > 100 OR NULL)

Atau berbagai ekspresi lainnya. Performanya hampir identik . COUNT()biasanya sangat sedikit lebih cepat daripada SUM(). Berbeda SUM()dan seperti Paul sudah berkomentar , COUNT()tidak pernah kembali NULL, yang mungkin nyaman. Terkait:

Sejak Postgres 9.4 ada juga FILTERklausa . Detail:

Lebih cepat dari semua hal di atas sekitar 5 - 10%:

COUNT(*) FILTER (WHERE amount > 100)

Jika kueri sesederhana kasus pengujian Anda, dengan hanya satu hitungan dan tidak ada yang lain, Anda dapat menulis ulang:

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

Yang merupakan raja sejati kinerja, bahkan tanpa indeks.
Dengan indeks yang berlaku, indeks dapat lebih cepat berdasarkan pesanan, terutama dengan pemindaian hanya indeks.

Tingkatan yang dicapai

Postgres 10

Saya menjalankan serangkaian tes baru untuk Postgres 10, termasuk FILTERklausa agregat dan menunjukkan peran indeks untuk jumlah kecil dan besar.

Pengaturan sederhana:

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

Waktu yang sebenarnya bervariasi sedikit karena kebisingan latar belakang dan spesifik dari tempat tidur uji. Menampilkan waktu terbaik khas dari serangkaian tes yang lebih besar. Dua kasus ini harus menangkap esensi:

Tes 1 penghitungan ~ 1% dari semua baris

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 <> biola di sini

Tes 2 menghitung ~ 33% dari semua baris

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 <> biola di sini

Tes terakhir di setiap set menggunakan pemindaian hanya indeks , itulah sebabnya itu membantu untuk menghitung sepertiga dari semua baris. Pemindaian indeks biasa atau indeks bitmap tidak dapat bersaing dengan pemindaian sekuensial ketika melibatkan sekitar 5% atau lebih dari semua baris.

Tes lama untuk Postgres 9.1

Untuk memverifikasi saya menjalankan tes cepat dengan EXPLAIN ANALYZEpada tabel kehidupan nyata di PostgreSQL 9.1.6.

74208 dari 184568 baris memenuhi syarat dengan kondisi tersebut kat_id > 50. Semua pertanyaan mengembalikan hasil yang sama. Saya berlari masing-masing seperti 10 kali secara bergantian untuk mengecualikan efek caching dan menambahkan hasil terbaik sebagai catatan:

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

Hampir tidak ada perbedaan nyata dalam kinerja.

Erwin Brandstetter
sumber
1
Apakah solusi FILTER mengalahkan variasi apa pun dari grup "lebih lambat"?
Andriy M
@AndriyM: Saya melihat waktu yang lebih cepat untuk agregat FILTERdaripada dengan ekspresi di atas (pengujian dengan hal 9.5). Apakah Anda mendapatkan yang sama? ( WHEREMasih raja kinerja - jika memungkinkan).
Erwin Brandstetter
Belum punya PG berguna, jadi tidak tahu. Pokoknya, saya hanya berharap Anda akan memperbarui jawaban Anda dengan angka waktu untuk solusi terakhir, hanya untuk kelengkapan :)
Andriy M
@AndriyM: Saya akhirnya berkeliling untuk menambah tolok ukur baru. The FILTERsolusi adalah biasanya lebih cepat dalam tes saya.
Erwin Brandstetter
11

Ini adalah pengujian saya pada 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

Melihat masing-masing berlari dan batch secara terpisah

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

Hasil setelah menjalankan 5 kali (dan berulang) cukup tidak meyakinkan.

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

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

Ini menunjukkan bahwa ada jauh lebih variabilitas dalam kondisi berjalan daripada ada perbedaan antara implementasi, bila diukur dengan granularity timer SQL Server. Versi mana pun dapat muncul di atas, dan varians maksimum yang pernah saya dapatkan adalah 2,5%.

Namun, mengambil pendekatan yang berbeda:

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

Dari bacaan saya, akan terlihat bahwa versi SUM melakukan sedikit lebih banyak. Itu melakukan COUNT selain SUM. Karena itu, COUNT(*)berbeda dan harus lebih cepat daripadaCOUNT([Expr1004]) (melewatkan NULLs, lebih banyak logika). Pengoptimal yang masuk akal akan menyadari bahwa [Expr1004]di SUM([Expr1004])dalam versi SUM adalah tipe "int" dan karenanya menggunakan register integer.

Bagaimanapun, sementara saya masih percaya COUNTversi akan lebih cepat di sebagian besar RDBMS, kesimpulan saya dari pengujian adalah bahwa saya akan pergi dengan SUM(.. 1.. 0..)di masa depan, setidaknya untuk SQL Server tanpa alasan lain selain PERINGATAN ANSI yang dimunculkan ketika menggunakan COUNT.

孔夫子
sumber
1

Dalam pengalaman saya Membuat jejak, untuk kedua metode dalam Permintaan sekitar 10.000.000 Saya Melihat Hitung (*) menggunakan sekitar dua kali CPU dan berjalan sedikit lebih cepat. tapi Pertanyaan saya tanpa filter.

Menghitung(*)

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

Jumlah (1)

CPU...........: 3859  
Execution time:  681 ms  
Marco Antonio Avila Arcos
sumber
Anda harus menentukan RDBMS mana yang telah Anda gunakan untuk melakukan tes ini.
EAmez