暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Oracle分区和执行计划相关的几个问题

原创 听见风的声音 2025-03-07
561

1 统计信息的查看

在进行数据库性能分析时,如果发现了一条SQL语句的执行计划有问题,走了错误的访问路径或者是执行了错误的连接顺序。这条SQL语句涉及到数据库对象(表和索引)的统计信息是否准确、是否过时是首先应该考虑到的。
Oracle提供了相应的视图,可以从这些视图里查询到统计信息的搜集时间,核对统计信息是否准确,涉及到表的统计信息的视图有2个,另外有一个视图可以查询基于分区的列的统计信息,在下面一一列出。

2.1表统计信息
SELECT TABLE_NAME,PARTITION_NAME,PARTITION_POSITION,OBJECT_TYPE,NUM_ROWS,BLOCKS, SAMPLE_SIZE,LAST_ANALYZED FROM user_TAB_STATISTICS WHERE TABLE_NAME='SALES' ORDER BY PARTITION_NAME nulls first; ----------+--------------+------------------+-----------+--------+------+-----------+-----------------------+ TABLE_NAME|PARTITION_NAME|PARTITION_POSITION|OBJECT_TYPE|NUM_ROWS|BLOCKS|SAMPLE_SIZE|LAST_ANALYZED | ----------+--------------+------------------+-----------+--------+------+-----------+-----------------------+ SALES | | |TABLE | 918843| 14084| 918843|2025-01-16 06:15:47.000| SALES |SALES_2018 | 1|PARTITION | 0| 0| |2025-01-20 03:22:46.000| SALES |SALES_H1_2019 | 2|PARTITION | 79445| 1006| 79445|2025-01-20 03:22:46.000| SALES |SALES_H2_2019 | 3|PARTITION | 99389| 1006| 99389|2025-03-04 06:20:55.000|

视图user_TAB_STATISTICS可以查询表的统计信息,如果是分区表,每个分区的统计信息也可以看到。

2.2 列的统计信息
SELECT TABLE_NAME,COLUMN_NAME,NUM_DISTINCT,DENSITY,NUM_NULLS, SAMPLE_SIZE,LAST_ANALYZED FROM USER_TAB_COL_STATISTICS WHERE table_name='SALES' AND COLUMN_NAME='TIME_ID'; ----------+-----------+------------+--------------------+---------+-----------+-----------------------+ TABLE_NAME|COLUMN_NAME|NUM_DISTINCT|DENSITY |NUM_NULLS|SAMPLE_SIZE|LAST_ANALYZED | ----------+-----------+------------+--------------------+---------+-----------+-----------------------+ SALES |TIME_ID | 1459|0.000685400959561343| 0| 918843|2024-11-22 06:55:52.000|

视图USER_TAB_COL_STATISTICS可以查询表的每一列的统计信息,查到的列的统计信息是表级的,不涉及到分区表里面的分区。

2.3 列的分区统计信息
SELECT TABLE_NAME,PARTITION_NAME,COLUMN_NAME,NUM_DISTINCT,DENSITY,NUM_NULLS,SAMPLE_SIZE,LAST_ANALYZED FROM USER_PART_COL_STATISTICS WHERE table_name='SALES' AND COLUMN_NAME='TIME_ID' ORDER BY PARTITION_NAME; ----------+--------------+-----------+------------+----------------------+---------+-----------+-----------------------+ TABLE_NAME|PARTITION_NAME|COLUMN_NAME|NUM_DISTINCT|DENSITY |NUM_NULLS|SAMPLE_SIZE|LAST_ANALYZED | ----------+--------------+-----------+------------+----------------------+---------+-----------+-----------------------+ SALES |SALES_2018 |TIME_ID | 0| 0| 0| |2024-09-09 08:20:31.000| SALES |SALES_H1_2019 |TIME_ID | 181|0.00000629366228208194| 0| 79445|2024-11-22 06:56:01.000| SALES |SALES_H2_2019 |TIME_ID | 184|0.00000503073780800692| 0| 99389|2024-11-22 06:56:02.000|

视图USER_PART_COL_STATISTICS可以查询分区表中每一列的分区级的统计信息。在检查列的统计信息时,要注意采用采样大小和最后分析时间,判断统计信息是否过时或者是采样比例不合适导致统计信息不准确。
索引的统计信息比较简单,可以从USER_IND_STATISTICS里面查询得到。

2 统计信息的收集

在检验统计信息的收集时,先写一个sql语句,查看一个统计信息各个视图的最后更新时间

SELECT 'table_statistics' sta_name,TABLE_NAME,PARTITION_NAME,LAST_ANALYZED FROM user_TAB_STATISTICS WHERE TABLE_NAME = 'SALES' AND (OBJECT_TYPE = 'TABLE' OR PARTITION_NAME = 'SALES_H1_2019') UNION SELECT 'TAB_COL_STATISTICS' sta_name,TABLE_NAME,NULL,LAST_ANALYZED FROM USER_TAB_COL_STATISTICS WHERE table_name = 'SALES' AND COLUMN_NAME = 'TIME_ID' UNION SELECT 'PART_COL_STATISTICS' sta_name,TABLE_NAME,PARTITION_NAME,LAST_ANALYZED FROM USER_PART_COL_STATISTICS WHERE table_name = 'SALES' AND COLUMN_NAME = 'TIME_ID' AND PARTITION_NAME = 'SALES_H1_2019'; --这条语句的查询结果如下 STA_NAME |TABLE_NAME|PARTITION_NAME|LAST_ANALYZED | -------------------+----------+--------------+-----------------------+ table_statistics |SALES | |2025-01-16 06:15:47.000| table_statistics |SALES |SALES_H1_2019 |2025-01-20 03:22:46.000| TAB_COL_STATISTICS |SALES | |2024-11-22 06:55:52.000| PART_COL_STATISTICS|SALES |SALES_H1_2019 |2024-11-22 06:56:01.000|

查询的前两行数据来自user_TAB_STATISTICS表,第一行是表的最后分析时间,第二行是其中一个分区的最后分析时间(为了显示起来紧凑一些,只显示一个分区),第三行来自USER_TAB_COL_STATISTICS,显示的是SALES表中一个列的最后分析时间,第四行来自USER_PART_COL_STATISTICS,显示的是SALES表上TIME_ID列SALES_H1_2019的最后分析时间。

2.1 使用ANALYZE TABLE

使用ANALYZE命令收集表的信息是比较早的方式,自Oracle12c以后得官网文档中有下面的提示
The use of ANALYZE for the collection of optimizer statistics is obsolete.
If you want to collect optimizer statistics, use the DBMS_STATS package, which lets you collect statistics in parallel, global statistics for partitioned objects, and helps you fine tune your statistics collection in other ways. See Oracle Database PL/SQL Packages and Types Reference for more information on the DBMS_STATS package.
Use the ANALYZE statement only for the following cases:
To use the VALIDATE or LIST CHAINED ROWS clauses
To collect information on freelist blocks

Oracle推荐使用 DBMS_STATS包来收集统计信息,它支持并行及不同的收集粒度,不过这种方式在Oracle23AI中仍可使用

ANALYZE TABLE sh.sales COMPUTE STATISTICS; Table analyzed.

运行上一节的查询语句,返回结果如下

STA_NAME |TABLE_NAME|PARTITION_NAME|LAST_ANALYZED | -------------------+----------+--------------+-----------------------+ table_statistics |SALES | |2025-01-16 06:15:47.000| table_statistics |SALES |SALES_H1_2019 |2025-03-06 08:25:59.000| TAB_COL_STATISTICS |SALES | |2024-11-22 06:55:52.000| PART_COL_STATISTICS|SALES |SALES_H1_2019 |2025-03-06 08:25:59.000|

这个查询返回的结果比较奇怪,分区的相关统计信息的最后分析时间更新了,表的反而没有更新。

2.2 使用DBMS_STATS.GATHER_TABLE_STATS包

使用这个过程最简单的方式运行(第一参数是owner,第二个参数是表名),

begin
DBMS_STATS.GATHER_TABLE_STATS(
    'SH', 'SALES');
end; 
```查询语句的返回结果如下
```sql
STA_NAME           |TABLE_NAME|PARTITION_NAME|LAST_ANALYZED          
-------------------+----------+--------------+-----------------------
table_statistics   |SALES     |              |2025-03-06 08:40:49.000
table_statistics   |SALES     |SALES_H1_2019 |2025-03-06 08:40:47.000
TAB_COL_STATISTICS |SALES     |              |2025-03-06 08:40:49.000
PART_COL_STATISTICS|SALES     |SALES_H1_2019 |2025-03-06 08:40:47.000

表和分区的统计信息的最后分析时间都更新le,从各个视图的最后更新的时间来看,在分析时是采用了并行优化。

2.3 DBMS_STATS.GATHER_TABLE_STATS存储过程的使用

这个存储过程的语法如下

DBMS_STATS.GATHER_TABLE_STATS ( ownname VARCHAR2, tabname VARCHAR2, partname VARCHAR2 DEFAULT NULL, estimate_percent NUMBER DEFAULT to_estimate_percent_type (get_param('ESTIMATE_PERCENT')), block_sample BOOLEAN DEFAULT FALSE, method_opt VARCHAR2 DEFAULT get_param('METHOD_OPT'), degree NUMBER DEFAULT to_degree_type(get_param('DEGREE')), granularity VARCHAR2 DEFAULT GET_PARAM('GRANULARITY'), cascade BOOLEAN DEFAULT to_cascade_type(get_param('CASCADE')), stattab VARCHAR2 DEFAULT NULL, statid VARCHAR2 DEFAULT NULL, statown VARCHAR2 DEFAULT NULL, no_invalidate BOOLEAN DEFAULT to_no_invalidate_type ( get_param('NO_INVALIDATE')), stattype VARCHAR2 DEFAULT 'DATA', force BOOLEAN DEFAULT FALSE, context DBMS_STATS.CCONTEXT DEFAULT NULL, -- non operative options VARCHAR2 DEFAULT get_param('OPTIONS'));

可以看到,这个存储过程还是比较复杂的,选项比较多。在使用这个存储过程来收集统计信息时,尽量的使用默认选项,这样用起来比较简单。这个过程除了用户名和表名外,都有默认选项。有几个选项的默认值不容易看出来,下面详细说明一下。
granularity只对分区表有效,是收集的粒度,在Oracle 23AI中,这个选项的默认值是‘AUTO’,它的解释是Oracle根据分区的类型来确定搜集的粒度。如果默认选项下统计信息不能符合要求,就要选择‘ALL’,收集所有(全局,分区,子分区)统计信息,也可以分别收集全局、分区、子分区的统计信息,或者不同的组合。
METHOD_OPT的默认值是default is FOR ALL COLUMNS SIZE AUTO.SIZE AUTO的解释是Oracle根据列的数据分布和列上的负载确定是否收集列的直方图。
cascade决定是否收集表上创建的索引的统计信息,这个选项的默认设置是auto,即是否收集统计信息由Oracle根据索引的情况来确定。

如果只收集一个分区的统计信息(这种情况应该会碰到),使用下面的语句

begin DBMS_STATS.GATHER_TABLE_STATS( 'SH', 'SALES','SALES_H1_2019',granularity=>'PARTITION'); end;

如果不设置granularity选项,会收集全局和填入的这个分区的统计信息。

3 执行计划评估行数异常

3.1 语句执行计划的基数同表中实际的行数差异巨大

下面这条sql语句查询SALES表的两个连续的分区,查询的范围包括这两个连续分区的所有数据,这条语句返回的结果如下

--sql1 SELECT count(*) FROM SH.SALES WHERE TIME_ID BETWEEN to_date('2019-01-01','YYYY-MM-DD') AND to_date('2019-12-31','YYYY-MM-DD'); COUNT(*)| --------+ 178834|

这条语句的执行计划如下

--------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop | --------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 8 | 547 (1)| 00:00:01 | | | | 1 | SORT AGGREGATE | | 1 | 8 | | | | | | 2 | PARTITION RANGE ITERATOR| | 229K| 1794K| 547 (1)| 00:00:01 | 2 | 3 | |* 3 | TABLE ACCESS FULL | SALES | 229K| 1794K| 547 (1)| 00:00:01 | 2 | 3 | ---------------------------------------------------------------------------------------------------

执行计划显示的rows(返回的行数,也称为基数)是229K,实际的行数应该是178834(178K左右),这两个值的差别是十分明显的。

3.2 改成union all形式
--sql2 SELECT count(*) FROM (SELECT * FROM SH.SALES WHERE TIME_ID between to_date('2019-01-01','YYYY-MM-DD') AND to_date('2019-06-30','YYYY-MM-DD') UNION all SELECT * FROM SH.SALES WHERE TIME_ID between to_date('2019-07-01','YYYY-MM-DD') AND to_date('2019-12-31','YYYY-MM-DD')); COUNT(*)| --------+ 178834|

执行计划如下

--------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop | --------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 547 (1)| 00:00:01 | | | | 1 | SORT AGGREGATE | | 1 | | | | | | | 2 | VIEW | | 178K| | 547 (1)| 00:00:01 | | | | 3 | UNION-ALL | | 178K| 1397K| 547 (1)| 00:00:01 | | | | 4 | PARTITION RANGE SINGLE| | 79445 | 620K| 274 (1)| 00:00:01 | 2 | 2 | |* 5 | TABLE ACCESS FULL | SALES | 79445 | 620K| 274 (1)| 00:00:01 | 2 | 2 | | 6 | PARTITION RANGE SINGLE| | 99389 | 776K| 274 (1)| 00:00:01 | 3 | 3 | |* 7 | TABLE ACCESS FULL | SALES | 99389 | 776K| 274 (1)| 00:00:01 | 3 | 3 | ---------------------------------------------------------------------------------------------------

改成union all的形式之后,执行计划显示的行数同语句的返回结果是一致的。

3.3 原因分析

设置10053事件,执行sql1,从跟踪文件里截取的访问路劲分析相关部分如下

SINGLE TABLE ACCESS PATH Single Table Cardinality Estimation for SALES[SALES] SPD: Return code in qosdDSDirSetup: NOCTX, estType = TABLE kkecdn: Single Table Predicate:"SALES"."TIME_ID">=TO_DATE(' 2019-01-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss') Column (#3): TIME_ID(DATE) AvgLen: 8 NDV: 1459 Nulls: 0 Density: 0.000685 Min: 2458485.000000 Max: 2459945.000000 Estimated selectivity: 1.000000 , col: #3 kkecdn: Single Table Predicate:"SALES"."TIME_ID"<=TO_DATE(' 2019-12-31 00:00:00', 'syyyy-mm-dd hh24:mi:ss') Estimated selectivity: 0.250000 , col: #3 Table: SALES Alias: SALES Card: Original: 918843.000000rsel = 0.250000 Rounded: 229711 Computed: 229711.181353 Non Adjusted: 229711.181353

在Card部分(计算访问路径的基数)中,Original为918843.000000,这是表SALES的总行数,rsel = 0.250000,计算的结果是这两个相乘,取近似值229711,这也是执行计划中rows列显示的值。Oracle优化器在计算操作的返回的行数时,并没有采用分区的统计信息,却选择了表的统计信息的值,在表的各分区数据分布不那么均匀的情况下,这里的计算有着较大的偏差也就顺理成章了。
这个偏差有可能会影响到执行计划的选择,从而导致Oracle选择了不是最优的执行计划。如果将sql1改写成union的形式,写成sql2的形式路径分析部分如下:

kkecdn: Single Table Predicate:"SALES"."TIME_ID">=TO_DATE(' 2019-01-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss') Column (#3): NewDensity:0.000434, OldDensity:0.000006 BktCnt:79445.000000, PopBktCnt:79445.000000, PopValCnt:181, NDV:181 Column (#3): TIME_ID(DATE) Part#: 1 AvgLen: 8 NDV: 181 Nulls: 0 Density: 0.000434 Min: 2458485.000000 Max: 2458665.000000 Histogram: Freq #Bkts: 181 UncompBkts: 79445 EndPtVals: 181 ActualVal: yes Column (#3): TIME_ID(DATE) AvgLen: 8 NDV: 181 Nulls: 0 Density: 0.000434 Min: 2458485.000000 Max: 2458665.000000 Histogram: Freq #Bkts: 181 UncompBkts: 79445 EndPtVals: 181 ActualVal: yes Estimated selectivity: 1.000000 , endpoint value predicate, col: #3 kkecdn: Single Table Predicate:"SALES"."TIME_ID"<=TO_DATE(' 2019-06-30 00:00:00', 'syyyy-mm-dd hh24:mi:ss') Estimated selectivity: 1.000000 , endpoint value predicate, col: #3 Table: SALES Alias: SALES Card: Original: 79445.000000 Rounded: 79445 Computed: 79445.000000 Non Adjusted: 79445.000000

union all的两个子语句的路径分析是分别进行的,在计算操作的基数时,采用了分区的统计信息,为了节省篇幅,这里只截取了一个子语句的相关部分。这种情况下,优化器对基数的计算更准确。
以上可以看出,在单分区操作时,Oracle优化器在路径分析时会采用这个分区的统计信息,而当一个操作跨越多个分区时,Oracle优化器在路径分析时会采用表的统计信息。在写sql语句时,应该避免多个分区的操作,或者是将多个分区的操作写成union或者union all(不去重)的形式,尽可能保证Oracle使用分区级的统计信息。

4 写在后面

在数据库中,分区是比较负载的操作,分区的设计、管理和运维和非分区表相比都复杂了许多,涉及到分区的操作,需要全面分析操作的影响,审慎执行。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论