WHERE 与 GROUP BY 的性能对比

WHERE vs. HAVING performance with GROUP BY

所以我被分配去评估两个查询的性能并得出了一个令人惊讶的结果。我事先被告知 HAVINGWHERE 慢,因为它只在访问行后过滤结果。这似乎是合理的,并且 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

假设是:

基于此我的估计是:

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 BYHAVING 的行为是否会导致执行时间缩短?

编辑:查询计划

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

事情是这样的:

  1. 根据 Oracle 执行计划,两个查询都在执行完整的 table 扫描。也就是说,他们正在阅读 table 的 ALL THE ROWS。没有区别。

  2. HAVING 查询执行 GROUP BY(哈希)生成 3 行。然后,它将过滤器应用于那 3 行,并 returns 结果。

  3. 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;
/