这个基数在解释计划中是如何计算的?

How is this cardinality being calculated in Explain plan?

我正在分析以下指令的“解释计划”

SELECT *  FROM friends WHERE SUBSTR(activity,1,2) = '49';

和 Oracle SQL 开发人员告诉我它的基数为 1513,成本为 1302。

这些计算是如何进行的?可以用指令复制(用 select 计算并获得相同的值)?

解释计划生成的基数可以基于 许多 因素,但在您的代码中,Oracle 可能只是猜测 SUBSTR 表达式将 return table.

中所有行的 1%

例如,我们可以通过创建一个包含 151,300 行的简单 table 来重新创建您的基数估计:

drop table friends;

create table friends(activity varchar2(100));
create index friends_idx on friends(activity);

insert into friends select level from dual connect by level <= 1513 * 100;
begin
    dbms_stats.gather_table_stats(user, 'FRIENDS', no_invalidate => false);
end;
/

生成的解释计划估计查询将 return table 的 1%,即 1513 行:

explain plan for SELECT *  FROM friends WHERE SUBSTR(activity,1,2) = '49';
select * from table(dbms_xplan.display);


Plan hash value: 3524934291
 
-----------------------------------------------------------------------------
| Id  | Operation         | Name    | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |         |  1513 |  9078 |    72   (6)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| FRIENDS |  1513 |  9078 |    72   (6)| 00:00:01 |
-----------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - filter(SUBSTR("ACTIVITY",1,2)='49')

上面的代码是最简单的解释,但是您的查询可能会发生许多其他奇怪的事情。 运行 EXPLAIN PLAN FOR SELECT... 然后 SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY); 通常足以调查基数。请特别注意“注意”部分,以防出现意外问题。

并未记录所有这些基数规则和功能。但是如果您有很多空闲时间,并且想了解这一切背后的数学原理,运行 一些 10053 跟踪文件并阅读 Jonathan Lewis' 博客和书籍。他的书中也解释了“成本”是如何产生的,但计算起来很复杂,不值得担心。

为什么 Oracle 不计算完美的基数估计?

在 运行查询之前计算实际基数的成本太高。要为 SUBSTR 操作创建一个始终完美的估计,Oracle 必须 运行 类似于以下查询:

SELECT SUBSTR(activity,1,2), COUNT(*)
FROM friends
GROUP BY SUBSTR(activity,1,2);

对于我的示例数据,上述查询 returns 99 计数,并确定原始查询的基数估计应为 1111。

但是上面的查询必须先读取FRIENDS.ACTIVITY中的所有数据,这需要索引快速全扫描或全table扫描。然后必须对数据进行排序或散列以获得每组的计数(这可能是一个 O(N*LOG(N)) 操作)。如果 table 很大,中间结果将无法放入内存,必须写入然后从磁盘读取。

预先计算基数比实际查询本身要多一些工作。结果可能会被保存,但是存储这些结果可能会占用大量 space,而且数据库如何知道谓词将再次被需要?即使存储了预先计算的基数,一旦有人修改 table,这些值也可能变得一文不值。

这整个工作都假设函数是确定性的。虽然 SUBSTR 可以可靠地工作,但如果有像 DBMS_RANDOM.VALUE 这样的自定义函数呢?这些问题在理论上都是不可能的(停机问题),而且在实践中非常困难。相反,优化器依赖于诸如 DBA_TABLES.NUM_ROWS(从上次收集统计数据时开始)* 0.01 用于“复杂”谓词的猜测。

动态采样

Dynamic sampling,也称为动态统计,将预先 运行 部分您的 SQL 语句以创建更好的估计。您可以设置要采样的数据量,通过将值设置为 10,Oracle 将有效地 运行 提前确定基数。此功能显然可能非常慢,并且有很多奇怪的边缘情况和其他我没有在这里讨论的功能,但对于您的查询,它可以创建 1,111 行的完美估计:

EXPLAIN PLAN FOR SELECT /*+ dynamic_sampling(10) */ * FROM friends WHERE SUBSTR(activity,1,2) = '49';
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

Plan hash value: 3524934291
 
-----------------------------------------------------------------------------
| Id  | Operation         | Name    | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |         |  1111 |  6666 |    72   (6)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| FRIENDS |  1111 |  6666 |    72   (6)| 00:00:01 |
-----------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - filter(SUBSTR("ACTIVITY",1,2)='49')
 
Note
-----
   - dynamic statistics used: dynamic sampling (level=10)

动态重新优化

Oracle 可以在 运行 时跟踪行数并相应地调整计划。此功能无法帮助您进行简单的示例查询。但是如果 table 被用作连接的一部分,当基数估计变得更重要时,Oracle 将构建多个版本的解释计划并根据实际基数使用一个。

在下面的解释计划中,您可以看到估计仍然是旧的 1513。但是如果实际数字在 运行 时低得多,Oracle 将禁用 HASH JOIN 操作意味着对于大量行,将切换到更适合较少行数的 NESTED LOOPS 操作。

EXPLAIN PLAN FOR
SELECT *
FROM friends friends1
JOIN friends friends2
  ON friends1.activity = friends2.activity
WHERE SUBSTR(friends1.activity,1,2) = '49';

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(format => '+adaptive'));

Plan hash value: 215764417
 
-----------------------------------------------------------------------------------------
|   Id  | Operation               | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------
|     0 | SELECT STATEMENT        |             |  1530 | 18360 |   143   (5)| 00:00:01 |
|  *  1 |  HASH JOIN              |             |  1530 | 18360 |   143   (5)| 00:00:01 |
|-    2 |   NESTED LOOPS          |             |  1530 | 18360 |   143   (5)| 00:00:01 |
|-    3 |    STATISTICS COLLECTOR |             |       |       |            |          |
|  *  4 |     TABLE ACCESS FULL   | FRIENDS     |  1513 |  9078 |    72   (6)| 00:00:01 |
|- *  5 |    INDEX RANGE SCAN     | FRIENDS_IDX |     1 |     6 |   168   (2)| 00:00:01 |
|     6 |   TABLE ACCESS FULL     | FRIENDS     |   151K|   886K|    70   (3)| 00:00:01 |
-----------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - access("FRIENDS1"."ACTIVITY"="FRIENDS2"."ACTIVITY")
   4 - filter(SUBSTR("FRIENDS1"."ACTIVITY",1,2)='49')
   5 - access("FRIENDS1"."ACTIVITY"="FRIENDS2"."ACTIVITY")
 
Note
-----
   - this is an adaptive plan (rows marked '-' are inactive)

表达统计

表达式统计信息告诉 Oracle 收集其他类型的统计信息。我们可以强制 Oracle 收集有关 SUBSTR 表达式的统计信息,然后这些统计信息可用于更准确的估计。在下面的例子中,最终的估计实际上只是略有不同。单独的表达式统计在这里并不能很好地工作,但在这种情况下这只是运气不好。

SELECT dbms_stats.create_extended_stats(extension => '(SUBSTR(activity,1,2))', ownname => user, tabname => 'FRIENDS')
FROM DUAL;

begin
    dbms_stats.gather_table_stats(user, 'FRIENDS');
end;
/

EXPLAIN PLAN FOR SELECT * FROM friends WHERE SUBSTR(activity,1,2) = '49';

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

Plan hash value: 3524934291
 
-----------------------------------------------------------------------------
| Id  | Operation         | Name    | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |         |  1528 | 13752 |    72   (6)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| FRIENDS |  1528 | 13752 |    72   (6)| 00:00:01 |
-----------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - filter(SUBSTR("ACTIVITY",1,2)='49')

表达统计和直方图

通过添加直方图,我们终于创建了与您的老师所描述的非常相似的东西。收集表达式统计信息后,直方图将保存有关最多 255 个不同范围或桶中唯一值数量的信息。在我们的例子中,由于只有 99 个唯一行,直方图将完美地将“49”的行数估计为“1111”。

--(There are several ways to gather histograms. Instead of directly forcing it, I prefer to call the query
-- multiple times so that Oracle will register the need for a histogram, and automatically create one.)
SELECT * FROM friends WHERE SUBSTR(activity,1,2) = '49';
SELECT * FROM friends WHERE SUBSTR(activity,1,2) = '49';

begin
    dbms_stats.gather_table_stats(user, 'FRIENDS');
end;
/

EXPLAIN PLAN FOR SELECT * FROM friends WHERE SUBSTR(activity,1,2) = '49';

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

Plan hash value: 3524934291
 
-----------------------------------------------------------------------------
| Id  | Operation         | Name    | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |         |  1111 |  9999 |    72   (6)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| FRIENDS |  1111 |  9999 |    72   (6)| 00:00:01 |
-----------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - filter(SUBSTR("ACTIVITY",1,2)='49')

总结

Oracle 不会自动预 运行 所有谓词以完美估计基数。但是我们可以使用多种机制让 Oracle 对我们关心的少量查询执行非常相似的操作。

当您考虑绑定变量时,情况会变得更加复杂 - 如果值“49”经常变化怎么办? (自适应游标共享可以帮助解决这个问题。)或者如果修改了大量的行,我们如何快速更新统计信息呢? (在线统计数据收集和增量统计数据可以提供帮助。)

优化器并没有真正优化。时间够了。