这个基数在解释计划中是如何计算的?
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”经常变化怎么办? (自适应游标共享可以帮助解决这个问题。)或者如果修改了大量的行,我们如何快速更新统计信息呢? (在线统计数据收集和增量统计数据可以提供帮助。)
优化器并没有真正优化。时间够了。
我正在分析以下指令的“解释计划”
SELECT * FROM friends WHERE SUBSTR(activity,1,2) = '49';
和 Oracle SQL 开发人员告诉我它的基数为 1513,成本为 1302。
这些计算是如何进行的?可以用指令复制(用 select 计算并获得相同的值)?
解释计划生成的基数可以基于 许多 因素,但在您的代码中,Oracle 可能只是猜测 SUBSTR
表达式将 return table.
例如,我们可以通过创建一个包含 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”经常变化怎么办? (自适应游标共享可以帮助解决这个问题。)或者如果修改了大量的行,我们如何快速更新统计信息呢? (在线统计数据收集和增量统计数据可以提供帮助。)
优化器并没有真正优化。时间够了。