WHERE 与 GROUP BY 的性能对比
WHERE vs. HAVING performance with GROUP BY
所以我被分配去评估两个查询的性能并得出了一个令人惊讶的结果。我事先被告知 HAVING
比 WHERE
慢,因为它只在访问行后过滤结果。这似乎是合理的,并且 this question on SQL clause execution order 加强了这一点。
但是,我根据一些假设估计了以下查询的性能,似乎使用 HAVING
执行速度实际上更快!
SELECT status, count(status)
FROM customer
GROUP BY status
HAVING status != 'Active' AND status != 'Dormant'
SELECT status, count(status)
FROM customer
WHERE status != 'Active' AND status != 'Dormant'
GROUP BY status
假设是:
- table
CUSTOMER
有 100 000 条记录
- 访问一行的成本是 0.01 毫秒(SELECT + COUNT)
- 执行一个子句的成本是0.005ms
- 客户状态分为三种,上面两种和'Deceased'
- 有 15 000 'Deceased' 个客户
基于此我的估计是:
First query:
Accessing all rows, FROM: 100 000 * 0.01ms = 1000ms
GROUP BY: 100 000 * 0.005ms = 500ms
HAVING (2 conditions, 3 groups): 2 * 3 * 0.005ms = 0.03ms
SELECT and COUNT results: 15 000 * 0.01ms = 150ms
Total execution time: 1.65003s
Second query:
Accessing all the rows, FROM: 1000ms
WHERE: 2 * 100 000 * 0.005ms = 1000ms
GROUP BY: 15 000 * 0.005ms = 75ms
SELECT and COUNT results: 15 000 * 0.01ms = 150ms
Total execution time: 2.225s
结果是因为GROUP BY
只产生了三组,非常容易过滤,而WHERE
需要一条一条过滤记录
由于我天真地依赖权威,我假设我在某处犯了错误,或者提供的假设是错误的。
GROUP BY
与 HAVING
的行为是否会导致执行时间缩短?
编辑:查询计划
PLAN_TABLE_OUTPUT /* With HAVING */
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 5 | 35 | 4 (25)| 00:00:01 |
|* 1 | FILTER | | | | | |
| 2 | HASH GROUP BY | | 5 | 35 | 4 (25)| 00:00:01 |
| 3 | TABLE ACCESS STORAGE FULL| CUSM | 5 | 35 | 3 (0)| 00:00:01 |
------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("STATUS"<>'Active' AND "STATUS"<>'Dormant')
PLAN_TABLE_OUTPUT /* With WHERE */
-----------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 7 | 4 (25)| 00:00:01 |
| 1 | HASH GROUP BY | | 1 | 7 | 4 (25)| 00:00:01 |
|* 2 | TABLE ACCESS STORAGE FULL| CUSM | 1 | 7 | 3 (0)| 00:00:01 |
-----------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - storage("STATUS"<>'Active' AND "STATUS"<>'Dormant')
filter("STATUS"<>'Active' AND "STATUS"<>'Dormant')
事情是这样的:
根据 Oracle 执行计划,两个查询都在执行完整的 table 扫描。也就是说,他们正在阅读 table 的 ALL THE ROWS。没有区别。
HAVING 查询执行 GROUP BY(哈希)生成 3 行。然后,它将过滤器应用于那 3 行,并 returns 结果。
WHERE 查询在读取每一行(规范中为 100,000)后对其应用过滤器,将它们减少到 15,000。最后,它将这些(散列)分组为 1 行,returns 一行。
我认为在您描述的情况下,WHERE 查询将过滤器应用于所有 100,000 行,而 HAVING 查询延迟过滤器并且仅将其应用于 3 行。这使得 HAVING 查询更快。
现在,不要假设此结果将适用于您这样的每个查询。 Oracle 在使用 table 统计方面非常聪明。该计划将来会根据您添加到 table 的真实数据而改变。 5 行的计划绝不代表 100,000 行的计划。
对这个结果持保留态度。现实世界的场景要复杂得多。
您的一个假设是错误的:HAVING 比 WHERE 慢,因为它仅在访问 和散列 行后过滤结果。
正是散列部分使 HAVING 条件比 WHERE 条件更昂贵。散列需要写入数据,这在物理上和算法上都可能更昂贵。
理论
散列需要写入和读取数据。理想情况下,散列数据将在 O(n)
时间内 运行。但在实践中会有散列冲突,这会减慢速度。实际上,并非所有数据都适合内存。
这两个问题可能是灾难性的。在最坏的情况下,内存有限,散列需要多次传递,复杂度接近 O(n^2)
。在临时 table 空间中写入磁盘比写入内存慢几个数量级。
这些是您需要担心的数据库性能问题。与读取、写入和连接数据的时间相比,运行 简单条件和表达式的恒定时间通常是无关紧要的。
在您的环境中尤其如此。操作 TABLE ACCESS STORAGE FULL
表示您正在使用 Exadata。根据平台的不同,您可能会在芯片中利用 SQL。这些高级条件可以完美地转换为在存储设备上执行的低级指令。这意味着您对执行条款成本的估计可能高出几个数量级。
练习
创建一个包含 100,000 行的示例 table:
create table customer(id number, status varchar2(100));
insert into customer
select
level,
case
when level <= 15000 then 'Deceased'
when level between 15001 and 50001 then 'Active'
else 'Dormant'
end
from dual
connect by level <= 100000;
begin
dbms_stats.gather_table_stats(user, 'customer');
end;
/
运行 循环中的代码显示 WHERE
版本的速度大约是 HAVING
版本的两倍。
--Run times (in seconds): 0.765, 0.78, 0.765
declare
type string_nt is table of varchar2(100);
type number_nt is table of number;
v_status string_nt;
v_count number_nt;
begin
for i in 1 .. 100 loop
SELECT status, count(status)
bulk collect into v_status, v_count
FROM customer
GROUP BY status
HAVING status != 'Active' AND status != 'Dormant';
end loop;
end;
/
--Run times (in seconds): 0.39, 0.39, 0.39
declare
type string_nt is table of varchar2(100);
type number_nt is table of number;
v_status string_nt;
v_count number_nt;
begin
for i in 1 .. 100 loop
SELECT status, count(status)
bulk collect into v_status, v_count
FROM customer
WHERE status != 'Active' AND status != 'Dormant'
GROUP BY status;
end loop;
end;
/
所以我被分配去评估两个查询的性能并得出了一个令人惊讶的结果。我事先被告知 HAVING
比 WHERE
慢,因为它只在访问行后过滤结果。这似乎是合理的,并且 this question on SQL clause execution order 加强了这一点。
但是,我根据一些假设估计了以下查询的性能,似乎使用 HAVING
执行速度实际上更快!
SELECT status, count(status)
FROM customer
GROUP BY status
HAVING status != 'Active' AND status != 'Dormant'
SELECT status, count(status)
FROM customer
WHERE status != 'Active' AND status != 'Dormant'
GROUP BY status
假设是:
- table
CUSTOMER
有 100 000 条记录 - 访问一行的成本是 0.01 毫秒(SELECT + COUNT)
- 执行一个子句的成本是0.005ms
- 客户状态分为三种,上面两种和'Deceased'
- 有 15 000 'Deceased' 个客户
基于此我的估计是:
First query:
Accessing all rows, FROM: 100 000 * 0.01ms = 1000ms
GROUP BY: 100 000 * 0.005ms = 500ms
HAVING (2 conditions, 3 groups): 2 * 3 * 0.005ms = 0.03ms
SELECT and COUNT results: 15 000 * 0.01ms = 150ms
Total execution time: 1.65003s
Second query:
Accessing all the rows, FROM: 1000ms
WHERE: 2 * 100 000 * 0.005ms = 1000ms
GROUP BY: 15 000 * 0.005ms = 75ms
SELECT and COUNT results: 15 000 * 0.01ms = 150ms
Total execution time: 2.225s
结果是因为GROUP BY
只产生了三组,非常容易过滤,而WHERE
需要一条一条过滤记录
由于我天真地依赖权威,我假设我在某处犯了错误,或者提供的假设是错误的。
GROUP BY
与 HAVING
的行为是否会导致执行时间缩短?
编辑:查询计划
PLAN_TABLE_OUTPUT /* With HAVING */
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 5 | 35 | 4 (25)| 00:00:01 |
|* 1 | FILTER | | | | | |
| 2 | HASH GROUP BY | | 5 | 35 | 4 (25)| 00:00:01 |
| 3 | TABLE ACCESS STORAGE FULL| CUSM | 5 | 35 | 3 (0)| 00:00:01 |
------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("STATUS"<>'Active' AND "STATUS"<>'Dormant')
PLAN_TABLE_OUTPUT /* With WHERE */
-----------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 7 | 4 (25)| 00:00:01 |
| 1 | HASH GROUP BY | | 1 | 7 | 4 (25)| 00:00:01 |
|* 2 | TABLE ACCESS STORAGE FULL| CUSM | 1 | 7 | 3 (0)| 00:00:01 |
-----------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - storage("STATUS"<>'Active' AND "STATUS"<>'Dormant')
filter("STATUS"<>'Active' AND "STATUS"<>'Dormant')
事情是这样的:
根据 Oracle 执行计划,两个查询都在执行完整的 table 扫描。也就是说,他们正在阅读 table 的 ALL THE ROWS。没有区别。
HAVING 查询执行 GROUP BY(哈希)生成 3 行。然后,它将过滤器应用于那 3 行,并 returns 结果。
WHERE 查询在读取每一行(规范中为 100,000)后对其应用过滤器,将它们减少到 15,000。最后,它将这些(散列)分组为 1 行,returns 一行。
我认为在您描述的情况下,WHERE 查询将过滤器应用于所有 100,000 行,而 HAVING 查询延迟过滤器并且仅将其应用于 3 行。这使得 HAVING 查询更快。
现在,不要假设此结果将适用于您这样的每个查询。 Oracle 在使用 table 统计方面非常聪明。该计划将来会根据您添加到 table 的真实数据而改变。 5 行的计划绝不代表 100,000 行的计划。
对这个结果持保留态度。现实世界的场景要复杂得多。
您的一个假设是错误的:HAVING 比 WHERE 慢,因为它仅在访问 和散列 行后过滤结果。
正是散列部分使 HAVING 条件比 WHERE 条件更昂贵。散列需要写入数据,这在物理上和算法上都可能更昂贵。
理论
散列需要写入和读取数据。理想情况下,散列数据将在 O(n)
时间内 运行。但在实践中会有散列冲突,这会减慢速度。实际上,并非所有数据都适合内存。
这两个问题可能是灾难性的。在最坏的情况下,内存有限,散列需要多次传递,复杂度接近 O(n^2)
。在临时 table 空间中写入磁盘比写入内存慢几个数量级。
这些是您需要担心的数据库性能问题。与读取、写入和连接数据的时间相比,运行 简单条件和表达式的恒定时间通常是无关紧要的。
在您的环境中尤其如此。操作 TABLE ACCESS STORAGE FULL
表示您正在使用 Exadata。根据平台的不同,您可能会在芯片中利用 SQL。这些高级条件可以完美地转换为在存储设备上执行的低级指令。这意味着您对执行条款成本的估计可能高出几个数量级。
练习
创建一个包含 100,000 行的示例 table:
create table customer(id number, status varchar2(100));
insert into customer
select
level,
case
when level <= 15000 then 'Deceased'
when level between 15001 and 50001 then 'Active'
else 'Dormant'
end
from dual
connect by level <= 100000;
begin
dbms_stats.gather_table_stats(user, 'customer');
end;
/
运行 循环中的代码显示 WHERE
版本的速度大约是 HAVING
版本的两倍。
--Run times (in seconds): 0.765, 0.78, 0.765
declare
type string_nt is table of varchar2(100);
type number_nt is table of number;
v_status string_nt;
v_count number_nt;
begin
for i in 1 .. 100 loop
SELECT status, count(status)
bulk collect into v_status, v_count
FROM customer
GROUP BY status
HAVING status != 'Active' AND status != 'Dormant';
end loop;
end;
/
--Run times (in seconds): 0.39, 0.39, 0.39
declare
type string_nt is table of varchar2(100);
type number_nt is table of number;
v_status string_nt;
v_count number_nt;
begin
for i in 1 .. 100 loop
SELECT status, count(status)
bulk collect into v_status, v_count
FROM customer
WHERE status != 'Active' AND status != 'Dormant'
GROUP BY status;
end loop;
end;
/