Để có hiệu suất tuyệt đối, SUM nhanh hơn hay COUNT?


31

Điều này liên quan đến việc đếm số lượng hồ sơ phù hợp với một điều kiện nhất định, ví dụ invoice amount > $100.

Tôi có xu hướng thích

COUNT(CASE WHEN invoice_amount > 100 THEN 1 END)

Tuy nhiên, điều này là hợp lệ

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

Tôi đã nghĩ rằng COUNT thích hợp hơn vì 2 lý do:

  1. Truyền đạt ý định, đó là COUNT
  2. COUNT có thể liên quan đến một i += 1hoạt động đơn giản ở đâu đó, trong khi SUM không thể tin vào biểu thức của nó là một giá trị nguyên đơn giản.

Có ai có sự thật cụ thể về sự khác biệt trên RDBMS cụ thể không?

Câu trả lời:


32

Bạn chủ yếu trả lời câu hỏi cho mình rồi. Tôi có một vài miếng để thêm:

Trong PostgreSQL (và các RDBMS khác hỗ trợ booleanloại), bạn có thể sử dụng booleantrực tiếp kết quả kiểm tra. Truyền nó tới integerSUM():

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

Hoặc sử dụng nó trong một NULLIF()biểu thức và COUNT():

COUNT(NULLIF(amount > 100, FALSE))

Hoặc đơn giản OR NULL:

COUNT(amount > 100 OR NULL)

Hoặc nhiều cách diễn đạt khác. Hiệu suất gần như giống hệt nhau . COUNT()thường là rất nhanh hơn một chút so với SUM(). Không giống như SUM()và như Paul đã nhận xét , COUNT()không bao giờ trở lại NULL, có thể thuận tiện. Liên quan:

Kể từ Postgres 9,4 cũng có FILTERđiều khoản . Chi tiết:

nhanh hơn tất cả những điều trên khoảng 5 - 10%:

COUNT(*) FILTER (WHERE amount > 100)

Nếu truy vấn đơn giản như trường hợp thử nghiệm của bạn, chỉ với một số lượng duy nhất và không có gì khác, bạn có thể viết lại:

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

Đó là vua thực sự của hiệu suất, thậm chí không có chỉ số.
Với một chỉ mục có thể áp dụng, nó có thể nhanh hơn bằng các đơn đặt hàng cường độ, đặc biệt là với các lần quét chỉ mục.

Điểm chuẩn

Hậu 10

Tôi đã chạy một loạt các thử nghiệm mới cho Postgres 10, bao gồm FILTERmệnh đề tổng hợp và chứng minh vai trò của một chỉ số cho số lượng nhỏ và lớn.

Thiết lập đơn giản:

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

Thời gian thực tế thay đổi khá nhiều do tiếng ồn nền và đặc thù của giường thử nghiệm. Hiển thị thời gian tốt nhất điển hình từ một bộ thử nghiệm lớn hơn. Hai trường hợp nên nắm bắt bản chất:

Kiểm tra 1 đếm ~ 1% của tất cả các hàng

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 <> fiddle ở đây

Kiểm tra 2 đếm ~ 33% của tất cả các hàng

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 <> fiddle ở đây

Thử nghiệm cuối cùng trong mỗi bộ đã sử dụng quét chỉ mục , đó là lý do tại sao nó giúp đếm một phần ba tất cả các hàng. Quét chỉ mục đơn giản hoặc quét chỉ mục bitmap không thể cạnh tranh với quét tuần tự khi liên quan đến khoảng 5% trở lên của tất cả các hàng.

Bài kiểm tra cũ cho Postgres 9.1

Để xác minh, tôi đã chạy thử nghiệm nhanh EXPLAIN ANALYZEtrên bảng thực tế trong PostgreQuery 9.1.6.

74208 trong số 184568 hàng đủ điều kiện với điều kiện kat_id > 50. Tất cả các truy vấn trả về cùng một kết quả. Tôi đã chạy từng lượt như vậy 10 lần để loại trừ các hiệu ứng bộ đệm và thêm kết quả tốt nhất như ghi chú:

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

Hầu như không có sự khác biệt thực sự trong hiệu suất.


1
Giải pháp LỌC có đánh bại bất kỳ biến thể nào từ nhóm "chậm hơn" không?
Andriy M

@AndriyM: Tôi thấy thời gian tổng hợp nhanh hơn một chút FILTERso với các biểu thức ở trên (thử nghiệm với pg 9.5). Bạn có nhận được như vậy? ( WHEREvẫn là vua biểu diễn - nếu có thể).
Erwin Brandstetter

Không có PG tiện dụng, vì vậy không thể nói. Dù sao, tôi chỉ đơn thuần hy vọng bạn cập nhật câu trả lời của mình với các số liệu thời gian cho giải pháp cuối cùng, chỉ để hoàn thiện :)
Andriy M

@AndriyM: Cuối cùng tôi đã có xung quanh để thêm điểm chuẩn mới. Các FILTERgiải pháp thường nhanh hơn trong các thử nghiệm của tôi.
Erwin Brandstetter

11

Đây là thử nghiệm của tôi trên 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

Nhìn vào các lần chạy riêng lẻ và các đợt riêng biệt

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

Kết quả sau khi chạy 5 lần (và lặp lại) là không thuyết phục.

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

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

Nó cho thấy rằng có nhiều sự thay đổi trong các điều kiện chạy hơn là có sự khác biệt giữa việc thực hiện, khi được đo bằng độ chi tiết của bộ định thời SQL Server. Cả hai phiên bản đều có thể đứng đầu, và phương sai tối đa tôi từng có là 2,5%.

Tuy nhiên, thực hiện một cách tiếp cận khác:

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

Từ đọc của tôi, có vẻ như phiên bản SUM làm được nhiều hơn một chút. Nó đang thực hiện một COUNT ngoài SUM. Đã nói rằng, COUNT(*)là khác nhau và nên nhanh hơn COUNT([Expr1004])(bỏ qua NULL, logic hơn). Trình tối ưu hóa hợp lý sẽ nhận ra rằng [Expr1004]trong SUM([Expr1004])phiên bản SUM là loại "int" và do đó sử dụng một thanh ghi số nguyên.

Trong mọi trường hợp, mặc dù tôi vẫn tin rằng COUNTphiên bản sẽ nhanh hơn trong hầu hết RDBMS, nhưng kết luận của tôi từ thử nghiệm là tôi sẽ tiếp tục SUM(.. 1.. 0..)trong tương lai, ít nhất là đối với SQL Server không vì lý do nào khác ngoài CẢNH BÁO ANSI được nêu ra khi sử dụng COUNT.


1

Theo kinh nghiệm của tôi Tạo dấu vết, cho cả hai phương thức trong Truy vấn khoảng 10.000.000 Tôi nhận thấy rằng Count (*) sử dụng khoảng hai lần CPU và chạy nhanh hơn một chút. nhưng Truy vấn của tôi không có bộ lọc.

Đếm(*)

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

Tổng (1)

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

Bạn nên chỉ định RDBMS nào bạn đã sử dụng để thực hiện kiểm tra này.
EAmez
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.