【问题标题】:How is this cardinality being calculated in Explain plan?在解释计划中如何计算这个基数?
【发布时间】:2022-01-09 11:22:15
【问题描述】:

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

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

Oracle SQL Developer 告诉我它的基数为 1513,成本为 1302。

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

【问题讨论】:

  • 简短的回答是,不。成本是优化器分配给操作员的人工数字;它本身没有任何意义,但是将一个运算符与另一个运算符进行比较很有用。基数是对运算符在一轮执行中将产生的行数的估计。此数字基于表统计信息、索引访问、堆访问以及您可能无法查看的各种其他输入。
  • 无法通过计算 Friends 表中元组的数量并将该值除以存在的不同“活动”值的数量(按前两位数字分类)来获得 CARDINALITY 的近似值左边?

标签: sql oracle database-design oracle-sqldeveloper


【解决方案1】:

解释计划生成的基数可以基于许多因素,但在您的代码中,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' 经常更改怎么办? (自适应光标共享可以帮助解决这个问题。)或者如果修改了大量的行怎么办,我们如何快速更新统计信息? (在线统计收集和增量统计可以提供帮助。)

优化器并没有真正优化。只有足够的时间来满足。

【讨论】:

  • 我的老师说基数显示了根据用于搜索的列中值的可变性计算得出的统计值......并且可以通过计算朋友表中的元组数并将其除以得到根据存在的“活动”的不同值的数量,根据左侧前两位数字的分类来确定价值……这是对的吗?我怎么能这样做?
  • @Kuru 我认为您的老师正在描述数据库可以计算基数的理论上最好的事情。在实践中,Oracle 有几个特性可以模仿这种行为,但没有一个是完美的。我将在今天晚些时候更新我的问题并提供更多详细信息。
猜你喜欢
  • 2012-04-16
  • 2011-03-10
  • 1970-01-01
  • 1970-01-01
  • 2012-12-26
  • 2023-03-10
  • 1970-01-01
  • 2010-09-09
  • 2017-04-23
相关资源
最近更新 更多