1. 数据库优化基本知识
I/O
数据库的基本作用就是实现对数据的管理与查询。随之而来的就是大量的IO操作, 在海量数据的情况下,数据库的性能问题有80%以上和IO有关。优化ORACLE数据库的I/O性能一般有两个方面,一是减少处理时间,二是减少等待事件。
数据块
oracle每次执行i/o时候就是以oracle块为单位。数据库的逻辑结构包括:数据块,区,段,表空间,数据块是数据库存储基础,是数据库的最小逻辑单元。默认oracle块大小是8k。oracle块是处理update、insert、select数据事务的最小单位,当用户从表中选择数据时,将在oracle块上读取数据。
数据块结构
1、基本组成
块头:存放一些基本信息,如物理位置,块所属的段类型(数据段、索引段、回滚段等)
表目录:如果块中存储的数据为表数据,则表目录中保存这个表的相关信息
行目录:如果块中存储的数据为表数据,则行目录中保存数据行的相关信息。
行记录:真正存放数据的区域,这部分空间已被使用。
自由空间:未使用的区域,用于新行的插入或者已经存在行的更新。
2、自由空间(free space) (主要用于insert、update时使用)
Insert、update的时候块的自由空间也会减少。当使用DELETE语句删除块中的记录或者使用UPDATE语句把列的值更改成一个更小值的时候,Oracle会释放出一部分自由空间。当insert或update的值超出了自由空间的大小则会发生行迁移或者行连接。
l 行链接
当第一次插入行时,由于行太长而不能容纳在一个数据块中时,就会发生行链接。在这种情况下,oracle会先把自由空间使用与该块链接的一块或多块数据块来容纳该行的数据。行连接经常在插入比较大的行时才会发生,如包含long, long row, lob等类型的数据。在这些情况下行链接是不可避免的。导致了在一次读取过程中要读取多个数据块,引起I/O性能下降。
l 行迁移
当update后的行长度大于修改前的行长度,并且该数据块中的自由空间已经比较小而不能完全容纳该行的数据时,就会发生行迁移。在这种情况下,Oracle会将整行的数据迁移到一个新的数据块上,而将该行原先的空间只放一个指针,指向该行的新的位置,注意,即使发生了行迁移,发生了行迁移的行的rowid 还是不会变化,这也是行迁移会引起数据库I/O性能降低的原因。
参考资料:http://docs.oracle.com/cd/E11882_01/server.112/e40540/logical.htm#CNCPT1063
用analyze分析发生行迁移、行连接的行。
3、自由空间的管理
对于块中的自由空间,Oracle提供两种管理方式:自动管理,手动管理。
1)Oracle使用位图(bitmap)来管理和跟踪数据块,这种块的空间管理方式叫“自动管理”。自动管理有下面的好处:
◆易于使用
◆更好地利用空间
◆可以对空间进行实时调整
2)块中自由空间的手动管理(手动管理比较复杂)
用户可以通过PCTFREE, PCTUSED来调整块中空间的使用,这种管理方式叫手动管理。一般调PCTFREE,相对于自动管理,手动管理方式比较麻烦,不容易掌握,容易造成块中空间的浪费。
PCTFREE参数用于指定块中必须保留的最小空闲空间百分例。之所以要预留这样的空间,是因为UPDATE时,需要这些空间。如果UPDATE时,没有空余空间,Oracle就会分配一个新的块,这会产生行迁移(Row Migrating)。
Select table_name,pct_free fromuser_tables
PCTUSED也是用于设置一个百分比,当块中已使用的空间的比例小于这个百分比的时候,这个块才被标识为有效状态。只有有效的块才被允许插入数据。
sql 的解析过程
1) 运用HASH算法,得到一个HASH值,这个值可以通过V$SQLAREA.HASH_VALUE 查看
2) 到shared pool 中的 library cache 中查找是否有相同的HASH值,如果存在,则无需硬解析,进行软解析
3) 如果shared pool不存在此HASH值,则进行语法检查,查看是否有语法错误
4) 如果没有语法错误,就进行语义检查,检查该SQL引用的对象是否存在,该用户是否具有访问该对象的权限
5) 如果没有语义错误,对该SQL进行解析,生成解析树,执行计划
6) 生成ORACLE能运行的二进制代码,运行该代码并且返回结果给用户
硬解析和软解析都在第5步进行
硬解析通常是昂贵的操作,大约占整个SQL解析过程的70%左右的时间,硬解析会生成执行树,执行计划,等等。
当再次执行同一条SQL语句的时候,由于发现library cache中有相同的HASH值,这个时候不会硬解析,而会软解析, 其实软解析就是跳过了生成解析树,生成执行计划这个耗时又耗CPU的操作,直接利用生成的执行计划运行该SQL语句。
执行以下4个sql,观察生成硬解析与执行计划的情况
Sql1:select *from emp where empno=7788;
Sql2:select *from Emp where empno=7788;
Sql3:select *from emp where empno=7788;
Sql4:select *from emp where empno=7788;
执行结果如下,可以看到4句sql由于1与4完全相同,则发生了一次硬解析一次软解析,执行次数为2并且使用了同一个执行计划。2与3由于一点细微的改变则各自硬解析了一次并且各自重新生成了一个执行计划,即便执行计划是相同的。
SQL_TEXT |
SQL_ID |
PARSE_CALLS |
HASH_VALUE |
EXECUTIONS |
PLAN_ADDRESS |
PLAN_HASH_VALUE |
select * from Emp where empno=7788 |
gmjqmqnsyufus |
1 |
837630808 |
1 |
31EDE2C8 |
2113372157 |
select * from emp where empno=7788 |
2cv6qqj01b9wu |
2 |
1075160986 |
2 |
31EEE1AC |
2113372157 |
select * from emp where empno=7788 |
3kpy13pt6qy2f |
1 |
1919645774 |
1 |
31EDEA10 |
2113372157 |
总结
降低HWM:尽量用TRUNCATE代替DELETE、重构表
减少因内存不足导致的等待:减少union、distinct、减少orderby,这些占内存
减少网络传输等待:减少dblink,将要访问的远程的表接过来一次,以后直接访问这个表
2. 执行计划(数据库访问数据的路径)
执行计划是一个很复杂的课题,这里根据本轮优化来简单介绍一下如何使用执行计划进行优化。
执行计划简单的讲就是数据库如何访问数据的路径,从数据库访问到一条数据的方法有多种,执行计划会从众多方案中通过各种比较选出开销最小(CBO模式)一个访问路径,它会因很多因素的改变受到影响。
oracle访问数据的方法
1) 全表扫描(Full Table Scans,FTS) (扫描表的所有块)
为实现全表扫描,Oracle读取表中所有的行,并检查每一行是否满足语句的WHERE限制条件一个多块读操作可以使一次I/O能读取多块数据块(db_block_multiblock_read_count参数设定),而不是只读取一个数据块,这极大的减 少了I/O总次数,提高了系统的吞吐量,所以利用多块读的方法可以十分高效地实现全表扫描,而且只有在全表扫描的情况下才能使用多块读操作。在这种访问模式下,每个数据块只被读一次。
使用FTS的前提条件:在较大的表上不建议使用全表扫描,除非取出数据的比较多,超过总量的5% —— 10%,或你想使用并行查询功能时。
2) 通过ROWID的表存取(Table Access by ROWID或rowid lookup)
行的ROWID指出了该行所在的数据文件、数据块以及行在该块中的位置,所以通过ROWID来存取数据可以快速定位到目标数据上,是Oracle存取单行数据的最快方法。
这种存取方法不会用到多块读操作,一次I/O只能读取一个数据块。我们会经常在执行计划中看到该存取方法,如通过索引查询数据。
3) 索引扫描(Index Scan或index lookup)(索引也是放在数据块上的,执行索引时先从数据块上找到索引,再根据索引找到数据所在的数据块,比 ROWID多了一步)
我们先通过index查找到数据对应的rowid值(对于非唯一索引可能返回多个rowid值),然后根据rowid直接从表中得到具体的数据,这 种查找方式称为索引扫描或索引查找(index lookup)。一个rowid唯一的表示一行数据,该行对应的数据块是通过一次i/o得到的,在此情况下该次i/o只会读取一个数据库块。
在索引中,除了存储每个索引的值外,索引还存储具有此值的行对应的ROWID值。有4种类型的索引扫描
(1)索引唯一扫描(index unique scan)
(2)索引范围扫描(index range scan):在非唯一索引上都使用索引范围扫描。使用index rang scan的3种情况:
(a)在唯一索引列上使用了range操作符(><<>>=<= between)
(b)在组合索引上,只使用部分列进行查询,导致查询出多行
(c)对非唯一索引列上进行的任何查询。
(3)索引全扫描(index full scan)(count某一列时)
(4)索引快速扫描(index fast full scan)
每种访问方式都有其特定使用场景,如果在一个场景下出现了不适合的访问方式很有可能会引起效率的下降,这也是优化中主要的优化原则。
Oracle连接方法
连接发生在一对表或数据行源之间,当在from子句中存在多张表时,优化器将决定哪种连接运算对于每一张表来说效率最高。
常见连接方法:嵌套循环连接、散列连接、排序-合并及笛卡尔,每种方法都有一定的适合条件。每个连接方式都有两个分支,访问的第一张表叫驱动表,访问的第二张表叫被驱动表,通常优化器预估返回行最小的表通常作为驱动表。(连接示意图)
嵌套循环用在一个表大一个表小的情况,哈希循环用在等值连接的情况,merge join用在不等值连接。
nested loop(嵌套循环):(关联条件的列要有索引,驱动表的数据远小于被驱动表)
存在着两个循环,一个是外部循环,提取驱动表中符合条件的每条记录。另外一个是内部循环,根据外循环中提取的每条记录对内部表进行连接查询相应的记录。由于这两个循环是嵌套进行的,故此种连接方法称为嵌套循环连接。
特点:
1.一个大表和一个小表(驱动表)连接,连接方式可以是等值或者是不等值
2.驱动表数据较小或者内部表已连接的列有唯一性索引或者高度可选的非唯一性索引,效率很高
3.能快速读取结果集中第一批记录而不必等待整个结果集完全确定下来
嵌套循环连接返回前几行的记录是非常快的,这是因为使用了嵌套循环后,不需要等到全部循环结束再返回结果集,而是不断地将查询出来的结果集返回。在这种情况下,终端用户将会快速地得到返回的首批记录,且同时等待Oracle内部处理其他记录并返回。如果查询的驱动表的记录数非常多,或者被驱动表的连接列上无索引或索引不是高度可选的情况,嵌套循环连接的效率是非常低的。
hash join(散列连接)(只适合出现在等值连接的情况下):
哈希连接分为两个阶段,如下。
1、 构建阶段:优化器首先选择一张相对较小的表做为驱动表,运用哈希函数对连接列进行计算产生一张哈希表。通常这个步骤是在内存(hash_area_size)里面进行的,因此运算很快。
2、 探测阶段:优化器对被驱动表的连接列运用同样的哈希函数计算得到的结果与前面形成的哈希表进行探测返回符合条件的记录。这个阶段中如果被驱动表的连接列的值没有与驱动表连接列的值相等的话,那么这些记录将会被丢弃而不进行探测
特点:
1.一般两张相同大小的表连接,初始参数hash_join_enable=true
2.只能是等价连接,只能是CBO模式
3.只有一张源表需要排序,可能比merge join更快,因为只需要对一张源表排序;
也可能比nested loop更快,因为处理内存中的hash表比处理b-tree索引更快
4.可能会使用到临时表空间,所以最好pag_aggregate_target设置的比较大
哈希连接比较适用于返回大数据量结果集的连接。
merge join(排序-合并):两个互相连接的表按连接列的值先排序,排序完后形成的结果集再互相进行合并连接提取符合条件的记录(用在>、>=、<=等情况下的连接)
特点:
1.首先对2张表的连接列进行排序后再连接
2.当缺乏数据选择性或者有效索引时,或者2个表都比较庞大,可能比nested loop更有效
3.只能是等值连接,可能使用temp表空间
排序合并连接比较适用于返回大数据量的结果。
排序合并连接在数据表预先排序好的情况下效率是非常高的,也比较适用于非等值连接的情况,比如>、>=、<=等情况下的连接(哈希连接只适用于等值连接)
笛卡尔积
笛卡尔积连接发生在当一张表的所有行与另一张表的所有行连接的时候,因此这种连接的结果集等于两个表的数据行数相乘。在实际应用中不使用或者避免出现这种连接。
执行计划中每个字段的意义
operation:执行的操作类型,如Table access或sort。
options:操作的附加信息。例如,以TABLE SCAN为例,选项可能是full或by rowid。
object_node:如果是分布式查询,这一列表示用于引用对象的数据库链接。对于并行查询,它的值可能对应一个临时的结果集
object_name:对象名称。
object_type:对象的类型(表,索引等)
cost:优化器估算出来的此操作的相对成本。
cardinality:优化器预期这一步将返回的记录数。
bytes:预计这一步将返回的字节数。
partition_start:如果要执行分区消除(partitionelimination),这一列表明要访问的分区范围的起始分区。它也可能包含关键字key或者row location,表明要访问的分区将在运行时确定。
partition_end:表明将要访问的分区范围的结束分区。
cpu_cost:估算出来的操作的cpu成本。
io_cost:估算出来的操作的io成本。
temp_space:估算出来的这一步操作所使用的临时存储的空间大小(如用来排序的内存或磁盘空间)。
access_predicates【访问条件】:sql语句中,确定如何在当前步骤中提取记录的子句。它可以包含提供给索引检索或表连接的子句。在这块把数据给过滤掉,一般会用到索引
filter_predicates【过滤条件】:sql语句中确定如何对记录进行过滤的子句,如where子句在非索引列上的条件。一般不用索引,都是全表扫描,可以作为优化重点关注的地方!
time:优化器为这一步执行估算的时间消耗。
根据执行计划进行优化的一般步骤
将瓶颈sql块单独取出,查看其执行计划,
首先检查其中使用了全表扫描的对象,判断其是否合适。引发错误使用的情形通常是:
1) 没有建立合适的索引列导致全表扫描
2) 非函数索引列使用了函数引发全表扫描
3) 对象统计信息过旧或未收集导致全表扫描、
4) 对于位图索引,直方图信息的缺失有时也会导致错误的全表扫描
然后检查其中无用的表,确认无用的表可以直接去掉减少访问步骤减小系统开销。
接着检查其中是否有重复访问的表,查看是否可以减少访问次数一般可以通过使用with或者建立中间表来优化。
优化访问路径,数据库优化器也有不那么聪明的时候,有时候它生成的访问路径可能并不是最优的,可尝试使用hint来改变访问路径进行优化
常用hint:
/*+full( )*/表明对表选择全局扫描的方法.
/*+use_nl( )*/在多表关联查询中,指定使用nest loops方式进行多表关联。
/*+use_hast( )*/在多表关联查询中,指定使用hash join方式进行多表关联。
/*+index( )*/ 使用指定表的指定索引
/*+ append */ 以直接加载的方式将数据加载入库
/*+leading( )*/在多表关联查询中,指定哪个表作为驱动表,即告诉优化器首先要访问哪个表上的数据。
/*+ parallel() */ 在sql中指定执行的并行度,这个值将会覆盖自身的并行度
看Description的基数(执行结果返回的数据的行数)和自己预估的基数是否大致一直,不一致则考虑其它影响效率的情况,如统计信息比较旧等。
3. 统计信息
10g的统计信息自动收集策略
Oracle10g中统计信息默认可以自动收集,由GATHER_STATS_JOB作业收集得到,只有当数据库对象没有统计信息或者统计信息已经过期(Oracle 10G中是否过期的标准是数据库对象被修改的记录行数超过10%,该信息由Modification Monitoring来追踪完成)时才对该对象进行信息统计,该作业在数据库创建或升级时由Scheduler自动创建,
这些作业可以从视图DBA_SCHEDULER_JOBS中查到。
SELECT * FROM DBA_SCHEDULER_JOBS
通过以下包设置job的开启与关闭
Begin
Dbms_scheduler.enable(‘GATHER_STATS_JOB’);
END;
Begin
Dbms_scheduler.disable(‘GATHER_STATS_JOB’);
END;
通过以下表查看job执行日志
Select * from dba_scheduler_job_run_detailswhere job_name=’ GATHER_STATS_JOB’
默认情况下,Scheduler在维护窗口(maintenance window,目前227上默认启动时间为每晚上10:00至次日早上6:00及整个周六周日)打开时运行GATHER_STATS_JOB作业,作业GATHER_STATS_JOB则是通过调用系统内部过程DBMS_STATS.GATHER_DATABASE_STATS_JOB_PROC来完成信息统计的,该过程可根据数据库对象统计信息需求的优先级(即数据库对象被修改的多少)按先后顺序来完成统计信息收集任务。
可以通过以下表查看窗口信息
Select * from dbs_scheduler_windows
可以通过以下包修改窗口信息(例如将SUNDAY_WINDOW的开始时间修改为早上4点)
begin
dbms_scheduler.set_attribute
(
'SUNDAY_WINDOW','repeat_interval','freq=daily;byday=SUN;byhour=4;byminute=0; bysecond=0'
);
end;
Select * from dbs_scheduler_windows–查看是否修改成功
GATHER_STATS_JOB作业是否随维护窗口的关闭而关闭则由属性stop_on_window_close决定,stop_on_window_close的默认值为TRUE,此时GATHER_STATS_JOB作业随维护窗口的关闭而关闭。
可以通过以下匿名块查看stop_on_window_close属性
declare
valueboolean;
begin
dbms_scheduler.get_attribute('gather_stats_job','stop_on_window_close',value);
dbms_output.PUT_LINE('Check Result: '||casevaluewhentruethen'True'else'False'end);
end;
可以通过以下包修改属性
execdbms_scheduler.set_attribute(' gather_stats_job
','stop_on_window_close',TRUE);
非默认情况时,Oracle10g可通过设置初始化参数 STATISTIC_LEVEL(STATISTICS_LEVEL = {ALL | TYPICAL | BASIC}),来控制是否启用统计信息自动收集功能
当其为默认值TYPICAL时,系统将自动收集所有主要的有关自身管理的信息以使系统提供最优性能,该值适合于绝大多数情况;
当取值ALL时,相对TYPICAL值系统增加timedOS statistics和plan execution statistics两项信息统计;
当取值 BASIC时:有关系统特性和功能的许多信息统计功能都将被关闭
查看当前值
Show parameter statistics_level
修改语法
ALTER SESSION SET statistics_level=all–修改当前会话
ALTER SYSTEM SET statistics_level=all–修改整个系统
10g的统计信息手动收集方法
除系统自动收集统计信息外,还可以通过手动调用包来收集统计信息
常用的包如下
dbms_stats.gather_table_stats();--收集指定表的统计信息
常用参数
Tabname–表名称
Partname—分区名称
estimate_percent—统计的样品比例,默认oracle自动选择
method_opt–统计方式,默认FOR ALL COLUMNS SIZE AUTO.
degree –并行度
cascade—是否级联收集索引信息,默认是不收集的
dbms_stats.gather_index_stats();--收集指定索引的统计信息
另外还有一个收集统计信息的命令analyze
analyzetable t1 computestatistics–收集表统计信息
以上包或命令常用在对单个对象收集信息,当对象较多或者需要对某个用户下的某类对象进行收集信息就要用到下面的包
dbms_stats.gather_schema_stats();--收集指定用户的统计信息
常用参数
Ownname–用户名称
estimate_percent—抽样比例,默认oracle自动选择
method_opt--统计方式,默认FOR ALL COLUMNSSIZE AUTO.
Degree–并行度
Granularity—收集统计信息的级别默认auto
-- 'AUTO' – 由过程自动决定收集的级别
-- 'GLOBAL AND PARTITION' – 收集全局以及分区信息
-- 'SUBPARTITION' – 收集子分区信息
-- 'PARTITION' – 收集分区信息
-- 'GLOBAL' – 收集全局信息
-- 'ALL' - 收集全局、分区、子分区信息
Cascade—是否收集索引信息,默认由oracle决定
options – 指定收集的对象
-- 'GATHER' – 收集用户的所有对象信息
-- 'GATHER AUTO' – 由oracle决定收集哪些对象
-- 'GATHER STALE' – 收集视图user_tab_modifications中的对象信息与系统自动收集的策略一致
-- 'GATHER EMPTY' – 收集当前统计信息为空的对象信息
在自动收集策略中也提到了一个收集统计信息的包
DBMS_STATS.GATHER_DATABASE_STATS_JOB_PRO()
这个包的统计原理是统计数据库中统计信息过久或者缺失或者对象在统计时间段内数据量变化大于10%(可以累积)
监控对象修改数据量的视图
select*fromuser_tab_modifications
对象中的数据发生变化后并不会立即进入到user_tab_modifications,可以利用以下包手工刷新
begin
Dbms_Stats.flush_database_monitoring_info();
end;
手动调用收集统计信息
begin
dbms_stats.gather_database_stats_job_proc();--通过实验发现对于分区表,只收集分区表的信息,全表的不收集(需要通过修改时间触发自动收集job才会收集全表的信息)
end;
当分区表修改数据量达到10%但没到全表的10%,则只收集分区表的信息,不收集全表的信息,当达到全表的10%则会收集全表的信息。
修改量可以累积当累积到10%后也会触发收集。
dbms_stats.gather_schema_stats()中的参数‘GATHER STALE'也是利用通过监控这个视图中的数据来决定收集的对象。
在收集统计信息时可能出现原来的优化方法在收集统计信息之前一直工作良好,但是在此之后,由于新收集的统计信息产生了不良计划,导致查询突然出错或效率降低。为避免这种情况,统计信息的收集作业在收集新信息之前保存当前的统计信息。如果出现问题,则可以返回到原有的统计信息,或者通过历史统计检查二者之间的不同之处,以解决问题。
查询系统保存统计信息时长
select DBMS_STATS.GET_STATS_HISTORY_RETENTIONfrom dual;
查询最早可用的统计信息时间
selectDBMS_STATS.GET_STATS_HISTORY_AVAILABILITY from dual;
还原统计用户统计信息
begin
dbms_stats.restore_schema_stats
(user,'09-6月 -16 10.26.32.927000000 下午 +08:00');
end;
在利用系统自动保存的信息进行还原的同时也可以利用命令手动导入导出用户统计信息
--创建收集统计信息的表stattab
begin
dbms_stats.create_stat_table('scott','stattab','users');
end;
--将用户统计信息导出到统计信息表stattab
begin
dbms_stats.export_schema_stats
(
ownname=>'scott',stattab=>'stattab',statown=>'scott'
);
end;
--收集用户的统计信息
begin
dbms_stats.gather_schema_stats
(
'scott',method_opt=>'forall columns',degree=>4
);
end;
--将用户统计信息还原
begin
dbms_stats.import_schema_stats
(ownname=>'scott',STATTAB=>'stattab',statown=>'scott');
end;
SQL动态采样
动态取样是为谓词和表/索引统计收集更加精确的信息从而提高服务器性能,信息越精确产生的性能更好。
可以使用动态取样的情况:
1.) 在收集的统计不能使用或会导致严重的估计错误时估计单表的谓词选择性;
2.) 估计没有统计的表/索引的统计;
3.) 估计统计过期的表和索引的统计;
动态取样特征由参数OPTIMIZER_DYNAMIC_SAMPLING控制,默认级别为2。
动态采样是在解析的时候对表进行采样收集统计信息,但不会写入user_tables
动态采样可以通过hint开启无论OPTIMIZER_DYNAMIC_SAMPLING目前的值是多少
取样级别范围从1..10
1级:满足以下条件则采样所有没被分析的表:
(1)查询中至少有一个未分析表;
(2)这个未分析表被关联另外一个表或者出现在子查询或非merge视图中;
(3)这个未分析表有索引;
(4)这个未分析表有多余动态采样默认的数据块数(默认是32块)。
2级:对所有未分析表进行动态采样。采样数据块数量是默认数量的2倍。
3级:在2级基础上加上那些使用了猜想选择消除表,采样数据块数量等于默认数量。对于未分析表,采样数量2倍于默认数量。
4级:在3级基础上加上那些有单表谓词关联2个或多个列,采样数据块数量等于默认数量。对于未分析表,采样数量2倍于默认数量。
5,6,7,8,9级在4级基础上分别使用2,4,8,32,128倍于默认动态采样数据块数量。
10级:在9级基础上对表中所有数据块进行采样。
新建一个t2表,不对其收集统计信息,查询其中一个数据
SQL> select /*+dynamic_sampling(t 0)*/ * from t2 where id=30101;
--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 127 | 3302 | 44 (0)| 00:00:01 |
|* 1 | TABLE ACCESS FULL| T2 | 127 | 3302 | 44 (0)| 00:00:01 |
--------------------------------------------------------------------------
SQL> select/*+dynamic_sampling(t 3)*/ * from t2 t where id=30101;
--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 26 | 45 (3)| 00:00:01 |
|* 1 | TABLE ACCESS FULL| T2 | 1 | 26 | 45 (3)| 00:00:01 |
--------------------------------------------------------------------------
Note
-----
- dynamic sampling used for this statement (level=0)–这个值需单独设置
通过在sql使用hint开启动态采样级别能够发现开启后访问的行数明显下降
4. 分区表
1.创建的分区表信息清单(数据采样时间201603)
表英文名 |
表中文名 |
总的数据量大小 |
总的数据量条数 |
每月的数据量大小 |
每月的数据量条数 |
f_ac_inter |
内部户事实表 |
9.14G |
35099569 |
0.23G |
896098 |
F_AC_JYMX_ALL |
交易明细中间表 |
2.7G |
11630307 |
0.06G |
272207 |
f_ac_jymx_d |
交易明细日表 |
3.6G |
11563443 |
0.08G |
240419 |
F_ac_ledger |
日总账 |
10.1G |
27214046 |
0.24G |
523420 |
T05_LOAN_PROVISION |
|
1.4G |
11094224 |
0.07G |
592680 |
T09_SUB_ACCU_BALANCE_H |
科目账户余额表 |
2.28G |
18127253 |
0.1G
|
904879 |
T05_DEPOSIT_PROVISION |
|
1.7G |
14865031 |
0.08g |
801128 |
t03_inter_acun_dtl |
内部户账务明细 |
6.09G |
33099597 |
0.15G |
896098 |
t05_core_acun_dtl |
核心账务明细 |
4.78G |
24347280 |
0.14G |
686731 |
T05_CORE_TRAD_DTL |
核心交易明细 |
23G |
27190137 |
0.6G |
682783 |
2.创建分区表前的分析
从表格统计信息,我们看到这些表都属于大表;其次在分析后台脚本后发现,这些表都是前台和后台脚本中用的比较频繁的表,同时,这些表在脚本查询中大都使用的是当月数据,因此,
我们把这些表改为以work_dt为分区列的月分区表
3.分区表类型
在本次的项目优化中,我们所使用的分区表类型是范围分区(range).
由于我们本次的生产数据库版本是ORACLE10g,因此,我们列举出了ORACLE10g的几种分区类型如下:
分区表类型 |
简述 |
RANGE(范围)分区 |
例如查询某个月的数据 |
LIST(列表)分区 |
例如查询某个地区的数据 |
HASH(哈希)分区 |
数据分配到每个分区的量是均衡的 |
组合分区 |
分为(range-list,range-hash) |
4.分区表优点与缺点
优点 |
缺点 |
1. 分区消除:减少访问路径 2. 记录清理:删除记录高效(truncate) 3. 分区转移(exchange partition 分区名 with table tabname);将某分区数据与表互换 |
1.分区表的缺点:分区过多,会加大Oracle对段的管理。同时内部会产生大量的递归调用。一般数据记录在100万以下的不 建议创建分区表。
|
5.分区表的常见使用方式
a) truncate分区数据
alter table range_part_tab truncatepartition p1 ;
b) drop分区
alter table range_part_tab droppartition p_max;
c) 添加分区
alter table range_part_tab add partitionp2013_02 values less than(to_date('2013-03-01','yyyy-m
m-dd'));
d) 常用视图
user_tab_partitions (查询一个分区表的各个分区名)
6.分区表的维护
分区的增加:在10G中没有自动增加分区的功能因此本轮优化中采取了手动维护的方式,通过脚本在每月1号增加下个月的分区。
分区统计信息收集:分区表的统计信息主要是通过数据库自动收集统计信息的策略实现的,但在日常执行过程中出现了每月2号(sysdate时间)引用了部分分区表的sql执行缓慢的情况,是因为新增的分区的统计信息是空的,因此在分区插入数据后导致了优化器没有准确的统计信息可供使用出现执行计划变差。目前采取的补救方案是每月3号早上在自动收集统计信息完成后将分区的统计信息备份一份,在下月2号之前恢复给新建分区上。使优化器有统计信息可以使用避免了执行计划变差。
分区数据接入:采取分区表后可以对分区进行截断操作即减少了执行时间也能避免之前delete而产生的高水位线。因此在对按月接入数据的分区表采取截断操作后再接入数据。
5. 索引:一种数据库结构,用来快速查找数据,一般包括根节点,叶节点。叶子节点存储索引条目,一般条目里包含索引键值(单列索引是一个值多列索引是多个值组合)与rowid
B树索引
目前数据库中最常用的索引,构造类似于二叉树,能根据键值提供一行或一个行集的快速访问,其中的’B’代表平衡, 通常使用在频繁使用查询谓词的列上,一般这类列的选择度都较高。
使用场景
1、当我们希望从表中只返回少量的数据(占比很小,这个比例通常经验值是5%,不过根据表的不同也有不用,一个瘦表(通常只有几列)可能在20%-30%,一个胖表(列很多或列很宽)可能在2%-3%)时会使用索引。如下例:
T1表插入从1到10w的数字只有一列,t3_2利用下例中的t3建表,建表sql
createtable t3_2asselectid,name,name||name||name||name c3,name||name||name||name||name c4 fromt3
分别在id列建立索引,收集统计信息
以上两个查询中的数值分别是全表扫描与索引范围查询的临界值
2、当我们想要查询大量数据,但是只要返回索引的列或者只通过索引列就能得到结果的话,索引也会起到作用
3、数据在磁盘上的物理组织也会对索引的使用有影响,如以下的例子,我们创建两个实验表t2/t3,向t2中顺序的插入10w条数据,同时生成一组随机数据。将t2按照随机数排序插入到t3中,目的是打乱数据的物理存储位置。
begin
for i in1..100000loop
insertinto t2 values(i,rpad(dbms_random.random,75,'*'));
endloop;
commit;
end;
createtable t3 asselect*from t2 orderbyname
createindex idx_1on t2(id)
createindex idx_2 on t3(id)
begin
dbms_stats.gather_table_stats
('scott','t2');
begin
dbms_stats.gather_table_stats
('scott','t3');
end;
end;
在对表进行收集统计信息后,分别对t2/t3查询相同范围的数据
select*from t2 whereidbetween87and1000
select*from t3 whereidbetween87and1000
以上三个查询的结果是相同的,但是相同的查询数据库的开销与io上升的差异十分明显。
原因是当向一个表中填充数据时如果按照行主键或者建立索引的列顺序填充,序号相邻的行存储位置一般也会相邻,当你发出一个范围查询的时候你想要的行通常也在同样的块上,即使你要查找大量的行,通过索引范围扫描的读取的块里也许就包含了你想要的行。如下图,相同数量的行数在未打乱顺序的t2中分布在11个数据块,在顺序被打乱的t3中分布在631个块。
如果行被分散的存储在不同位置上,此时强制使用索引范围扫描就会是个灾难,使用全表扫描反而更好。
总结
1、通过索引访问表中的数据占比越少越有效
2、如果能使用索引列回答问题(只用到索引列不用访问表)那么返回数据占比很大索引也是有效的
3、数据的物理组织有时未按照索引列或主键列有序的填充表,会影响索引的使用
4、空值会影响索引的使用,在有空值的列上通过与虚拟列建立组合索引,可以使优化器选择索引。而且索引的大小并没有明显变化
createtable t5 asselect u.OBJECT_NAME,u.DATA_OBJECT_IDfrom user_objects u
createindex t5_1on t5(DATA_OBJECT_ID)
createtable t6 asselect*from t5
createindex t6_1on t6(DATA_OBJECT_ID,0)
begin
dbms_stats.gather_table_stats
('scott','t5');
dbms_stats.gather_table_stats
('scott','t6');
end;
5、B树索引经过大量的插入删除操作以后一个是容易使树不平衡,再一个是删除后空间不回收。所以定期重建索引非常有必要。
位图索引
在B树索引中,索引键值与行之间存在一种一对一的关系,一个索引键值引向一行,而在位图索引中,一个索引键值则对应多行,位图索引通常适用于高度重复(相对于很多的行数,列值可能只有几个,列值/行数越接近0则越适合使用位图索引)而且经常只读的列,通常查询这种列返回的数据占比很大,因此也不适合使用B树索引。对比来看,B树索引通常是选择性的,位图索引位通常不是选择性的。位图索引的键值使用0,1存储,相较B树索引节省很大的空间另外位图索引可以存储NULL值。
使用场景
1、一个查询条件包含多个列,并且要创建索引的列只有几个不同的值及大量的聚合统计查询where条件中使用and/or/in
如截图中的查询例子。如果建立B树索引,为了高效的满足查询要求,就要建立2或者更多的索引组合来实现,这将会占用大量的数据库空间如果后期条件有调整维护起来也比较麻烦。
如果建立位图索引,oracle会对3个索引的位图使用and、or或not得到合并后的位图,如果有必要可以将位图中的‘1’转换成rowid来访问数据,如果是计数则直接统计1的个数。
如下例子:
createtable t4
(
a notnull,
b notnull,
c notnull
)
as
select
decode(ceil(dbms_random.value(1,2)),1,'M',2,'F'),
ceil(dbms_random.value(1,50)),
decode
(
ceil(dbms_random.value(1,5)),
1,'18and under',
2,'19-25',
3,'26-30',
4,'31-40',
5,'41and over'
)
fromdual connectbylevel<=100000;
createbitmapindex t4_1on t4(a);
createbitmapindex t4_2on t4(b);
createbitmapindex t4_3on t4(c);
begin
dbms_stats.gather_table_stats
('scott','t4');
end;
总结
1. 位图索引使用于低基数的列(比如说性别列,数据仓库中的维表的主键),相对于B树索引,它的count,and,or操作更有效
2. 位图索引存放的是0,1的比特位,相对于B树索引,占字节数特别少
使用位图索引要特别注意
1. 列的基数比较多,不适合位图索引,因为它会占用更多的存储空间
2.索引列DML频繁的列,不适合位图索引,容易造成死锁,原因是一个位图索引键值指向多行,如果一个会话修改了一行数据,大多数情况下这个键值所对应的所有行都会被锁定。大大影响到系统并发性。数据仓库项目中对于位图索引的维护一般建议先删掉索引加载完完数据后再建立索引
3.关于列偏态或称列倾斜、倾斜列对使用索引的影响,这种列的特点是数据大多集中在某几个值。这种情况下一般会影响索引的使用,通常情况下需要收集表的直方图信息来使优化器决定是否使用索引。
如下例子:
createtable t8
(a notnull,b notnull,c notnull)
as
select
'M',
ceil(dbms_random.value(1,50)),
decode
(
ceil(dbms_random.value(1,5)),
1,'18and under',
2,'19-25',
3,'26-30',
4,'31-40',
5,'41and over'
)
fromdual connectbylevel<=100000;
insertinto t8
select
'F',
ceil(dbms_random.value(1,50)),
decode
(
ceil(dbms_random.value(1,5)),
1,'18and under',
2,'19-25',
3,'26-30',
4,'31-40',
5,'41and over'
)
from dual connectbylevel<=10;
createbitmapindex t8_1on t8(a);
createbitmapindex t8_2on t8(b);
createbitmapindex t8_3 on t8(c);
begin
dbms_stats.gather_table_stats
('scott','t8');
end;
createtable t7
(
a notnull,
b notnull,
c notnull
)
as
select
'M',
ceil(dbms_random.value(1,50)),
decode
(
ceil(dbms_random.value(1,5)),
1,'18and under',
2,'19-25',
3,'26-30',
4,'31-40',
5,'41and over'
)
fromdual connectbylevel<=100000;
insertinto t7
select
'F',
ceil(dbms_random.value(1,50)),
decode
(
ceil(dbms_random.value(1,5)),
1,'18and under',
2,'19-25',
3,'26-30',
4,'31-40',
5,'41and over'
)
from dual connectbylevel<=10;
createbitmapindex t7_1on t7(a);
createbitmapindex t7_2on t7(b);
createbitmapindex t7_3 on t7(c);
begin
dbms_stats.gather_table_stats
('scott','t7',METHOD_OPT=>’forall columns size skewonly’);
end;
建立t8/t7,其中M对应10w条数据,f对应10条数据,这个一个典型的偏态列,区别是对t7收集了其直方图信息。
执行相同的查询,在查询t8表时无论查‘M’还是‘F’两个执行计划都使用了全表扫描,而且优化器认为两个值的基数一致都是5w左右。
在查询t7表时,可以发现执行计划明显不同,而且优化器识别出了两个值的基数不同,接近真实值。
分区表索引
在分区表中经常使用的两种索引,本地索引和全局索引。一般使用LOCAL索引较为方便,而且维护代价较低,并且LOCAL索引是在分区的基础上去创建索引,类似于在一个子表内部去创建索引,这样开销主要是区分分区上,很规范的管理起来;而相对的GLOBAL索引是全局类型的索引,根据实际情况可以调整分区的类别,而并非按照分区结构一一定义,相对维护代价较高一些,在应用过程中依据实际情况而定,来提高整体的运行性能。
根据经验总结了本地索引与全局索引使用技巧
1、如果使用全局索引,当对某一个分区进行ddl操作时,该索引就无效了,必须重建,这一点比较麻烦。(DDl操作对全局索引和本地索引的影响详见下表的总结)可以在语句后增加UPDATEindexes在截断分区的同时维护全局索引避免失效
2、如果索引字段是分区键(主要是range),那么就用local的。
3、如果索引字段是id、电话号码等类型的,那么就用global的。
4、如果分区间的数据是相互独立的,即不会被同时访问,使用local index 更好些。相反如果数据跨越多个分区,可能local index会更差些。
5、当分区中出现许多事务并且要保证所有分区中的数据记录的唯一性时采用全局索引。
各种操作对不同类型索引的影响
表级分区操作 |
本地分区索引 |
全局分区索引 |
增加分区 |
索引不受影响 |
索引不受影响 |
拆分分区 |
受拆分操作影响的分区上的索引被标记为UNUSABLE |
索引的所有分区都被标记为UNUSABLE |
移动分区 |
被移动的分区上的索引被标记为UNUSABLE |
索引的所有分区都被标记为UNUSABLE |
交换分区 |
被交换的分区上的索引被标记为UNUSABLE |
索引的所有分区都被标记为UNUSABLE |
合并分区 |
受合并操作影响的分区上的索引被标记为UNUSABLE |
索引的所有分区都被标记为UNUSABLE |
截断分区 |
索引不受影响 |
索引的所有分区都被标记为UNUSABLE |
删除分区 |
本地索引分区被删除,其余索引分区不受影响 |
索引的所有分区都被标记为UNUSABLE |
令数据只读 |
通过表空间隔离可以令分区级别索引数据只读 |
理论上可以令分区级别索引数据只读。 实际上无法实现,除非整个表是静态的 |
组合索引与单列索引
整体来讲,索引是为了更快的查找到数据而产生的,一般建立在选择度较高的列上(位图索引不在这个讨论范畴)。组合索引一般是通过多列组合来达到提高选择度的,代价是索引比较占用空间,如下例:
我们复制t3表为t3_1,同时在id与name上建立组合索引(IDX_T31),对比两个索引大小
由于name列的加入,t3_1索引的大小是t3的5倍多,接下里查询相同的值来对比执行计划中的消耗
可以看到虽然结果相同,但是使用组合索引的字节与cpu消耗均高出不少。(某些特定情况,比如说要取出的列都是索引列,那么就会减少一步从表中读数)
综合以上实验可以知道,虽然执行计划都选择了走索引但是两个索引所占用的物理空间与被引用时带来的开销有很大不同,当单列选择度较高的时候尽量选择单列索引,只有当单列索引的选择度不够高时才考虑使用组合索引来提高选择度。
函数索引
函数索引与B*树索引的结构存在很大相似性。区别就在于形成树结构的叶子节点上,保存的不是索引列的取值,而是经过特定的函数处理过的索引列值。这样的结构,进行搜索的时候,就可以直接使用到函数索引的叶子节点,获取到对应的rowid集合。
B树索引通常用在精确匹配或者小范围的查询,但是当进行模糊匹配时就会常常引起B树索引的失效,这时就可以用到函数索引。
函数索引的使用要注意以下几点
首先,函数索引的综合消耗要大于普通的B*树索引。相对于传统索引,函数索引要保证创造的函数列数据一致性和多次进行函数计算。这样的消耗要远大于普通B*树索引;
其次,函数索引的适应范围较小。函数索引生效的最大要素就是函数的使用和定义是100%相同。
最后特别注意在10gR2版本中,发生过删除函数索引导致引用这个表的存储过程编译失效的情况,临时解决方案是在删除这个索引后把所有失效的脚本重新编译一次。所以更要慎重选择使用函数索引。
函数索引通常是一种事后补救措施。一个良好设计的应用,一个划分合理的数据库逻辑结构,应该是可以避免函数操作数据列的SQL大量出现的。只有在系统上线之后,问题暴露出来,但没精力进行修改时或修改代价太大,才开始使用函数索引,保证系统功能能够实现。
6. 其他优化方案
除以上总结的优化方法外在本轮优化中还尝试使用了其他方法,主要如下:
l 定位脚本中的sql 瓶颈:使用pl/sql developer测试窗口中的profiler来定位是哪一段sql耗时较久
l 去掉orderby,排序是一项开销很大的操作,如果非必须可以去掉(需要考虑B树索引中的簇族因子,如果列的排序过于无序也会导致索引的失效参看B树索引部分,这是一个需要平衡的选择)
l 将leftjoin 改写成子查询,根据子查询的特性对于返回到外层查询的记录来说,子查询会每次执行一次。因此,必须保证任何可能的时候子查询都要使用索引,如果父查询只返回较少的记录,那么再次执行子查询的开销不会非常大。因此在连接左侧的表如果在执行计划中使用了全表扫描并且没有参与where的条件可以考虑转换成关联子查询。
l 使用语句级并行,用资源换时间多数使用在全表扫描情况下。
l 增加过滤条件,可能会更改业务逻辑需要慎重选择。
l 一段sql中对一张大表进行了全表查询部分列,这种情况,需要使用全量抽取改增量抽取的方式来优化