解释计划生成的基数可以基于许多因素,但在您的代码中,Oracle 可能只是猜测SUBSTR 表达式将返回表中所有行的 1%。
例如,我们可以通过创建一个包含 151,300 行的简单表来重新创建您的基数估计:
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;
/
生成的解释计划估计查询将返回表的 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);
对于我的示例数据,上述查询返回 99 个计数,并确定原始查询的基数估计应为 1111。
但是上面的查询要先从FRIENDS.ACTIVITY中读取所有数据,这需要索引快速全扫描或者全表扫描。然后必须对数据进行排序或散列以获得每组的计数(这可能是 O(N*LOG(N)) 操作)。如果表很大,中间结果将不适合内存,必须先写入然后从磁盘读取。
预先计算基数比实际查询本身要多。结果可能会被保存,但存储这些结果可能会占用大量空间,并且数据库如何知道将再次需要谓词?即使存储了预先计算的基数,一旦有人修改表,这些值也可能变得毫无价值。
整个工作都假设函数是确定性的。虽然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 可以在运行时跟踪行数并相应地调整计划。此功能无法帮助您进行简单的示例查询。但是如果表被用作连接的一部分,当基数估计变得更重要时,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' 经常更改怎么办? (自适应光标共享可以帮助解决这个问题。)或者如果修改了大量的行怎么办,我们如何快速更新统计信息? (在线统计收集和增量统计可以提供帮助。)
优化器并没有真正优化。只有足够的时间来满足。