优化04-选择率和直方图
选择率
在Oracle数据库中,选择率(Selectivity) 是优化器(CBO,基于成本的优化器)用来评估SQL语句中某个条件(如WHERE子句)过滤数据的比例的关键指标。它直接影响优化器选择执行计划的策略,例如决定是否使用索引或全表扫描。
选择率表示满足某个条件的行数占总行数的比例,
对于等值查询,参考Oracle的数据字典dba_tab_columns的DENSITY和NUM_DISTINCT字段。
col table_name for a20
col column_name for a20
col LOW_VALUE for a20
col HIGH_VALUE for a20
set line 400
select table_name,column_name,num_distinct,density,NUM_NULLS,LOW_VALUE,HIGH_VALUE
from dba_tab_columns where table_name='EMP';TABLE_NAME COLUMN_NAME NUM_DISTINCT DENSITY NUM_NULLS LOW_VALUE HIGH_VALUE
-------------------- -------------------- ------------ ---------- ---------- -------------------- --------------------
EMP EMPNO 14 .071428571 0 C24A46 C25023
EMP ENAME 14 .071428571 0 4144414D53 57415244
EMP JOB 5 .2 0 414E414C595354 53414C45534D414E
EMP MGR 6 .038461538 1 C24C43 C25003
EMP HIREDATE 13 .076923077 0 77B40C11010101 77BB0517010101
EMP SAL 12 .083333333 0 C209 C233
EMP COMM 4 .25 10 80 C20F
EMP DEPTNO 3 .333333333 0 C10B C11F
对于等值查询,如果该列没有空值和直方图统计信息,选择率就是DENSITY的值或(1/NUM_DISTINCT);如果有空值,则可选择率为:(1/NUM_DISTINCT)*(NUM_ROWS-NUM_NULLS)/NUM_ROWS
对于范围查询,选择率的计算方法就在上述基础上加入最大值和最小值的统计信息,这里就不多做赘述。
选择率和索引
选择率影响着一个SQL的执行计划,准确的来说,选择率影响表的访问方式(即全表扫描还是索引扫描)。Oracle的SQL优化器是基于成本的,我们称为CBO,CBO会依据选择率来确定对某一数据集的访问的成本(COST),从而选择成本最低的访问方式。
例如,表A有8行数据,在表A上对列col1有索引,列col1上有8个不同值,如果SQLA的谓词条件为col1的等值查询,对与SQLA的最优执行计划,CBO会选择索引扫描;如果表A对列col2有索引,列col2上只有2个不同值,如果SQLB的谓词条件为col2的等值查询,对与SQLB的最优执行计划,CBO可能会选择全表扫描,因为索引扫描的寻找叶子块+回表的成本可能会大于全表扫描的成本。
下面我们做一下选择率的测试
--创建表和索引
create table tab1(id int,name varchar2(10),gender varchar2(5));
create index idx_id on tab1(id);
create index idx_gender on tab1(gender);
--插入2000条数据,id列从1递增,name列为随机的5个字符串,gender列为随机的‘f’或‘m’。
DECLARE-- 定义记录类型和集合类型TYPE t_employee IS RECORD (id NUMBER,name VARCHAR2(5),gender CHAR(1));TYPE t_employee_tbl IS TABLE OF t_employee;v_data t_employee_tbl := t_employee_tbl(); -- 初始化集合
BEGIN-- 批量生成测试数据(200行)SELECT LEVEL AS id,-- 生成5位随机大写字母和数字组合(若只要字母可改用'X'参数)DBMS_RANDOM.STRING('X', 5) AS name,CASE WHEN DBMS_RANDOM.VALUE < 0.5 THEN 'm' ELSE 'f' END AS genderBULK COLLECT INTO v_dataFROM DUALCONNECT BY LEVEL <= 2000;-- 批量插入数据(使用FORALL提升性能)FORALL i IN 1 .. v_data.COUNTINSERT INTO scott.tab1 (id, name, gender)VALUES (v_data(i).id, v_data(i).name, v_data(i).gender);COMMIT; -- 提交事务
EXCEPTIONWHEN OTHERS THENROLLBACK; -- 异常回滚RAISE;
END;
/
--查看统计信息
ANALYZE TABLE scott.tab1 COMPUTE STATISTICS;
col table_name for a10
col column_name for a10
col LOW_VALUE for a20
col HIGH_VALUE for a20
set line 400
select table_name,column_name,num_distinct,density,NUM_NULLS,LOW_VALUE,HIGH_VALUE
from dba_tab_columns where table_name='TAB1';TABLE_NAME COLUMN_NAM NUM_DISTINCT DENSITY NUM_NULLS LOW_VALUE HIGH_VALUE
---------- ---------- ------------ ---------- ---------- -------------------- --------------------
TAB1 ID 2000 .0005 0 C102 C215
TAB1 NAME 2000 .0005 0 3030463839 5A5A555138
TAB1 GENDER 2 .5 0 66 6D
--分别以id和gener列为谓词条件查看
SQL> set autotrace traceonly;
SQL> select * from scott.tab1 where id=6;Execution Plan
----------------------------------------------------------
Plan hash value: 4102116554----------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 9 | 2 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| TAB1 | 1 | 9 | 2 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | IDX_ID | 1 | | 1 (0)| 00:00:01 |
----------------------------------------------------------------------------------------------SQL> select * from scott.tab1 where gender='f';1019 rows selected.Execution Plan
----------------------------------------------------------
Plan hash value: 2211052296--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1000 | 9000 | 3 (0)| 00:00:01 |
|* 1 | TABLE ACCESS FULL| TAB1 | 1000 | 9000 | 3 (0)| 00:00:01 |
--------------------------------------------------------------------------
直方图
上面都是该列上数据分布均匀的情况,如果数据分布不均匀,及时统计信息是最新的,但其执行计划可能不是最优的。下面我们测试,
假设一个中学6年级有2000名学生,期中考试分为ABCDE五个等级,其中大部分同学的分数都集中在B,那么查询分数为B的同学信息可能存在执行计划不优的情况。
--创建表和索引
SQL> create table tab2(id int,name varchar2(10),grade char(1));
SQL> create index grade_idx on tab2(grade);
--插入数据
DECLARE-- 定义记录类型和集合类型TYPE t_student IS RECORD (id NUMBER,name VARCHAR2(5),grade CHAR(1));TYPE t_student_tbl IS TABLE OF t_student;v_data t_student_tbl := t_student_tbl(); -- 初始化集合
BEGIN-- 批量生成测试数据(300行)SELECT LEVEL AS id,-- 生成5位随机大写字母和数字组合(若只要字母可改用'X'参数)DBMS_RANDOM.STRING('X', 5) AS name,CHR(65 + FLOOR(DBMS_RANDOM.VALUE(0,5))) AS grade -- 生成A-EBULK COLLECT INTO v_dataFROM DUALCONNECT BY LEVEL <= 2000;-- 批量插入数据(使用FORALL提升性能)FORALL i IN 1 .. v_data.COUNTINSERT INTO scott.tab2 (id, name, grade)VALUES (v_data(i).id, v_data(i).name, v_data(i).grade);COMMIT; -- 提交事务
EXCEPTIONWHEN OTHERS THENROLLBACK; -- 异常回滚RAISE;
END;
/把id为50-250的学生分数改为B
update tab2 set grade='B' where id>=50 and id <=2500;
commit;#查看统计信息
SQL> ANALYZE TABLE scott.tab2 COMPUTE STATISTICS;Table analyzed.select table_name,column_name,num_distinct,density,NUM_NULLS,LOW_VALUE,HIGH_VALUE2 from dba_tab_columns where table_name='TAB2';TABLE_NAME COLUMN_NAM NUM_DISTINCT DENSITY NUM_NULLS LOW_VALUE HIGH_VALUE
---------- ---------- ------------ ---------- ---------- -------------------- --------------------
TAB2 ID 2000 .0005 0 C102 C215
TAB2 NAME 2000 .0005 0 3031324C58 5A5A543245
TAB2 GRADE 5 .2 0 41 45#查询分数为B的学生
SQL> set autotrace traceonly statistic;
SQL> select * from scott.tab2 where grade='B';1963 rows selected.Execution Plan
----------------------------------------------------------
Plan hash value: 1237454846-------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 400 | 3600 | 3 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| TAB2 | 400 | 3600 | 3 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | GRADE_IDX | 400 | | 1 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------Predicate Information (identified by operation id):
---------------------------------------------------2 - access("GRADE"='B')Statistics
----------------------------------------------------------1 recursive calls0 db block gets272 consistent gets0 physical reads0 redo size59224 bytes sent via SQL*Net to client2037 bytes received via SQL*Net from client132 SQL*Net roundtrips to/from client0 sorts (memory)0 sorts (disk)1963 rows processed
--上面查询采用的索引扫描,逻辑读为272,如果强制让SQL使用全表扫描SQL> select /*+FULL(tab2) */ * from scott.tab2 where grade='B';1963 rows selected.Execution Plan
----------------------------------------------------------
Plan hash value: 2156729920--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 400 | 3600 | 3 (0)| 00:00:01 |
|* 1 | TABLE ACCESS FULL| TAB2 | 400 | 3600 | 3 (0)| 00:00:01 |
--------------------------------------------------------------------------Predicate Information (identified by operation id):
---------------------------------------------------1 - filter("GRADE"='B')Statistics
----------------------------------------------------------1 recursive calls0 db block gets138 consistent gets0 physical reads0 redo size54904 bytes sent via SQL*Net to client2037 bytes received via SQL*Net from client132 SQL*Net roundtrips to/from client0 sorts (memory)0 sorts (disk)1963 rows processed--逻辑读为138,
明明全表扫描的逻辑读更低,为什么CBO还是使用索引扫描的执行计划呢?因为统计信息不知道列grade分布不均匀,安装1/5的选择率生成的执行计划,这种情况可以通过收集列的直方图来解决。
收集之前确定grade没有直方图统计信息
select table_name,column_name,num_distinct,density,HISTOGRAMfrom dba_tab_columns where table_name='TAB2';
TABLE_NAME COLUMN_NAM NUM_DISTINCT DENSITY HISTOGRAM
---------- ---------- ------------ ---------- ---------------
TAB2 ID 2000 .0005 NONE
TAB2 NAME 2000 .0005 NONE
TAB2 GRADE 5 .2 NONE
收集grade列直方图
EXEC DBMS_STATS.GATHER_TABLE_STATS('SCOTT', 'TAB2', METHOD_OPT => 'FOR COLUMNS GRADE SIZE AUTO');
再次查看grade列统计信息
SQL> select table_name,column_name,num_distinct,density,HISTOGRAM2 from dba_tab_columns where table_name='TAB2';TABLE_NAME COLUMN_NAM NUM_DISTINCT DENSITY HISTOGRAM
---------- ---------- ------------ ---------- ---------------
TAB2 ID 2000 .0005 NONE
TAB2 NAME 2000 .0005 NONE
TAB2 GRADE 5 .00025 FREQUENCY
这时我们再次查看分数为B的学生信息
SQL> set autotrace traceonly;
SQL> select * from scott.tab2 where grade='B';1963 rows selected.Execution Plan
----------------------------------------------------------
Plan hash value: 2156729920--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1963 | 19630 | 3 (0)| 00:00:01 |
|* 1 | TABLE ACCESS FULL| TAB2 | 1963 | 19630 | 3 (0)| 00:00:01 |
--------------------------------------------------------------------------Predicate Information (identified by operation id):
---------------------------------------------------1 - filter("GRADE"='B')Statistics
----------------------------------------------------------1 recursive calls0 db block gets138 consistent gets0 physical reads0 redo size54904 bytes sent via SQL*Net to client2037 bytes received via SQL*Net from client132 SQL*Net roundtrips to/from client0 sorts (memory)0 sorts (disk)1963 rows processed
收集了直方图后,选择了成本更低的执行计划。