此博客内容根据《SQL优化核心思想》的流程来进行书写,仅作为个人学习记录使用。当然,有问题可以一起来探讨!
某个列唯一键(Distinct_Keys)的数量叫作基数。性别列只有男女,那么基数为2。
以test为例,其中的某个列的基数如下:
select count(distinct owner),count(distinct object_id),count(*) from test;

从中我们可以看出总共有72889行,OWNER列的基数为26,说明该列有大量的重复值,OBJECT_ID与总条数一至,可以理解为主键,OWNER分布如下:
- SQL> col OWNER for a15;
- SQL> select owner,count(*) from test group by owner order by 2 desc;

如果某个列基数很低,该列数据分布就会非常不均衡,由于该列数据 分布不均衡,会导致 SQL 查询可能走索引,也可能走全表扫描。在做 SQL 优化的时候,如果 怀疑列数据分布不均衡,我们可以使用 select 列,count(*) from 表 group by 列 order by 2 desc 来查看列的数据分布。
如果 SQL 语句是单表访问,那么可能走索引,可能走全表扫描,也可能走物化视图扫描。 在不考虑有物化视图的情况下,单表访问要么走索引,要么走全表扫描。现在,回忆一下走索 引的条件:返回表中 5%以内的数据走索引,超过 5%的时候走全表扫描。
基数与总行数的比值再乘以 100%就是某个列的选择性。
搜集某个表中的信息:
- BEGIN
- DBMS_STATS.GATHER_TABLE_STATS(ownname => 'user',
- tabname => 'xxx',
- estimate_percent => 100,
- method_opt => 'for all columns size 1',
- no_invalidate => FALSE,
- degree => 1,
- cascade => TRUE);
- END;
- /
查看某个表中每个列的基数和选择性:
- #这里注意用户名和表明都需要大写,否则查不到信息!!!!!!!
- col column_name for a15;
- select a.column_name,
- b.num_rows,
- a.num_distinct Cardinality,
- round(a.num_distinct / b.num_rows * 100, 2) selectivity,
- a.histogram,
- a.num_buckets
- from dba_tab_col_statistics a, dba_tables b
- where a.owner = b.owner
- and a.table_name = b.table_name
- and a.owner = 'user'
- and a.table_name = 'xxx';

查看占比最大列的详细数据分布:
- col OBJECT_NAME for a20;
- select *
- from (select object_name, count(*)
- from test
- group by object_name
- order by 2 desc)
- where rownum <= 10;

所以不管 object_name 传入任何值,最多返回101 行数据。
结论:
当一个列出现在 where 条件中,该列没有创建索引并且选择性大于 20%,那么该列就必须创建索引,从而提升 SQL 查询性能。当然,如果表只有几 百条数据,那我们就不用创建索引了。
在使用脚本之前先刷新数据库监控信息:
- begin
- dbms_stats.flush_database_monitoring_info;
- end;
- /
思路:
1.查询出哪个表的哪个列出现在where 条件中
- select r.name owner, o.name table_name, c.name column_name
- from sys.col_usage$ u, sys.obj$ o, sys.col$ c, sys.user$ r
- where o.obj# = u.obj#
- and c.obj# = u.obj#
- and c.col# = u.intcol#
- and r.name = 'SCOTT'
- and o.name = 'TEST';
2.查询出选择性大于等于 20%的列
- select a.owner,
- a.table_name,
- a.column_name,
- round(a.num_distinct / b.num_rows * 100, 2) selectivity
- from dba_tab_col_statistics a, dba_tables b
- where a.owner = b.owner
- and a.table_name = b.table_name
- and a.owner = 'SCOTT'
- and a.table_name = 'TEST'
- and a.num_distinct / b.num_rows >= 0.2;
3.确保这些列没有创建索引
- select table_owner, table_name, column_name, index_name
- from dba_ind_columns
- where table_owner = 'SCOTT'
- and table_name = 'TEST';
如下:
- ----注意修改用户名称(owner)和表名(table_name)
- ----使用sysdba用户操作
- select owner, column_name, num_rows, Cardinality, selectivity, 'Need index' as notice
- from (select b.owner,a.column_name,b.num_rows,a.num_distinct Cardinality,
- round(a.num_distinct / b.num_rows * 100, 2) selectivity
- from dba_tab_col_statistics a, dba_tables b
- where a.owner = b.owner
- and a.table_name = b.table_name
- and a.owner = 'SCOTT'
- and a.table_name = 'TEST')
- where selectivity >= 20
- and column_name not in (select column_name
- from dba_ind_columns
- where table_owner = 'SCOTT'
- and table_name = 'TEST')
- and column_name in
- (select c.name
- from sys.col_usage$ u, sys.obj$ o, sys.col$ c, sys.user$ r
- where o.obj# = u.obj#
- and c.obj# = u.obj#
- and c.col# = u.intcol#
- and r.name = 'SCOTT'
- and o.name = 'TEST');
当某个列基数很低,该列数据分布就会不均衡。数据分布不均衡会导致在查询该列的时候,要么走全表扫描,要么走索引扫描,这个时候很容易走错执行计划。 如果没有对基数低的列收集直方图统计信息,基于成本的优化器(CBO)会认为该列数据分布是均衡的。所以要对基数很低(<1%)的列进行直方图收集。当然,对没有出现在 where 条件中的列收集直方图完全是做无用功,浪费数据库资源。
脚本如下:
抓出必须创建直方图的列:
- select a.owner,
- a.table_name,
- a.column_name,
- b.num_rows,
- a.num_distinct,
- trunc(num_distinct / num_rows * 100,2) selectivity,
- 'Need Gather Histogram' notice
- from dba_tab_col_statistics a, dba_tables b
- where a.owner = 'SCOTT'
- and a.table_name = 'TEST'
- and a.owner = b.owner
- and a.table_name = b.table_name
- and num_distinct / num_rows<0.01
- and (a.owner, a.table_name, a.column_name) in
- (select r.name owner, o.name table_name, c.name column_name
- from sys.col_usage$ u, sys.obj$ o, sys.col$ c, sys.user$ r
- where o.obj# = u.obj#
- and c.obj# = u.obj#
- and c.col# = u.intcol#
- and r.name = 'SCOTT'
- and o.name = 'TEST')
- and a.histogram ='NONE';
直方图是用来帮助 CBO 在对基数很低、数据分布不均衡的列进行 Rows 估算的时候,可以得到更精确的Rows
什么叫回表:
当对一个列创建索引之后,索引会包含该列的键值以及键值对应行所在的 rowid。通过索引中记录的 rowid 访问表中的数据就叫回表。回表一般是单块读,回表次数太多会严重影响 SQL 性能,如果回表次数太多,就不应该走索引扫描了,应该直接走全表扫描。
例子:
- set line 150;
- set autot trace;
- select * from test where owner='SYS';

执行计划中框起来的(TABLE ACCESS BY INDEX ROWID)就是回表。索引返回多少行 数据,回表就要回多少次,每次回表都是单块读(因为一个 rowid 对应一个数据块)。该 SQL 返回了52343行数据,那么回表一共就需要52343次。
请思考:上面执行计划的性能是耗费在索引扫描中还是耗费在回表中?
实验:
为了消除 arraysize 参数对逻辑读的影响, 设置 arraysize=5000。arraysize 表示 Oracle 服务器每次传输多少行数据到客户端,默认为 15。如果一个块有 150 行数据,那么这个块就会被读 10 次,因为每次只传输 15 行数据到客户 端,逻辑读会被放大。设置了 arraysize=5000 之后,就不会发生一个块被读 n 次的问题了。
- set arraysize 5000
- select owner from test where owner='SYS';

从上面可知,索引扫描只耗费了 74 个逻辑读。
select * from test where owner='SYS';

- set autot off
- select count(distinct dbms_rowid.rowid_block_number(rowid)) blocks from test where owner = 'SYS';

SQL 在有回表的情况下,一共耗费了 1318个逻辑读,那么这 1318个逻辑读是怎么来的呢? SQL 返回的 52343 条数据一共存储在 1185 个数据块中,访问这 1185 个数据块就需要消耗 1185 个逻辑读,加上索引扫描的 122 个逻辑读,再加上 11 个逻辑读[其中 11=ROUND(52343/5000)],这 样累计起来刚好就是 1318 个逻辑读。 因此我们可以判断,该 SQL 的性能确实绝大部分损失在回表中! 更糟糕的是:假设 52343 条数据都在不同的数据块中,表也没有被缓存在 buffer cache 中, 那么回表一共需要耗费 52343 个物理 I/O,简直不可想象。
在无法避免回表的情况下,走索引如果返回数据量太多,必然会导致回表次数太多,从而 导致性能严重下降。类似于这样的 Select * from table where ... 就必须回表,所以我们必须严禁使用 Select *。类似于这样的 Select count(*) from table 这样的 SQL 就不需要回表。所以返回表中 5%以内的数据走索引、超过表中 5%的数据 走全表扫描。
集群因子用于判断索引回表需要消耗的物理 I/O 次数。
根据算法我们知道集群因子介于表的块数和表行数之间。
如果集群因子与块数接近,说明表的数据基本上是有序的,而且其顺序基本与索引顺序一 样。这样在进行索引范围或者索引全扫描的时候,回表只需要读取少量的数据块就能完成。
如果集群因子与表记录数接近,说明表的数据和索引顺序差异很大,在进行索引范围扫描 或者索引全扫描的时候,回表会读取更多的数据块。
集群因子只会影响索引范围扫描(INDEX RANGE SCAN)以及索引全扫描(INDEX FULL SCAN),因为只有这两种索引扫描方式会有大量数据回表。 集群因子不会影响索引唯一扫描(INDEX UNIQUE SCAN),因为索引唯一扫描只返回一 条数据。集群因子更不会影响索引快速全扫描(INDEX FAST FULL SCAN),因为索引快速全扫描不回表。
实验:
先对测试表 test 的 object_id 列创建一个索引 idx_id:
create index idx_id on test(object_id);

查看该索引的集群因子:
- col OWNER for a10;
- col CLUSTERING_FACTOR for a15;
- col index_name for a15;
- select owner, index_name, clustering_factor from dba_indexes where owner = 'SCOTT' and index_name = 'IDX_ID';

索引 idx_id 的叶子块中有序地存储了索引的键值以及键值对应行所在的 ROWID。
select * from (select object_id, rowid from test where object_id is not null order by object_id) where rownum<=5;

人工计算集群因子的 SQL 脚本:
- select sum(case
- when block#1 = block#2 and file#1 = file#2 then
- 0
- else
- 1
- end) CLUSTERING_FACTOR
- from (select dbms_rowid.rowid_relative_fno(rowid) file#1,
- lead(dbms_rowid.rowid_relative_fno(rowid), 1, null) over(order by object_id) file#2,
- dbms_rowid.rowid_block_number(rowid) block#1,
- lead(dbms_rowid.rowid_block_number(rowid), 1, null) over(order by object_id) block#2
- from test
- where object_id is not null);

表的总块数如下可知:
select count(distinct dbms_rowid.rowid_block_number(rowid)) blocks from test;

在第一节基数的时候我们知道,表的总行数为72889行,总块数为1412块,得知集群因子非常接近表的总块数。现在,我们来查看下面 SQL 语句的执行计
- set arraysize 5000
- set autot trace
- select * from test where object_id < 1000;

耗费了25个逻辑读。
接下来新建一个test2,来对数据进行随机排序。
- create table test2 as select * from test order by dbms_random.value;
-
- #在 object_id 列创建索引 idx_id2
- create index idx_id2 on test2(object_id);

查看索引 idx_id2 的集群因子。
- set autot trace
- select owner, index_name, clustering_factor from dba_indexes where owner = 'SCOTT' and index_name = 'IDX_ID2';

索引 idx_id2 的集群因子接近于表的总行数(72889),回表的时候会读取更多的数据块,现在我们 来看一下 SQL 的执行计划。
- set arraysize 5000
- set autot trace
- select /*+ index(test2) */ * from test2 where object_id <1000;

从上图得知,进行了1K+逻辑读。
通过上面实验我们得知,集群因子太大会严重影响索引回表的性能,集群因子影响的是索引回表的物理 I/O 次数。我们假 设索引范围扫描返回了 1 000 行数据,如果 buffer cache 中没有缓存表的数据块,假设这 1000 行数据都在同一个数据块中,那么回表需要耗费的物理 I/O 就只需要一个;假设这 1000 行数 据都在不同的数据块中,那么回表就需要耗费 1 000 个物理 I/O。因此,集群因子影响索引回 表的物理 I/O 次数。
切记不要尝试重建索引来降低集群因子,这根本没用,因为表中的数据顺序始终没变。 唯一能降低集群因子的办法就是根据索引列排序对表进行重建(create table new_table as select * from old_table order by 索引列),但是这在实际操作中是不可取的,因为我们无法照顾到每 一个索引。
集群因子只影响索引范围扫描和索引全扫描。当索引范围扫描,索引全扫描不回表或者返回数据量很少 的时候,不管集群因子多大,对 SQL 查询性能几乎没有任何影响。在进行 SQL 优化的时候,往往会建立合适的组合索引消除回表,或者建 立组合索引尽量减少回表次数。