SQL优化 第一章

发布于:2024-04-25 ⋅ 阅读:(29) ⋅ 点赞:(0)

        此博客内容根据《SQL优化核心思想》的流程来进行书写,仅作为个人学习记录使用。当然,有问题可以一起来探讨!

一、基数(CARDINALITY)

某个列唯一键(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%的时候走全表扫描。

二、选择性(SELECTIVITY)

基数与总行数的比值再乘以 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');
三、直方图(HISTOGRAM)

        当某个列基数很低,该列数据分布就会不均衡。数据分布不均衡会导致在查询该列的时候,要么走全表扫描,要么走索引扫描,这个时候很容易走错执行计划。 如果没有对基数低的列收集直方图统计信息,基于成本的优化器(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

四、回表(TABLE ACCESS BY INDEX ROWID)

什么叫回表:

        当对一个列创建索引之后,索引会包含该列的键值以及键值对应行所在的 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%的数据 走全表扫描。

五、集群因子(CLUSTERING FACTOR)

        集群因子用于判断索引回表需要消耗的物理 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 优化的时候,往往会建立合适的组合索引消除回表,或者建 立组合索引尽量减少回表次数。