###### 基数(CARDINALITY)
某一列唯一键(Distinct_Keys)的数量叫基数,比如性别列只有男女之分,所以基数是2。基数高低影响列的数据分布。
当查询结果是返回表中5%以内的数据时应该走[[索引]];当查询结果返回的是超过表中5%的数据时,应该走全表扫描。但是5%这个界限没有很绝对。查询时如果某个列基数很低,该数据分布就会非常不均衡,由于该列数据分布不均衡,会导致SQL查询可能走[[索引]],也可能走全表扫描。在做SQL优化的时候, 如果怀疑列的数据分布不均衡,可以使用select列,count(*) from 表 group by 列 order by 2 desc来查看列的数据分布。
###### 选择性(SELECTIVITY)
基数与总行数的比值再乘以100%就是某个列的选择性。
进行SQL优化的时候单看基数是没有意义的,基数必须对比总行数才有实际意义。
所以什么样的列必须建立索引呢?当一个列的选择性大于20%,说明该列的数据分布就比较均匀了。==当一个列出现在where条件中,该列没有索引并且选择性大于20%,那么该列就必须创建索引==,从而提升SQL查询性能。当然了,如果表的数据很少,也不用加索引。
==首先抛出第一个SQL优化核心思想:只有大表才会产生性能问题。==
以下是抓出表的哪个列出现在where条件中的脚本
```
查看表中每个列的基数和选择性
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 = 'SCOTT'
AND A.table_name = 'TEST'
```
**以下是抓出表的哪个列出现在where条件中的脚本:**
```
首先执行存储过程
begin
dbms_stats.flush_database_monitoring_info;
end;
//
select r.name owner,
o.name table_name,
c.name column_name,
equality_preds,---等值过滤
equijoin_preds, ---等值join 比如 where a.id=b.id
nonequijoin_preds, ---不等JOIN
range_preds,---范围过滤次数 > >= < <= between and
like_preds, ---LIKE过滤
null_preds, ---NULL过滤
timestamp
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'
/
```
然后查出选择性大于等于20%的列
```
SQL> 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;
```
并确保这些列没有创建索引
###### 直方图(HISTOGRAM)
数据分布不均衡会导致在查询该列的时候,要么走全表扫描要么走索引扫描,这时候就容易走错执行计划。
如果没有对基数低的列收集直方图统计信息,基于成本的优化器(CBO)会认为该列数据分布是均衡的。就会出现查询本该返回几条数据,但是Rows却有很多。所以在执行计划中的Rows是假的,Rows是根据统计信息以及一些数学公式算出来的。
**在做SQL优化的时候,经常要做的工作就是帮助CBO计算出比较精准的Rows。**但是CBO无法得到精确的统计信息,因为对表进行统计信息收集时一般都不会按照100%的标准采样收集,即使按照100%收集了表的统计信息表的数据也会变。
如果CBO每次都能计算得到精确的Rows就不用担心SQL走错执行计划了。
以下不要再生产环境上随便试:
为了让CBO选择正确的执行计划,我们需要对owner列收集直方图信息,从而告知CBO该列数据分布不均匀,让CBO在计算Rows的时候参考直方图统计。
###### 回表
当对一个列创建索引之后,索引会包含该列的键值以及键值对应行所在的rowid。通过索引中记录的rowid访问表中的数据就叫回表。回表一般是单块读,回表次数太多会严重影响SQL性能,如果回表次数太多,就不该走索引扫描而是全表扫描。
在进行SQL优化的时候,一定要注意回表次数!特别是要注意回表的物理I/O次数!
[[TABLE]] ACCESS BY INDEX ROWID在执行计划中就是回表操作
在无法避免回表的情况下,走索引如果返回数据量太多,必然会导致回表次数太多,从而导致性能严重下降。在Oracle12c中新功能批量回表([[TABLE]] ACCESS BY INDEX ROWID BATCHED)在一定程度上改善了单行回表的性能。
什么样的SQL必须回表?
```
select * from table where ...
```
什么样的SQL不需要回表?
```
select count(*) from table
```
当要查询的列也包含在索引中,这个时候就不需要回表了,所以通过建立组合索引来消除回表。
当一个SQL有多个过滤条件但是只在一个列或者部分列建立了索引,这时候会发生回表再过滤(TABLE ACCESS BY INDEX ROWID前面有*)也需要创建组合索引,进而消除回表再过滤,从而提升查询性能。
###### 集群因子(CLUSTERING FACTOR)
集群因子用于判断索引回表需要消耗的物理I/O次数。
索引的叶子块中有序地存储了索引的键值以及键值对应行所在的ROWID。
集群因子介于表的块数和行数之间。
如果集群因子与块数接近,说明表的数据基本上是有序的,而且其顺序基本与索引顺序一样。这样在进行索引范围或者索引全扫描的时候,回表只要读取少量的数据块就能完成。
如果集群因子与表记录数接近,说明表的数据和索引顺序差异很大,在进行索引范围扫描或者说要全扫描的时候,回表会读取更多的数据块。
集群因子只会影响索引范围扫描与索引全扫描,只有这两个会产生大量的回表。集群因子不会影响索引唯一扫描(INDEX UNIQUE SCAN),因为索引唯一扫描只返回一条数据。集群因子更不会影响索引快速扫描(INDEX FAST FULL SCAN),因为索引快速全扫描不回表。
```
集群因子计算SQL:
WITH T AS
(SELECT OWNER COLUMN_NAME,
LEAD(OWNER, 1, OWNER) OVER(ORDER BY OWNER) NEXT_COLUMN_NAME,
ROWID ROWID_NUM,
DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID) BLOCK_ID,
LEAD(ROWID) OVER(ORDER BY OWNER) NEXT_ROWID_NUM,
LEAD(DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID),
1,
DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID)) OVER(ORDER BY OWNER) NEXT_BLOCK_ID
FROM TEST A
WHERE OWNER IS NOT NULL
ORDER BY OWNER)
SELECT COUNT(*) 记录数,COUNT(DISTINCT BLOCK_ID) BLOCK_ID_SUM,
SUM(CASE
WHEN T.BLOCK_ID = T.NEXT_BLOCK_ID THEN
0
ELSE
1
END) + 1 集群因子,
SUM(CASE
WHEN T.COLUMN_NAME = T.NEXT_COLUMN_NAME AND
T.BLOCK_ID <> T.NEXT_BLOCK_ID THEN
1
ELSE
0
END) 值同_块不同
FROM T;
```
**集群因子影响的是索引回表的物理I/O次数**。我们假设所有范围扫描返回了1000行数据,如果buffer cache中没有缓存表的数据块,假设这1000行数据在同一个块中,那么回表需要耗费的物理I/O只要一个;假设这1000行数据在不同的数据块中,那么回表就要消耗1000个物理I/O。因此,集群因子影响索引回表的物理I/O次数。
注意:重建索引并不能降低集群因子,唯一能降低集群因子的办法就是根据索引列排序对表进行重建(create table new_table as select * from old_table order by 索引列),但是在实际中这是不可取的,因为我们没有办法照顾所有索引列。
当索引范围扫描,索引全扫描不回表或者返回数据量很少的时候,不管集群因子多大,对SQL查询性能几乎没有任何影响。在进行SQL优化时,往往会建立合适的组合索引消除回表,或者建立组合索引尽量减少回表次数。
###### 表与表之间的关系
关系型数据库中,表与表会进行关联:一种是1:1,一种是1:N,一种是N:N的关系。搞懂表的关系对于SQL优化等有巨大的帮助。
如果两表属于1:1关系,关联后返回的结果也是属于1的关系,数据不会重复。如果两表属于1:N,关联之后返回的结果集属于N的关系。如果是N:N,关联之后返回的结果集会产生局部范围的笛卡尔积,N:N一般不存在内/外连接中,只能存在于半连接或者反连接中。

##### 统计信息
统计信息主要分为表的、列的、索引的、系统的、数据字典的统计信息以及动态性能[[视图]]基表的统计信息。重点看表的列的索引的。
表的统计信息可以通过DBA_TABLES获取。
###### 统计信息重要参数设置
granularity 表示[[收集统计信息]]的粒度,该选项只对[[分区表]]生效,默认为AUTO,表示让Oracle更具表的分区类型自己判断如何收集[[分区表]]的统计信息。
estimate_percent 表示采样率,范围是0.000001-100。我们一般对小于1GB的表进行100%采样;对表在1GB-5GB的表采样50;对大于5GB的采样30%;对于特别大的进行先分区后采样。但是为了信息比较准确,建议采样率不低于30%。
method_opt 用于控制收集直方图策略。‘for all columns size 1’表示所有列都不收集直方图;‘for all columns size skewonly’表示表的所有列自动判断是否收集(工作中不要用);‘for all columns size auto’表示对出现在where条件中的列自动判断是否收集直方图;‘for all columns size repeat’表示之前哪些列收集了直方图当前就收集哪些。对于一个稳定的系统,因该选择repeat方式收集直方图。
no_invalidate 表示共享池中涉及到该表的游标是否立即失效,默认值为DBMS_STATS.AUTO_INVALIDATE,表示让Oracle自行判断,建议设置为FALSE,立即失效。有时候SQL执行缓慢是因为统计信息过期导致,重新收集了统计信息之后执行计划还没有更改,原因就在于这个参数不是false。
degree 表示收集统计信息的并行度,默认为NULL。
cascade 表示在收集表的统计信息的时候,是否级联收集索引的统计信息,默认值为DBMS_STATS.AUTO_CASCADE,表示让Oracle自己判断是否级联收集。一般设置为TRUE,一定收集。
###### 检查统计信息是否过期
查看统计信息通过DBA_TAB_STATISTICS表的STALE_STATS,如果为yes则表示过期了,如果是NO则没过期。
```
查看表的统计信息是否过期:
select owner, table_name, object_type, stale_stats, last_analyzed
from dba_tab_statistics
where owner = ''
and table_name = '';
```
```
查看统计信息为啥过期:
select table_name, table_owner, inserts, updates, deletes, timestamp
from all_tab_modifications
where table_owner = ''
and table_name = '';
```
刷新数据库监控信息不能在生产环境上做!
数据字典 all_tab_modifications 还可以判断那些表需要定期降低高水位,比如一个表经常进行insert、delete,那么这个表应该可以应该定期降低高水位,这个表的索引也应该定期重建。除此之外还可以判断哪些表是业务核心表。
###### 扩展统计信息
当where条件中有多个谓词过滤条件,但这些谓词过滤条件彼此是有关系的而不是相互独立的,这时我们可能需要收集扩展统计信息以便优化器能够估算出较为准确的行数(Rows)。
如果不想改写SQL则在Oracle11g之前可以使用动态采样(至少Level 4)。在Oracle11g之后,可以使用扩展统计信息将相关列组合成一个列。**需要注意的是,扩展统计信息只能用于等值查询,不能用于非等值查询。**
###### 动态采样
如果一个表从来没有收集过统计信息,默认情况下Oracle会对表进行动态采样(Level 2)以便优化器估算出较为准确的Rows,动态采样的最终目的就是为了让优化器能够评估出较为准确的Rows。
动态采样分11级:
level 0:不启用动态采样
level 1:当表(非分区表)没有收集过统计信息并且这个表要与另外的表进行关联(不能单表访问),同时该表没有索引,表的数据块必须大于32个,满足这些条件后,Oracle会随机扫描表中32个块,然后评估返回的Rows。
level 2:对于没有收集过统计信息的表启用动态采样,采样的块数为64个,如果表少于64块则有多少采多少。
level 3:对于没有收集过统计信息的表启用动态采样,采样的块数为64个。如果表已经收集过统计信息但是不能准确的估算出返回的Rows,而是靠猜,这时会随机扫描64个数据块进行采样。
level 4:对于没有收集过统计信息的表启用动态采样,采样的块数为64个。如果表已经收集过统计信息,但是表有两个或两个以上的过滤条件(AND/OR),这时会随机扫描64个数据块进行采样,相关列问题就必须启用至少level 4进行动态采样。level 4包含了level 3的采样数据。
level 5:收集满足level 4采样条件的数据,采样块数为128个。
level 6:收集满足level 4采样条件的数据,采样块数为256个。
level 7:收集满足level 4采样条件的数据,采样块数为512个。
level 8:收集满足level 4采样条件的数据,采样块数为1024个。
level 9:收集满足level 4采样条件的数据,采样块数为4086个。
level 10:收集满足level 4采样条件的数据,采样块数所有。
level 11:Oracle自动判断如何采样,采样块数由Oracle自动决定。
设置动态采样有两种方式:
```
1.设置参数
alter session set optimizer_dynamic_sampling=2;
2.添加HINT
select /*+ dynamic_sampling(3) */ * from .....
```
当系统中有全局临时表,就需要使用动态采样,因为全局临时表无法收集统计信息,我们建议对全局临时表至少启用level 4进行采样。
**当执行计划中表的Rows估算有严重偏差的时候,例如相关列问题,或者两表关联有多个连接,关联之后Rows算少,或者是where过滤条件中对列使用了substr、instr、like,又或者是whereguolv条件中有非等值过滤,或者group by之后导致Rows估算错误,此时我们可以考虑使用动态采样,同样建议设置为level 4。**
在数据仓库系统中,有些报表SQL很多行,过滤条件也比较复杂,导致优化器不能估算出较为准确的Rows而产生了错误的执行计划。可以考虑启用动态采样Level 6观察性能是否有改善。但是注意不要在系统级更改动态采样级别,加HINT就行。
###### 定制统计信息收集策略
一般会关闭数据库自带的统计信息收集JOB,根据情况制定收集统计信息收集策略。
##### 执行计划
如果是数据库自身原因导致SQL缓慢,我们需要通过分析[[等待事件]],做出相应处理。这里不做讨论。这里只侧重SQL的写法优化。
###### 获取执行计划常用方法
**使用AUTOTRACE查看执行计划**
```
set autot on:运行SQL并显示结果,执行计划和统计信息。
set autot trace:运行SQL但是不显示结果,会显示执行计划和统计信息。
set autot trace exp:运行该命令查询不执行DML执行,只显示执行计划。
set autot trace stat:运行SQL只显示统计信息。
set autot off:关闭AUTOTRACE
```
用AUTOTRACE查看执行计划会带来一个额外的好处,当SQL执行完毕后会在执行计划的末尾显示SQL在运行中耗费的一些统计信息。
recursive calls表示递归调用的次数。一个SQL第一次执行就会发生硬解析,在硬解析的时候,优化器会隐含地调用一些内部SQL,因此当一个SQL第一次执行时 recursive calls 会大于0;第二次执行等于0。但是如果SQL中有自定义函数,recursive calls 永远不会为0,自定函数调用了多少次就会显示多少次。
db block gets 表示有多少个块发生变化,一般情况下,只有DML语句才会导致块发生改变。但是如果有延迟块清除或者SQL语句中调用了返回CLOB的函数,db block gets 会大于0,这不奇怪。
consistent gets表示逻辑读,单位是块。在进行SQL优化时应减少逻辑读。通常逻辑读越小,性能越好,但是逻辑读并不是衡量SQL快慢的唯一标准,需结合I/O等其他因素共同判断。
怎么通过逻辑读判断一个SQL是否存在较大的优化空间?如果SQL的逻辑读远远大于SQL语句中所有表的段大小之和(假设所有表都走全表扫描,表的关联方式为HASH JOIN),那么该SQL就存在较大的优化空间。
physical reads 表示从磁盘读取了多少个数据块,如果表已经被缓存在buffer cache中,没有物理读则其值为0。
redo size 表示产生了多少字节的重做日志,一般查询语句这个为0。
bytes sent via SQL*Net to client 表示从数据库发送了多少字节到客户端。
bytes received via SQL*Net to client 表示从客户端发送了多少字节到服务端。
SQL*Net roundtrips to/from client 表示客户端与数据库服务端交互次数,我们可以通过设置arraysize减少交互次数。
sorts(memory)和sorts(disk)分别表示内存排序和磁盘排序次数。
rows processed 表示SQL一共返回了多少行数据。**这个是优化的时候最应该关心的,一般情况下,如果rows processed 很大,走HASH连接;rows processed 很小走循环嵌套。**
**使用EXPLAIN PLAN FOR 查看执行计划**
```
用法如下:
explain plan for SQL语句;
select * from table(dbms_xplan.display);
查看高级执行计划如下:
explain plan for SQL语句;
select * from table(dbms_xplan.display(NULL, NULL, 'advanced -projection'));
```
当需要控制半连接/反连接执行计划的时候,我们就可能需要查看高级执行计划。有时候需要使用SQL PROFILE 固定执行计划也可能要查看高级执行计划。

**查看带有A-TIME的执行计划**
```
用法如下:
alter session set statistics_level=all;
或者添加HINT:/*+ gather_plan_statistics */
然后执行如下查看带有A-TIME的执行计划
select * from table(dbms_xplan.display_cursor(NULL, NULL, 'allstats last'));
```

Starts 表示这个操作执行的次数。
E-Rows 表示优化器估算的行数,就是普通执行计划中的Rows。
A-Rows 表示真实的行数。
A-Time 表示累加的总时间。与普通执行计划不同的是,普通执行计划中的Time是假的,而A-Time是真实的。
Buffers 表示累加的逻辑读。
Reads 表示累加的物理读。
以上三种除了带有A-Time的都是估算的,不是真实的执行计划。真实的执行计划存在于V$SQL_PLAN中。注意,使用AUTOTRACE查看执行计划是SQL真正运行过但是其执行计划来自于PLAN_TABLE而非V$SQL_PLAN中。
**查看正在执行的SQL的执行计划**
有时需要抓取正在运行的SQL的执行计划,这时我们要获取SQL的SQL_ID以及SQL的CHILD_NUMEBR,然后带入下面的SQL,,就能获取正在运行的SQL的执行计划。
```
select * from table(dbms_xplan.display_cursor('sql_id',child_number))
```
###### 定制执行计划
Oracle的执行计划是树形结构,可以利用树形查询来定制执行计划。比如只有大表才有性能问题,因此可以将表的段大小添加到执行计划中。
###### 怎么通过查看执行计划建立索引

执行计划分为两部分,Plan hash value 和 Predicate Information 之间这部分主要是表的[[访问路径]]以及表的连接方式。另外一部分是谓词过滤信息,这部分位于 Predicate Information 下面,其非常重要。ID前面有*号表示发生了谓词过滤、HASH连接、走索引。
提问:如果 TABLE ACCESS FULL前面没有“\*”怎么办?
答:如果表很小,那么不需要理会,小表不会产生性能问题。如果表很大就要问开发是不是忘写条件了。如果真没条件,就要查看SQL语句访问了多少表的列,如果列不多,就可以把这些列组合起来,建立一个组合索引;
如果访问的列很多,就不能建立组合索引了,因为此时索引比表更大,可以通过开启并行查询、更改表的连接方式,让大表作为嵌套循环的被驱动表,同时在大表的连接列上建立索引。
提问:TABLE ACCESS FULL 前面有“\*”怎么办?
答:如果表很小无需理会;如果表很大使用“select count(*) from 表”查看有多少行数据,然后通过“select count(*) from 表 where *”对应的谓词条件,查看返回多少数据。如果返回行数在总行数的5%以内可以在过滤列上建立索引。如果已经存在索引但是没走索引,则检查统计信息(特别是直方图)信息。如果统计信息已经收集过了,使用HINT强制走索引。如果有多个过滤条件则建立组合索引将选择性高的列放在前面,选择性低的列放在后面。
提问:TABLE ACCESS BY INDEX ROWID 前面有“*”号怎么办?
答:TABLE ACCESS BY INDEX ROWID 前有“*”表示回表再过滤。说明数据没有在索引中过滤干净。当出现这种情况时可以将“*”下面的过滤条件包含在索引中,这样可以减少回表次数,提升查询性能。
如果索引返回的数据本身很少,回表也没有多少次,因此可以不用再创建组合索引。
###### 运用光标移动大法阅读执行计划
执行计划中最需关心的:Id,Operation,Name,Rows。
执行计划的Cost没有必要看,如果一个SQL都需要优化了那他的Cost大概率。
阅读执行计划的时候一般从上往下看,找到入口后再往上看。
如何找到执行计划:先将光标放到Id=0,然后向下向右移动光标,直到没有儿子的Id,这个Id就是执行计划的入口。
怎么判断哪个表与哪个表进行关联:先找到表在执行计划中的Id,然后看这个Id(或者是这个Id的父亲)与谁对齐(利用光标上下移动),他与谁对其就与谁关联。
如何在实战中利用此技巧:找到执行计划入口,检查入口Rows返回的真实行数与CBO估算的行数是否存在较大的差异。同时还可以利用执行计划找出表的关联。
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




