เพื่อประสิทธิภาพที่สมบูรณ์ 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;

ซึ่งเป็นราชาที่แท้จริงของการแสดงแม้ไม่มีดัชนี
ด้วยดัชนีที่ใช้งานได้มันจะเร็วขึ้นตามลำดับความสำคัญโดยเฉพาะการสแกนเฉพาะดัชนี

มาตรฐาน

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

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

74,208 184,568 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
โซลูชันตัวกรองเอาชนะรูปแบบใด ๆ จากกลุ่ม "ช้าลง" หรือไม่
Andriy M

@AndriyM: ฉันเห็นเวลารวมเร็วกว่าเล็กน้อยFILTERด้วยการแสดงออกข้างต้น (ทดสอบกับ pg 9.5) คุณได้รับเหมือนกันหรือไม่ ( WHEREยังคงเป็นราชาแห่งการแสดง - หากเป็นไปได้)
Erwin Brandstetter

ยังไม่ได้รับประโยชน์ PG ดังนั้นไม่สามารถบอกได้ อย่างไรก็ตามฉันแค่หวังว่าคุณจะอัปเดตคำตอบของคุณด้วยตัวเลขเวลาสำหรับการแก้ปัญหาครั้งสุดท้ายเพื่อความสมบูรณ์ :)
Andriy M

@AndriyM: ในที่สุดฉันก็ได้เพิ่มมาตรฐานใหม่ FILTERวิธีการแก้ปัญหาคือมักจะเร็วขึ้นในการทดสอบของฉัน
Erwin Brandstetter

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])(ข้าม NULLs ตรรกะมากขึ้น) เครื่องมือเพิ่มประสิทธิภาพที่เหมาะสมจะทราบว่า[Expr1004]ในSUM([Expr1004])รุ่น SUM เป็นประเภท "int" และใช้ประโยชน์จากการลงทะเบียนจำนวนเต็ม

ไม่ว่าในกรณีใดในขณะที่ฉันยังเชื่อว่าCOUNTรุ่นจะเร็วขึ้นใน RDBMS ส่วนใหญ่ข้อสรุปของฉันจากการทดสอบคือฉันจะไปด้วยSUM(.. 1.. 0..)ในอนาคตอย่างน้อยสำหรับ SQL Server โดยไม่มีเหตุผลอื่นนอกจาก ANSI WARNINGS เมื่อใช้งานCOUNT.


1

จากประสบการณ์ของฉันการติดตามสำหรับทั้งสองวิธีในแบบสอบถามประมาณ 10,000,000 ครั้งฉันสังเกตเห็นว่า Count (*) ใช้ CPU ประมาณสองเท่าและรันเร็วขึ้นเล็กน้อย แต่ข้อความค้นหาของฉันไม่มีตัวกรอง

นับ(*)

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

ซำ (1)

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

Yo ควรระบุว่า RDBMS ใดที่คุณใช้ทำการทดสอบนี้
EAmez
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.