为啥解释计划显示错误的行数?

Posted

技术标签:

【中文标题】为啥解释计划显示错误的行数?【英文标题】:Why does explain plan show the wrong number of rows?为什么解释计划显示错误的行数? 【发布时间】:2021-01-09 12:56:54 【问题描述】:

我正在尝试模拟this,为此我创建了以下过程来插入大量行:

create or replace PROCEDURE a_lot_of_rows is
    i carte.cod%TYPE;
    a carte.autor%TYPE := 'Author #';
    t carte.titlu%TYPE := 'Book #';
    p carte.pret%TYPE := 3.23;
    e carte.nume_editura%TYPE := 'Penguin Random House';

begin
    for i in 8..1000 loop
        insert into carte
        values (i, e, a || i, t || i, p, 'hardcover');
        commit;
    end loop;

    for i in 1001..1200 loop
        insert into carte
        values (i, e, a || i, t || i, p, 'paperback');
        commit;
    end loop;

end;

我在tip_coperta 列上创建了一个位图索引(它只能有'hardcover' 和'paperback' 的值),然后再插入1200 行。但是,解释计划给出的结果如下(在插入过程之前,表有 7 行,其中 4 行有tip_coperta = 'paperback'):

---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       |     4 |   284 |    34   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| CARTE |     4 |   284 |    34   (0)| 00:00:01 |
---------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - filter("TIP_COPERTA"='paperback')

【问题讨论】:

另请注意,您的查询使用任何索引 - TABLE ACCESS FULL 【参考方案1】:

座右铭:糟糕的统计数据比没有统计数据更糟糕

TLDR您的统计数据已过时,需要重新收集。如果您创建索引,则会自动收集 index 统计信息,但不会收集与您的案例相关的表格统计信息

让我们使用以下脚本模拟您的示例,以创建表格并用 1000 个精装本和 200 个平装本填充它。

create table CARTE 
(cod int,
 autor VARCHAR2(100),
 titlu VARCHAR2(100),
 pret NUMBER,
 nume_editura  VARCHAR2(100),
 tip_coperta VARCHAR2(100)
); 


insert into CARTE
(cod,autor,titlu,pret,nume_editura,tip_coperta)
select rownum,
       'Author #'||rownum ,
       'Book #'||rownum,
       3.23,
       'Penguin Random Number',
       case when rownum <=1000 then 'hardcover'
       else 'paperback' end
from dual connect by level <= 1200;       
commit;

这使新表没有优化器对象统计信息,您可以使用以下仅返回 NULLs 的查询来验证它

select NUM_ROWS, LAST_ANALYZED from user_tables where table_name = 'CARTE';

那么,我们来看看Oracle对表的印象是什么:

EXPLAIN PLAN  SET STATEMENT_ID = 'jara1' into   plan_table  FOR
select * from CARTE 
where tip_coperta = 'paperback'
;
--    
SELECT * FROM table(DBMS_XPLAN.DISPLAY('plan_table', 'jara1','ALL'));

上面的脚本为查询paberbacks 生成执行计划,您会看到Rows 很好(= 200)。这怎么可能?

---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       |   200 | 46800 |     5   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| CARTE |   200 | 46800 |     5   (0)| 00:00:01 |
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - filter("TIP_COPERTA"='paperback')

解释在计划输出的注释中-使用了动态采样。

基本上,Oracle 在解析查询时会执行一个附加查询来估计带有过滤谓词的行数。

Note
-----
   - dynamic statistics used: dynamic sampling (level=2)

动态采样对于很少使用的表来说很好,但是如果表是定期查询的,我们需要优化器统计以节省动态采样的开销

所以让我们收集统计数据

 exec dbms_stats.gather_table_stats(ownname=>user, tabname=>'CARTE' ); 

现在您看到统计信息已收集,总行数很好,并且 在列统计中创建了一个frequency 直方图 - 这对于估计具有特定值的记录数很重要!

select NUM_ROWS, LAST_ANALYZED from user_tables where table_name = 'CARTE';

  NUM_ROWS LAST_ANALYZED      
---------- -------------------
      1200 09.01.2021 16:48:26
      
select NUM_DISTINCT,HISTOGRAM  from user_tab_columns where table_name = 'CARTE' and column_name = 'TIP_COPERTA'; 

NUM_DISTINCT HISTOGRAM      
------------ ---------------
           2 FREQUENCY     
       

让我们检查一下统计信息在执行计划中的工作情况

EXPLAIN PLAN  SET STATEMENT_ID = 'jara1' into   plan_table  FOR
select * from CARTE 
where tip_coperta = 'paperback'
;
--    
SELECT * FROM table(DBMS_XPLAN.DISPLAY('plan_table', 'jara1','ALL'));

基本上我们看到同样正确的结果

---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       |   200 | 12400 |     5   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| CARTE |   200 | 12400 |     5   (0)| 00:00:01 |
---------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - filter("TIP_COPERTA"='paperback')
       

现在我们从表格中删除除了四个“平装本”之外的所有内容

delete from   CARTE 
where tip_coperta = 'paperback' and cod > 1004;
commit;

select count(*) from CARTE 
where tip_coperta = 'paperback'

  COUNT(*)
----------
         4
     

通过此操作,统计数据过时,并根据过时的数据给出错误的结果。在重新收集统计信息之前,将出现此错误结果。

EXPLAIN PLAN  SET STATEMENT_ID = 'jara1' into   plan_table  FOR
select * from CARTE 
where tip_coperta = 'paperback'
;
--    
SELECT * FROM table(DBMS_XPLAN.DISPLAY('plan_table', 'jara1','ALL'));

---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       |   200 | 12400 |     5   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| CARTE |   200 | 12400 |     5   (0)| 00:00:01 |
---------------------------------------------------------------------------

制定一项政策,使您的统计信息保持最新!

【讨论】:

谢谢@JonHeller - 喜欢我的英语;)【参考方案2】:

对于这个基数来说,表的统计数据很重要。

您需要等到自动统计数据收集任务启动并获取新的统计数据,或者您可以自己做:

exec dbms_stats.gather_table_stats(null,'carte',method_opt=>'for all columns size 1 for columns size 254 TIP_COPERTA')

这将强制在TIP_COPERTA 列上而不是在其他列上存在直方图(您可能希望使用for all columns size skewfor all columns size auto 或者甚至只是让它默认为任何设置的首选method_opt 参数是。有关此参数的详细信息,请阅读this article。

在某些更高版本的 Oracle 中,根据您运行它的位置,您可能还拥有Real-Time Statistics。即使在传统的 DML 之后,Oracle 也会在此更新您的统计信息。

请务必记住,基数估计不需要完全准确,您也可以获得合理的执行计划。一个常见的经验法则是,它应该在一个数量级之内,即使这样,大多数时候你也可能会没事。

【讨论】:

【参考方案3】:

要获得行数的估计值,Oracle 需要您分析表(或索引)。创建索引时,会进行自动分析。

【讨论】:

我已经删除了位图索引,然后重新创建了它,但我仍然得到与解释计划相同的行数。 也许您的行为并未使 Oracle 保存在内存中的计划无效。已经有一段时间了,我似乎记得您必须指出指数下降是否会使现有计划无效。此外,在不知道它是关于什么陈述的情况下对解释计划进行哲学思考非常有趣。

以上是关于为啥解释计划显示错误的行数?的主要内容,如果未能解决你的问题,请参考以下文章

数据表中的错误:无法显示正确的行数

当我已经使用游标时,为啥我得到“精确提取返回的行数超过请求的行数”?

dataGridView1添加的行数 运行的时候为啥不能直接显示出来?

部分错误Swift中的行数

分段错误的行数

错误:iPhone SDK 中 UITableView 部分中的行数