首页 ORACLE+UNIX易錯問題集錦

ORACLE+UNIX易錯問題集錦

举报
开通vip

ORACLE+UNIX易錯問題集錦ORACLE+UNIX易錯問題集錦 ORACLE+UNIX ---------/ 本文描述的ORALCE及UNIX易错问题全部来自我工作中的经验所得,其中不少错误的理解让 我吃尽了苦头,有的甚至让我付出了惨痛的代价。学习ORACLE和UNIX有很多资料,系统学习最好的方法就是从官方文档或各类相关书籍中连贯的学习,这样的系统学习是必不可少的!可是描述 整个知识体系的书籍大多不会对某些易错点做特别详尽的强调,毕竟重心不一样。于是我就萌发这 样的想法:―把这些容易犯错的地方归纳整理成文,让有基础的朋友加深印象,避免...

ORACLE+UNIX易錯問題集錦
ORACLE+UNIX易錯問題集錦 ORACLE+UNIX ---------/ 本文描述的ORALCE及UNIX易错问题全部来自我工作中的经验所得,其中不少错误的理解让 我吃尽了苦头,有的甚至让我付出了惨痛的代价。学习ORACLE和UNIX有很多资料,系统学习最好的 方法 快递客服问题件处理详细方法山木方法pdf计算方法pdf华与华方法下载八字理论方法下载 就是从官方文档或各类相关书籍中连贯的学习,这样的系统学习是必不可少的!可是描述 整个知识体系的书籍大多不会对某些易错点做特别详尽的强调,毕竟重心不一样。于是我就萌发这 样的想法:―把这些容易犯错的地方归纳整理成文,让有基础的朋友加深印象,避免犯错,理解掌握 更多实用技巧,同时对自己也是一种促进!‖经过一段时间回忆、思考、总结,终于能静下心来,将 其整理出来,并与大家共享,想法终于实现了!在写这篇文章的时候,我又有了很多新的收获,因 为在写作的过程中,光自己清楚是不够的,―给人一杯水,自己要有一桶水‖,要想办法让看的人明白,是一件很不容易的事。由于要不断的实验和推敲,写完本文时我对ORACLE及UNIX又有了新的认识,感觉很快乐!当然,如果大家看过后能将其中部分经验成为自己的经验,能有所得,避免 犯我犯过的种种错误,并加深了对ORALCE及UNIX的理解,那我将觉得更快乐! 我不会很具体说明每个易错点后面的语法知识,那也不是本文的重点,进一步扩展和深入研究 有待于今后的共同努力。为了能让易错问题完整再现,我基本上是将易错点做了详尽的步骤描述, 有兴趣的人照我步骤执行,完全可以重演!我特别重视实验,相信实践是检验真理的唯一 标准 excel标准偏差excel标准偏差函数exl标准差函数国标检验抽样标准表免费下载红头文件格式标准下载 ,只 有实验出来的结果是正确的,得出的结论才是可信的,否则就有可能是在误人子弟!本文设法构造 出大量实验来证明自己的观点,我觉得自己做实验步骤和方法还是有一定价值的,大家可以尝试学 习使用我的实验方法,以后学习研究及解决问题,最好首先想到用实验来推敲解决。此外每小节结 束处,都给出了一个总结,希望能加深大家的印象!部分小节我还做了引申联想。属于易错问题以 外的内容在本文中将较少提及,比如学习AWK+SED+SHELL,使用PL/SQL的技巧、数据库优化思路、表和索引及锁的各种机制、备份恢复及高可用性 方案 气瓶 现场处置方案 .pdf气瓶 现场处置方案 .doc见习基地管理方案.doc关于群访事件的化解方案建筑工地扬尘治理专项方案下载 、10g新特性等原理性的东西就不在本文 中体现了。知识博大精深,我始终觉得自己知之甚少!后续我会陆续对这些知识点进行学习理解思 考后单独整理成文,结合实例形成一系列step by step的文章,和大家共同进步!本文仅是围绕―易错‖主题,突出向大家展现ORACLE和UNIX中的各类陷阱。 我的写作风格是:提出问题----------构造例子证明-----------总结--------引申联想(部分),希望大家阅读时能习惯我的这种风格。 由于本人水平有限,理解错误和考虑不周之处欢迎大家批评指导补充完善,谢谢! 1 ORACLE 1 1.1 现实中大家应该都有用到并行度,并行度有一个显著特点就是可以最大限度的利用到多个CPU, 在系统不忙或者是错开高峰期的时候,应该是一个非常不错的选择,那在使用并行度时,一般都会 遇到哪些问题呢?讨论如下: 1.1.1DML Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb SQL> drop table ljb_parallel; Table dropped SQL> create table ljb_parallel as select * from dba_objects; Table created 先来看查询语句用并行度的情况,可以看出有利用到并行度,没问题。 SQL> explain plan for select /*+parallel(a,4)*/ count(*) from ljb_parallel a; Explained SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT -------------------------------------------------------------------------------------------------------------------- Plan hash value: 2855666597 -------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Cost (%CPU)| Time | -------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 55 (0)| 00:00:01 | | 1 | SORT AGGREGATE | | 1 | | | | 2 | PX COORDINATOR | | | | | | 3 | PX SEND QC (RANDOM) | :TQ10000 | 1 | | | | 4 | SORT AGGREGATE | | 1 | | | | 5 | PX BLOCK ITERATOR | | 60761 | 55 (0)| 00:00:01 | | 6 | TABLE ACCESS FULL| LJB_PARALLEL | 60761 | 55 (0)| 00:00:01 | -------------------------------------------------------------------------------------------------------------------- Note - dynamic sampling used for this statement 17 rows selected 接着看看DDL语句用并行度的情况,查看如下,发现也有用到并行度,也没问题。 2 SQL> explain plan for create table ljb_parallel2 parallel as select * from ljb_parallel; Explained SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT ---------------------------------------------------------------------------------------------------------------------- Plan hash value: 1946516457 ---------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Ti ---------------------------------------------------------------------------------------------------------------------- | 0 | CREATE TABLE STATEMENT | | 60761 | 10M| 125 (1)| 00 | 1 | PX COORDINATOR | | | | | | 2 | PX SEND QC (RANDOM) | :TQ10000 | 60761 | 10M| 56 (2)| 00 | 3 | LOAD AS SELECT | LJB_PARALLEL2 | | | | | 4 | PX BLOCK ITERATOR | | 60761 | 10M| 56 (2)| 00 | 5 | TABLE ACCESS FULL | LJB_PARALLEL | 60761 | 10M| 56 (2)| 00 ---------------------------------------------------------------------------------------------------------------------- Note - dynamic sampling used for this statement 16 rows selected SQL> drop table ljb_parallel2; Table dropped SQL> create table ljb_parallel2 as select * from ljb_parallel; Table created 最后来看看DML更新语句用并行度的情况,这次却发现根本用不到并行度! SQL> explain plan for insert /*+parallel(a,4)*/ into ljb_parallel a select * from ljb_parallel2 ; Explained SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT --------------------------------------------------------------------------------------------------------------------- Plan hash value: 383256514 --------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time --------------------------------------------------------------------------------------------------------------------- | 0 | INSERT STATEMENT | | 58563 | 9M| 203 (3)| 00:00:0 | 1 | TABLE ACCESS FULL| LJB_PARALLEL2 | 58563 | 9M| 203 (3)| 00:00:0 --------------------------------------------------------------------------------------------------------------------- Note - dynamic sampling used for this statement 12 rows selected 究竟是什么原因呢?原来DML语句要用到并行度,就必须先做alter session enable parallel dml; 这样参数调整设置才可以生效! 执行命令如下: SQL> alter session enable parallel dml; alter session enable parallel dml 3 ORA-12841: 无法改变事务处理中的会话并行 DML 状态 大家是否觉得奇怪,为什么会提示这个错误,这里先卖个关子不说,在后面并行度的另外类型 的错误中会做描述,这里先做COMMIT,提交事务。 SQL> commit; Commit complete 这下会话可以允许更改了。 SQL> alter session enable parallel dml; Session altered 继续进行测试,看看现在并行DML能否生效。 SQL> explain plan for insert /*+parallel(a,4)*/ into ljb_parallel a select * from ljb_parallel2 ; Explained 查看如下,发现真的OK了,从执行计划中分明清楚的看到了利用到了并行度。 SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT ------------------------------------------------------------------------------------------------------------------- Plan hash value: 326818390 ------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| -------------------------------------------------------------------------------------------------------------------- | 0 | INSERT STATEMENT | | 58563 | 9M| 203 (3)| | 1 | PX COORDINATOR | | | | | | 2 | PX SEND QC (RANDOM) | :TQ10001 | 58563 | 9M| 203 (3)| | 3 | LOAD AS SELECT | LJB_PARALLEL | | | | | 4 | BUFFER SORT | | | | | | 5 | PX RECEIVE | | 58563 | 9M| 203 (3)| | 6 | PX SEND ROUND-ROBIN| :TQ10000 | 58563 | 9M| 203 (3)| | 7 | TABLE ACCESS FULL | LJB_PARALLEL2 | 58563 | 9M| 203 (3)| --------------------------------------------------------------------------------------------------------------------- Note - dynamic sampling used for this statement 18 rows selected 到这里大家应该看明白了,如果不注意到这点,写了脚本却根本没有达到自己想要的目的,却 又浑然不知,无论在计费系统还是结算系统中,我都有看到数据库中存在这样的并行DML语句,但 是由于没有改变会话属性,很遗憾,其实都没有生效! 另外还有一种情况,就是表本身设置了并行度,而不是用/*+parallel(a,4)*/之类 HINT,这种情 况和HINT是一样的:―DML之外的SQL语句可以直接用到并行度,但是DML如果没有加alter session enable parallel dml ,也是用不到并行度的‖,简要举例: 重新开一窗口 SQL> select table_name,degree from user_tables where table_name='LJB_PARALLEL'; TABLE_NAME DEGREE ------------------------------ --------------------------------- LJB_PARALLEL 1 SQL> alter table ljb_parallel parallel 4; Table altered 4 SQL> select table_name,degree from user_tables where table_name='LJB_PARALLEL'; TABLE_NAME DEGREE ------------------------------ -------------------------------- LJB_PARALLEL 4 SQL> explain plan for insert into ljb_parallel select * from ljb_parallel2; Explained 果然没有用到并行度 SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT ------------------------------------------------------------------------------------------------------------------- Plan hash value: 383256514 ------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time -------------------------------------------------------------------------------------------------------------------- | 0 | INSERT STATEMENT | | 58563 | 9M| 203 (3)| 00:00:0 | 1 | TABLE ACCESS FULL| LJB_PARALLEL2 | 58563 | 9M| 203 (3)| 00:00:0 --------------------------------------------------------------------------------------------------------------------- Note - dynamic sampling used for this statement 12 rows selected SQL> commit; Commit complete 对SESSION做了并行设置后如下 SQL> alter session enable parallel dml; Session altered OK,DML语句可以用到并行度了 SQL> explain plan for insert into ljb_parallel select * from ljb_parallel2; Explained SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT -------------------------------------------------------------------------------------------------------------------- Plan hash value: 326818390 -------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| -------------------------------------------------------------------------------------------------------------------- | 0 | INSERT STATEMENT | | 58563 | 9M| 203 (3)| | 1 | PX COORDINATOR | | | | | | 2 | PX SEND QC (RANDOM) | :TQ10001 | 58563 | 9M| 203 (3)| | 3 | LOAD AS SELECT | LJB_PARALLEL | | | | | 4 | BUFFER SORT | | | | | | 5 | PX RECEIVE | | 58563 | 9M| 203 (3)| | 6 | PX SEND ROUND-ROBIN| :TQ10000 | 58563 | 9M| 203 (3)| | 7 | TABLE ACCESS FULL | LJB_PARALLEL2 | 58563 | 9M| 203 (3)| ---------------------------------------------------------------------------------------------------------------------- 5 Note - dynamic sampling used for this statement 18 rows selected :大家要切记,在并行操作中默认情况并行查询和并行DDL操作是可以成功的,但是如果 你想让并行DML能生效,alter session enable parallel dml;的这步操作是必不可少的! 1.1.2 看到这个标题后,也许会有很多人发问,真会有这样的错误吗?是的,这样错误的提示一般是 ORA-12838: 无法在并行模式下修改之后读/修改对象 让通过一系列实验来模拟出这种错误: 当前表是有并行度的,并行度为4 SQL> select table_name,degree from user_tables where table_name='LJB_PARALLEL'; TABLE_NAME DEGREE ------------------------------ ----------------------------------- LJB_PARALLEL 4 向表中插入数据 SQL> insert into ljb_parallel select * from ljb_parallel2; 62644 rows inserted 不提交继续插入数据,问题马上出现了 SQL> insert into ljb_parallel select * from ljb_parallel2; insert into ljb_parallel select * from ljb_parallel2 ORA-12838: 无法在并行模式下修改之后读/修改对象 SQL> select count(*) from ljb_parallel; select count(*) from ljb_parallel ORA-12838: 无法在并行模式下修改之后读/修改对象 真的报错了,这个时候对ljb_parallel表无论是更新还是查询都做不了了,咋回事? SQL> commit; Commit complete 提交后发现可以查询了 SQL> select count(*) from ljb_parallel; COUNT(*) --------- 187932 提交后更新也可以了 SQL> insert into ljb_parallel select * from ljb_parallel2; 62644 rows inserted 通过实验可以看出,如果并行的表更新未提交,在此基础上做任何操作无论更新还是查询 都会失败,因此并行度的表其实还是有危险性的。从这里至少得出一个结论,如果应用是多表分别 插入带并行度的表,并且是有事务性,需要最后一起提交的,这个应用一定会失败!所以尽量避免 把表本身的属性设置为并行,这样的话会留下隐患,还是建议应用如果一定要用并行度,用HINT做 设置就好了,因此最好最后要做如下操作 SQL> alter table ljb_parallel noparallel; Table altered 6 另外如果表中只是索引有并行度,会存在这样的问题吗,实验如下,得出结论,只是表中索引有 并行度,并不存在表带并行度的问题,不提交继续操作是不会报错的。 SQL> create index idx_ljb_parallel on ljb_parallel(object_id); Index created SQL> select table_name,index_name,degree from user_indexes where table_name='LJB_PARALLEL'; TABLE_NAME INDEX_NAME DEGREE ------------------------------ ------------------------------ ----------------------------------------------------- LJB_PARALLEL IDX_LJB_PARALLEL 1 SQL> alter index IDX_LJB_PARALLEL parallel 4; Index altered SQL> select table_name,index_name,degree from user_indexes where table_name='LJB_PARALLEL'; TABLE_NAME INDEX_NAME DEGREE ------------------------------ ------------------------------------------------------------------------------------- LJB_PARALLEL IDX_LJB_PARALLEL 4 SQL> insert into ljb_parallel select * from ljb_parallel2; 62644 rows inserted SQL> insert into ljb_parallel select * from ljb_parallel2; 62644 rows inserted 曾有一次,我把结算系统互联互通指标报表的过程做了修改,修改处就是将其中的语句增加并行 度,想提高运行的效率,修改完后我发现前台操作界面好象真的快了许多。可是还没高兴多久,我 发现前台查询的结果都是空的。再去跟踪数据库执行日志才发现,该过程执行根本中间就出错了, 报ORA-12838。原来我这个过程也是对结果表进行多次插入后一起提交了,结果表被我增加并行度 后就不能继续操作未提交的数据,所以我的修改根本就是失败了! 这里说完了大家还记得我在前面的DML无法生效的时候说卖了个关子吗,希望看完本小节后, 再回头考虑考虑,应该能明白了。 高水平位插入的时候,也有类似的问题,比如使用append的时候,具体实验如下: SQL> insert /*+append*/ into ljb_test select * from ljb_test; 4 rows inserted SQL> select * from ljb_test; select * from ljb_test ORA-12838: 无法在并行模式下修改之后读/修改对象 1.1.3 这一小节我觉得特重要,很多人喜欢动不动在语句后面加上parallel n ,认为这样能用到多个CPU,很好,效率高,形成了一个习惯,我认为这个很不好!这里说两点。 1.1.3.1 机器的设备是有限的,如果人人写脚本都加并行,即便你机器有100多个CPU,也很快都被抢 完,你用并行度的语句占用了大量的CPU,别人就只能用剩下的少量的CPU,必然影响了别人的正常使用。在宁夏出差期间,一次我在结算主机上对用户数统计的过程增加了并行度的语句,因为有 一整个月的数据因某种原因漏处理了,为了赶上进度,我尝试选择并行方式,开始我跟踪一天话单 7 的处理速度,没加并行度的时候,50分钟处理完,加上并行度后,30分钟处理完,快了20分钟,很开心。到后来我发现事情不但没那么顺利,相反还糟透了。我想的太单纯了,当时生产恰好很安 静,后来后台处理程序开始处理采集送来的话单,我吃惊的发现,生产600条原始话单,处理了3个小时还差一大半没结束(原本大致20分钟),而用户数统计程序的速度已降至3小时还没处理完半天 记录 混凝土 养护记录下载土方回填监理旁站记录免费下载集备记录下载集备记录下载集备记录下载 的。查看数据库等待事件,发现都是并行的等待,再看OS层面,vmstat 查看的结果,有的队列高达50,远远超过了CPU的个数8个,系统已经慢的一塌糊涂了。尝试停掉用户数统计过 程,发现生产程序在10分种内很快处理完毕。我换个方案,停了生产处理程序,用户数统计程序在 30分钟内处理完毕一天话单!最后的解决方法呢,当然是把我在用户数统计程序中加上的并行代码 全部去掉,一切恢复正常。 以上是我在宁夏的一个很深刻的体会!并行度是个好东西,但是一定要规划使用,慎用! 1.1.3.2 有这样的情况吗?当然!大家是否有注意到前面我1.1章节中出现的并行度的执行计划,其中 有一个PX COORDINATOR,这叫并行协调管理。也就是说如果你写parallel 4 ,那你一般来说是用上3个CPU来工作,还有1个CPU是用来协调管理另外3个CPU。这个PX COORDINATOR可理解为发号施令者,矛盾协调者。 但是协调管理是需要开销的,当SQL语句本身非常快,比如可在0.0001秒执行完毕,加入这个协调管理后,由于协调开销,导致速度变为了 0.00015秒了(协调管理总会有一个最少开销时长),对于OLTP系统而言,如果该应用是一天查1亿次,那整个查询速度就增加了100000000*0.00005=5千秒,相当可观啊。 本小节中我没有做实验来证明什么,只是用文字说明了并行存在的坏处,也举了自 身经历的例子,希望能有助于大家对并行有更全面的认识,要有―大任务加并行放在系统闲时执行,本身很快的小任务无须考虑并行!‖的观点。好刀用在刀刃上! 1.1.4 涉及到并行操作会产生排序,排序是SQL语句中要特别注意的地方,并行会产生排序这点 可以做实验证明如下: SQL> select statistic#,name from v$statname where name like '%sort%'; STATISTIC# NAME ---------- ---------------------------------------------------------------- 341 sorts (memory) 342 sorts (disk) 343 sorts (rows) SQL> select statistic#,value from v$mystat where statistic# in (341,342,343); STATISTIC# VALUE ---------- -------------------------- 341 605 342 0 343 65410 SQL> drop table ljb_test; Table dropped 8 这里顺道可以看出drop表也能产生排序,从605到617 SQL> select statistic#,value from v$mystat where statistic# in (341,342,343); STATISTIC# VALUE ---------- --------------------------- 341 617 342 0 343 65410 SQL> create table ljb_test as select * from dba_objects; Table created 以下也顺道看出create table 不会产生排序 SQL> select statistic#,value from v$mystat where statistic# in (341,342,343); STATISTIC# VALUE ---------- ----------------------------- 341 617 342 0 343 65410 继续实验看看用并行度建表是什么情况 SQL> drop table ljb_test; Table dropped SQL> select statistic#,value from v$mystat where statistic# in (341,342,343); STATISTIC# VALUE ---------- -------------------------- 341 630 342 0 343 65410 SQL> create table ljb_test parallel 4 as select * from dba_objects; Table created 可以看出,刚才用普通模式建表没有产生排序,现在用并行模式,内存排序由630增至639 SQL> select statistic#,value from v$mystat where statistic# in (341,342,343); STATISTIC# VALUE ---------- ------------------------ 341 639 342 0 343 128118 接着继续实验如下 SQL> select statistic#,value from v$mystat where statistic# in (341,342,343); STATISTIC# VALUE ---------- ------------------------- 341 672 342 0 343 190825 SQL> insert into ljb_test select * from ljb_test; 62684 rows inserted 以下可以看出普通插入操作排序由672增加到673,增加值为1 9 SQL> select statistic#,value from v$mystat where statistic# in (341,342,343); STATISTIC# VALUE ---------- --------------------------- 341 673 342 0 343 190825 继续观察并行插操作的内存使用情况 SQL> select statistic#,value from v$mystat where statistic# in (341,342,343); STATISTIC# VALUE ---------- --------------------------- 341 685 342 0 343 190825 SQL> insert into ljb_test select /*+parallel(a,4)*/ * from ljb_test a; 62685 rows inserted 发现内存排序从685增加到687,增加值为2 SQL> select statistic#,value from v$mystat where statistic# in (341,342,343); STATISTIC# VALUE ---------- ---------------------------- 341 687 342 0 343 190848 接着实验让DML并行生效的情况 SQL> select statistic#,value from v$mystat where statistic# in (341,342,343); STATISTIC# VALUE ---------- ----------------------------- 341 699 342 0 343 190848 SQL> alter session enable parallel dml; Session altered SQL> insert /*+parallel(b,4)*/ into ljb_test b select /*+parallel(a,4)*/ * from ljb_test a; 62686 rows inserted 发现内存排序由699增加到702,增加到3,又多增加一个排序! SQL> select statistic#,value from v$mystat where statistic# in (341,342,343); STATISTIC# VALUE ---------- ------------------------- 341 702 342 0 343 190894 下一小节准备描述索引,这里提前研究一下索引,如果并行插入的表是带索引的,排序会是什 么情况呢? 以下因为当时数据库重启过,所以值并不连贯。 SQL> select statistic#,value from v$mystat where statistic# in (341,342,343); 10 STATISTIC# VALUE ---------- ---------------------------------- 341 339 342 0 343 63914 SQL> insert into ljb_test select /*+parallel(a,4)*/ * from ljb_test a; 62720 rows inserted SQL> select statistic#,value from v$mystat where statistic# in (341,342,343); STATISTIC# VALUE ---------- ------------------------------- 341 346 342 0 343 63948 发现内存排序由339增加到346,增加到7,这下排序数量! 并行语句无论是DDL还是DML还是查询并行,三个方式都会用到排序,如果用户PGA 设置不够大,sort_area_size区不足,或者是用户数据量过于庞大,或者说执行并行操作的次数过多, 完全可能会导致大的排序动作,有可能出现大量排序在临时表空间中进行,速度越来越慢,甚至出 现临时表空间不够的错误!最后还发现,如果并行插入的时候,需要维护索引,那排序数量又会比 无索引的情况大大增加许多!结算组的兄弟就遇到过这样的问题,执行用户数统计脚本的时候最后 居然提示临时表空间不够,该表也是存在索引的,后续去掉并行问题就解决,真正的原理,就在我 上面的描述中。 这里特别举了并行操作导致排序主要是因为正好最近遇到过这样的一个案例,大 家是否有考虑过到底有那些操作有可能会导致发生较大排序呢,在此我举出下面几种情况,希望大 家操要特别留意以下操作和简单诊断方法 1、需要进行排序的操作: A、创建索引及重建索引; B、涉及到索引维护的并行插入 C、order by或者group by(尽可能对索引字段排序) D、Distinct E、union all/intersect/minus F、sort-merge join G、analyze命令(仅可能使用estamate而不是compute) 2、诊断及措施 Select * from v$sysstat where name like ?%sort%‘; Sort(disk):要求Io去临时表空间的排序数目 Sort(memory):完全在memory中完成的排序数目 Sort(rows):被排序的行数合计 Sort(disk)/ Sort(memory)<5%,如果超过5%,增加sort_area_size的值。 SELECT disk.Value disk,mem.Value mem,(disk.Value/mem.Value)*100 ratio FROM v$sysstat disk,v$sysstat mem WHERE mem.NAME=‘sorts (memory)‘ AND disk.NAME=‘sorts (disk)‘; 1.2 数据库中索引是一个非常重要的概念,起着非常重要的作用,但是如果没有经验和足够的知识, 11 有时就会用不上索引,导致系统运行缓慢,什么样的错误导致索引用不上呢,一起研究如下: 1.2.1 1.2.1.1 具体举例来说明 SQL> create table test as select * from zhjs_app.tg_log where rownum<=500; Table created SQL> create index IDX_TG_LOG_BATCH_CODE on test (SUBSTR(BATCH_CODE,6,6)); Index created 表和函数索引索引建好后,看看是否能用到索引 SQL> explain plan for select count(*) from test where SUBSTR(BATCH_CODE,6,6)='010102'; Explained 查询发现始终无法用到该索引 SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT ---------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost | ---------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | | | 1 | SORT AGGREGATE | | | | | |* 2 | TABLE ACCESS FULL | TEST | | | | ---------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): ------------------------------------------------------ 2 - filter(SUBSTR("TEST"."BATCH_CODE",6,6)='010102') Note: rule based optimization 15 rows selected 究竟怎么回事?这里我要重点说明一下,函数索引要满足两个条件,一个是查询要基于规则 COST的,才能生效,第二个是query_rewrite_enabled 这个参数要是true,第二个条件到9i以后并不怎 么必要了,关键是第一点。 试着给这个查询加上一个索引的HINT,看系统能走索引吗? SQL> explain plan for select /*+index(a,IDX_TG_LOG_BATCH_CODE)*/ count(*) from test a where SUBSTR(BATCH_CODE,6,6)='010102'; Explained 查询真的走了索引 SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT -------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost | 0 | SELECT STATEMENT | | 1 | 17 | 1 | | 1 | SORT AGGREGATE | | 1 | 17 | | 12 |* 2 | INDEX RANGE SCAN | IDX_TG_LOG_BATCH_CODE | 12 | 204 | 1 | -------------------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access(SUBSTR("A"."BATCH_CODE",6,6)='010102') Note: cpu costing is off 15 rows selected 为什么加上HINT后能建索引吗,这里涉及到了解HINT的一个常识,ORACLE的所有HINT中, 除了/*+RULE*/外,加了HINT,ORACLE都会选择基于代价的查询,也就是COST的查询方式,所以 满足了我刚才提的条件,所以索引就用上了。 当然如果不用HINT,可以有另外一个选择,就是将表分析后,让ORACLE优化器自动选择走 COST而不是走基于RULE的优化器模式,这样,也满足了我刚才说的条件,具体操作如下: SQL> analyze table test compute statistics for table for all indexes for all indexed columns; Table analyzed SQL> explain plan for select count(*) from test a where SUBSTR(BATCH_CODE,6,6)='010102'; Explained SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT -------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost | -------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 17 | 2 | | 1 | SORT AGGREGATE | | 1 | 17 | | |* 2 | INDEX RANGE SCAN | IDX_TG_LOG_BATCH_CODE | 418 | 7106 | 2 | -------------------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access(SUBSTR("A"."BATCH_CODE",6,6)='010102') Note: cpu costing is off 15 rows selected 果然,该查询也走索引,达到目的! 函数索引必须要在基于代价的优化器模式下才可以运行,因此想利用到函数索引,必须 要对表进行分析或者是建立HINT强制让ORACLE走COST模式。我们结算项目就有不少地方用到函 数索引,希望引起大家的注意! 1.2.1.2 现构造ljb_test表内容如下 SQL> create table ljb_test(year varchar2(4)); Table created SQL> insert into ljb_test values ('2009'); 1 row inserted SQL> insert into ljb_test values ('2008'); 1 row inserted 13 SQL> commit; Commit complete SQL> select to_date(year,'yyyy') from ljb_test; TO_DATE(YEAR,'YYYY') -------------------------------- 2009-03-01 2008-03-01 如果要让这个to_date(year,'yyyy')建立索引该怎么做了,很多人可能会立即回答,建函数索引! 那实验一下,看建函数索引会是一种什么情况 SQL> create index idx_ljb_test on ljb_test(to_date(year,'yyyy')); ORA-01743: 仅能编制纯函数的索引 失败了!这个提示是否让摸不着头脑! 真正原因是如上查询是在三月份查的结果,如果是四月份,查询出的结果将会如下 SQL> select to_date(year,'yyyy') from ljb_test; TO_DATE(YEAR,'YYYY') ---------------------------------- 2009-04-01 2008-04-01 这点大家应该不难明白吧,该函数充满了不确定性!这就是函数索引无法创建的原因,函数索 引是预先在索引中存储了函数索引的结果以备使用,现在这个函数值不确定了,怎么存储函数结果? 通过本小节的学习,大家应该能加深了对函数索引的认识吧, 因为不确定结果根本不能被函数索引预存储,所以要失败!我就曾经要建立一个这样不确定函数的函数索引而失败,最后选择了其它方式! 1.2.1.3 ORA-30553 我们平时建的函数索引一般都是对ORACLE的自带函数做函数索引,如upper()等等,但是如果 我们要进行基于自定义函数的索引的建立的时候,必须使用DETERMINISTIC关键字,否则会报 ORA-30553错误,这点要引起大家的注意。 Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb SQL> SQL> drop table ljb_test; Table dropped SQL> create table ljb_test as select * from user_objects ; Table created SQL> create or replace function f_minus1(i int) 2 return int 3 is 4 begin 5 return(i-1); 6 end; 7 / Function created 14 建完函数后我们试着建立函数索引,发现建立失败 SQL> create index idx_ljb_test on ljb_test (f_minus1(object_id)); create index idx_ljb_test on ljb_test (f_minus1(object_id)) ORA-30553: 函数不能确定 将函数加上DETERMINISTIC关键字重建 SQL> create or replace function f_minus1(i int) 2 return int DETERMINISTIC 3 is 4 begin 5 return(i-1); 6 end; 7 / Function created 现在发现加上DETERMINISTIC关键字后的自定义函数可以建立函数索引成功了! SQL> create index idx_ljb_test on ljb_test (f_minus1(object_id)); Index created SQL> analyze table ljb_test compute statistics for table for all indexes; Table analyzed SQL> explain plan for select * from ljb_test where f_minus1(object_id)=23; Explained 验证得出该函数索引可以在语句中被正常的使用到。 SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT -------------------------------------------------------------------------------- Plan hash value: 3905012767 -------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU) -------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 3 | 273 | 2 (0) | 1 | TABLE ACCESS BY INDEX ROWID| LJB_TEST | 3 | 273 | 2 (0) |* 2 | INDEX RANGE SCAN | IDX_LJB_TEST | 1 | | 1 (0) -------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("LJB"."F_MINUS1"("OBJECT_ID")=23) 14 rows selected :大家在工作中用到自定义函数的时候也很多,因为自定义函数可以直接在SQL中调用, 简化代码,给编写数据库应用带来了很大的方便,但是与此同时我们要考虑SQL中用到的自定义 函数是否能用的上索引,我们开发人员除了关注实现外也要同时考虑到性能,因此我们在建自定 义函数的时候尽量考虑加上DETERMINISTIC的关键字,以方便将来用上函数索引。 :写完该小节,大家明白了使用自定义函数一个很关键的一点,就是如果要用到 自定义函数的函数索引,必须要有DETERMINISTIC的关键字,不过这个DETERMINISTIC到底是什么 15 东西啊,为什么自定义函数的函数索引能否建成功完全依赖这个关键字,这么神奇!原来对于指定 了DETERMINISTIC的函数,在一次调用中,对于相同的输入,只进行一次调用。这要求函数的创建 者来保证DETERMINISTIC的正确性,如果这个函数的返回值和输入参数没有确定性关系,会导致函 数结果异常的。这里我举个例子,大家体会一下 Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb 建立一个带DETERMINISTIC关键字的函数,功能就是返回值为1 SQL> CREATE OR REPLACE FUNCTION F_DETERMINISTIC RETURN NUMBER DETERMINISTIC 2 AS 3 BEGIN 4 DBMS_LOCK.SLEEP(0.1); 5 RETURN 1; 6 END; 7 / Function created SQL> set timing on 发现执行时间非常快,0.765秒完成 SQL> SELECT F_DETERMINISTIC FROM user_tables; F_DETERMINISTIC --------------- 1 1 这里略去另外73个记录(值都为1) 75 rows selected Executed in 0.765 seconds 用不带关键字DETERMINISTIC的方式建立函数,功能也是返回值为1 SQL> CREATE OR REPLACE FUNCTION F_DETERMINISTIC RETURN NUMBER 2 AS 3 BEGIN 4 DBMS_LOCK.SLEEP(0.1); 5 RETURN 1; 6 END; 7 / Function created Executed in 0.047 seconds 发现执行速度慢了好多,8.469秒 SQL> SELECT F_DETERMINISTIC FROM user_tables; F_DETERMINISTIC --------------- 1 1 这里也略去另外73个记录(值都为1) 75 rows selected Executed in 8.469 seconds 16 其实速度相差如此之大的原因是什么呢?那是因为带DETERMINISTIC的函数对于相同的输入只会运算一次,认为值是一样的,就把上次算出来的结果直接引用了,所以为什么第一种会执行速度这 么快,因为第一种情况下根本该自定义函数就只调用了1次,然后另外那74次结果都是直接考虑到输入相同,直接把第1次调用的结果拿来用而已。而第二种其实是调用了75次。我这里故意举了无输入值函数方式来做实验因为这样比较特殊,没有输入就是表示输入相同值的含义!(有输入的情况 我也实验过了,一样!)为什么ORACLE要对自定义函数做这个DETERMINISTIC限制呢?我思考 了一下,应该是有这两个原因吧:1、可避免我们建立一个不确定输出值的函数!比如我上一小节的 函数索引建立失败(不过那不是建立自定义函数,是ORACLE自带函数失败),就和相同输入返回不同结果有关系。2、SQL中写函数函数很容易出现性能问题,比如你写个不好的函数,又被大量调 用,那估计执行起来麻烦要大了,ORACLE这样做了,可以让写自定义函数的人在调用的时候一次 执行多次使用结果,速度也可以快很多。这两点是我猜测,有机会找ORACLE公司的人确认一下。 1.2.2 count(*) 正常情况下,count(*)效率最高时一定是用到索引,因为count(*)并不返回任何一个列,只需返回一个总记录数即可,这种情况由于无需回表,所以索引可看成一个瘦表,这样同样扫描全部,全 扫描小的多的索引块能回答记录数,当然效率更高。 生产中,不少应用是有加count(*)代码,要对此引起重视,测试如下: Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb SQL> drop table test; Table dropped 建测试表和索引 SQL> create table test as select * from dba_objects where object_id is not null; Table created SQL> create index idx_test on test (object_id); Index created SQL> analyze table test compute statistics for table for all indexes for all indexed columns; Table analyzed SQL> explain plan for select count(*) from test ; Explained 发现该查询根本没有用到索引 SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT -------------------------------------------------------------------------------------------------- Plan hash value: 1950795681 -------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Cost (%CPU)| Time | -------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 201 (2)| 00:00:03 | | 1 | SORT AGGREGATE | | 1 | | | | 2 | TABLE ACCESS FULL| TEST | 62640 | 201 (2)| 00:00:03 | -------------------------------------------------------------------------------------------------- 17 9 rows selected 那如果加上提示,让系统强制使用索引可以吗? SQL> explain plan for select /*+index(a,idx_test)*/ count(*) from test a ; Explained 发现加上hint后,根本起不了作用,系统仍然走全表扫描 SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT --------------------------------------------------------------------------------------------------- Plan hash value: 1950795681 --------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Cost (%CPU)| Time | --------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 201 (2)| 00:00:03 | | 1 | SORT AGGREGATE | | 1 | | | | 2 | TABLE ACCESS FULL| TEST | 62640 | 201 (2)| 00:00:03 | --------------------------------------------------------------------------------------------------- 9 rows selected 问题出在哪呢?原来真正的原因是索引不存储空值,这是非常重要的知识点!如果数据库记录 中索引列有空值,那索引就不会存储空的那个列的信息,这样,如果想count(*)从索引这个―瘦表‖回 答记录数问题就可能出错,因为有可能统计少了,oracle当然无论如何都不会走索引,即便你加上hint 强制执行,它都不答应,它不能冒统计错误的风险。 那怎么办呢,那试着给object_id 加上is not null,明确告诉ORACLE该索引列值不会为空,看看 能否走索引 SQL> explain plan for select count(*) from test where object_id is not null ; Explained 这下真走索引了! SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT ------------------------------------------------------------------------------------------------------------- Plan hash value: 1190062564 ------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 4 | 14 (0)| 00:00:01 | | 1 | SORT AGGREGATE | | 1 | 4 | | | |* 2 | INDEX FULL SCAN| IDX_TEST | 62640 | 244K| 14 (0)| 00:00:01 | ------------------------------------------------------------------------------------------------------------ Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter("OBJECT_ID" IS NOT NULL) 14 rows selected 当然,在代码中加where object_id is not null是一个方法,最好的方法就是,如果该列确实不允许为空,就把它的索引改为主键,主键列不允许为空,这点ORACLE它明白! SQL> drop index idx_test; 18 Index dropped SQL> alter table test add constraint test_pk primary key (object_id); Table altered SQL> analyze table test compute statistics for table for all indexes for all indexed columns; Table analyzed SQL> explain plan for select count(*) from test ; Explained 再看加上主键后的表的执行计划,select count(*) from test 也走索引了 SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT --------------------------------------------------------------------------------------------------- Plan hash value: 3830891886 --------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Cost (%CPU)| Time | --------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 13 (0)| 00:00:01 | | 1 | SORT AGGREGATE | | 1 | | | | 2 | INDEX FULL SCAN| TEST_PK | 62640 | 13 (0)| 00:00:01 | --------------------------------------------------------------------------------------------------- 9 rows selected 通过此例可以看出,应用中如果某列不允许为空,并且应用中有大量对该表count(*)的情况,最好选择主键,或者指定该列is not null,这样就可以让count(*)用上索引,提高效率。 1.2.3max()min() 前面说完了索引与count(*)的关系,现在来讨论另外一种聚合查询max(),min()与索引的关系,大家觉得这个聚合查询能用的到索引吗? 通过上一小节的学习后,可能有人会回答:―可以用得上,但是索引列必须要建主键或者要写 where column is not null就可以用到了。‖对于这样的回答应该值得肯定,非常正确!看来前面没白讲 了。不过用上了什么样的索引扫描方式呢?上一小节的方式是INDEX FULL SCAN,大家一定有印象,现在如果是要让max()和min()利用上索引,也是走这个INDEX FULL SCAN扫描方式吗? 大家想一想索引的结构是什么样的?索引结构是从root到branch最后到leaf,好象一个金字塔。最下面的叶子层(也就是金字塔的底部)其实是有序的,比如从左到右值是从小到大,或者从大到小。 这样一来大家认为取max()或者 min()还需要INDEX FULL SCAN吗,找到头或尾不就找到最大或最小值,还需要遍历leaf吗? 于是ORACLE的另一种索引扫描类型就横空出世了index full scan(max/min)。多了(max/min)的关键字!index full scan(max/min)蕴含着stopkey的机制,从最左边或者最右边的叶子节点开始扫描,读到第一个值后就停止扫描。 查看max()的查询,发现果然是走 INDEX FULL SCAN (MIN/MAX) SQL> explain plan for select max(object_id) from ljb_test where object_id is not null; Explained SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT ---------------------------------------------------------------------------------------------------------------------- 19 Plan hash value: 613051030 ---------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU) ---------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 13 | 2 (0) | 1 | SORT AGGREGATE | | 1 | 13 | | 2 | FIRST ROW | | 49190 | 624K| 2 (0) |* 3 | INDEX FULL SCAN (MIN/MAX)| IDX_LJB_TEST | 49190 | 624K| 2 (0) ---------------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 3 - filter("OBJECT_ID" IS NOT NULL) Note - dynamic sampling used for this statement 19 rows selected 查看min()的查询,发现也走了INDEX FULL SCAN (MIN/MAX) SQL> explain plan for select min(object_id) from ljb_test where object_id is not null; Explained SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT ----------------------------------------------------------------------------------------------------------------------- Plan hash value: 613051030 ----------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU) ----------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 13 | 2 (0) | 1 | SORT AGGREGATE | | 1 | 13 | | 2 | FIRST ROW | | 49190 | 624K| 2 (0) |* 3 | INDEX FULL SCAN (MIN/MAX)| IDX_LJB_TEST | 49190 | 624K| 2 (0) ----------------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 3 - filter("OBJECT_ID" IS NOT NULL) Note - dynamic sampling used for this statement 19 rows selected 到此大家应该完全明白了max()和min()的时候,执行计划中会走INDEX FULL SCAN (MIN/MAX) 的原因了吧,在获取正确的信息后,ORACLE对此类查询自然就会选择这样的扫描方式,希望大家 能理解其中选择这样方式扫描的原理!也许有人说,知道这个也没用,ORACLE自己就会选怎么走 索引吧,这个NDEX FULL SCAN (MIN/MAX)的知识点知道也没意义。其实我认为,多理解点东西 总是有用的,尤其是原理性方面,比如我现在再问这样一个问题:select min(object_id),max(object_id) from ljb_test where object_id is not null 这个语句ORACLE怎么处理?大家怎么回答? 让我实验一下吧(很多人猜还是INDEX FULL SCAN (MIN/MAX)): 下面执行结果出来了,走的索引扫描类型是INDEX FULL SCAN,看不到(MIN/MAX)的关键字, 20 咋回事? SQL> explain plan for select min(object_id),max(object_id) from ljb_test where object_id is not null; Explained SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT ------------------------------------------------------------------------------------------------------------------- Plan hash value: 1341606234 ------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time ------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 13 | 61 (4)| 00:0 | 1 | SORT AGGREGATE | | 1 | 13 | | |* 2 | INDEX FAST FULL SCAN| IDX_LJB_TEST | 49190 | 624K| 61 (4)| 00:0 ------------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter("OBJECT_ID" IS NOT NULL) Note ----- - dynamic sampling used for this statement 18 rows selected 原来这样的SQL语句是表示ORACLE要利用该索引同时取到这两个值,INDEX FULL SCAN (MIN/MAX)是无法一次取到两个值的,所以ORACLE不得不选择了INDEX FULL SCAN ,把叶子的 索引扫了个遍,同时取到了两个值。 明白了原理,处理起问题就简单了,改写代码如下: SQL> explain plan for 2 select max, min 3 from (select max(object_id) max from ljb_test where object_id is not null) a, 4 (select min(object_id) min from ljb_test where object_id is not null) b; 已解释。 现在终于走了INDEX FULL SCAN (MIN/MAX)索引了,大家看到这个INDEX FULL SCAN (MIN/MAX)威力还是非常大的,走了两次INDEX FULL SCAN (MIN/MAX),居然代价才4,远远低 于一次INDEX FULL SCAN的代价61 SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT ----------------------------------------------------------------------------------------------------------------------------------------- Plan hash value: 3189180828 ----------------------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ----------------------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 26 | 4 (0)| 00:00:01 | | 1 | NESTED LOOPS | | 1 | 26 | 4 (0)| 00:00:01 | 21 | 2 | VIEW | | 1 | 13 | 2 (0)| 00:00:01 | | 3 | SORT AGGREGATE | | 1 | 13 | | | | 4 | FIRST ROW | | 49190 | 624K| 2 (0)| 00:00:01 | |* 5 | INDEX FULL SCAN (MIN/MAX)| IDX_LJB_TEST | 49190 | 624K| 2 (0)| 00:00:01 | | 6 | VIEW | | 1 | 13 | 2 (0)| 00:00:01 | | 7 | SORT AGGREGATE | | 1 | 13 | | | | 8 | FIRST ROW | | 49190 | 624K| 2 (0)| 00:00:01 | |* 9 | INDEX FULL SCAN (MIN/MAX)| IDX_LJB_TEST | 49190 | 624K| 2 (0)| 00:00:01 | ----------------------------------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 5 - filter("OBJECT_ID" IS NOT NULL) 9 - filter("OBJECT_ID" IS NOT NULL) PLAN_TABLE_OUTPUT ---------------------------------------------------------------------------------- Note - dynamic sampling used for this statement :max() 和 min() 是大家常用的使用频率很高的sql写法,计费项目各种报表中需要这样 编写的地方比比皆是!希望大家能对这样的查询建立索引,在保证该列不空的情况下,就有可能利 用到INDEX FULL SCAN (MIN/MAX)这个索引扫描方式,能为查询性能带来很大的提高,另外只要 善于思考,还可以通过改写SQL的方式,将原本利用不到INDEX FULL SCAN (MIN/MAX)查询方式的语句select min(object_id),max(object_id) from ljb_test where object_id is not null利用上INDEX FULL SCAN (MIN/MAX)。希望这个能启发开发人员多利用现有的SQL知识,编写出高效的SQL语句。 :大家记得前面我有提到index full scan(max/min)蕴含着stopkey的机制,有优化基础的朋友一定认识这个stopkey,经常在分页查询的执行计划中,看到有这样的关键字,基本上可以认 为这个查询的执行计划是正确的。 比如select * from (select * from table where id= order by name desc) where rownum<11;这样的语句具体的意思就是id为某个值的时候,根据name做排序,然后取前10行.这个语句存在2个部分:id为某个值,name降序。假设我现在存在这一个索引(id,name desc)这个索引的结构也是id相同的情况下按照name的降序排列,这个索引同时满足前面的两个条件,因此就能提高速度,只要从索引中读取出10个rowid,然后根据这10个rowid来回表,这时候速度肯定很快的,因此类似这类的分页语句可以根据sql语句的原意来创建索引,就能提高速度,但是如果where条件里出现非等于的条件,那么不管怎么创建索引都无法满足前面的2个条件(根据索引的结构就很容易明白这点),就必须根据字段的选择性来创建 合适的索引. 1.2.4 这个观点很多人都这么认同,看执行计划,发现没走索引,就觉得出问题了,其实这个认识是 错的,基于代价的CBO是这样认为的:―不管是索引还是全表扫描,哪种方式代价低,就胜出,衡 量出全表扫描代价比索引低,就会放弃使用索引‖。这个话题如果展开,内容将非常多,这里就不详 细描述了,现仅构造一例来证明自己的观点 SQL> drop table ljb_test; Table dropped 22 SQL> create table ljb_test as select * from scott.emp; Table created SQL> insert into ljb_test select * from ljb_test; 14 rows inserted SQL> / 28 rows inserted 重复多次插表,让表记录大些,好更接近生产真实环境。 SQL> select count(*) from ljb_test; COUNT(*) ------------------ 458752 SQL> create index idx_ljb_test on ljb_test(empno); Index created 下面将该表的索引列empno值全部设为88,目的是数据库如利用该索引列查询将返回全部值, 这个索引就成为一个选择性很差的索引了。原理是:索引要有一次INDEX RANGE SCAN,而通过 索引再来找其他列的信息,需要回表,要再来一次TABLE ACCESS BY INDEX ROWID,这里存在 了两个动作,就很有可能比不上直接TABLE ACCESS FULL全表扫描一种动作了。 SQL> update ljb_test set empno=88 ; 458752 rows updated SQL> commit; Commit complete SQL> analyze table ljb_test compute statistics for table for all indexes for all indexed columns; Table analyzed SQL> explain plan for select * from ljb_test where empno=88; Explained 查看执行计划,发现果真是走全表扫描,ORACLE认为全表扫代价更低,放弃了索引。 SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT -------------------------------------------------------------------------------------------------------------- Plan hash value: 2595830654 -------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | -------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 455K| 17M| 517 (6)| 00:00:07 | |* 1 | TABLE ACCESS FULL| LJB_TEST | 455K| 17M| 517 (6)| 00:00:07 | ---------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter("EMPNO"=88) 13 rows selected 如果你强制使用索引,也能走索引,但是结果会怎样呢? SQL> explain plan for select /*+index(a,idx_ljb_test)*/ * from ljb_test a where empno=88; Explained 可以看出来,,索引是走了,但是开销却更大了!目的不是一定要让系统走索引, 23 而是让系统更快! SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT ------------------------------------------------------------------------------------------------------------------------ Plan hash value: 3905012767 ------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU) --------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 455K| 17M| 4080 (2) | 1 | TABLE ACCESS BY INDEX ROWID| LJB_TEST | 455K| 17M| 4080 (2) |* 2 | INDEX RANGE SCAN | IDX_LJB_TEST | 455K| | 1420 (2) ------------------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("EMPNO"=88) 14 rows selected 索引并不总是比全表扫描来的快的,最好的方式是让ORACLE正确收集到所有信息,由 自己计算出来的COST的大小来决定选择索引还是全表扫描,代价更小的就会胜出! 1.2.5 数据库的 设计 领导形象设计圆作业设计ao工艺污水处理厂设计附属工程施工组织设计清扫机器人结构设计 是很灵活多变的,要有跳出常规思维的想法。结算组有个兄弟问我:―能不能只对 某个字段的某些特定的值建索引?‖问这话值得表扬,因为他会思考,认为这样的索引要比普通索引―个头‖来的小一点,所以会更块! 确实有只对列的某些值建索引的方法,测试如下: Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb SQL> drop table ljb_test; Table dropped SQL> create table ljb_test (id int ,status varchar2(2)); Table created 这里表示只对status列的值为N的情况建索引 SQL> create index id_status on ljb_test (Case when status= 'N' then 'N' end); Index created 插入很多为Y的值 SQL> insert into ljb_test select rownum ,'Y' from dual connect by rownum<=100000; 100000 rows inserted 插入单条值为N的记录 SQL> insert into ljb_test select 1 ,'N' from dual; 1 row inserted SQL> commit; Commit complete 24 分析的目的是为了让查询在基于COST模式下,因为只为字段的部分值建索引本质是, 要在COST模式下才生效。 SQL> analyze table ljb_test compute statistics for table for all indexes for all indexed columns; Table analyzed 以下这个select * from ljb_test where (case when status='N' then 'N' end)='N‘写法不能变,如果是 select * from ljb_test where status='N'将无效!我见过有些人设置了选择性索引,却这样调用的,结果根 本起不到任何效果! SQL> explain plan for select * from ljb_test where (case when status='N' then 'N' end)='N'; Explained 可以看出来代价还是比较低的,效果不错! SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT ---------------------------------------------------------------------------------------------------------------------- Plan hash value: 3317130416 ---------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| T ---------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 10 | 2 (0)| 0 | 1 | TABLE ACCESS BY INDEX ROWID| ljb_test | 1 | 10 | 2 (0)| 0 |* 2 | INDEX RANGE SCAN | ID_STATUS | 1 | | 1 (0)| 0 ---------------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access(CASE "STATUS" WHEN 'N' THEN 'N' END ='N') 14 rows selected 接着删除掉选择性索引,建普通索引比较一下效果 SQL> drop index id_status; Index dropped SQL> create index id_normal on ljb_test(status); Index created SQL> explain plan for select * from ljb_test where status='N'; Explained 发现同样是走了索引,但是代价却高出许多! SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT ------------------------------------------------------------------------------------------------------------------------ Plan hash value: 2648582318 ------------------------------------------------------------------------------------------------------------------------ | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| T ------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1000 | 10000 | 40 (0)| 0 | 1 | TABLE ACCESS BY INDEX ROWID| ljb_test | 1000 | 10000 | 40 (0)| 0 |* 2 | INDEX RANGE SCAN | ID_NORMAL | 400 | | 91 (0)| 0 -------------------------------------------------------------------------------------------------------------------------- 25 Predicate Information (identified by operation id): ------------------------------------------------------------ 2 - access("STATUS"='N') 14 rows selected 不难看两者效率差别巨大,所以在你知道某些字段值的分布,并确定某些字段的特定值比较少,而你的应用却经常查询这些特定的少量值的时候,可以用这个方法, 于是选择性索引就闪亮登场,有了大展身手的机会了! 介绍完了选择性索引,大家能回答为什么更快吗?很多人会说:―这简单,选择性索引只选择少的特定值做索引,那索引块就很小,搜索起来更快!‖回答正确!不过你能回答索引块变小是多小,和普通索引差距有多少?能量化为具体值吗?下面我来回答这个问题。 以下是选择性索引id_status建好后,索引的情况 SQL> analyze index id_status validate structure; Index analyzed SQL> select name,btree_space,lf_rows,height from index_stats; NAME BTREE_SPACE LF_ROWS HEIGHT ------------------------------ ----------- ---------- ------------------------------------------------------ ID_STATUS 8000 1 1 接着是看id_normal,普通索引的情况 SQL> select name,btree_space,lf_rows,height from index_stats; NAME BTREE_SPACE LF_ROWS HEIGHT ------------------------------ ----------- ---------- ------------------------------------------------------- ID_NORMAL 1464032 100001 2 很明显,选择索引的大小为8K,而普通索引的大小为1464K;选择索引的高度为1,而 普通索引的的高度为2。差异相当显著!通过本小节应该学会巧妙的将选择性索引应用到的工作中 去,会带来意想不到效果! 1.2.6 我提这个话题主要是为了说明一个排序的道理。很多人可能忽略了sql语句中排序这块,其实很 多时刻,避免了排序可以大大提升SQL语句的性能,这是非常重要的一点!现在我来举例说明降序 索引避免排序: SQL*Plus: Release 10.2.0.1.0 - Production on 星期六 3月 14 14:26:15 2009 Copyright (c) 1982, 2005, Oracle. All rights reserved. 连接到: Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 - Production With the Partitioning, OLAP and Data Mining options SQL> drop table ljb_test; 表已删除。 SQL> create table ljb_Test as select * from dba_objects; 表已创建。 SQL> create index idx_ljb_test on ljb_test (owner,object_id); 索引已创建。 SQL> set autot trace exp 26 设置该列为非空的目的是因为索引不存空值 SQL> alter table ljb_Test modify owner not null; 表已更改。 SQL> set linesize 1000 通过HINT让以下查询走索引,发现有个SORT ORDER BY排序的过程,正常情况下用到索引的 语句不要排序,因为索引已经排序过了,但是为什么如下语句还要再排序呢,因为目前的查询是该 索引既要从owner列往前读又要从object_type往后读,此是ORACLE只能把所有行数据收集起来, 然后排序实现这样了。 SQL> select /*+index(a,idx_ljb_test)*/ * from ljb_test a order by owner desc ,object_type asc; 执行计划 ------------------------------------------------------------------------------------------------------------------------------------------------- Plan hash value: 1087399357 ------------------------------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time | ------------------------------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 70371 | 11M| | 4812 (1)| 00:00:58 | | 1 | SORT ORDER BY | | 70371 | 11M| 28M| 4812 (1)| 00:00:58 | | 2 | TABLE ACCESS BY INDEX ROWID| LJB_TEST | 70371 | 11M| | 2070 (1)| 00:00:25 | | 3 | INDEX FULL SCAN | IDX_LJB_TEST | 70371 | | | 197 (2)| 00:00:03 | ------------------------------------------------------------------------------------------------------------------------------------------------- Note ----- - dynamic sampling used for this statement 那如果使用了如下(owner desc,object_type asc ) desc降序索引后,这个时候索引的存储方式比 起原先的存储方式改变了!是以owner照升序存储,以object_type按降序存储,这样查询的时候, owner从索引存储的一边从大读到小,object_type则是从索引的另一边从小读到大! SQL> drop index idx_ljb_test; 索引已删除。 SQL> create index idx_ljb_test on ljb_test(owner desc,object_type asc); 索引已创建。 现在执行如下,大家可以看到,排序操作消失了! SQL> select /*+index(a,idx_ljb_test)*/ * from ljb_test a order by owner desc ,object_type asc; 执行计划 ------------------------------------------------------------------------------------------------------------------------------------ Plan hash value: 2893417570 --------------------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | --------------------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 70371 | 11M| 2986 (1)| 00:00:36 | | 1 | TABLE ACCESS BY INDEX ROWID| LJB_TEST | 70371 | 11M| 2986 (1)| 00:00:36 | | 2 | INDEX FULL SCAN | IDX_LJB_TEST | 70371 | | 243 (1)| 00:00:03 | --------------------------------------------------------------------------------------------------------------------------------------- Note - dynamic sampling used for this statement 27 此外本小节的试验我做了大半天才成功,为什么,因为我最初是select /*+index(a,idx_ljb_test)*/ * from ljb_test a where owner=‘LJB‘ order by owner desc ,object_type asc; 大家注意到我写的这个条件了吗?这样一写,owner该列值为唯一的,也就失去了排序的意义, 因此该语句等同于select /*+index(a,idx_ljb_test)*/ * from ljb_test a where owner=‘LJB‘ order by object_type asc; 变成是单列排序了,所以实验当然不成功!平时工作中发现问题并总结出来,真正 试验的时候却因为一个小小的失误而花费了大量时间无法实验成功,希望这小段插曲能让大家加深 本小节的印象。 本节有一定的典型性,降序索引在日常工作中尤其是出报表的sql语句,应该能发挥比较重要的作用,要时刻对排序敏感,很多情况下,临时表空间暴涨,查询速度太慢等都是因为排序 太多,PGA的排序区装不下,不得不放到临时表空间去排序,临时表空间是磁盘和内存的速度无法 比较,当然要慢,而且临时表空间一般不是设置无限扩大的,不好的语句过多过大的排序最终将导 致ORACLE报临时表空间不足的错而退出,大家一定也经常有见过临时表空间不足而报的错误 吧。另外 本小节实验结束后,忽然想到,其实可以做一下更加深入的研究,大家觉得如果 索引是这样建立的create index idx_ljb_test on ljb_test(owner desc,object_type asc);而的查询转变为select /*+index(a,idx_ljb_test)*/ * from ljb_test a order by owner asc ,object_type desc; 也就是将owner的desc转变为asc,将object_type的asc转变为desc,那先前建的这个索引还能避免排序吗? 有兴趣的朋友可以跟我一起再向下一起深入研究一下 执行如下语句查询后发现, owner和object_type倒过来后又要使用排序了! SQL> select /*+index(a,idx_ljb_test)*/ * from ljb_test a order by owner asc,object_type desc; 执行计划 ------------------------------------------------------------------------------------------------------------------------------------------------- Plan hash value: 1087399357 ------------------------------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time | -------------------------------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 68286 | 11M| | 5647 (1)| 00:01:08 | | 1 | SORT ORDER BY | | 68286 | 11M| 27M| 5647 (1)| 00:01:08 | | 2 | TABLE ACCESS BY INDEX ROWID| LJB_TEST | 68286 | 11M| | 2986 (1)| 00:00:36 | | 3 | INDEX FULL SCAN | IDX_LJB_TEST | 68286 | | | 243 (1)| 00:00:03 | ------------------------------------------------------------------------------------------------------------------------------------------------ Note ----- - dynamic sampling used for this statement 是否就得出结论,遇到这样的排序查询,如果升序和降序的两列倒过来,对应的索引就要重新 建,否则还是无法避免排序? 我想很多人都要回答YES了,因为实验的结果都出来了。 不过事实上并非如此,注意到,这个时候索引走的INDEX FULL SCAN,如果改变直接使用降序索引的HINT,会是什么效果呢? SQL> select /*+ index_desc(ljb_test)*/ * from ljb_test order by owner asc,object_type desc; 执行计划 ------------------------------------------------------------------------------------------------------------------------------------- Plan hash value: 318971137 -------------------------------------------------------------------------------------------------------------------------------------- 28 | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | --------------------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 68286 | 11M| 2986 (1)| 00:00:36 | | 1 | TABLE ACCESS BY INDEX ROWID| LJB_TEST | 68286 | 11M| 2986 (1)| 00:00:36 | | 2 | INDEX FULL SCAN DESCENDING| IDX_LJB_TEST | 68286 | | 243 (1)| 00:00:03 | -------------------------------------------------------------------------------------------------------------------------------------- Note - dynamic sampling used for this statement 吃惊吧,怎么现在又没有排序了,那怎么选择这样的索引方式呢?我的回答是,表和索引有了 足够的信息让ORACLE知道,ORACLE在正确的分析下,得出正确的代价后,认为让排序消失的 这个INDEX FULL SCAN DESCENDING是合理的,自然就选择了这种方式,于是倒过来升序和降 序的查询一样也可以省略了排序。看来INDEX FULL SCAN DESCENDING这样的扫描方式也应该引起的注意! 1.2.7 索引可以加快查询的速度,很多人意识到这点后,就特别喜欢用索引,动不动就在表上建索引, 有时为了自己非常特殊的冷门需求,也为表建某索引,用完后也不将索引清除掉。我这里要特别说 明一下,随意建索引的态度是要不得的! 为什么这么说呢?这里要注意索引有一个不利的地方,就是索引一定会影响DML的性能,往不带索引表中插入值的速度和带索引的表中插入的速度相比,不带索引的表的插入速度一定更快, 原因很明显,DML语句要进行维护索引的开销,所以会慢,这无须做实验就能证明了。另外随着索 引的不断增大,维护的开销将越来越大,如果一张表的索引数量再一增多,开销将会进一步增大! 生产中要严格控制索引的数量,避免某些更新频繁的表更新的性能太低影响了系统的正常运行, 如何控制索引的数量呢? 1、首先可以执行如下命令获取需要监控的索引的语句,然后执行提取出来的语句 select 'alter index '||owner||'.'||index_name||' monitoring usage;' from dba_indexes where table_owner='BILL' 2、保证上面提取的语句执行过后,通过一段时间的观察,查看下列语句得出索引使用的情况, 获取那些索引不常用的信息 select io.name index_name, t.name table_name, decode(bitand(i.flags, 65536), 0, 'NO', 'YES') monitoring, decode(bitand(ou.flags, 1), 0, 'NO', 'YES') used, ou.start_monitoring start_monitoring, ou.end_monitoring end_monitoring from sys.obj$ io, sys.obj$ t, sys.ind$ i, sys.object_usage ou where io.owner# = (select user# from sys.user$ where name='&用户名') and i.obj# = ou.obj# and io.obj# = ou.obj# and t.obj# = i.bo# 3、观察第2步查询出来的结果,删除不常使用的索引,控制索引的数量。 对此大家应该掌握了如何观察和控制索引的方法,希望大家能熟悉并经常在工作中应用。 29 此外当索引的数量和大小达到某个临界值的时候,DML操作如插入的速度,可能会忽然慢到难 以忍受的地步,可能从原先比插无索引表的速度慢3-5倍,到一下子慢到比无索引表的插入速度慢 10多倍。此时考虑删除索引,等插入完成后再重建索引,也是一个好方法,也许会快很多!这个我 很有体会!在计费环境组上班期间,我维护DSG数据库同步软件的时候发现,把生产的超级大表 account_item和查询库的account_item做全同步时,同步表包含同步索引的操作方式让我等了8个多小时才同步好,而我选择单独同步表不同步索引,1小时内同步完成,接着我手动建索引,在1小时左右完成,这样加起来的速度比合并操作要快许多!从这里也说明了索引非常影响大表的DML性能! 现实应用中,控制索引的数量是非常重要的,大家要有索引不能随便建的意识,如果为 了某个特定的需求建立了索引,日后不常用,要及时删除这个索引,避免将来对DML操作造成严重的影响。另外要经常利用上面提供的三个步骤,及时维护生产中的索引,观察和控制索引的数量! 有时必须进行大批量插入动作,某些索引又不得不建的情况下,可以考虑分开执行,索引先删除, 等插完数据,再将索引重新建起来。 使用EXP/IMP工具进行数据迁移时,可以考虑对索引进行两种方式控制以提高 EXP/IMP的速度。1、就是不使用索引,等数据迁移过来后,在新的数据库中手动建立自己需要的 索引,毕竟数据更重要,索引可以慢慢建,选择性的建。这样就在EXP的选项中设置INDEXES=N,这样加快了导出的速度也加快的导入的速度。2、在IMP的时候选择INDEXFILE参数,让索引先生成文件,先不要导入数据库中,等数据全部导入库后再建选择性的从INDEXFILE中建索引,即便把INDEXFILE的索引全部建到新库中,大数据迁移时,分步操作的速度也更快。这两点是我写本 小节的引申联想。 1.2.8 有不少人发现遇到索引使用不到的情况,这个时候除了NULL,索引代价比全表扫高外,多半就是遇到了隐式转换问题了,什么叫隐式转换,比如select * from ljb_test where id=123,而实际这个时候id的列为varchar2(20)而不是number型,这就出现了隐式转换。实际转化为select * from ljb_test where to_number(id)=123在这种情况下,这个索引列id就用不到索引了! 举例操作如下 SQL> drop table ljb_test; Table dropped SQL> create table ljb_test (id varchar2(20)); Table created SQL> create index idx_ljb_test on ljb_test(id); Index created SQL> explain plan for select * from ljb_test where id='10'; Explained SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT --------------------------------------------------------------------------------------------------------------- Plan hash value: 36299861 ---------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time ---------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 12 | 1 (0)| 00:00:01 30 |* 1 | INDEX RANGE SCAN| IDX_LJB_TEST | 1 | 12 | 1 (0)| 00:00:01 ------------------------------------------------------------------------------------------------------------------ id): Predicate Information (identified by operation --------------------------------------------------- 1 - access("ID"='10') Note ----- - dynamic sampling used for this statement 17 rows selected SQL> explain plan for select * from ljb_test where id=10; Explained SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT ---------------------------------------------------------------------------------------------------------- Plan hash value: 2595830654 ------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ---------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 12 | 2 (0)| 00:00:01 | |* 1 | TABLE ACCESS FULL| LJB_TEST | 1 | 12 | 2 (0)| 00:00:01 | ------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter(TO_NUMBER("ID")=10) Note ----- - dynamic sampling used for this statement 17 rows selected 请特别注意两个执行计划的不同,第一个1 - access("ID"='10')而第二个是1 - filter(TO_NUMBER("ID")=10) ,可以很清楚的看出,第二个其实根本就是一个函数,所以这第二 种写法是肯定用不到索引的!否则也就不存在什么函数索引的概念了! :这样的错误在现实中应该是很常见的,很多被DBA逮住的性能低下造成大表全表扫描 的语句大多都是这种情况!在写SQL的时候要特别留意关键谓词处字段的类型,查询中遵循一个简 单的规则,是number的就填number,是varchar的就添加引号写varchar,避免隐式转换!说个小插 曲,宁夏电信曾经出现一个执行频度很高的查询不走选择性索引,影响整体性能的情况,后来定位 出该查询语句不走的原因是走了隐式转换的语句(类似filter(TO_NUMBER("ID")=10),该业务是 新上的,原因居然是测试环境和生产环境表结构不一样,在测试环境中是number型,而生产环境却 是varchar型,所以没加引号的查询在测试环境用的上索引,到生产就用不到了。最后处理方法很简 单,避免隐式转换解决问题,不过这里还要强调另外一点,务必要保证生产和测试环境一致,否则 很容易出问题,这个是我提的另一个注意点! 31 1.3 1.3.1 delete 做一系列测试如下: 连接到: Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 - Production With the Partitioning, OLAP and Data Mining options SQL> create table ljb_test as select * from dba_objects; SQL> select count(*) from ljb_test; COUNT(*) ---------------------- 62658 SQL> set autotrace traceonly statistics 可以看出这个查询用了881个逻辑读 SQL> select count(*) from ljb_test; 统计信息 ---------------------------------------------------------------------- 0 recursive calls 0 db block gets 881 consistent gets 0 physical reads 0 redo size 410 bytes sent via SQL*Net to client 385 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 1 rows processed 删除ljb_test表后重新插入 SQL> delete from ljb_test; 已删除62658行。 SQL> commit; 提交完成。 SQL> set autotrace traceonly statistics 可以看出,ljb_test表已经是空表了,但是查询仍然占用了881个逻辑读,所以查询速度一定不比原先的快,测试的时候考虑加上alter system flush buffer_cache的步骤,每次将内存中缓存的块去掉,保证比较两者执行速度时的公平性! SQL> select count(*) from ljb_test; 统计信息 ------------------------------------------------------------------ 0 recursive calls 0 db block gets 32 881 consistent gets 877 physical reads 0 redo size 407 bytes sent via SQL*Net to client 385 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 1 rows processed truncate表就可以做到释放空间了(alter table move等也可以释放表空间),操作如下: SQL> truncate table ljb_test; 表被截断。 SQL> alter system flush buffer_cache ; 系统已更改。 果然逻辑读仅剩5个了,相比原先881个大大下降,查询速度自然会快了很多! SQL> select count(*) from ljb_test; 统计信息 ------------------------------------------------------------------- 1 recursive calls 1 db block gets 5 consistent gets 2 physical reads 96 redo size 407 bytes sent via SQL*Net to client 385 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 1 rows processed 表被delete删除后空间不会释放,因此如果你对大表做了大量delete后,再进行查询, 即便返回的结果已经非常少,甚至已经是空表,查询需要花费的代价和时间还是和未delete前一样, 这点要引起大家的注意! 1.3.2 delete 从上节操作可知,truncate操作可以释放表空间,但是truncate不能带条件,如truncate table xxx where ….的写法是不允许的(当然如果是操作分区表,可以允许truncate一个分区),所以现实中, 对数据库操一定是使用delete比truncate多的多,那是否空间总不释放,空间越浪费越多,每次查询 遍历的块也会越来越多,导致操作越来越慢吗? 实验如下: Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb SQL> drop table test; Table dropped 33 SQL> create table test tablespace cdma as select * from dba_objects; Table created 查看一下该表大小,为8388608个字节 SQL> select segment_name,bytes,tablespace_name from user_segments where segment_name='TEST'; SEGMENT_NAME BYTES TABLESPACE_NAME ------------------------------- ----------------------------------------------------- TEST 8388608 CDMA SQL> delete from test; 62654 rows deleted SQL> commit; Commit complete 确实,发现删除提交后,表大小仍然为8388608个字节个字节,并没释放,此时如果查询该表 记录,虽然有记录,但是和刚才没记录查询一样慢,这点在前面已经说过了。 SQL> select segment_name,bytes,tablespace_name from user_segments where segment_name='TEST'; SEGMENT_NAME BYTES TABLESPACE_NAME ------------------------------- ----------------------------------------------------- TEST 8388608 CDMA 那如果再次插入呢?是否空间大小是8388608的两倍呢? SQL> insert into test select * from dba_objects; 62654 rows inserted SQL> commit; Commit complete 惊奇的发现,大小仍然一样 SQL> select segment_name,bytes,tablespace_name from user_segments where segment_name='TEST'; SEGMENT_NAME BYTES TABLESPACE_NAME ------------------------------- ----------------------------------------------------- TEST 8388608 CDMA 下面用另外一个方式来查看,show_space是显示表详细情况的过程 SQL> set serverout on SQL> exec show_space('TEST','auto'); Total Blocks............................1024 Total Bytes.............................8388608 Unused Blocks...........................125 Unused Bytes............................1024000 Last Used Ext FileId....................77 Last Used Ext BlockId...................820489 Last Used Block.........................3 PL/SQL procedure successfully completed SQL> delete from test; 62654 rows deleted 34 SQL> commit; 删除再次插入,仍然大小一样,而且所有的细项都保持不变! Commit complete SQL> exec show_space('TEST','auto'); Total Blocks............................1024 Total Bytes.............................8388608 Unused Blocks...........................125 Unused Bytes............................1024000 Last Used Ext FileId....................77 Last Used Ext BlockId...................820489 Last Used Block.........................3 PL/SQL procedure successfully completed delete是不能立即释放空间,但是却是可以被重用,也就是某个应用经常删除再经常插 入,并不会有太多的空块!对于频繁插入和更新的表,运行慢,不该怀疑是因为里面有太多空块。 1.3.3 alter table move 通过前面两节学习可知,deltete不会释放表空间,但是可以重用,也就是插入可以填补空洞, 当然现实应用中确实是存在经常删除很少插入的情况,这样就存在了释放表空间优化数据库的可行 性了,truncate有不能带条件的缺陷,自然就想到用alter table move重移表空间的方法。这里要注意三个要素 1、 alter table move 省略了tablespace XXX, 表示用户移到自己默认的表空间,因此当前表空 间至少要是该表两倍大,这很好理解,由于易错所以提出,就不再细说了。 2、 alter table move过程中会导致索引失效,必须要考虑重新索引 3、 alter table move过程中会产生锁,应该避免在业务高峰期操作! 就第二点和第三点做实验说明如下吧 Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb 先获取该SESSION的SID,方便实验观察 SQL> select sid from v$mystat where rownum=1; SID -------------------- 160 SQL> create table ljb_test as select * from dba_objects; Table created SQL> select count(*) from ljb_test; COUNT(*) ------------------- 62659 SQL> create index idx_test on ljb_test(object_id); Index created 查询当前该SESSION并无锁 SQL> select * from v$lock where sid=160; 35 ADDR KADDR SID TYPE ID1 ID2 LMODE REQUEST CTIME BLOCK -------- -------- ---------- ---- ---------- ---------- ---------- ---------- ---------- ----------------------------------------- 查看索引状态也正常! SQL> select index_name,table_name,status from user_indexes where table_name='LJB_TEST'; INDEX_NAME TABLE_NAME STATUS ------------------------------ ------------------------------ ----------------------------------------------- IDX_TEST LJB_TEST VALID alter table ljb_test move; 重新再开一个窗口 执行如下命令,发现锁已经产生了 select * from v$lock where sid=160; ADDR KADDR SID TYPE ID1 ID2 LMODE REQUEST CTIME BLOCK -------- -------- ------ ---- ------- ---------- ------ -------- ------ ------------------------------------------------------------------ 2043451C 20434530 160 CF 0 0 4 0 0 0 1FA072BC 1FA073D8 160 TX 917534 592 6 0 1 0 204344C0 204344D4 160 HW 76 323783147 6 0 0 0 1F9C4224 1F9C423C 160 TM 84825 0 6 0 0 0 204342F4 20434308 160 TT 76 16 4 0 0 0 1F9C377C 1F9C37C4 160 TS 76 323783147 6 0 0 0 不过由于alter table move命令未结束,索引仍然有效! SQL> select index_name,table_name,status from user_indexes where table_name='LJB_TEST'; INDEX_NAME TABLE_NAME STATUS ------------------------------ ------------------------------ ---------------------------------------------------- IDX_TEST LJB_TEST VALID 等alter table ljb_test move;命令结束后,再查看发现锁消失了 SQL> select * from v$lock where sid=160; ADDR KADDR SID TYPE ID1 ID2 LMODE REQUEST CTIME BLOCK -------- -------- ---------- ---- ---------- ---------- ---------- ---------- ---------- ------------------------------------------ 但是索引却失效了! SQL> select index_name,table_name,status from user_indexes where table_name='LJB_TEST'; INDEX_NAME TABLE_NAME STATUS ------------------------------ ------------------------------ ---------------------------------------------------- IDX_TEST LJB_TEST UNUSABLE 这个实验说明:除了知道alter table move命令可以释放空间(当然这语句最根本的作用还是移动表到不同的表空间去,这里只是借用它可以释放空间的一个特性),还要了解该动作会锁表 直到命令结束,而且会导致索引失效,属于危险命令,建议千万不要在业务高峰期操作。 1.3.4 普通表空间的建表空间、扩表空间大家都很熟悉了。这里我单说临时表空间,当然是因为它有 着特殊性,我发现很多人不注意这些特殊性而犯错。现将临时表空间和普通表空间的差异描述如下: 1.临时文件查看的视图为dba_temp_files,非传统的dba_data_files 语句为:select * from dba_temp_files 36 2.临时表空间建立语句特殊,要在tablespace 前加temporary关键字 CREATE TEMPORARY TABLESPACE temp01 TEMPFILE ?/u02/oradata/temp01.dbs‘ SIZE 500M TABLESPACE GROUP tempgroup_a; 这里很容易犯一个错误,我经常看到有人不加这个tempory关键字,建了表空间,而名字也起什么 TEMP之类的, ,这个是披着临时表空间外衣的普通表空间! 3.增加临时表空间的语句特殊,要加上tempfile关键字 ALTER TABLESPACE TEMP1 ADD tempfile '/oradata/xo1/temp03_8g.dbf' SIZE 8192M; 4.在unix 环境下建临时表空间是稀疏的建立的 你也许体会到,在unix 环境下,临时表空间建立语句非常快,你建8G的临时表空间,几秒就 建好了,而建8G普通表空间,即便在顶级存储上也要至少20秒。这是什么原因呢,如果你去看生成的临时表空间文件,你会发现文件大小并没有8G,可能非常小,在你往临时表空间写内容的时 候,你会发现该临时文件变大了,有可能最终用达到8G。这个其实很危险,或许OS只剩下4G 存储空间装临时文件,由于是稀疏的建立的,所以存储的下,一旦程序异常,多用了临时表空间, 那这个4G空间就很快被用完了,,这是要注意的情况! 临时表空间从查看视图、建表空间语法、增加临时表空间和建立表空间的物理方式都和 普通表有着很大的差异,需要引起大家的注意。 1.3.5 我发现周围很多人的应用存在这样一种现象:为了顺利处理某些复杂的较大业务逻辑,他们在 应用中建立了很多中间表来做临时处理,对这些中间表他们是删除再插入忙的不亦乐乎...... 以上的这样操作其实是非常不好的,这里我说三点:1、不了解临时表的人建立的临时中间表,其实就是普通表,他们没加global temporary的关键字,就不是真正意义的临时表,因此在处理完数 据后必须自己删除再重用,这无疑给系统多了开销。2、你建的临时表对于别人来说是可见的,存在被人锁表及误删数据的可能。3、如果你的程序想提高并发性,那你的临时表是否要考虑建立多个? 而真正临时表global temporary table 对于不同SESSION是独立的,各自不影响,却可以同名,是应用使用并发的绝好前提! 现在先通过一系列实验来了解临时表的特性: Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb 建立基于会话的临时表要记得关键字global temporary,及on commit preserve rows(基于会话) SQL> create global temporary table ljb_tmp_session on commit preserve rows 2 as select * from dba_objects where 1=2; Table created 通过以下可看出当前表的属性为全局临时表,类型为基于会话 SQL> select table_name,temporary,duration from user_tables where table_name='LJB_TMP_SESSION'; TABLE_NAME TEMPORARY DURATION ------------------------------ --------- ---------------------------------------------------- LJB_TMP_SESSION Y SYS$SESSION 建立基于会话的临时表要记得关键字global temporary,及on commit delete rows(基于事务) SQL> create global temporary table ljb_tmp_transaction on commit delete rows 2 as select * from dba_objects where 1=2; 37 Table created 通过以下可看出当前表的属性为全局临时表,类型为基于事务 SQL> select table_name, temporary, DURATION from user_tables where table_name='LJB_TMP_TRANSACTION'; TABLE_NAME TEMPORARY DURATION ------------------------------ --------- -------------------------------------------------------- LJB_TMP_TRANSACTION Y SYS$TRANSACTION 分别向ljb_tmp_transaction和ljb_tmp_session 插入数据( SQL3.4) SQL> insert all 2 into ljb_tmp_transaction 3 into ljb_tmp_session 4 select * from dba_objects; 125304 rows inserted 查看两表分别都已插入62652条记录了。 SQL> select session_cnt,transaction_cnt from (select count(*) session_cnt from ljb_tmp_session), 2 (select count(*) transaction_cnt from ljb_tmp_transaction); SESSION_CNT TRANSACTION_CNT ----------- ----------------------------------------------------- 62652 62652 提交后发现基于事务的临时表数据消失了。 SQL> commit; Commit complete SQL> select session_cnt,transaction_cnt from (select count(*) session_cnt from ljb_tmp_session), 2 (select count(*) transaction_cnt from ljb_tmp_transaction); SESSION_CNT TRANSACTION_CNT ----------- ---------------------------------------------------- 62652 0 断开连接后继续登陆 SQL> disconnect Not logged on SQL> connect ljb/ljb Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb 再次查询发现基于会话的临时表数据也消失了! SQL> select session_cnt,transaction_cnt from (select count(*) session_cnt from ljb_tmp_session), 2 (select count(*) transaction_cnt from ljb_tmp_transaction); SESSION_CNT TRANSACTION_CNT ----------- -------------------------------------------------- 0 0 实验至此可知,无论是基于事务还是基于会话的临时表,对于其他SESSION都是不可 见的,换句话说数据只存在于当前SESSION中。基于事务的临时表,在本SESSION中有任何提交 动作,数据也就立即消失了,基于会话的临时表在SESSION生存期内提交数据仍然存在,并且可以 回滚,没脱离SESSION和普通表的操作没有什么区别。 38 基于会话和基于事务的临时表有什么区别呢?可以这样认为:如果应用很简单,不要对临时表 做多次组合处理,比如合并,删除等多次操作,就采用基于事务的,更简单!如果有涉及到对临时 表要做组合操作的,就必须选择基于事务的临时表,选错了就会在组合操作中数据丢失,出大问题 了!从本文易错主题来说,这个临时表入选本文还是很有资格的,特容易出错! 省外结算系统的程序版本采用的是联创统版的,在数据库设计上还有一些不尽人意之处,但是 后台入库的接口程序处理模式却设计不错,就是采用将原始话单经一系列处理后的数据入全局临时 表,这个最大的好处就是程序并行成为可能,无论多少个连接数连上来,就有多少个临时表做为接 口表与程序处理接应,表名虽然一样,但是各自SESSION独立,整体程序的可伸缩性大大增加了! 1.3.5 一般而言,临时表空间满的错误主要和排序有关系,大量的排序导致PGA中无法完成,超出sort_area_size的尺寸,最终到临时表空间进行排序,最后由于超过了临时表空间设置的上限而出错, 因此在写sql的时候要尽量避免排序,请大家注意我在1.1.4小节,1.2.5两小节中都有提到关于排序的内容,希望引起大家的重视。当然除了排序外,多表关联中写法不当导致迪卡尔乘积之类的操作 也是罪魁祸首之一。 以下操作是我前面就提到过的,需要较大用到排序,在操作的时候要尽量多考虑,比如增大 sort_area_size尺寸,分批量操作,分步操作,将某些用户操作用在指定的临时表空间、优化SQL语 句等等方式。 A、创建索引及重建索引; B、涉及到索引维护的并行插入 C、order by或者group by(尽可能对索引字段排序) D、Distinct E、union/intersect/minus F、sort-merge join G、analyze命令(仅可能使用estamate而不是compute) 这里我特别想说一点就是,有的时候某程序异常导致temp表空间暴涨,进程KILL该进程后 遇到temp空间还一直不能释放的情况,此时可以考虑将用户默认的临时表进行切换。具体方法 如下 SQL> select name from v$tempfile; NAME ------------------------------------------------------------------------------ C:\ORACLE\PRODUCT\10.2.0\ORADATA\RQRQ\TEMP01.DBF 查询用户的默认表空间(由于太长怕影响版面问题,我只显示一部分) SQL> select username,temporary_tablespace from dba_users; USERNAME TEMPORARY_TABLESPACE ------------------------------ ----------------------------------------------------------- OUTLN TEMP MDSYS TEMP MGMT_VIEW TEMP SYS TEMP TESTZHJS_APP TEMP YXL TEMP LJB TEMP 39 建临时表空间temp2 SQL> create temporary tablespace temp2 tempfile 'C:\ORACLE\PRODUCT\10.2.0\ORADATA\RQRQ\TEMP02.DBF' 2 size 50M; Tablespace created 将数据库默认表空间切换为temp2 SQL> alter database default temporary tablespace temp2; Database altered 查看结果,发现默认表空间已经切换成功了! SQL> select username,temporary_tablespace from dba_users; USERNAME TEMPORARY_TABLESPACE ------------------------------------------------------------------------------------------- OUTLN TEMP2 MDSYS TEMP2 MGMT_VIEW TEMP2 SYS TEMP2 TESTZHJS_APP TEMP2 YXL TEMP2 LJB TEMP2 这样如果切换成功了,原本不能释放的临时表空间可以考虑删除,就解决问题了! 接下来继续探讨,大家还记得前面我说过避免临时表空间满的时候,有提到―将某些用户操作用 在指定的临时表空间‖的说法,就是说数据库中可以存在多个临时表空间由不同操作用户指定使用的 方式,这个可行吗? 修改yxl用户为temp表空间 SQL> alter user yxl temporary tablespace temp; User altered 查看发现,当前数据库用户下yxl用户的默认临时表空间已经是temp了,而其他用户都是切换 的temp表空间。 SQL> select username,temporary_tablespace from dba_users; USERNAME TEMPORARY_TABLESPACE ------------------------------------------------------------------------------------------- OUTLN TEMP2 MDSYS TEMP2 MGMT_VIEW TEMP2 SYS TEMP2 TESTZHJS_APP TEMP2 YXL TEMP LJB TEMP2 另:以下脚本可以查询是谁占用了临时表空间,对于定位问题起到一定的作用。 SELECT r.name 回滚段名, rssize/1024/1024/1024 "RSSize(G)", s.sid, s.serial#, s.username 用户名, s.status, s.sql_hash_value, 40 s.SQL_ADDRESS, s.MACHINE, s.MODULE, substr(s.program, 1, 78) 操作程序, r.usn, hwmsize/1024/1024/1024,shrinks ,xacts FROM sys.v_$session s,sys.v_$transaction t,sys.v_$rollname r, v$rollstat rs WHERE t.addr = s.taddr and t.xidusn = r.usn and r.usn=rs.USN Order by rssize desc :本小节简单了描述了临时表空间满的原因,并介绍了解决方法,另外详细介绍了切换临 时表空间的方法,以及强调了临时表空间可以有多个,不同用户可以切换选择选择不同的表空间, 有效的避免争用的方法,希望能对大家理解认识及解决临时表空间满问题带来一定的帮助! 1.3.6 nologging 大家都知道,oracle数据库恢复的根本是依据redo,没有了redo恢复就成为了一种空谈,数据库 中最重要的就是日志文件了!但是现实应用中是有需要关闭日志操作的时候,比如数据大批量迁移, 减少日志将会减少ORALCE极大的开销,从而大大提高性能!等迁移完毕再把表操作的nologging 属性更改为logging属性。虽然日志关闭意味着无法从redo中恢复数据,不过可以在数据迁移完毕 对整个数据库做个全备,然后日后再在此基础上做增量备份,如此说来,数据还是安全的!但是这 里要强调一下,很多时候数据的丢失就是因为这个nologging,维护及开发人员做了这个操作却没有 及时通知DBA或系统集成部人员做数据库全备,甚至一直就没将nologging属性更改回来,结果一 旦数据库遭到破坏,nologging设置的这部分数据就再也找不回来了,一定要切记! Nologging 操作和普通操作差别在哪呢?实验如下: 首先查看建普通表需要多少日志开销 SQL> @C:\oracle\product\10.2.0\db_1\RDBMS\ADMIN\mystat.sql "redo size" NAME VALUE --------------------------------------------------------------- ---------------------- redo size 43791620 SQL> create table ljb_test as select * from dba_objects; Table created SQL> @C:\oracle\product\10.2.0\db_1\RDBMS\ADMIN\mystat2.sql NAME V DIFF ---------------------------------------------------------------- ---------- ----------------------- redo size 51083404 7,291,784 结论出来了,该建表操作产生了7M左右的日志,那加nologging后的建表会产生多少日志呢? SQL> @C:\oracle\product\10.2.0\db_1\RDBMS\ADMIN\mystat.sql "redo size" NAME VALUE ---------------------------------------------------------------- ------------------------------------------------- redo size 51083404 SQL> create table ljb_test_nologging nologging as select * from dba_objects; Table created SQL> @C:\oracle\product\10.2.0\db_1\RDBMS\ADMIN\mystat2.sql NAME V DIFF 41 ---------------------------------------------------------------- ---------- ----------------------------------------- redo size 51179652 96,248 可以看出,加上nologing的操作,只产生了96K左右的日志,相差非常显著! 很多人喜欢使用/*+append*/这种方式来插入数据,这个HINT的含义可以理解为:直接插入到表 的最后,不加这个HINT的时候插入首先会在高水平位以下寻找空的空间插入,加了就直接往高水 平位以上插入了,可以提高速度. 脚本大致如:insert /*+append*/ into ljb_test select * from ljb_test; 在使用/*+append*/的时候能使插入的速度加快多少呢?做实验测试如下: SQL> @C:\oracle\product\10.2.0\db_1\RDBMS\ADMIN\mystat.sql NAME VALUE --------------------------------------------------------------------------- redo size 73757008 SQL> insert into ljb_test_nologging select * from ljb_test_nologging; 62700 rows inserted 可以看出,表属性虽被设置为nologging,在没加append情况下,还是要产生7M左右的日志 SQL> @C:\oracle\product\10.2.0\db_1\RDBMS\ADMIN\mystat2.sql NAME V DIFF ---------------------------------------------------------------- ---------- ---------------- redo size 80905104 7,148,096 SQL> rollback; 加append的情况为: SQL> @C:\oracle\product\10.2.0\db_1\RDBMS\ADMIN\mystat.sql NAME VALUE ----------------------------------------- ------------------------------------------ redo size 81374156 SQL> insert /*+append*/ into ljb_test_nologging select * from ljb_test_nologging; 62700 rows inserted 发现nologging和append的组合才使insert的威力发挥到最大,目前查看仅产生23K日志 SQL> @C:\oracle\product\10.2.0\db_1\RDBMS\ADMIN\mystat2.sql NAME V DIFF ----------------------------------------------------------------------------------------------------- redo size 81397528 23,372 再来看看普通表的插入情况 SQL> @C:\oracle\product\10.2.0\db_1\RDBMS\ADMIN\mystat.sql "redo size" NAME VALUE --------------------------------------------------------------------------------------------------------- redo size 664 可以看出普通表插入数据后要产生7M左右数据 SQL> insert into ljb_test select * from ljb_test; 62701 rows inserted SQL> @C:\oracle\product\10.2.0\db_1\RDBMS\ADMIN\mystat2.sql NAME V DIFF ---------------------------------------------------------------- ------------------------------------------- redo size 7096932 7,096,268 SQL> rollback; 42 SQL> @C:\oracle\product\10.2.0\db_1\RDBMS\ADMIN\mystat.sql NAME VALUE ------------------------------------------------------------------------------------------------------------- redo size 14787464 普通表插入加append后也是产生7M左右数据,只是减少了一点点而以 SQL> insert /*+append*/ into ljb_test select * from ljb_test; 62701 rows inserted SQL> @C:\oracle\product\10.2.0\db_1\RDBMS\ADMIN\mystat2.sql NAME V DIFF -------------------------------------------------------------------------------------------------------------------- redo size 21995836 7,208,372 nologging方式在建表的时候可以大大减少日志,普通表需要7M,而NOLOGGING方式只要96K,而在插入数据的时候,如果表是NOLOGGING模式下的表,加APPEND 只产生23K日志,而未加APPEND的插入,产生了7M的日志。而在普通表模式下,无论是否加append都要产生7M左右日志,因此基本append对普通表来说是没有什么性能上的提升!只有nologging属性表和append组合,才能发挥出最大的威力,取得性能上的提高! 另外,将本次查询日志的脚本内容贴上 Mystat.sql 脚本内容如下: set verify off column value new_val V define S="&1" select a.name,b.value from v$statname a,v$mystat b where a.statistic#=b.statistic# and lower(a.name) like '%' || lower('&S')||'%' / Mystat2.sql脚本内容如下 set verify off select a.name,b.value V,to_char(b.value-&V,'999,999,999,999') diff from v$statname a,v$mystat b where a.statistic#=b.statistic# and lower(a.name) like '%' || lower('&S')||'%' / 前面通过对v$mysta和v$statname的研究,统计过排序的时候排序次数是如何增长, 接着还通过查看stat_indexes观察了索引的大小变化,本小节再次通过v$mystat与v$statname结合分析的方式察看了日志改变的规律,那SQL语句的操作如何分析undo值的变化,有量化的方法吗? 观察undo变化的方法是查看 v$rollstat视图,大家可以通过每次执行SQL的前后,分别执行select usn,RSSIZE ,WRITES from v$rollstat; 取每次writes的差值就可以明白undo的变化情况,有兴趣的可以自行 研究,这里就不深入描述了。 43 1.3.7 ora-01555回滚段太旧错误是oracle中非常著名的错误,大家一定都有遇到吧,为什么会有这样 的错误呢? 其实说到本质处是因为ORACLE一致读特性,简单的说就是oracle在开始查询的某一时刻起, 查询结果就已确定,如果由于数据量非常大或者是SQL写的不合理,导致查询时间非常长,比如早上8点开始查询,到9点才查询结束,这一小时期间即便查询的数据被更新了,最终查出来的还是 8点这个时刻即未更新前结果,会从回滚段中读出来它未更新时的数据,这就是一致读! 那ora-01555回滚段太旧错误怎么产生呢?就是从回滚段中读不出它未更新时的数据(比如被多 次重写,找不回来了),一致读失败了,此时就出错了! 下面来模拟一个实验,步骤如下,我经常用下列实验方法来构造出这个错误 首先查看当前的回滚段设置如下 SQL> show parameter undo NAME TYPE VALUE ------------------------------------ ----------- ---------------------------------------- undo_management string AUTO undo_retention integer 900 undo_tablespace string UNDOTBS2 重建一回滚表空间,大小只有2M,并且不能扩展 SQL> create undo tablespace undo_small 2 datafile 'C:\oracle\product\10.2.0\oradata\rqrq\undo_small.dbf' size 2m 3 autoextend off 4 / Tablespace created 以下命令将当前回滚表空间切换到新建的回滚表空间 SQL> alter system set undo_tablespace=undo_small; System altered SQL> show parameter undo NAME TYPE VALUE ------------------------------------ ----------- --------------------------------------- undo_management string AUTO undo_retention integer 900 undo_tablespace string UNDO_SMALL 执行如下过程: 请特别注意这个commit,以下动作是循环内每执行一次就提交一次,这样2m的回滚段表空间很 快就被多次重写了,这样不断操作后,查询数据就有可能从回滚段中读不出前镜像了 SQL> begin 2 for x in (select rowid rid from ljb_test) 3 loop 4 update ljb_test set object_name=lower(object_name) where rowid=x.rid; 5 commit; 6 end loop; 44 7 end; 8 / 过程执行未结束立即开另一窗口,执行如下查询语句,请注意这个dbms_lock.sleep(0.01),由于查 询速度很快,我特意加了这个dbms_lock.sleep,让查询速度较慢,每条记录至少需要查询0.01秒, 构造出查询慢的模拟环境来! SQL> declare 2 cursor c is 3 select /*+ first_rows */ object_name from ljb_test order by object_id; 4 l_object_name ljb_test.object_name%type; 5 l_rowcnt number:=0; 6 begin 7 open c; 8 loop 9 fetch c into l_object_name; 10 exit when c%notfound; 11 dbms_lock.sleep(0.01); 12 l_rowcnt:=l_rowcnt+1; 13 end loop; 14 close c; 15 exception 16 when others then 17 dbms_output.put_line('rows fetched ='||l_rowcnt); 18 raise; 19 end; 20 / declare * 第 1 行出现错误: ORA-01555: 快照过旧: 回退段号 26 (名称为 "_SYSSMU26$") 过小 ORA-06512: 在 line 18 果然,错误模拟出来了!这就是著名的ORA-01555:错误! 扩展一下思路,此时如果执行如下命令就会有得到另外一个错误ORA-30036: SQL> delete from ljb_test; delete from ljb_test ORA-30036: 无法按 8 扩展段 (在还原表空间 'UNDO_SMALL' 中) 这个表示回滚段空间太小,比如我这个命令失败的原因是,delete from ljb_test 的时候,oracle 需要考虑将ljb_test的记录先存储在回滚段中以备回退,但是目前空间只有2M,并且不可扩展,只 要ljb_test的记录大于2m,存储不下,自然要失败! 下面继续实验就可以证明我的观点,由于只是删除部分数据,量很少,可以成功! SQL> delete from ljb_test where owner='LJB'; 216 rows deleted 实验做完了,别忘了将回滚段空间改变回来 不过更改之前,先必须保证当前回滚段的活动事务都结束,可用如下命令查询 SQL> SELECT a.name,b.status FROM v$rollname a, v$rollstat b WHERE 45 2 a.name IN ( SELECT segment_name FROM dba_segments WHERE tablespace_name = 'UNDO_SMALL') AND a.usn = b.usn; 更新回原来的回滚段表空间 SQL> alter system set undo_tablespace='UNDOTBS2'; System altered 再次查看,发现回滚段已经更改回来了 SQL> show parameter undo NAME TYPE VALUE ------------------------------------ ----------- ----------------------------------------- undo_management string AUTO undo_retention integer 900 undo_tablespace string UNDOTBS2 :ORACLE的01555快照太旧错误最主要的原因是因为一致读失败了,这个错误其实只 是查询不成功,或许再查的时候,由于时间点不一样了,要查询的数据不会被频繁更新了,又查询 成功了,所以遇到这个错误暂时不要惊慌!可以从几个方面来解决这样的问题:1、也就是最重要的,就是要优化的语句,如果一个一小时的查询缩短到5分钟,那01555的错误就大大降低了。2、大家还记得我在show parameter undo的时候看到的undo_retention参数吧,这个参数表示多长时间回滚段 不被更新,可以设置相对长一点时间。3、过程中千万不要将commit写在循环内,这个大忌,本例我就是利用了这个构造出这个错误的,提交意味着回滚段可以改写,如此频繁的提交,01555错误不出才怪啊。4、大的查询可以放在业务更新不怎么高峰的时候,这样也可以有效的避免01555错误。 最后大家还要注意我顺便构造出来的ORA-30036: 无法按扩展段的错误,这个错误和01555完全不一样,这个是由于回滚段太小又无法扩展,由于没有commit又无法重用,回段段空间不够用导致的 错误,大家要区别对待! 大家应该很熟悉EXP/IMP的ORACLE导入导出工具,其中有一个CONSISTENT参数,相当于导出时设定了 SET TRANSACTION READ ONLY 所有导出的数据都是一致的,不会受到导出过程中的数据更新影响。这样的话数据库EXP导出时间从8点到9点花了一小时,导出的数据库的所有的表都会保持8点时刻的记录,否则8点半的时候导完了A表,8点50分源数据库更新了A表和B表,会把B表的最新记录导入,因为A表导出过就不会再理会了,这样就整体不一 致了。如果A、B表是主外键表,本来需要同时更新才能保持约束正确,这里就被破坏了。这个是 EXP/IMP中CONSISTENT的含义,希望大家能好好体会其中含义。 1.4 1.4.1 1.4.1.1 先讨论分区表的局部索引在分区交换和分区删除等情况下是否会失效 SQL> connect ljb/ljb Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb SQL> create table part_tab1 (id int) 46 partition by range (id) ( partition p1 values less than (10), partition p2 values less than (20), partition p3 values less than (maxvalue) ) ; Table created SQL> insert into part_tab1 select rownum from dual connect by rownum <=50; 50 rows inserted SQL> commit; Commit complete SQL> create index idx_id on part_tab1(id) local; Index created 查看索引状态,都是有效果的 SQL> select index_name,partition_name,status from user_ind_partitions where index_name='IDX_ID'; INDEX_NAME PARTITION_NAME STATUS ------------------------------ ------------------------------ -------------------------------------------- IDX_ID P1 USABLE IDX_ID P2 USABLE IDX_ID P3 USABLE 删除分区表分区1 SQL> alter table part_tab1 truncate partition p1 ; Table truncated SQL> select count(*) from part_tab1 partition(p1); COUNT(*) ------------------- 0 查看索引并没有失效 SQL> select index_name,partition_name,status from user_ind_partitions where index_name='IDX_ID'; INDEX_NAME PARTITION_NAME STATUS ------------------------------ ------------------------------ ------------------------------------------------ IDX_ID P1 USABLE IDX_ID P2 USABLE IDX_ID P3 USABLE 说明对于局部索引,直接TURNCATE分区是不会影响分区索引的!!! 再实验分区交换的情况,步骤如下: SQL> create table normal_tab1(id int); Table created SQL> insert into normal_tab1 select rownum from dual connect by rownum<10; 9 rows inserted SQL> commit; Commit complete SQL> select count(*) from normal_tab1; COUNT(*) 47 ---------------------- 9 SQL> create index idx_norm on normal_tab1 (id); Index created SQL> alter table part_tab1 exchange partition p1 with table normal_tab1 including indexes; Table alteredSQL> select count(*) from normal_tab1; COUNT(*) ---------------------- 0 SQL> select count(*) from part_tab1 partition(p1); COUNT(*) -------------------- 9 分区交换成功后,索引仍然有效! SQL> select index_name,partition_name,status from user_ind_partitions where index_name='IDX_ID'; INDEX_NAME PARTITION_NAME STATUS ------------------------------ ------------------------------ ------------------------------------------------- IDX_ID P1 USABLE IDX_ID P2 USABLE IDX_ID P3 USABLE 由此也可见分区索引的交换加上关键字including indexes 后索引是生效的。 SQL> alter table part_tab1 exchange partition p1 with table normal_tab1 ; Table altered SQL> select index_name,partition_name,status from user_ind_partitions where index_name='IDX_ID'; INDEX_NAME PARTITION_NAME STATUS ------------------------------ ------------------------------ ---------------------------------------------------- IDX_ID P1 UNUSABLE IDX_ID P2 USABLE IDX_ID P3 USABLE 说明如果没有加including indexes进行分区交换,该索引立马失效。甚至导致原来的临时表的索 引也失效了,也要重建 SQL> select index_name ,status from user_indexes where index_name='IDX_NORM'; INDEX_NAME STATUS ------------------------------ ------------------------------------- IDX_NORM UNUSABLE SQL> alter index idx_norm rebuild; Index altered SQL> alter index idx_id rebuild partition p1; Index altered 局部索引truncate分区不会导致索引失效。但是如果交换分区的时候,交换的临时表没有索引,或者有索引,没有用including indexes的关键字,会导致局部的索引失效。 48 1.4.1.2 接着讨论全局索引在分区交换和删除分区下是否会失效 SQL> drop index idx_id; Index dropped SQL> create index idx_id on part_tab1(id); Index created SQL> select index_name,table_name,status from user_indexes where table_name='PART_TAB1'; INDEX_NAME TABLE_NAME STATUS ------------------------------ ------------------------------ --------------------------------------------------------- IDX_ID PART_TAB1 VALID 发现全局索引和准备置换的索引根本不能匹配,如果想置换索引,分区表必须是LOCAL索引! SQL> alter table part_tab1 exchange partition p1 with table normal_tab1 including indexes; alter table part_tab1 exchange partition p1 with table normal_tab1 including indexes ORA-14098: ALTER TABLE EXCHANGE PARTITION 中的表索引不匹配 那删除分区全局索引会失效吗,操作如下,发现仍然是有效的! SQL> alter table part_tab1 truncate partition p1 ; Table truncated SQL> select index_name,table_name,status from user_indexes where table_name='PART_TAB1'; INDEX_NAME TABLE_NAME STATUS ------------------------------ ------------------------------ --------------------------------------------------------- IDX_ID PART_TAB1 VALID 其实有的时候实验出来的结果也要推敲一下,其实删除分区全局索引是会导致索引失效的, 只不过刚才TRUNCATE的恰好是一个空分区,所以没有印象,继续操作如下, truncate另一分区就 可以知道了 SQL> alter table part_tab1 truncate partition p2; Table truncated SQL> select index_name,table_name,status from user_indexes where table_name='PART_TAB1'; INDEX_NAME TABLE_NAME STATUS ------------------------------ ------------------------------ --------------------------------------------------------- IDX_ID PART_TAB1 UNUSABLE 如果删除掉置换表的索引,可以进行交换吗? QL> drop index idx_norm; Index dropped 发现置换成功了! SQL> alter table part_tab1 exchange partition p1 with table normal_tab1 including indexes; Table altered SQL> select count(*) from part_tab1 partition(p1); COUNT(*) --------------------- 9 此时查看索引,发现全局索引也失效了! SQL> select index_name,table_name,status from user_indexes where table_name='PART_TAB1'; 49 INDEX_NAME TABLE_NAME STATUS ------------------------------ ------------------------------ --------------------------------------------------------- IDX_ID PART_TAB1 UNUSABLE 全局索引如果置换的普通表有索引,置换根本不成功,除非把置换表索引删除,但是一 旦这样置换成功,全局索引也就失效了。truncate分区会使全局索引失效,但是如果truncate的分区 是空的,那就不会使整个索引失效 1.4.2 如果你需要匹配的某列必须遍历所有分区才能查询到,而不是在某分区中直接能定位到,这时 千万别考虑用局部索引!索引有一个特点:―索引是很平的!‖对于几十万的记录,利用索引搜索到某条记录可能需要0.1秒,记录增为几百万甚至千万,同样的检索也可能只需要0.2秒或者再多一点, 从索引的叶子结构可看出,几百万的数据的表,索引的树可能是3层,而几千万几亿的数据也只会最多到4层这样。虽说局部索引遍历区域一定比全局索引遍历区域少,甚至少的多,由于索引很平, 局部索引比全局索引快的并不太不明显,比如全局索引搜索0.2秒,局部索引为0.1秒。然而,如果 你查询用的是局部索引,又必须去每个分区去搜索,那性能就差别很大了,此时查到数据的时间就 是0.1*100=10秒(假设表有100个分区),这10秒可是0.2秒的50倍了。 正确选择分区表的索引类型非常重要,设计不合理将会带来很大的性能问题。 1.4.3 分区是一个很好的技术,它把数据根据partition区分出的数据分别存储在一个不同分区中,查询更新删除都在各自分区中执行,为用户管理带来极大便利,不过现实中存在这样一种情况,比如 某分区表中某记录变更了地区号字段area_code的值,从0591变更成0592,而area_code恰好为分 区字段,那记录必然会从0591分区转移到0592分区,这种情况虽不多见,但是也是会有的,遇到 这样的情况,需要注意什么吗?会遇到什么麻烦吗? 模拟实验如下 SQL> create table part_tab1 (id int) partition by range (id) ( partition p1 values less than (10), partition p2 values less than (20), partition p3 values less than (maxvalue) ); 表已创建。 SQL> insert into part_tab1 select rownum from dual connect by rownum <=50; 已创建50行。 SQL> commit; 提交完成。 SQL> SQL> select * from part_tab1 partition (p1); ID ------------------------------------------------ 50 1 2 3 4 5 6 7 8 9 已选择9行。 SQL> select t.*,t.rowid from part_tab1 partition (p1) t; ID ROWID ---------- ------------------------------------------ 1 AAAazAAAEAAATRFAAA 2 AAAazAAAEAAATRFAAB 3 AAAazAAAEAAATRFAAC 4 AAAazAAAEAAATRFAAD 5 AAAazAAAEAAATRFAAE 6 AAAazAAAEAAATRFAAF 7 AAAazAAAEAAATRFAAG 8 AAAazAAAEAAATRFAAH 9 AAAazAAAEAAATRFAAI 已选择9行。 SQL> select t.*,t.rowid from part_tab1 partition(p2) t; ID ROWID ---------- ----------------------------------------- 10 AAAazBAAEAAATRNAAA 11 AAAazBAAEAAATRNAAB 12 AAAazBAAEAAATRNAAC 13 AAAazBAAEAAATRNAAD 14 AAAazBAAEAAATRNAAE 15 AAAazBAAEAAATRNAAF 16 AAAazBAAEAAATRNAAG 17 AAAazBAAEAAATRNAAH 18 AAAazBAAEAAATRNAAI 19 AAAazBAAEAAATRNAAJ 已选择10行。 这里介绍一下rowid的含义 SQL> select dbms_rowid.rowid_object('&1') data_object_id#, 2 dbms_rowid.rowid_relative_fno('&1') rfile#, 3 dbms_rowid.rowid_block_number('&1') block#, 4 dbms_rowid.rowid_row_number('&1') row# from dual; 输入 1 的值: AAAazAAAEAAATRFAAA DATA_OBJECT_ID# RFILE# BLOCK# ROW# 51 --------------- ---------- ---------- ---------------------------------------------- 109760 4 78917 0 SQL> select dbms_rowid.rowid_object('&1') data_object_id#, 2 dbms_rowid.rowid_relative_fno('&1') rfile#, 3 dbms_rowid.rowid_block_number('&1') block#, 4 dbms_rowid.rowid_row_number('&1') row# from dual; 输入 1 的值: AAAazAAAEAAATRFAAB DATA_OBJECT_ID# RFILE# BLOCK# ROW# --------------- ---------- ---------- ---------------------------------------------- 109760 4 78917 1 下面这个操作很关键,因为把id=1转化成id=12如果成功,就是跳到另一个分区中去了 SQL> update part_tab1 set id=12 where id=1; update part_tab1 set id=12 where id=1 * 第 1 行出现错误: ORA-14402: 更新分区关键字列将导致分区的更改 很遗憾,这个操作失败了! 原来此需要修改数据库参数,要让表具备enable row movement属性才可以,操作如下: SQL> alter table part_tab1 enable row movement; 表已更改。 这下可以更新了。 SQL> update part_tab1 set id=12 where id=1; 已更新 1 行。 SQL> commit; 提交完成。 SQL> select * from part_tab1 partition(p1); ID ----------------------------- 2 3 4 5 6 7 8 9 已选择8行。 SQL> select t.*,t.rowid from part_tab1 partition(p1) t; ID ROWID ----------------------------------------- 2 AAAazAAAEAAATRFAAB 3 AAAazAAAEAAATRFAAC 4 AAAazAAAEAAATRFAAD 5 AAAazAAAEAAATRFAAE 52 6 AAAazAAAEAAATRFAAF 7 AAAazAAAEAAATRFAAG 8 AAAazAAAEAAATRFAAH 9 AAAazAAAEAAATRFAAI 已选择8行。 SQL> select t.*,t.rowid from part_tab1 partition(p2) t; ID ROWID ---------- ---------------------------------------- 10 AAAazBAAEAAATRNAAA 11 AAAazBAAEAAATRNAAB 12 AAAazBAAEAAATRNAAC 13 AAAazBAAEAAATRNAAD 14 AAAazBAAEAAATRNAAE 15 AAAazBAAEAAATRNAAF 16 AAAazBAAEAAATRNAAG 17 AAAazBAAEAAATRNAAH 18 AAAazBAAEAAATRNAAI 19 AAAazBAAEAAATRNAAJ 12 AAAazBAAEAAATRNAAK 已选择11行。 由此可知分区数据转移导致rowid记录改变了,普通表的变更是不会改变rowid的,如下 SQL> create table normal_tab1(id int); 表已创建。 SQL> insert into normal_tab1 select rownum from dual connect by rownum<=10; 已创建10行。 SQL> commit; 提交完成。 SQL> select t.*,t.rowid from normal_tab1 t; ID ROWID ---------- -------------------------------------------- 1 AAAazDAAEAAATRdAAA 2 AAAazDAAEAAATRdAAB 3 AAAazDAAEAAATRdAAC 4 AAAazDAAEAAATRdAAD 5 AAAazDAAEAAATRdAAE 6 AAAazDAAEAAATRdAAF 7 AAAazDAAEAAATRdAAG 8 AAAazDAAEAAATRdAAH 9 AAAazDAAEAAATRdAAI 10 AAAazDAAEAAATRdAAJ 已选择10行。 SQL> update normal_tab1 set id=999 where id=1; 已更新 1 行。 SQL> commit; 53 提交完成。 SQL> select t.*,t.rowid from normal_tab1 t; ID ROWID --- -------------------------------------- 999 AAAazDAAEAAATRdAAA 2 AAAazDAAEAAATRdAAB 3 AAAazDAAEAAATRdAAC 4 AAAazDAAEAAATRdAAD 5 AAAazDAAEAAATRdAAE 6 AAAazDAAEAAATRdAAF 7 AAAazDAAEAAATRdAAG 8 AAAazDAAEAAATRdAAH 9 AAAazDAAEAAATRdAAI 10 AAAazDAAEAAATRdAAJ 已选择10行。 通过本节描述大家知道alter table part_tab1 enable row movement后ORACLE可允许在 分区表中转移数据。不过还要注意自己包含rowid的sql代码,很多情况下这样的代码会让查询更快,但是如果你操作分区表,分区数据转移后rowid 改变的情况就要让你考虑修改脚本了。 1.4.4 我认为这是非常重要的一点,可以说没有意识将查询落到分区的现象非常普遍!我注意到很多 人操作的表是分区表却没意识到分区,他们索引没用到特关注(其实很多时候索引没用到还是对 的!),分区有无用到根本就回答不上来!如果你的表有100个分区,你的查询能落到某个分区中,那你的查询语句的速度理论上要比不指定分区要快99倍! 现在我举结算生产应用中的一个例子 Connected to: Oracle9i Enterprise Edition Release 9.2.0.5.0 - 64bit Production With the Partitioning, OLAP and Oracle Data Mining options JServer Release 9.2.0.5.0 – Production 我看到局方人员经常查错单表tl_err_y_voice表,以便从错单中进行回收入正常单,查询语句如下(见explain plan for后面的语句): SQL> explain plan for 2 select * from tl_err_y_voice where error_code=134 3 and substr(start_datetime,1,8)>='20090301' 4 and substr(start_datetime,1,8)<='20090302'; Explained. SQL> set linesize 1000 查看执行计划发现该查询走的是PARTITION RANGE ALL,从1分区遍历到最后12个分区 SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT ---------------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost | Pstart| Pstop | ---------------------------------------------------------------------------------------------------------------------------------- 54 | 0 | SELECT STATEMENT | | 170 | 135K| 8023 | | | | 1 | PARTITION RANGE ALL | | | | | 1 | 12 | |* 2 | TABLE ACCESS FULL | TL_ERR_Y_VOICE | 170 | 135K| 8023 | 1 | 12 | ------------------------------------------------------------------------------------------------------------------------------------ Predicate Information (identified by operation id): --------------------------------------------------- PLAN_TABLE_OUTPUT ---------------------------------------------------------------------------------------------------------------------------------------- 2 - filter("TL_ERR_Y_VOICE"."ERROR_CODE"=134 AND SUBSTR("TL_ERR_Y_VOICE"."START_DATETIME",1,8)>='20090301' AND SUBSTR("TL_ERR_Y_VOICE"."START_DATETIME",1,8)<='20090302') Note: cpu costing is off 17 rows selected. 查看该表结构后发现该表是以month_id做为分区的,加上这个month_id=3对结果没有任何影 响,但是对查询的速度却有了巨大的提高 SQL> explain plan for 2 select * from tl_err_y_voice where error_code=134 3 and substr(start_datetime,1,8)>='20090301' 4 and substr(start_datetime,1,8)<='20090302' 5 and month_id=3; Explained. 继续查看执行计划发现这下PARTITION RANGE ALL的关键字不见了,Pstart为3, Pstop也 是3,说明从原来的走12个分区改变为走1个分区,就是第三个分区。代价大大降低,查询速度也 明显加快了许多倍! SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT ----------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost | Pstart| Pstop | --------------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 817 | 595 | | | |* 1 | TABLE ACCESS FULL | TL_ERR_Y_VOICE | 1 | 817 | 595 | 3 | 3 | --------------------------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): ------------------------------------------------------------------------------------------------------------------------------- PLAN_TABLE_OUTPUT 1 - filter("TL_ERR_Y_VOICE"."ERROR_CODE"=134 AND SUBSTR("TL_ERR_Y_VOICE"."START_DATETIME",1,8)>='20090301' AND SUBSTR("TL_ERR_Y_VOICE"."START_DATETIME",1,8)<='20090302' AND "TL_ERR_Y_VOICE"."MONTH_ID"=3) Note: cpu costing is off 17 rows selected. :本小结强调了应用中要善于利用分区,要避免PARTITION RANGE ALL这样的关键字 出现在执行计划中,在加上分区字段不影响查询结果的情况下务必要将分区字段加进去,并举了生 产结算系统查询的一个例子来说明问题。当然分区也要合理,我见过宁夏计费的欠费表 55 account_item_owe,由于他的分区字段建立的极不合理,查询的时候根本没有能用到分区的可能,导 致该表的优化工作无法从分区进行考虑,这个也是要注意的一个地方,建立了分区表,就一定要考 虑将来能合理使用上! 此外要注意另外一个注意点:如果在分区表上面创建本地的主键或者唯一键约束的时候,需要 有分区关键字,而全局约束没有这个限制,如。 代码: SQL> alter table TEST1 add constraint pk_test_id primary key (ID) using index local; ORA-14039: partitioning columns must form a subset of key columns of a UNIQUE index SQL> alter table TEST1 add constraint pk_test_id primary key (ID,ends) using index local; Table altered 1.5 1.5.1 analyze table invalid structure 根据ORACLE文档所描述,如下情况要考虑重建索引 1. analyze index your_index_name validate structure; 2. 查询索引碎片: select name,del_lf_rows, lf_rows, round((del_lf_rows/(lf_rows+0.0000000001))*100) frag_pct from index_stats 一般如果该索引的frag_pct>10%,那么就需要rebuild index 当然analyze index your_index_name validate structure除了可以分析素因是否需要重建外,还可以让还能获得其他有用信息,能让了解当前索引的大小结构高度等等,以确定优化方案。还要特别留 意另一个知识点:―该命令将会导致锁!‖而且操作大表,该命令执行时间会相当长,会导致生产因此挂起一堆处理该表的进程! 这个实验在本地不好模仿,因为数据量不够大分析很快就结束了,跟踪不到锁,我在结算系统 环境上停应用后做分析如下: Connected to Oracle9i Enterprise Edition Release 9.2.0.5.0 Connected as zhjs_app SQL> select * FROM V$mystat where rownum=1; SID STATISTIC# VALUE ---------- ---------- ---------------------------------------- 74 0 1 SQL> analyze table fact_y_voice_settle validate structure; 这个时候另开一个窗口,执行以下命令查看所以的情况 SQL> select addr,sid,type,id1,id2,lmode from v$Lock where sid=74; ADDR SID TYPE ID1 ID2 LMODE ---------------- ---------- ---- ---------- ---------- --------------------------------------------------- 00000003CF5E3C18 74 TM 18405502 0 4 果然产生锁,之前v$lock的sid=74是没锁记录的,现在有了!此时任何对该表的非查询操作都会挂起。 大家是否觉这个analyze table fact_y_voice_settle validate structure 命令很眼熟,不错, 56 在前面讲解选择性索引的时候我已提到过,大家可以回头再看看。有时知道多了反而增加了风险, 你懂的如何使用validate structure分析索引结构,进行性能研究,忽视了其操作会导致锁,结果没等 你研究完,系统程序挂起了一片(我在环境组的时候曾经就这样犯过错,好在及时发现),所以谨慎 的态度要贯穿在自己的工作中,制定的规矩要遵守,不可自以为是,时刻不可大意! 1.5.2 在关系型数据库中,主键和外键是非常常见的设计,但是这里有非常重要的一点在应用中经常 被忽略,如果不加会导致死锁,让分析试验如下: A Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb SQL> create table ljb_test_p (x int primary key); Table created SQL> create table ljb_test_c (x references ljb_test_p); Table created SQL> insert into ljb_test_p select rownum from dual connect by rownum<=1000; 1000 rows inserted SQL> commit; Commit complete SQL> select * from v$mystat where rownum=1; SID STATISTIC# VALUE ---------- ---------- ---------------------------------- 142 0 1 这时往ljb_test_c表插入记录,暂不提交 SQL> insert into ljb_test_c values(2); 1 row inserted SQL> select * from ljb_test_c; X ------------------------------ 2 锁的情况如下: SQL> select * from v$lock where sid=142; ADDR KADDR SID TYPE ID1 ID2 LMODE REQUEST CTIME BLOCK -------- -------- ---------- ---- ---------- ---------- ---------- ---------- -------------------------------------------------------------------------- 1F9C4224 1F9C423C 142 TM 85051 0 2 0 48 0 1F9C42E8 1F9C4300 142 TM 85053 0 3 0 48 0 1FA252C4 1FA253E0 142 TX 1048609 869 6 0 48 0 SQL> select object_name from user_objects where object_id=85051; OBJECT_NAME ------------------------ LJB_TEST_P SQL> select object_name from user_objects where object_id=85053; 57 OBJECT_NAME ------------------------ LJB_TEST_C SQL> select xidusn,xidslot,xidsqn from v$transaction where xidsqn=869; XIDUSN XIDSLOT XIDSQN ---------- ---------- ------------------------------------- 16 33 869 登陆B窗口 SQL> select * from v$mystat where rownum=1; SID STATISTIC# VALUE ---------- ---------- -------------------------------------- 149 0 1 发现删除ljb_test_p表的任何一条记录都会被HANG住 SQL> delete from ljb_test_p where x=1; 发现HNANG住 ( delete from ljb_test_p where x=2;和x=1一模一样!) 回A窗口 锁情况如下 SQL> select * from v$lock where sid in (142,149); ADDR KADDR SID TYPE ID1 ID2 LMODE REQUEST CTIME BLOCK -------- -------- ---------- ---- ---------- ---------- ---------- ---------- ---------- ------------------------------------------------------------- 1F9C4224 1F9C423C 142 TM 85051 0 2 0 174 0 1F9C42E8 1F9C4300 142 TM 85053 0 3 0 174 1 1F9C43AC 1F9C43C4 149 TM 85051 0 3 0 24 0 1F9C4470 1F9C4488 149 TM 85053 0 0 4 24 0 1FA252C4 1FA253E0 142 TX 1245229 872 6 0 174 0 ------------------------------------------------------------------------------------------------------------------------------------------------------ 外键未建索引,子表任意更新一条记录,父表被锁住,无法进行更新操作 将上述插入动作都回退,重新操作,现在对外键建上索引! A SQL> create index idx_ljb_test_c on ljb_test_c(x); Index created SQL> insert into ljb_test_c values (2); 1 row inserted SQL> select * from v$lock where sid=(142,149); ADDR KADDR SID TYPE ID1 ID2 LMODE REQUEST CTIME BLOCK -------- -------- ---------- ---- ---------- ---------- ---------- ---------- ---------- -------------------------------------------------------------- 1F9C4224 1F9C423C 142 TM 85051 0 2 0 12 0 1F9C42E8 1F9C4300 142 TM 85053 0 3 0 12 0 1FA257E8 1FA25904 142 TX 851978 868 6 0 12 0 B SQL> delete from ljb_test_p where x=1;(除了x=2外其他删除也实验,都没问题) 1 row deleted SQL> select * from v$lock where sid in (142,149); 58 ADDR KADDR SID TYPE ID1 ID2 LMODE REQUEST CTIME BLOCK -------- -------- ---------- ---- ---------- ---------- ---------- ---------- ---------- --------------------------------------------------------------- 1F9C4224 1F9C423C 142 TM 85051 0 2 0 144 0 1F9C42E8 1F9C4300 142 TM 85053 0 3 0 144 0 1F9C43AC 1F9C43C4 149 TM 85051 0 3 0 36 0 1F9C4470 1F9C4488 149 TM 85053 0 2 0 36 0 1FA2486C 1FA24988 149 TX 786474 895 6 0 36 0 1FA257E8 1FA25904 142 TX 851978 868 6 0 144 0 得出总结:外键建索引后,主表除了x=2外的所有其他值删除都不被锁住。 接着再在B窗口执行 delete from ljb_test_p where x=2; HANG住了 锁的情况如下: SQL> select * from v$lock where sid in (142,149); ADDR KADDR SID TYPE ID1 ID2 LMODE REQUEST CTIME BLOCK -------- -------- ---------- ---- ---------- ---------- ---------- ---------- ---------- -------------------------------------------------------------- 204342F4 20434308 149 TX 851978 868 0 4 48 0 1F9C4224 1F9C423C 142 TM 85051 0 2 0 297 0 1F9C42E8 1F9C4300 142 TM 85053 0 3 0 297 0 1F9C43AC 1F9C43C4 149 TM 85051 0 3 0 189 0 1F9C4470 1F9C4488 149 TM 85053 0 2 0 189 0 1FA2486C 1FA24988 149 TX 786474 895 6 0 189 0 1FA257E8 1FA25904 142 TX 851978 868 6 0 297 1 外键建索引非常必要!建立了以后,子表任意更新一条记录,父表可以进行更新操作, 只有在更新父表的记录和子表外键记录对应上,才会产生事务锁! 1.5.3 BITMAP 不知道大家对这个bitmap索引是否陌生,这是个索引,不过由于建立了这类索引,极易出现锁 表,这就是我在锁的章节中来陈述索引的原因。 存在就一定有理由,既然有bitmap索引这个东西,就一定有他的好处,这个索引的好处是什么 呢?什么情况下使用bitmap索引呢? 好处肯定是查询能更快,性能能更高之类的,但是一定不会因为这个系统中到处都使用bitmap 索引,相反现行环境很少应用有看到用bitmap索引的。只有在满足如下两个条件情况下,才可以考 虑建立bitmap索引:首先是索引列的相异基数比较低,比如某列是标明性别的,那该列的值一般来 说只有三个:男,女,不详,即便该表记录上亿也是如此。还可举例比如上传标志字段,无非是0, 1这样表成功失败的值。这就是我说的相异基数比较低的情况,这样的情况也不少见! 缺少了这个 条件,bitmap索引就没有了快的优势了。其次是建bitmap索引的表必须是很少更新的,原因就是bitmap索引易被锁表,满足了第一个条件,在查询库中如果有合适的时机利用上bitmap索引,将也 会带来性能上的提升。 现在来模拟一下bitmap索引易被锁表的情况。 Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb 59 登陆SID=146 session SQL> select * from v$mystat where rownum=1; SID STATISTIC# VALUE ---------- ---------- ------------------------------ 146 0 1 SQL> create table ljb_test_bitmap (id int); Table created 建bitmap索引如下 SQL> create bitmap index idx_bitmap_id on ljb_test_bitmap(id); Index created SQL> insert into ljb_test_bitmap values (1); 1 row inserted SQL> select * from v$lock where sid=146; ADDR KADDR SID TYPE ID1 ID2 LMODE REQUEST CTIME BLOCK -------- -------- ------ ---- ---------- ------- -------- ---------- ------- ------------------------------------------------------------------- 1F9C4224 1F9C423C 146 TM 85156 0 3 0 3 0 1FA2F9F4 1FA2FB10 146 TX 917533 653 6 0 3 0 接着连到一个新的sid=147的session SQL> select * from v$mystat where rownum=1; SID STATISTIC# VALUE ---------- ---------- ---------------------------------- 147 0 1 插入如下语句,发现该语句马上被HANG住,无法执行下去了 SQL> insert into ljb_test_bitmap values (1); 查看当前的锁的情况,果然出先了锁等待 SQL> select * from v$lock where sid in (146,147); ADDR KADDR SID TYPE ID1 ID2 LMODE REQUEST CTIME BLOCK -------- -------- ------ ---- ---------- ---------- ------ ---------- -------- ----------------------------------------------------------------- 204342F4 20434308 147 TX 917533 653 0 4 24 0 1F9C4224 1F9C423C 146 TM 85156 0 3 0 123 0 1F9C42E8 1F9C4300 147 TM 85156 0 3 0 24 0 1FA23E14 1FA23F30 147 TX 1310750 649 6 0 24 0 1FA2F9F4 1FA2FB10 146 TX 917533 653 6 0 123 1 当然,如果sid=147的session 执行不是1的值的时候是不会有锁的。 sid=147的session中强制取消刚才插入1值的操作,更换插入语句如下: SQL> insert into ljb_test_bitmap values (2); 1 row inserted 可以执行成,锁的情况如下 SQL> select * from v$lock where sid in (146,147); ADDR KADDR SID TYPE ID1 ID2 LMODE REQUEST CTIME BLOCK -------- -------- ------ ---- ---------- ------- -------- ---------- -------- ------------------------------------------------------------------- 1F9C4224 1F9C423C 146 TM 85156 0 3 0 978 0 1F9C42E8 1F9C4300 147 TM 85156 0 3 0 162 0 1FA23E14 1FA23F30 147 TX 720921 664 6 0 162 0 60 1FA2F9F4 1FA2FB10 146 TX 917533 653 6 0 978 0 到此本小节就要结束了,大家也通过实验明白了bitmap易被锁表的道理,这里我想 再顺道再说一个细节,大家是否注意到被锁表的v$lock视图中block=1的地方对应的锁的类型 是TX,在上讲1.5.2节中,外键未键索引导致的影响中我也列了v$lock视图,其中全表被锁的 时候,锁中block=1的地方对应的锁的类型是TM,这两者有什么区别呢?TX是事务锁,而TX 是表锁,二者的差别后续再一起深入研究。 1.5.4 DDLDDL 大多DDL都带有排它DDL锁,比如alter table test add column col1; 执行这个动作的时候,整张表都会被锁住,所有对该表的操作都会失败,还有其他常见的如create index 的DDL语句,也是如此,因此生产中切不可在业务高峰期做这样的操作,非常危险!现在举CREATE INDEX产生DDL 锁的例子如下: SQL> select * from v$mystat where rownum=1; SID STATISTIC# VALUE ---------- ---------- ------------------------------------------ 142 0 1 SQL> create table ljb_test as select * from dba_objects; Table created SQL> insert into ljb_test select * from ljb_test; 62689 rows inserted SQL> commit; Commit complete 建立索引如下 SQL> create index idx_ljb_test on ljb_test(owner); Index created 在索引未建好的时候迅速登陆到已经准备好命令的另一窗口进行查询 SQL> select * from v$lock where sid in (142,144); ADDR KADDR SID TYPE ID1 ID2 LMODE REQUEST CTIME BLOCK -------- -------- ----- ---- ---------- ---------- ------- -------- ----- ------------------------------------------------------------------- 204344C0 204344D4 144 DL 85223 0 3 0 4 0 2043451C 20434530 144 DL 85223 0 3 0 4 0 1F9C4224 1F9C423C 144 TM 85223 0 4 0 4 0 1F9C42E8 1F9C4300 144 TM 18 0 3 0 4 0 1F9C377C 1F9C37C4 144 TS 76 323712875 6 0 4 0 1FA12444 1FA12560 144 TX 1048612 903 6 0 4 0 SQL> select object_Name,object_type from user_objects where object_id=85223; OBJECT_NAME OBJECT_TYPE -------------------- ------------------------------------ LJB_TEST TABLE 可以看出,如果索引建立的时间很长,在这段时间内,所有的对数据库的更新都将失败!除了 这点外,在本小节中我想特别再说明一个知识点,如果急需建立一个索引,等不及到夜晚等其他时 61 刻再建,又不想产生锁影响该表的并发应用,该怎么办呢? 这是本小节的另一重点,这里我告诉大家一个方法,就是在建索引的时候加上online的关键字,这样能够有效的防止其他DDL操作的产生,但是可以不影响表数据的更新,即DML操作的正常进行。具体实验如下: 登陆sid=144的窗口 SQL> drop index idx_Ljb_test ; Index dropped SQL> create index idx_ljb_test on ljb_test(owner) online; Index created 在未执行完脚本后迅速登陆到另一窗口执行如下查询 SQL> select * from v$lock where sid=144; 可以看出TM的 ADDR KADDR SID TYPE ID1 ID2 LMODE REQUEST CTIME BLOCK -------- -------- ----- ---- ------- ---------- ----- ------- ------- ------------------------------------------------------------------ 204344C0 204344D4 144 DL 85223 0 3 0 6 0 2043451C 20434530 144 DL 85223 0 3 0 6 0 1F9C4224 1F9C423C 144 TM 85223 0 2 0 3 0 1F9C42E8 1F9C4300 144 TM 85229 0 4 0 3 0 1F9C377C 1F9C37C4 144 TS 76 323712883 6 0 3 0 1FA1E024 1FA1E140 144 TX 917517 670 6 0 3 0 可以看出ONLINE关键字已经改变了建立索引的方法。并不增加一个排他DDL锁来防止数据修改,而只会得到表级上的一个低级 mode=2的锁。这样既有效的防止其他DDL发生,同时还维护了DML语句的正常进行。ORACLE这样做其实是在DDL期间,对应用在该表的操作先暂时 维护起来形成一个记录,最终再把维护的这些记录更新到建立的索引中去,这样提高了并发性又能 让建索引进行下去。当然,可以想象这样建立索引的命令一定是比普通索引来的慢,因为多了维护 的开销。 看完本小节后,希望大家对各种DDL语句会产生DDL锁要有一定的敏感度,另外,DDL语句中的CREATE INDEX+ONLINE的方式是可以允许在生产并发应用上使用的,大家可以了解这个知 识点,但是也不要滥用ONLINE,生产环境还是安全第一! 2 2.1 这个是个忠告,无论生产库还是测试库,都要让数据库处于归档模式下!希望大家一定要注意, 即便你没做任何备份的相关操作,也可能帮你把数据库中的误操作丢失的资料找回来,只要你处于 归档模式下。当然生产中一定是要有备份恢复机制的,并且要按照严格合理的方式进行部署。 仅举一例: 宁夏出差期间,当地计费有个兄弟在后台误删数据库文件,我帮忙恢复了,红色部分为所做的操作,给大家做参考一下,他并没有做任何备份,也缺少备份的意识,但是好在他的数据库是归档 模式下。 以下是我在本机先做的模拟实验情况 62 连接到: Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 - Production With the Partitioning, OLAP and Data Mining options SQL> archive log list 数据库日志模式 存档模式 ---(这个相当重要,还好他们的数据库是归档模式下的) 自动存档 启用 存档终点 USE_DB_RECOVERY_FILE_DEST 最早的联机日志序列 734 下一个存档日志序列 736 当前日志序列 736 SQL> shutdown immediate ORACLE 例程已经关闭。 删除C:\oracle\ljb\test.dbf (UNIX环境下不必关闭数据库就能删除数据文件了,这也就是UNIX下这么 容易误删的原因了!) 然后启动数据库 SQL> startup ORACLE 例程已经启动。 Total System Global Area 289406976 bytes Fixed Size 1248576 bytes Variable Size 79692480 bytes Database Buffers 201326592 bytes Redo Buffers 7139328 bytes 数据库装载完毕。 ORA-01157: 无法标识/锁定数据文件 80 - 请参阅 DBWR 跟踪文件 ORA-01110: 数据文件 80: 'C:\ORACLE\LJB\TEST.DBF' SQL> alter database create datafile 'c:\oracle\ljb\test.dbf' ; 数据库已更改。 SQL> recover datafile 'c:\oracle\ljb\test.dbf' ORA-00279: 更改 10711227356568 (在 02/27/2009 15:20:28 生成) 对于线程 1 是必需的 ORA-00289: 建议: C:\ORACLE\PRODUCT\10.2.0\FLASH_RECOVERY_AREA\RQRQ\ARCHIVELOG\2009_02_27\O1_MF_1 _732_%U_.ARC ORA-00280: 更改 10711227356568 (用于线程 1) 在序列 #732 中 指定日志: {=suggested | filename | AUTO | CANCEL} AUTO ORA-00279: 更改 10711227356830 (在 02/27/2009 15:24:13 生成) 对于线程 1 是必需的 ORA-00289: 建议: C:\ORACLE\PRODUCT\10.2.0\FLASH_RECOVERY_AREA\RQRQ\ARCHIVELOG\2009_02_27\O1_MF_1 _733_%U_.ARC ORA-00280: 更改 10711227356830 (用于线程 1) 在序列 #733 中 ORA-00278: 此恢复不再需要日志文件 63 'C:\ORACLE\PRODUCT\10.2.0\FLASH_RECOVERY_AREA\RQRQ\ARCHIVELOG\2009_02_27\O1_MF_1 _732_4TH58Y6T_.ARC' ORA-00279: 更改 10711227356841 (在 02/27/2009 15:24:38 生成) 对于线程 1 是必需的 ORA-00289: 建议: C:\ORACLE\PRODUCT\10.2.0\FLASH_RECOVERY_AREA\RQRQ\ARCHIVELOG\2009_02_27\O1_MF_1 _734_%U_.ARC ORA-00280: 更改 10711227356841 (用于线程 1) 在序列 #734 中 ORA-00278: 此恢复不再需要日志文件 'C:\ORACLE\PRODUCT\10.2.0\FLASH_RECOVERY_AREA\RQRQ\ARCHIVELOG\2009_02_27\O1_MF_1 _733_4TH59POZ_.ARC' 已应用的日志。 完成介质恢复。 SQL> alter database open; 数据库已更改。 查看c:\oracle\ljb下,果然有了test.dbf文件 本机完成如上测试后,在计费AIX主机上进行恢复,帮忙他们找回丢失的数据文件。相信这个例子能让大家明白,即便不是DBA,没人叫你制定备份与恢复的策略,你只要将数据库设置为归档模式下,找回丢失的数据就成了一种可能。 2.2 很多人认为,对表记录进行DML操作后,如delete+commit后,不能再rollback回来了,数据 也就丢了,其实并非如此。 如果数据库在正确的备份恢复策略下,可以通过恢复数据库到误删除时间点的方式来恢复,不 过这个较麻烦,不到万不得以不考虑这样操作,ORACLE有了一个很好很实用的机制,就是闪回机制,可以将数据闪回到你误操作前的时间点,让你找回你的数据。 下面我做一个实验 SQL> drop table ljb_test; Table dropped SQL> create table ljb_test(id int); Table created SQL> insert into ljb_test select rownum from dual connect by rownum<=10; 10 rows inserted SQL> commit; Commit complete SQL> select count(*) from ljb_test; COUNT(*) ----------------- 10 当前表的记录是10条,记下当前的时间点20090309 17:09:00 SQL> select to_char(sysdate,'yyyymmdd hh24:mi:ss') from dual; TO_CHAR(SYSDATE,'YYYYMMDDHH24: ------------------------------------------------------ 64 20090309 17:09:00 接着将ljb_test表删除并提交! SQL> delete from ljb_test; 10 rows deleted SQL> commit; Commit complete 数据已经丢了吗?并没有,可以用闪回找回来,请注意下面的语法 SQL> select count(*) from ljb_test as of timestamp to_timestamp('20090309 17:09:00','yyyymmdd hh24:mi:ss'); COUNT(*) ------------------ 10 果然,输入刚才那个时间点,闪回查询返回10条记录,于是还原就简单了,直接insert回去。 SQL> insert into ljb_test select * from ljb_test as of timestamp to_timestamp('20090309 17:09:00','yyyymmdd hh24:mi:ss'); 10 rows inserted SQL> commit; Commit complete SQL> select count(*) from ljb_test; COUNT(*) -------------------- 10 到此为止,大功告成了,是不是觉得很简单! 不过现实中要考虑的问题是很多的,你是否有考虑过可以恢复多久以前的数据?比如我 今天删除了一些记录,过好几天后才发现,可以闪回吗?比如我把表drop了,可以闪回吗?truncate 了,可以闪回吗? 这里不多讨论了,简单说一下,9i中闪回的功能比较弱, drop和truncate无能为力了,如果是10g的话,表drop了,可以通过垃圾站回收,truncate则是通过闪回整个数据库来找回,至于过好几 天才发现能否闪回,取决于回滚段中的记录是否被重写,可以了解参数undo_retention,单位为秒,表示记录被保护多长时间不被重写,设置大点,能闪回历史数据就更多些。 SQL> show paramete undo_retention; NAME TYPE VALUE ------------------------------------ ----------- ---------------------------------- undo_retention integer 900 2.3 drop 上一小节我介绍了找回删除提交后数据的方法,介绍了闪回功能,大家一定记得我还说了10g 可以从回收站找回被drop的表。这是10g新特性,目前结算和计费都是9i数据库,无法使用该特性,本想不做详细说明,但是恰好今天宁夏计费数据库环境,有人不小心drop了某表,由于是10g环境, 我便顺道帮其恢复了,因此我想,多学还是好的,总会有用到的时候,让一起进行恢复实验操作如 下: Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb 65 首先将本地的回收站的东西清空(很像WINDOWS回收站) SQL> select count(*) from user_recyclebin; COUNT(*) --------------- 60 以下命令为清空所有回收站内容 SQL> PURGE RECYCLEBIN; Done SQL> select count(*) from user_recyclebin; COUNT(*) ------------------- 0 清空完毕后建测试表ljb_testdrop SQL> create table ljb_testdrop as select * from dba_objects; Table created 现在回收站还是空的 SQL> SELECT object_name,original_name,type ,droptime FROM user_recyclebin; OBJECT_NAME ORIGINAL_NAME TYPE DROPTIME ------------------------------ -------------------------------- ------------------------- ------------------- 接着开始drop表了 SQL> drop table ljb_testdrop; Table dropped 再看回收站的视图user_recyclebin,已经有内容了 SQL> SELECT object_name,original_name,type ,droptime FROM user_recyclebin; OBJECT_NAME ORIGINAL_NAME TYPE DROPTIME ------------------------------ -------------------------------- ------------------------- ----------------------------------------------------- BIN$+H/pmI5LT7yvAWIHhotbGA==$0 LJB_TESTDROP TABLE 2009-03-10:16:14:33 再确认一下,表确实已经不存在了 SQL> select * from ljb_testdrop; select * from ljb_testdrop ORA-00942: 表或视图不存在 现在关键的步骤开始了,执行回收命令,请大家记住语法! SQL> FLASHBACK TABLE ljb_testdrop TO BEFORE DROP; Done 查看回收站的视图user_recyclebin已经没记录了 SQL> SELECT object_name,original_name,type ,droptime FROM user_recyclebin; OBJECT_NAME ORIGINAL_NAME TYPE DROPTIME ------------------------------ -------------------------------- ------------------------- ------------------- 再次查看数据库,该表终于回来了! SQL> select count(*) from ljb_testdrop; COUNT(*) ------------------- 62550 到此恢复成功,操作相对简单,不过大家要注意另一问题,如果你的ljb_testdrop表create完后 66 drop,接着create再drop,此时回收站中会出现两条记录,并且表名都是ljb_testdrop,你该如何恢 复?是这样,FLASHBACK TABLE ljb_testdrop TO BEFORE DROP可帮你恢复最近删除的表,如果 你想把最早一次被删除的表也恢复,你必须用前面你看到的user_recyclebin视图中的OBJECT_NAME 字段的值来恢复,没错,就是你看到的像达芬奇密码一样复杂的 BIN$+H/pmI5LT7yvAWIHhotbGA==$0,(这个这么复杂,所以肯定不会重复)而且你恢复回去的时 候由于原始表名存在了,你必须要重命名,类似如下命令 Flash TABLE "BIN$lfTbzOjISXaw8u0BIO7pNA==$0" TO BEFORE DROP rename to ljb_testdrop2 看起来有点神奇,还是演示一下吧 Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb 当前垃圾站为空 SQL> SELECT object_name,original_name,type ,droptime FROM user_recyclebin; OBJECT_NAME ORIGINAL_NAME TYPE DROPTIME ------------------ ------------------------------------------------------------------------------------------ SQL> select count(*) from ljb_testdrop; COUNT(*) ------------------ 62550 将当前的ljb_testdrop表drop掉 SQL> drop table ljb_testdrop; Table dropped 查看user_recyclebin视图,有了一条记录 SQL> SELECT object_name,original_name,type ,droptime FROM user_recyclebin; OBJECT_NAME ORIGINAL_NAME TYPE DROPTIME ------------------------------ -------------------------------- -------------------------------------------------------------- BIN$VgNjSLWrTzSEtnnRNGdCBQ==$0 LJB_TESTDROP TABLE 2009-03-10:16:37:13 再次建立ljb_testdrop表,不过表结构内容和刚才的不一样 SQL> create table ljb_testdrop as select * from dual; Table created SQL> select count(*) from ljb_testdrop; COUNT(*) ------------------- 1 再次drop ljb_testdrop表 SQL> drop table ljb_testdrop; Table dropped 查看回收站user_recyclebin,发现记录有两条了 SQL> SELECT object_name,original_name,type ,droptime FROM user_recyclebin; OBJECT_NAME ORIGINAL_NAME TYPE DROPTIME ------------------------------ ---------------- ----------------------------------------------------------------------------- BIN$g3a4qQqRQaqcmBrs1tDxdQ==$0 LJB_TESTDROP TABLE 2009-03-10:16:38:17 BIN$VgNjSLWrTzSEtnnRNGdCBQ==$0 LJB_TESTDROP TABLE 2009-03-10:16:37:13 用传统命令,直接就恢复了最近drop的一张表,大家应该猜到,是count(*)=1的那张 SQL> FLASHBACK TABLE ljb_testdrop TO BEFORE DROP; 67 Done SQL> SELECT object_name,original_name,type ,droptime FROM user_recyclebin; OBJECT_NAME ORIGINAL_NAME TYPE DROPTIME ------------------------------ ----------------- -------- ----------------------------------------------------------------- BIN$VgNjSLWrTzSEtnnRNGdCBQ==$0 LJB_TESTDROP TABLE 2009-03-10:16:37:13 果然是后被删除的那张,记录数为1 SQL> select count(*) from ljb_testdrop; COUNT(*) ----------------- 1 接着再恢复旧的一张,注意两点,一个是用object_name中的表名,一个是要考虑重命名,否则 恢复回去和原表ljb_testdrop同名了。 SQL> FLASHBACK TABLE "BIN$VgNjSLWrTzSEtnnRNGdCBQ==$0" TO BEFORE DROP rename to ljb_testdrop2; Done 查看回收站user_recyclebin再没记录了 SQL> SELECT object_name,original_name,type ,droptime FROM user_recyclebin; OBJECT_NAME ORIGINAL_NAME TYPE DROPTIME ------------------------------ ----------------------------------------------------------------------- 检查这个ljb_testdrop2,发现正是最早被删除的那张表,记录数是62550的那张 SQL> select count(*) from ljb_testdrop2; COUNT(*) ------------------ 62550 本小节到此结束了,我觉得ORACLE在人性化方面还是做的不错!误删并提交了记录能帮你找回,误删了整个表在10g中还有回收站助你一臂之力,期待计费结算系统日后都升级到10g, 10g功能强大,除了闪回增强外,还有很多其他新特性,实用的有EXPDB/IMPDB速度增快,RMAN 恢复备份工具功能增强,SQL语句功能增强等等,10g之旅将会给带来更多的惊喜! 2.4 truncate 前面说了找回delete并提交的数据,也说了drop后的表如何从回收站中找回,现在再来考虑第三种可能的误操作,truncate命令! Truncate的恢复相对delete表和drop表来说,要困难很多,这个时候要考虑ORACLE 10g的新 特性,闪回数据库。 现做简单实验如下: Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb 目前ljb_test表有记录! SQL> select count(*) from ljb_test; COUNT(*) ------------------ 62666 开始truncate该表 SQL> truncate table ljb_test; 68 Table truncated 查询当前时间为20090314 12:21:29 SQL> select to_char(sysdate,'yyyymmdd hh24:mi:ss') from dual; TO_CHAR(SYSDATE,'YYYYMMDDHH24: ------------------------------------------------------ 20090314 12:21:29 怎么办,表被truncate了,当然是想法子恢复回来了,首先将数据库置于MOUNT模式 SQL> shutdown immediate 数据库已经关闭。 已经卸载数据库。 ORACLE 例程已经关闭。 SQL> startup mount ORACLE 例程已经启动。 Total System Global Area 289406976 bytes Fixed Size 1248576 bytes Variable Size 83886784 bytes Database Buffers 197132288 bytes Redo Buffers 7139328 bytes 数据库装载完毕。 执行如下命令,将数据库设置为闪回模式 SQL> alter database flashback on; 数据库已更改。 开始闪回数据库,发现出错 SQL> flashback database to timestamp to_timestamp('20090314 12:19:00' ,'yyyymmdd hh24:mi:ss'); ERROR at line 1: ORA-38724: Invalid option to the FLASHBACK DATABASE command. 怎么会出错呢? 原来这个时候闪回的语法要严格按照数据库现有日期格式执行,数据库现有日期格式为 SQL> select value from nls_database_parameters where parameter='NLS_DATE_FORMAT'; VALUE ----------------------- DD-MON-RR 更改后执行如下恢复 SQL> flashback database to timestamp to_timestamp('2009-03-14 12:19:00' ,'yyyy-mm-dd hh24:mi:ss'); 闪回完成。 提示成功了,将数据库打开,请注意这个restlogs! SQL> alter database open resetlogs; 数据库已更改。 现在让登陆进去看看结果 SQL> connect ljb/ljb 已连接。 该表记录终于找回来了,非常激动人心吧! SQL> select count(*) from ljb_test; COUNT(*) 69 ------------------ 62666 除了上面说的格式问题外,还要另外注意一点,就是这个闪回的时间是有限的,要有足够的闪 回空间,否则将无法将数据库闪回到时间太前的时间,比如该例子说明到早上10点就无法闪回了。 SQL> flashback database to timestamp to_timestamp('2009-03-14 10:00:00','yyyy-mm-dd hh24:mi:ss'); ERROR at line 1: ORA-38729: Not enough flashback database log data to do FLASHBACK. 本小节通过闪回了整个数据库找回了被truncate的数据,不过现实中这样闪回整个数据 库的操作可能只适合在开发库和测试库,生产中做这样操作的可能性非常小,首先是闪回整个庞大 的数据库要足够的空间,其次是你如果闪回了你当前失误点时刻的数据库,好象时光倒流了,但是 你是并发用户数据库,别人的正常操作过程也被你给回退了。所以生产中即便要做这样的操作,也 要有非常严谨的考虑和规划才开始操作。 本例是讲述了如何找回被truncate的表,大家想想,虽然我把delete+commit, drop表,truncate table三个最容易发生的误操作分别用闪回查询,回收站和闪回数据库三种不同的 方法找回,实际上闪回数据库的方法是包含了恢复delete+commit 和drop两类,整个数据库都可以回到某个时刻,被delete+commit和drop的表能不恢复回来吗!此外被drop的表如果回收站被人清空了,估计也就不得不依赖闪回数据库了。不过闪回数据库和回收站必须依赖oracle 10g版本数据库才可以,这点要牢记! 3SQL 3.1 请大家注意看下面两个语句,都出现了空格,一般而言,我敢说,写这样的语句的人员99%并 不是真正为了写进空格,而是笔误! Insert into tab1 Select * from tab2 where name=‘ abc ?; insrt into tab3 select * from tab4 where substr(start_date,1,8)=‘ 20090309 ?; 插入数据后,他们一般是会按常理这么去查数据的 Select * from tab1 where name=‘ abc ? select * from tab3 where substr(start_date,1,8)=‘20090309?; 当然一无所获,最糟糕的是,对报表量统计还有可能产生影响,所以这个要特别留意!那该怎 么解决和避免这样的错误呢,很简单,大家利用神奇的dual表来做个trim的实验。 SQL> select trim(' abc ') from dual; TRIM('ABC') ---------------- abc OK,如果确认了字符不该有空格,就update tab1 set name=trim(name) 把记录重整。 本小节的错误其实还是非常容易发生的,在我身上就发生过,我是查询语句中加了空格 导致查询数据出问题,浪费不少时间查找原因,最终才发现是查询笔误导致! 70 3.2 很多开发的朋友在执行过程中常会遇到权限问题,在SQL中可以执行的语句,在过程中却无法运行,究竟是怎么回事?这里我对过程所需要的权限做个简单的介绍。 1、过程中处理不同用户的表,必须要有显示授权! C:\Documents and Settings\fujitsu>sqlplus "/ as sysdba" SQL*Plus: Release 10.2.0.1.0 - Production on 星期一 3月 9 12:44:59 2009 Ljb用户为dba 权限 Copyright (c) 1982, 2005, Oracle. All rights reserved. SQL> create user yxl identified by yxl; 用户已创建。 SQL> grant dba to ljb; 授权成功。 登陆yxl用户,建表yxl_test SQL> connect yxl/yxl SQL> create table yxl_test as select * from dba_objects; Table created 再次登陆ljb用户,发现dba权限具备更新yxl用户下yxl_test表的能力! SQL> connect ljb/ljb; SQL> delete from yxl.yxl_test; 62675 rows deleted 在ljb用户下建过程如下,发现提示错误,找不到yxl.yxl_test表? SQL> create or replace procedure p_ljb_test 2 as 3 begin 4 delete from yxl.yxl_test; 5 commit; 6 end; 7 / Warning: Procedure created with compilation errors SQL> show error Errors for PROCEDURE LJB.P_LJB_TEST: LINE/COL ERROR -------- --------------------------------- 4/17 PL/SQL: ORA-00942: 表或视图不存在 4/1 PL/SQL: SQL Statement ignored 原来,在过程中如果A用户要操作B用户表的时候,A用户仅有角色权限是不够的,要B用户 显式的将该表权限授权给A用户。 登陆yxl用户,授权yxl_test表所有权限给 ljb用户 SQL> grant all on yxl_test to ljb; Grant succeeded 然后现在回到ljb用户下继续建过程,终于成功了! SQL> create or replace procedure p_ljb_test 2 as 71 3 begin 4 delete from yxl.yxl_test; 5 end; 6 / Procedure created 看来即便拥有dba role,还不能访问不同用户的表,role 2利用AUTHID CURRENT_USER is传入执行者权限 回到yxl用户 大家还记得该用户是新建的,并且也是被授权dba的,让在这个用户下建立一下过程 SQL> create or replace procedure p_yxl_test 2 as 3 begin 4 execute immediate 'create table yxl_test1 (id int)'; 5 end; 6 / Procedure created SQL> exec p_yxl_test; begin p_yxl_test; end; ORA-01031: 权限不足 ORA-06512: 在 "YXL.P_YXL_TEST", line 4 ORA-06512: 在 line 1 还是应验了那句话,role 那怎么办呢?大家可能马上想到了用SYSDBA用户给YXL用户显式的授予create any table权 限!这个确实可行,但是如果涉及很多表,那就比较麻烦了,一个简便的方法是, AUTHID CURRENT_USER is传入执行者权限,这样,该用户在SQL下具备的权限,在过程中也具备了, 就可以在过程中执行了。 下面让实验看看,果然成功! SQL> create or replace procedure p_yxl_test 2 authid current_user 3 as 4 begin 5 execute immediate 'create table yxl_test1(id int)'; 6 end; 7 / Procedure created SQL> exec p_yxl_test; PL/SQL procedure successfully completed 查看表也已经建好了。 SQL> select count(*) from yxl_test1; COUNT(*) --------------------- 0 细心的朋友可能会问,刚才第一个过程中处理不同用户的表,必须要有显示授权!这个案例是 72 否可以改动一下,在脚本里加上AUTHID CURRENT_USER is传入执行者权限,不就可以不要显式授权了吗?事实并非如此,操作不同用户下的表,过程中是一定要有该表拥有者授权才可以 操作的,授什么样的权就能做什么样的操作,用AUTHID CURRENT_USER is是无用的,大家 可以继续往下看。 SQL> connect yxl/yxl SQL> revoke all on yxl_test from ljb; Revoke succeeded SQL> connect ljb/ljb 实验结果,ljb用户在过程中操作的yxl下的yxl_test表,由于没有授权,加上AUTHID CURRENT_USER is 也不行,操作不同用户的表,显式授权华山一条路! SQL> create or replace procedure p_ljb_test 2 authid current_user as 3 begin 4 delete from yxl.yxl_test; 5 end; 6 / Warning: Procedure created with compilation errors SQL> show err Errors for PROCEDURE LJB.P_LJB_TEST: LINE/COL ERROR -------- --------------------------------- 4/13 PL/SQL: ORA-00942: 表或视图不存在 4/1 PL/SQL: SQL Statement ignored role在过程中不可见,可以用单独得到某中权限或者是AUTHID CURRENT_USER is传入执行这权限方式处理,另外过程中如果要执行不同用户的表,用传入执行者权限是不够的,必 须显式得到表的权限 3.3 DDL 大家都知道DDL语句,比如create index create table alter table drop table alter index 等等,但 是大家是否能很强烈的意识到,做这个动作时,同时就在当前session做了commit的动作,而且即便这个DDL操作失败了,比如create table语法错,drop 的table不存在,这个commit动作也已完成! 下面做实验证明如下: Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb SQL> drop table ljb_test ; Table dropped SQL> create table ljb_test as select * from dba_objects; Table created SQL> select count(*) from ljb_test; COUNT(*) ----------------- 62679 SQL> delete from ljb_test; 73 62679 rows deleted 这个时候对该表建索引 SQL> create index inx_ljb_test on ljb_test(object_id); Index created 登到另一会话查看,发现ljb_test表记录已经真的清空了,说明已经提交了。 SQL> select count(*) from ljb_test; COUNT(*) ----------------- 0 接着做一个实验,可能让更加惊讶! SQL> insert into ljb_test select * from dba_objects; 62680 rows inserted 请注意这个索引,我是故意写错命令的,object_aa列根本不存在! SQL> create index inx_ljb_test on ljb_test(object_aa); create index inx_ljb_test on ljb_test(object_aa) ORA-00904: "OBJECT_AA": 标识符无效 登陆另一会话查看,发现ljb_test记录已经有了,说明已经提交了! SQL> select count(*) from ljb_test; COUNT(*) ----------------- 62680 至此大家应该明白,DDL语句执行的第一步就是COMMIT,然后才是执行DDL本身, 命令本身能否执行不管,只要DDL关键字出现,系统就提交!很多人喜欢在过程里用DDL语句, 要是对这点没有强烈的意识,就很容易出错,比如你是执行一些列更新语句,但是要在一个事务里, 就是一起成功或失败。如果语句中间任何一个地方出现了DDL,那这个事务的原子性就被破坏了,而你却毫不知情! :在1.5.4小节中我对DDL会产生DDL锁做了描述,本小节又阐述了DDL无论执行 成功与否都提交的特性, 此外由于DDL操作改变了表结构,还可能会对数据库中的包和过程产生 影响,会让这些对象产生一个重编译的过程,造成library cache lock的发生,业务高峰期操作很危险,因此结合这三点,大家一定要对DDL动作多加小心! 3.4 insert into Insert into a select * from b 在数据库代码中大量看到,大家在工作中是否有遇到a表和b表字段 值大小、类型、个数完全一样的情况下,即通过如下方式都不返回记录的情况下,还出现了列不匹 配的错误的情况。 select * from user_tab_columns where table_name='A' minus select * from user_tab_columns where table_name='B'; select * from user_tab_columns where table_name='B' minus select * from user_tab_columns where table_name='A'; 是否觉得很奇怪,原来这个时候还和查询展现的列的顺序有关系,具体实验如下 SQL> create table ljb_test1 (id1 varchar2(2),id2 number(10) ,id3 varchar2(20),id4 number(2)); 74 Table created SQL> create table ljb_test2 (id2 number(10),id3 varchar2(20),id1 varchar2(2),id4 number(2)); Table created SQL> insert into ljb_test2 select * from ljb_test1; 0 rows inserted SQL> insert into ljb_test1 values (1,2,30000,4); 1 row inserted SQL> commit; Commit complete SQL> insert into ljb_test2 select * from ljb_test1; insert into ljb_test2 select * from ljb_test1 ORA-12899: 列 "LJB"."LJB_TEST2"."ID1" 的值太大 (实际值: 5, 最大值: 2) 那问题怎么解决了,很简单 SQL> drop table ljb_test2; Table dropped SQL> create table ljb_test2 select * from ljb_test1; Table created 现在再执行插入看看,OK,终于可以成功了! SQL> insert into ljb_test2 select * from ljb_test1; 1 row inserted 为什么insert into ljb_test2 select * from ljb_test1为什么会失败?因为insert into +select 语句的本 质是insert into t values ( 列1,列2......) values ( 1,2,3...),最后简化为 insert into t select * from t2的! 这类错误还有隐蔽性,报错还好,万一列顺序不一致,但是由于互相之间的列又恰好 不冲突,那就得到错误的情况了,即值没有插入正确的列中,比如结算金额插入计费金额字段而计 费金额插入结算金额字段而浑然不知! 3.5 insert 无意间看到有同事在过程中写大致内容如下的代码: begin for I in select id,name from ljb_test1 where id1>5 loop insert into ljb_test1(id1,id2) select i.id1 ,i.id2 from ljb_test1; end loop; for I in select id,name from ljb_test where id1<>2 loop insert into ljb_test2(id1,id2) select i.id1 ,i.id2 from z_test; end loop; for I in select id,name from ljb_test where id1=2 loop insert into ljb_test3(id1) select i. id1 from ljb_test; end loop; commit; end ; 不难看出过程的逻辑:―根据ljb_test表的id1 列值的不同,分别向ljb_test1、ljb_test2、ljb_test3 三张表插入记录。‖看明白了后我想询问大家,此过程中实现的功能用单条sql能实现吗? 确实可以实现,ORACLE提供的语法是这样的: 75 INSERT ALL WHEN condition THEN insert_into_clause values_clause [WHEN condition THEN] [insert_into_clause values_clause] …… [ELSE] [insert_into_clause values_clause] 语法知道后,写法就简单了,以下单条sql和过程完全等价: Insert All when id1>5 then into ljb_test1(id1, id2) values(id1,id2) when id1<>2 then into ljb_test2(id1,id2) values(id1,id2) else into ljb_test3 values(id1) select id1,id2 from ljb_test; commit; :通过两个例子的比较,可以看出两者的代码后者来的简单的多,因此要多学习ORACLE 的知识点,这样可以丰富的选择方式,简化的代码! 写到这里有人会问,这有啥用,上面的过程不也实现的好好的吗?这里我想说明 两点: 1、要多了解ORALCE数据库,知识面越丰富写SQL的时候选择就越多,比如这个脚本比起过程至少写起来就显的简单多了。 2、Thomas Kyte曾说过大致这样的ORACLE优化哲学:―尽量用一条SQL完成工作;如果无法 用一条SQL实现则考虑PL/SQL;PL/SQL无法实现考虑用JAVA;JAVA无法实现考虑用C;C 无法实现考虑放弃该任务。‖他说的很有道理,现实中我曾经多次比较过单条SQL和过程实现 相同逻辑后的代价的差别,每次都是单条SQL性能要高的多,在本小节中也不例外,insert 多 表插入的写法比过程实现性能高出了许多! 以下为runstats_pkg 包为Thomas Kyte开发的性能比较小工具,就利用这个测试一下两者的性能 set serveroutput on size 100000 execute runstats_pkg.rs_start Insert All when id1>5 then into ljb_test1(id1, id2) values(id1,id2) when id1<>2 then into ljb_test2(id1,id2) values(id1,id2) else into ljb_test3 values(id1) select id1,id2 from ljb_test; commit; execute runstats_pkg.rs_middle begin for I in select id,name from ljb_test1 where id1>5 loop insert into ljb_test1(id1,id2) select i.id1 ,i.id2 from ljb_test1; end loop; for I in select id,name from ljb_test where id1<>2 loop insert into ljb_test2(id1,id2) select i.id1 ,i.id2 from z_test; end loop; for I in select id,name from ljb_test where id1=2 loop insert into ljb_test3(id1) select i. id1 from ljb_test; 76 end loop; commit; end ; 最后用runstas_pkg包的rs_stop过程来比较两者性能比较的结果,并输出如下: SQL> execute runstats_pkg.rs_stop(1000); Run1 ran in 1271hsec Run2 ran in 2997hsec run 1 ran in 42.41% of the time Name Run1 Run2 Diff STAT...redo size 3,416 7,420 4,004 LATCH.library cache lock 147 1,691 1,544 LATCH.library cache pin 616 3,549 2,933 LATCH.shared pool 332 5,795 5,463 LATCH.library cache 1,406 6,926 5,520 LATCH.row cache objects 989 7,548 6,559 LATCH.cache buffers chains 2,492 11,199 8,707 STAT...physical read total byt 9,152 16,384 7,232 Run1 latches total versus runs ----difference and pct Run1 Run2 Diff Pct 12,850 63,576 50,726 20.21% 仔细分析这个测试结果可以看出,前者脚本是一条SQL语句,后者是用过程的逻辑判断来插入数据,两者在代码上前者明显简单的多,代码也少的多。在runstats性能包测试中发现前者的运行时间只有后者的40%,并且在接下来的性能报告中显示前者的REDO和栓锁等都远小于后者。此外大家如果善于联想应该还会想到MERGE语句和WITH子句及分析函数,还有10g推出的model + partition的SQL写法,这些和INSERT INTO 有异曲同工之秒,都是封装简化代码,使用这些使得 单条SQL实现复杂ORACLE业务逻辑成为了可能,同时也是性能得以提升,大家可以多实验一下。 :大家有没有想过如果我们同时往两个表里插数据,又要保持被插两表数据 相同的时候,就有可能遇到麻烦了。“Insert into tab1 select * from t where 条件” 接着再写“insert into tab2 select * from t where 条件” 。这个时候我们能保证插入tab1和tab2数据是一致的吗?很明显不行,因为这两个动作执行有先后顺序的,你无论如何都不能保证两个命令同时执行!只要差0.01 秒,此时源表t就有可能因为并发而导致值变化了,如果应用是要插入N张表,那就更不可能一致了!你要两表保持一致只要加上锁的机制,那就对并发产生影响了。而INSERT ALL是在设计之初就考虑好了,将一份结果集同时拷贝至N个目标表,既提高了性能又不影响并发,希望大家好好体 会! 3.6or 3.6.1 or <> 这里举一个or语句易出错的例子,步骤如下: SQL> drop table ljb_test; Table dropped SQL> create table ljb_test (col1 int ,col2 int); 77 Table created SQL> insert into ljb_test (col1,col2) values (22,19); 1 row inserted SQL> insert into ljb_test (col1,col2) values (21,22); 1 row inserted SQL> insert into ljb_test (col1,col2) values (90,40); 1 row inserted SQL> commit; Commit complete SQL> select * from ljb_test; COL1 COL2 ------------------------------------------------------------------------------------ 22 19 21 22 90 40 以下查询语句本意是想统计出col1<>22或者co2<>22的记录,结果查询返回所有记录! SQL> select * from ljb_test where (col1<>22 or col2<>22); COL1 COL2 ------------------------------------------------------------------------------------ 22 19 21 22 90 40 显然上述结果不是想要的。其实仔细分析知道,实质上select * from ljb_test where (col1<>22 or col2<>22)和select * from ljb_test写法几乎一模一样,唯一能排除的是COL1=COL2=22的情况,我经常看到有同事犯了这样的错误,该处确实很容易出错,希望能引起大家的注意。 3.6.2 or and 上一小节讲述了or和<> 组合容易错,其实or和and 组合如果不注意括弧把逻辑控制好,也是 很容易出错的!比如想从数据字典dba_segments中统计出表空间名为ZHJS_TEMP,并且以owner字段以―ZHJS_‖打头或者是以―TEST_‖打头的记录,结果统计SQL语句编写如下: select * from dba_segments where tablespace_name='ZHJS_TEMP' and owner LIKE 'ZHJS_%' or owner like 'TEST_%'; 这样的结果ORACL会认为是select * from dba_segments where (tablespace_name='ZHJS_TEMP' and owner LIKE 'ZHJS_%')or (owner like 'TEST_%'); 两个or 之间任意一个成立就统计出来,这样当然结果错了。所以要写成如下:select * from dba_segments where tablespace_name='ZHJS_TEMP' and (owner LIKE 'ZHJS_%' or owner like 'TEST_%');将括弧 在后面括起来。 这个例子可能大家觉得很简单,确实如此!但是现实中如果粗心了,就很容易犯这样的 错误,而且这类非语法错误的逻辑错误,很容易让人出错了却难以察觉,最终在统计发现有差异的 时候才开始排除,就比较辛苦了。我就遇到过这类问题,核对报表时发现不准,最后才发现是问题 出在这个低级错误这里。以后大家在SQL中有OR逻辑的时候,尽量用括弧括起来,比如你真是这 样的需求select * from dba_segments where tablespace_name='ZHJS_TEMP' and owner LIKE 'ZHJS_%' or owner like 'TEST_%';那请你改写为select * from dba_segments where 78 (tablespace_name='ZHJS_TEMP' and owner LIKE 'ZHJS_%' )or owner like 'TEST_%';好让看你代码 的人很明确你不是笔误! 3.7NULL 3.7.1NULL 在写SQL的时候特别要留意空值对的影响,如果不注意空值问题,很可能就会得出个错误的结果,现举例说明如下: 平常应用中,我经常遇到两表关联后,利用关联字段互相更新某列的应用,不过我经常看到有 人在这里因为没考虑空值而出现了错误: Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb SQL> drop table ljb_test1; Table dropped SQL> drop table ljb_test2; Table dropped SQL> create table ljb_test1 (id1 int,id2 int); Table created SQL> create table ljb_test2 (id1 int,id2 int); Table created SQL> insert into ljb_test1 (id1,id2) values( 1,17); 1 row inserted SQL> insert into ljb_test1 (id1,id2) values( 2,18); 1 row inserted SQL> insert into ljb_test1 (id1,id2) values( null,18); 1 row inserted SQL> insert into ljb_test2 (id1,id2) values( 1,27); 1 row inserted SQL> insert into ljb_test2 (id1,id2) values( 2,28); 1 row inserted SQL> insert into ljb_test2 (id1,id2) values( null,29); 1 row inserted SQL> commit; Commit complete 查询两表记录情况情况,各自都有三条记录,但是分别都有空值存在 SQL> select * from ljb_test1; ID1 ID2 --------------------------------------- ---------------------------------- 1 17 2 18 18 79 SQL> select * from ljb_test2; ID1 ID2 --------------------------------------- -------------------------------- 1 27 2 28 29 接下来利用ljb_test2去更新ljb_test1,关联字段为id1,将ljb_test2表的id2的值去更新ljb_test1 表的id2的值。 SQL> update ljb_test1 set id2=(select id2 from ljb_test2 where ljb_test1.id1=ljb_test2.id1); 3 rows updated 更新完毕后发现,ljb_test1表记录确实被更新了,不过却丢失了id1为空的那条记录 SQL> select * from ljb_test1; ID1 ID2 --------------------------------------- ------------------------------- 1 27 2 28 SQL> select * from ljb_test2; ID1 ID2 --------------------------------------- ------------------------------- 1 27 2 28 29 上述脚本未考虑空值,闯了大祸,平空丢了数据,回退数据 SQL> rollback; Rollback complete 更改脚本,添加了如下代码where exists (select 1 from ljb_test2 where ljb_test1.id1=ljb_test2.id1), 确保更新的结果集是ljb_test2的id1列在ljb_test1中的id1列中都能找到的结果集。避免了空值问题! SQL> update ljb_test1 set id2=(select id2 from ljb_test2 where ljb_test1.id1=ljb_test2.id1) where exists (select 1 from ljb_test2 where ljb_test1.id1=ljb_test2.id1); 2 rows updated 接着查看,发现ljb_test1记录被正确更新了,这下空值记没丢失了,仍然是三条记录。 SQL> select * from ljb_test1; ID1 ID2 --------------------------------------- -------------------------------------- 1 27 2 28 18 SQL> select * from ljb_test2; ID1 ID2 --------------------------------------- --------------------------------------- 1 27 2 28 29 NULL空值会造成表更新过程中数据的丢失,要千万小心操作! 80 3.7.2NULL=NULL Null值=Null值吗?有的人回答说Null=Null,有的人说Null<>Null,到底是等还是不等?先做 实验看看 Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb SQL> select * from dual ; DUMMY ----- X SQL> select * from dual where 1=1; DUMMY ----- X SQL> select * from dual where 1=2; DUMMY ----- 通过上述操作知道,可以用dual来证明一下Null到底等于不等于Null 先看看是否相等? SQL> select * from dual where null=null; DUMMY ----- 看来是不等,回答不等的朋友高兴了,继续实验如下 SQL> select * from dual where null<>null; DUMMY ----- 这下大家都有点摸不着头脑了,怎么条件也是假?也就是说在ORACLE中null=null或者 null<>null都不是对的! 再看下面的写法 SQL> select * from dual where null is null; DUMMY ----- X 看来也不是不完全不等的! 这里这样理解才是正确的:―ORACLE把NULL值看成了一个未知的东西!‖这点很特 殊,在SQL Server和Sybase等其他版本的数据库中不是这样的,他们认为NULL=NULL是正确的, 因此要特别留意ORACLE在NULL上的特殊性,再回到前一小节中丢失数据看看,应该更加深刻的 明白了为什么会丢失的原因,如果是SQL Server,前面的更新是不会丢失数据的,只是空值列对应的id2列会被test2表的id1为空的id2的值更新! 3.7.3NULL 继续做实验如下,还是null值,不过这回null值出现在id2列了。 Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 81 Connected as ljb 查看表记录情况如下: SQL> select * from ljb_test1; ID1 ID2 ------------------------------------------------------------------ 3 1 17 2 18 3 16 SQL> select * from ljb_test2; ID1 ID2 ------------------------------------------------------------------- 5 1 27 2 28 仍用欠考虑空值的脚本进行更新: SQL> update ljb_test1 set id2=(select id2 from ljb_test2 where ljb_test1.id1=ljb_test2.id1); 4 rows updated 查看结果,发现更新没有导致丢失了数据,却是更新出问题了,把ljb_test1表原来的id1=3, id2=16的记录更新为id2为null了,这个显然不是要的结果!为什么会这样呢? 由于在ljb_test2中不存在id1字段值为3的记录.这样在UPDATE的时候,ljb_test2表中找不到 就会用null去UPDATE t1表id1=3对应的id2列的值,于是值由16更新为null SQL> select * from ljb_test1; ID1 ID2 ------------------------------------------------------------------- 3 1 27 2 28 3 SQL> select * from ljb_test2; ID1 ID2 -------------------------------------------------------------------- 5 1 27 2 28 回退程序 SQL> rollback; Rollback complete 用改进后的脚本执行如下 SQL> update ljb_test1 set id2=(select id2 from ljb_test2 where ljb_test1.id1=ljb_test2.id1) where exists (select 1 from ljb_test2 where ljb_test1.id1=ljb_test2.id1); 2 rows updated 现在才是正确的结果,没有出现更新错误的情况了! 82 SQL> select * from ljb_test1; ID1 ID2 --------------------------------------- -------------------------- 3 1 27 2 28 3 16 SQL> select * from ljb_test2; ID1 ID2 --------------------------------------- ---------------------------- 5 1 27 2 28 NULL空值会造成表更新过程中数据的更新错误,要千万小心操作! 3.7.4NULL 大家还记得我在前面讲索引易错点时说的count(*)用不到索引这小节吗? select count(*) from test 原来用不到索引,我加了―where object_id is not null‖后便用走索引了,后来我又换个思 路,对object_id改建主键后,索引也可以用到了,究其根本,就是因为索引不能存储NULL! 本小节我将简要的说明索引和NULL的关系 SQL> drop table ljb_test; Table dropped SQL> create table ljb_test (x int,y int); Table created SQL> create unique index idx_ljb_test on ljb_test(x,y); Index created SQL> insert into ljb_test values(1,1); 1 row inserted SQL> insert into ljb_test values(1,null); 1 row inserted SQL> insert into ljb_test values(null,1); 1 row inserted SQL> insert into ljb_test values(null,null); 1 row inserted 下面执行前面我多次使用过的analyze index idx_ljb_test validate structure;命令 SQL> analyze index idx_ljb_test validate structure; Index analyzed 查看索引存储的行数,很清楚的看明白了,当前索引只存储了三行,而实际有四行,未存储的 就是null的那行记录 SQL> select name,lf_rows from index_stats; NAME LF_ROWS ------------------------------ -------------------------------------------- 83 IDX_LJB_TEST 3 SQL> insert into ljb_Test values(1,null); insert into ljb_Test values(1,null) ORA-00001: 违反唯一约束条件 (LJB.IDX_LJB_TEST) 看来建了唯一索引后前面的数据不允许插入很正常,那试验一下插入null,null是什么情况? SQL> insert into ljb_Test values(null,null); 1 row inserted 最终的结果是发现插入成功了!有就是说ORACLE并不认为这个null,null值是重复插入过的,也就是说情况如我说的4.7.2小节的情况,oracle把索引当成了一种未知,也就是null既不等于null也不完全不等于null 最后执行一个统计语句,这下发现奇怪了,怎么在统计分类中,oracle又把null归为了一类,原来在oracle中考虑唯一性的时候(null,null)与(null,null不同,而聚合查询语句又认为是一样的了! 看来这个null要好好体会一下了。 SQL> select x,y,count(*) from ljb_test group by x,y having count(*)>1; X Y COUNT(*) --------------------------------------- --------------------------------------- 2 ORACLE的索引不能存储空值,这点大家通过实验应该有了比较深刻的体会!另外再 问大家一点,select * from test_ljb where object_id is null ,假如object_id有索引,这个索引能用到吗?一起研究了这么多,这个问题应该变的相当容易回答吧! 3.7小节分四点描述了NULL值要小心之处,大家是否还记得我之前在1.2.2 小节中描述的count(*)用不到索引的情况,这也是由于NULL的原因,紧接着1.2.3小节中的max()和min()描述中,虽然重点是说index full scan(max/min)的执行计划,但是我的代码中都有加is not null,否则也是根本不走索引的。 除此之外,NULL值还会影响执行计划,这点是最近几天我才明白的,实验操作如下: SQL> connect ljb/ljb; Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb SQL> alter system flush buffer_cache; System altered SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT ------------------------------------------------------------------------------------------- Plan hash value: 3547749009 ------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 3 | 57 | 6 (0)| 00:00:01 | |* 1 | FILTER | | | | | | | 2 | TABLE ACCESS FULL| DEPT | 4 | 76 | 2 (0)| 00:00:01 | |* 3 | TABLE ACCESS FULL| EMP | 3 | 39 | 2 (0)| 00:00:01 | ------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 84 1 - filter( NOT EXISTS (SELECT /*+ */ 0 FROM "EMP" "EMP" WHERE LNNVL("DEPTNO"<>:B1))) 3 - filter(LNNVL("DEPTNO"<>:B1)) 17 rows selected 为了公平起见,我在查询的时候都用alter system flush buffer_cache;把缓存清空一下!大家注意 执行计划中的这个FILTER,一般来说FILTER的效率要低于HASH JOIN ANTI ,大家以后可以记得 这个HASH JOIN ANTI 执行计划中的关键字。在本例中代价为6 SQL> alter system flush buffer_cache; System altered SQL> explain plan for select * from dept where deptno NOT IN ( select deptno from emp ) ; Explained 我将查询重新改写,加上NULL以后,发现执行计划完全改变,从FILTER改变为HASH JOIN ANTI ,并且代价由6变5,降低了。 SQL> explain plan for select * from dept where deptno NOT IN ( select deptno from emp where deptno is not null) and deptno is not null; Explained SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT ------------------------------------------------------------------------------------------- Plan hash value: 474461924 ------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 3 | 96 | 5 (20)| 00:00:01 | |* 1 | HASH JOIN ANTI | | 3 | 96 | 5 (20)| 00:00:01 | | 2 | TABLE ACCESS FULL| DEPT | 4 | 76 | 2 (0)| 00:00:01 | |* 3 | TABLE ACCESS FULL| EMP | 1 | 13 | 2 (0)| 00:00:01 | ------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - access("DEPTNO"="DEPTNO") 3 - filter("DEPTNO" IS NOT NULL) 16 rows selected 原因是这里涉及到查询重写query rewrite的概念,具体原因日后在整优化文档的时候另行总结,ORACLE博大精深,多研究总是会给带来惊喜的。! 3.8 char 写完上一小节后联想到了char这个数据类型,大家应该知道这个定长的数据类型,char(20)就 表示固定20个字节长度,如果你插入的值不足20个字节,就会用空格补充上,这里我要顺便说一下,空格并不等于NULL值,很多人把这个混为一谈,大家要注意! Char类型容易很容易在查询上出错,尤其是和VARCHAR2()混用的时候,具体错误构造如下: Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb 85 SQL> drop table ljb_test; Table dropped SQL> create table ljb_test (id1 char(20),id2 varchar2(20)); Table created SQL> insert into ljb_test ('oracle','oracle'); SQL> insert into ljb_test values ('oracle','oracle'); 1 row inserted Executed in 0.062 seconds 查询ljb_test表,记录如下 SQL> select * from ljb_test; ID1 ID2 -------------------- -------------------- oracle oracle 分别用id1=‘oracle‘和id2=‘oracle‘都能查到记录 SQL> select * from ljb_test where id1='oracle'; ID1 ID2 -------------------- -------------------- oracle oracle SQL> select * from ljb_test where id2='oracle'; ID1 ID2 -------------------- -------------------- oracle oracle 但是我们如果查询id1=id2的列居然发现查询不出记录! SQL> select * from ljb_test where id1=id2; ID1 ID2 -------------------- -------------------- 是空格导致的原因,只有用trim去除空格后,才能相等! SQL> select * from ljb_test where trim(id1)=id2; ID1 ID2 -------------------- -------------------- oracle oracle 大家明白了错误产生的原因了吧,char(20)必须存储20位,如果不足20位会用空格代 替,而varchar2(20)是存储实际的大小,所以id1=id2在大多数情况下是不会等的! 我觉的char完全不该出现在应用中,我们应该全部用varchar2(20)的方式来替代char(20),为 什么呢?我举三点原因:1、存储空间的节省,varchar2(20)如果实际只有4个字节,就只存储4个字 节,而char(20)如果只有4个字节也多占用16个字节大小。2、由于char()导致数据库表记录占用的块增多,那必然影响了数据库操作的性能,大家说在同等条件下操作大一点表快还是小一点的表快 呢?3、数据库中统计两列相等的情况还是很常见的,而varchar2()和char()字段的对等写法就很及易让人出错! 86 3.9 前面NULL的研究到此告一段落了,不过对于上个案例来说,除了NULL外,还有一个不得不说的地方,就是这样的UPDATE语句非常容易出现一个著名错误:ORA-01427: 单行子查询返回多行。大家看现在的ljb_test1表的id1列有重复记录,就是id1=2有两条记录 SQL> select * from ljb_test1; ID1 ID2 -------------------------------------------------------------------- 2 1 17 2 18 3 16 SQL> select * from ljb_test2; ID1 ID2 --------------------------------------- ------------------------------ 5 1 27 2 28 在ljb_test1中id1=2有两条重复记录,在ljb_test2中只有一条记录,这个时候如果利用ljb_test1去更新ljb_test2表,就立即报错如下: SQL> update ljb_test2 set id2=(select id2 from ljb_test1 where ljb_test2.id1=ljb_test1.id1) 2 where exists (select 1 from ljb_test1 where ljb_test2.id1=ljb_test1.id1); update ljb_test2 set id2=(select id2 from ljb_test1 where ljb_test2.id1=ljb_test1.id1) where exists (select 1 from ljb_test1 where ljb_test2.id1=ljb_test1.id1) ORA-01427: 单行子查询返回多个行 不考虑NULL的写法也一样! SQL> update ljb_test2 set id2=(select id2 from ljb_test1 where ljb_test2.id1=ljb_test1.id1); update ljb_test2 set id2=(select id2 from ljb_test1 where ljb_test2.id1=ljb_test1.id1) ORA-01427: 单行子查询返回多个行 注意到,如果两表完全一致,ljb_test1.id1和ljb_test2.id1一一对应,都是唯一的,那就不可能出现这样错误!如果两边不一致,ljb_test1的id1值多余ljb_test2的id1值或者反过来,就要特别注意了!讲白点就是,多的表允许用少的表来更新,被更新表可以不唯一,但是用来更新的表一定要是有唯一记录。 还要特别说明一点,如果需要更新的两边的结果集都有重复,那别管是利用ljb_test1更新ljb_test2还是利用ljb_test2更新ljb_test1,都休想成功了,要有这个意识! update操作的时候,要特别注意ORA-01427: 单行子查询返回多个行 的错误,两表更新的时候尽量是两表结果集都是不重复记录,如果有重复记录要注意更新的方向。 3.10 Update 前面我讲述了insert 多表插入,用一个简单的sql实现了过程中数条sql语句实现的功能,大家还有印象吧。再考考大家,如下sql单条语句如何实现? SQL> select * from ljb_test; ID1 ID2 ID3 87 ---- ------------------ -------------------------------------------------------------- 1 2 1 2 6 2 2 3 3 该表ljb_test有如下业务需求,即下面三个业务逻辑用单条SQL语句实现 update ljb_test set id2=id1+id2 where id3=1; update ljb_test set id2=id2- id1 where id3=2; update ljb_test set id2=id1*id2 where id3=3; 也就是,最终要查出如下的结果 ID1 ID2 ID3 ---- --------------------- ------------------------------------------------------------ 1 3 1 2 4 2 2 6 3 其实平时就应该有强烈的简化sql代码的意识,一般来说越简洁的sql效率越高,事实上写出冗 长复杂sql的人一般对ORACLE认识较肤浅,真正高手总愿意思考如何简化代码,利用自己丰富的 知识和善于动脑的优点,写出优雅简洁的代码。本节我内容来源项目组的一位同事的提问,在我的 提示下他利用case语句最终写出了单条sql实现的脚本,不过他漏考虑了一个陷阱。 以下是同事想到的脚本写法 update ljb_test set id2=(select (case when id3=1 then id1+id2 when id3=2 then id2-id1 when id3=3 then id1*id2 end) from ljb_test); 好象没什么问题,不过一执行,发现报如下错误无法执行: ORA-01427: 单行子查询返回多个行 这个报错是不是很眼熟啊,不错,正是上一节讲过的内容,看来更新语句中,这样的陷阱比比 皆是啊!查出多行数据而更新单条,咋办,加上条件限制就OK了 更改代码后执行如下: SQL> update ljb_test t1 set id2=( 2 select (case when id3=1 then id1+id2 when id3=2 then id2-id1 when id3=3 then id1*id2 end) from ljb_test t2 where t1.id3=t2.id3); 3 rows updated 查询结果,成功了! SQL> select * from ljb_test; ID1 ID2 ID3 ---- --------------------- ------------------------------------------------------------ 1 3 1 2 4 2 2 6 3 到此本小节要结束了,本节目的一来是强调要有写单条简单SQL的意识,二来是再次提醒了更 新中返回多行的01427错误,希望引起大家的注意,另外大家要善于思考,不断深入研究,比如如 无需更新表记录,又要查出这样的结果,该怎么办呢?方法如下: 先将数据回滚 SQL> rollback; Rollback complete 执行了如下脚本查询后,发现该查询内容正是要的结果!这点也值得注意,有时要实现某种报 88 表需求,但是又不允许改变基表,就可以采用类似这样的构造的方式,要有构造的意识,大家好好 体验一下! SQL> select id1,decode(id3,1,id1+id2,2,id2-id1,3,id1*id2),id3 from ljb_test; ID1 DECODE(ID3,1,ID1+ID2,2,ID2-ID1 ID3 ---- ------------------------------ ----------------------------------------------------------------------------------- 1 3 1 2 4 2 2 6 3 只要大家善于动脑善于学习利用各种知识,完全可以写出简洁的代码,提高性能避免错 误! 3.11 SQL 本小节要讲述的内容很有普遍性,无论计费还是结算的procedure中,处处可见动态SQL的影子,什么时候考虑使用动态SQL呢?可以这么说,在非动态SQL无法实现该逻辑时,才考虑选择 动态SQL,比如SQL语句的某些字段是靠变量传入的,在这种情况下,用动态SQL来实现是别无他法了。 我一直强调别无选择才在procedure中使用动态SQL,是因为考虑到了性能。我曾问周围同事 procedure中动态SQL和普通SQL相比性能如何?回答不知道的有,回答动态SQL性能更高的居然也有!这里我特别强调一下,动态SQL的性能要远低于普通SQL,因为procedure的动态SQL在执行的时候才编译,而procedure中的普通SQL是在procedure建好同时就编译,执行的时候省略了编 译时间,性能当然更高! 曾见到有同事在procedure中写类似如下代码: execute immediate 'delete from test'; 这样的代码完全不必要!因为该procedure中替换execute immediate 'delete from test';为 delete from test;后,procedure依然可执行,那为什么要用动态SQL呢?二者的性能我比较过,后者好的多, 用的正是3.5小节中提到过的runstas包,具体就不将实验过程贴出了! 在的工作中要尽量避免动态SQL,寻找可以用静态SQL替代动态SQL的机会,减少程序处理中的编译过程,从而提高效率! 3.12 很多人不是太清楚数据库中函数function与过程procedure的差别,大家在工作中一般是procedure用的多而function用的少,在问及差别的时候,有人回答说是函数要有RETURN返回值,而我反问过程不是也有返回值OUT,并且还可以做为另一过程的输入的时候,却答不上来。还有人 回答是函数一定要有返回值,而过程却不一定需要。这样的回答算基本正确。不过我最想听到的回 答是另一点:―调用的区别‖。就是函数可在SQL语句中直接调用,大家想想 to_number(),to_char(),to_date(),max(),min(),count(*),upper()等等ORACLE自带函数的时候其实就明白了,其实也能写出自定义函数如to_number_myself()等等在SQL中直接使用,过程procedure当然不能做到这样,这才是函数function最重要的并且和过程procedure区别最明显的地方! 其实如果善于动脑,函数function还是很强大的,我提一个小小需求,看看大家如何实现?就 是:―如何统计数据库下某一用户(如LJB)下的所有的表的数据的记录数‖? 89 听起来有第点难实现,很多人可能想到在过程里实现输出结果,但是思路一般比较繁琐,这里 列出我用函数实现的方案,非常的简单! SQL> connect ljb/ljb; Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb 建立一个统计记录的自定义函数f_ljb_count_tab SQL> create or replace function f_ljb_count_tab (p_tab in varchar2) 2 return number 3 is 4 l_cnt number; 5 begin 6 execute immediate 'select count(*) from ' || p_tab into l_cnt; 7 return l_cnt; 8 end; 9 / Function created 然后开始调用,结果出来了,很简单吧! SQL> select table_name as 表名, f_ljb_count_tab(table_name) as 记录 from dba_tables where owner='LJB'; 表名 记录数 ------------------------------ ----------------------------------------------------------------- LJB_PARALLEL 375864 LJB_PARALLEL2 62644 ADFSADFD 0 C 0 MERGE_1 9 MERGE_2 16 LJB_TEST 100001 ORACLE中函数和过程中最本质的差别应该在调用方式上的差别,这点大家一定要牢记! ORACLE中有很多系统自带函数,功能都很强大,调用都很方便,应该受这方面的启发,尝试写一 些自定义函数实现某些功能后在SQL中调用,使代码简化。不过这里还有一点要特别注意,在SQL 中调用自定义函数,一定要注意调用的次数,如果调用次数太多,会对性能造成一定影响,而且要 避免函数调函数套太多层,这里要合理设计。比如本文我提的这个需求在生产中几乎是无法实现的, 因为生产中用户下表可能有几千张,其中最大的记录可能有几十亿,要统计出结果来没个几天几夜 是不行的。我本机数据库LJB用户下表少,记录少,所以很快就统计出来了,这只是一个思路,为 了让大家记住函数的好处,这个脚本千万别在生产中执行,否则这个结束不了的count(*)会很容易拖 跨系统的。 3.13 以前比较少接触树形查询的例子(start with connect by),到宁夏做结算系统建设的时候,因为 接触到报表,需要分析展现的各种原子树指标树的父子关系,就开始大量接触这样的查询写法了, 这里我把使用中容易出错的地方总结出来,本人是吃过亏了,希望大家注意。 还是开始构造例子,并实验如下: 90 SQL> connect ljb/ljb; Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb SQL> drop table ljb_test; Table dropped SQL> create table ljb_test( 2 id1 number(8), 3 id2 varchar2(20), 4 id3 number(8), 5 flag number 6 ); Table created SQL> insert into ljb_test values(1,'aaa',0,0); 1 row inserted SQL> insert into ljb_test values(11,'aaa1',1,0); 1 row inserted SQL> insert into ljb_test values(12,'aaa2',1,0); 1 row inserte SQL> insert into ljb_test values(111,'aaa11',11,0); 1 row inserted SQL> insert into ljb_test values(112,'aaa12',11,0); 1 row inserted SQL> insert into ljb_test values(121,'aaa21',12,0); 1 row inserted SQL> insert into ljb_test values(122,'aaa22',12,0); 1 row inserted SQL> insert into ljb_test values(1,'aaa',0,1); 1 row inserted SQL> insert into ljb_test values(11,'aaa1',1,1); 1 row inserted SQL> insert into ljb_test values(12,'aaa2',1,1); 1 row inserted SQL> insert into ljb_test values(111,'aaa11',11,1); 1 row inserted SQL> insert into ljb_test values(112,'aaa12',11,1); 1 row inserted SQL> insert into ljb_test values(121,'aaa21',12,1); 1 row inserted SQL> insert into ljb_test values(122,'aaa22',12,1); 1 row inserted SQL> select * from ljb_test; ID1 ID2 ID3 FLAG --------- -------------------- --------- ------------------------------------------- 1 aaa 0 0 91 11 aaa1 1 0 12 aaa2 1 0 111 aaa11 11 0 112 aaa12 11 0 121 aaa21 12 0 122 aaa22 12 0 1 aaa 0 1 11 aaa1 1 1 12 aaa2 1 1 111 aaa11 11 1 112 aaa12 11 1 121 aaa21 12 1 122 aaa22 12 1 我如果脚本编写如下,大家查看得到了正确的结果,看起来是否很舒服,各个父子关系展现的 井井有条,但是大家注意到没,我的flag=0是写了三处地方: SQL> select a.*,level,sys_connect_by_path(a.id1 ,'\') id1_path,sys_connect_by_path(a.id2, '\') id2_path 2 from ljb_test a 3 where flag=0 4 start with id3=0 5 and flag=0 6 connect by id3 = prior id1 7 and flag=0; ID1 ID2 ID3 FLAG LEVEL ID1_PATH ID2_PATH --------- -------------- ---------- ---------- ------------- ------------------------------------------------------ 1 aaa 0 0 1 \1 \aaa 11 aaa1 1 0 2 \1\11 \aaa\aaa1 111 aaa11 11 0 3 \1\11\111 \aaa\aaa1\aaa11 112 aaa12 11 0 3 \1\11\112 \aaa\aaa1\aaa12 12 aaa2 1 0 2 \1\12 \aaa\aaa2 121 aaa21 12 0 3 \1\12\121 \aaa\aaa2\aaa21 122 aaa22 12 0 3 \1\12\122 \aaa\aaa2\aaa22 7 rows selected 只要有一处没写,结果都是重复不对的,大家看这个查询明显有重复记录如3 \1\11\111,总 记录数也由7条变为11条。这里大家要切记,条件在三处都要写! SQL> select a.*,level,sys_connect_by_path(a.id1 ,'\') id1_path,sys_connect_by_path(a.id2, '\') id2_path 2 from ljb_test a 3 where flag=0 4 start with id3=0 5 and flag=0 6 connect by id3 = prior id1; 92 ID1 ID2 ID3 FLAG LEVEL ID1_PATH ID2_PATH ----- -------------- ---------- ---------- -------------------- ------------------------------------------------------------ 1 aaa 0 0 1 \1 \aaa 11 aaa1 1 0 2 \1\11 \aaa\aaa1 111 aaa11 11 0 3 \1\11\111 \aaa\aaa1\aaa11 112 aaa12 11 0 3 \1\11\112 \aaa\aaa1\aaa12 12 aaa2 1 0 2 \1\12 \aaa\aaa2 121 aaa21 12 0 3 \1\12\121 \aaa\aaa2\aaa21 122 aaa22 12 0 3 \1\12\122 \aaa\aaa2\aaa22 111 aaa11 11 0 3 \1\11\111 \aaa\aaa1\aaa11 112 aaa12 11 0 3 \1\11\112 \aaa\aaa1\aaa12 121 aaa21 12 0 3 \1\12\121 \aaa\aaa2\aaa21 122 aaa22 12 0 3 \1\12\122 \aaa\aaa2\aaa22 11 rows selected 如果start with id3=0后面的and flag=0也没写,那就重复更多了,这里我就不再展现了,只把记 录数展现给大家看 SQL> select count(*) 2 from ljb_test a 3 where flag=0 4 start with id3=0 5 connect by id3 = prior id1; COUNT(*) ------------------------- 21 通过这个例子看出在写树形查询语句的时候,条件最好要在start with前,start with 后和 connect by 后三处都要写上,这样才能有的避免重复展现记录,另外就我的结算项目经历来看,熟 练使用树型查询是非常重要的,我想计费系统中应该也是如此。利用该查询可以方便开展各种帐目 属性的配置工作,给工作带来很大的方便! 3.14 sequencecache 开发中使用sequence是极其常见的,使用中要注意什么呢?特别想说的就是sequence是产生 一系列连续的不重复数字,但是这个序列很容易产生断号,尤其是在有cache的时候,如果应用 有特别要求,一定要中间不丢连续号,那就要小心了。具体演示如下: SQL> connect ljb/ljb Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb 建立一个sequence 并设置cache为20 SQL> create sequence ljb_test_seq 2 minvalue 1 3 maxvalue 1000 4 start with 1 5 increment by 1 6 cache 20 93 7 order; Sequence created 刚刚创建的序列必须先用 nextval 来获取一个初始值,否则会提示08002错误 SQL> select ljb_test_seq.currval from dual; ORA-08002: 序列 LJB_TEST_SEQ.CURRVAL 尚未在此会话中定义 SQL> select ljb_test_seq.nextval from dual; NEXTVAL --------------------- 1 SQL> select ljb_test_seq.currval from dual; CURRVAL ----------------------- 1 SQL> alter system flush shared_pool; System altered 内存清空后查看当前的序列还是1 SQL> select ljb_test_seq.currval from dual; CURRVAL ---------- 1 但是我们惊奇的发现下一个序列居然不是2,而是21,清空内存后丢了20个连续号码 SQL> select ljb_test_seq.nextval from dual; NEXTVAL ---------- 21 再确认一下,再次清空内存! SQL> alter system flush shared_pool; System altered 发现当前的序列为21 SQL> select ljb_test_seq.currval from dual; CURRVAL ---------- 21 不过下一个序列变成是41而不是22了,清空内存后又丢了20个 SQL> select ljb_test_seq.nextval from dual; NEXTVAL --------------- 41 通过这个实验我们明白了,sequence的cache原来是把后面要用的序列先保存到cache 中缓存起 来,以便下一次从内存中读出来,加快速度!cache 20就是缓存20个,而我这没次都用清空内存的 方式,所以导致序列不断丢失! 还有什么情况会丢失连续号呢?继续实验如下,现在我们不再使用alter system flush shared_pool 了,看看怎么丢失连续号码的: 先建一测试表,让序列插入表中 94 SQL> drop table ljb_test; Table dropped SQL> create table ljb_test(id1 int); Table created 插入如下几条记录 SQL> insert into ljb_test values (ljb_test_seq.nextval); 1 row inserted SQL> insert into ljb_test values (ljb_test_seq.nextval); 1 row inserted SQL> insert into ljb_test values (ljb_test_seq.nextval); 1 row inserted 查询表ljb_test,发现确有记录插入,并且都是连续号码 SQL> select * from ljb_test; ID1 ------------------------------ 42 43 44 SQL> commit; Commit complete 继续再插入一条,发现还是连续的,没问题! SQL> insert into ljb_test values (ljb_test_seq.nextval); 1 row inserted SQL> select * from ljb_test; ID1 -------------------------- 42 43 44 45 如果我们这个时候回滚,发现序号不再连续了! SQL> rollback; Rollback complete SQL> insert into ljb_test values (ljb_test_seq.nextval); 1 row inserted SQL> select * from ljb_test; ID1 --------------------------------------- 42 43 44 46 此外如果插入失败,序号也不会连续!这里就不再构造例子了。 通过上述实验,我们可以明白,其实sequece要保证连续中间不断号是很困难的,如果 95 我们是采用cache的形式,重启数据库或者内存被清空等情况都会导致缓存在cache中的连续号被清空,就产生断号了,其实即便我们不设置cache,比如设置了nocache的关键字,一样会丢失,前面 说过,回滚或者插入失败都会导致序列断号,这可和cache无关啊。因此这里我想说的是如果你要 保证你的应用的该列是绝对要连续的,考虑sequence是很难保证的!即便nocache都不行!要考虑编写代码自己来解决问题了。 但是实际应用中,90%以上的序列都不会绝对要求是连续不中断的,此时sequence+cache就发挥了巨大的作用了。我的观点是没有特别的不断号需求,最好都使用sequence,并且加上cache,并且尽量不要太小,当然值太大也浪费,我们测试的结果是一般不要超过300,再往上性能提升不明显了。带cache的sequence能带来两个好处:1、速度能提高,2、RAC环境可避免争用,两个实 例可各自缓存一部分sequence值。我记得在环境组上班的时候曾经和同事一起优化过sequence,就是要求开发将所有的带sequence的脚本都加上cache ,后来测试发现对表的插入速度有了显著的提 高,并且也缓解了RAC环境的争用。 此外我还想说一点,既然sequence+nochache 也不能绝对保证不断号,这个nocache也没什么用了,所以sequece指定nocache的方式可以抛弃掉了。 4 4.1 TNS 宁夏出差期间半夜接到一兄弟的求救电话,告之所有和ORACLE数据库连接的客户端都连接失 败,运用程序全部挂起,提示为监听程序无法识别请求。我询问之前做过何操作,电话回答刚刚因 为参数调整重启了主机,数据库和监听服务也因此而重启,不过启动过程很正常,无任何错误提示! 听完描述后我又向他确认是数据库先启还是监听先启,回答是数据库先启,监听后启。至此我 猜到了问题所在,叫他尝试停掉所有应用,先关闭数据库,重启监听后再启动数据库看看,结果问 题立即解决,所有客户端都正常连上数据库,程序运行正常。 这究竟是怎么回事呢? 问题出在ORACLE的listener.ora中的配置上,当前该用户使用的是动态注册方式(即listener被动加载服务),由实例的pmon进程在listener中注册服务,这样在数据库启动的时候如果监听未启, pmon则寻找不到listener,自然注册失败,因此我提出让他换个顺序先启动监听后启动数据库,这 样数据库在实例启动后pmon可找到linstener,注册服务成功,也就OK了。 当然,解决这个连接故障还有别的方法,由于当时我无法到现场,否则可以无须停应用, 无须重启动数据库,只要将listener.ora中的SID_LIST_LISTENER模块增加类似如下部分,然后 重启监听,问题也能得到解决! (SID_DESC = (SID_NAME = LJB) (ORACLE_HOME = C:\oracle\product\10.2.0\db_1) ) 这是什么原理呢?这里修改其实是将其由动态组注册模式(即listener被动加载服务)修改为静态注册模式(即listener主动加载服务),静态注册就是实例启动时读取listener.ora文件的配置,将实例和服务注册到监听程序(SID_NAME=LJB)。无论何时启动一个数据库,默认地都有两条信息注册到监听器中:数据库服务器对应的实例和服务。这样就无须停应用改变数据库和监听的启动顺序来解决问题了。更改为静态注册模式后,无论数据 96 库还是监听谁先启都一样了! ORACLE中的动态注册和静态注册是监听的两种模式,希望大家能对此加深印象, 避免客户端连接失败等错误发生! 4.2 db_recovery_file_dest_size 该参数是闪回恢复区,主要在10g中使用,由于这个只是和闪回恢复有关系,与传统备份恢复无关,很可能引起大家的忽视,没有对该区域大小做正确合理的规划,从而导致生产挂起,用户无 法登陆的情况。 这里举例子如下: 登陆数据库后,发现数据库只能进入MOUNT状态,无法打开数据库 C:\Documents and Settings\fujitsu>sqlplus "/ as sysdba" SQL*Plus: Release 10.2.0.1.0 - Production on 星期日 3月 8 20:35:45 2009 Copyright (c) 1982, 2005, Oracle. All rights reserved. 连接到: Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 - Production With the Partitioning, OLAP and Data Mining options SQL> select name from dba_users; select name from dba_users * 第 1 行出现错误: ORA-01219: 数据库未打开: 仅允许在固定表/视图中查询 SQL> alter database open; alter database open * 第 1 行出现错误: ORA-16014: 日志 1 的序列号 791 未归档, 没有可用的目的地 ORA-00312: 联机日志 1 线程 1: 'C:\ORACLE\PRODUCT\10.2.0\ORADATA\RQRQ\REDO01.LOG' SQL> show parameter db_recovery_file NAME TYPE VALUE ------------------------------------ ----------- ------------------------------------------------------------------------ db_recovery_file_dest string C:\oracle\product\10.2.0/flash_recovery_area db_recovery_file_dest_size big integer 2G 检查flash recovery area的使用情况: SQL> select * from v$flash_recovery_area_usage; FILE_TYPE PERCENT_SPACE_USED PERCENT_SPACE_RECLAIMABLE NUMBER_OF_FILES ------------ ------------------ ------------------------- ---------------------------------------------------------------------------- CONTROLFILE 0 0 0 ONLINELOG 0 0 0 ARCHIVELOG 71.07 0 62 BACKUPPIECE 0 0 0 97 IMAGECOPY 0 0 0 FLASHBACKLOG 0 0 0 已选择6行。 计算flash recovery area已经占用的空间: SQL> select sum(percent_space_used)*3/100 from v$flash_recovery_area_usage; SUM(PERCENT_SPACE_USED)*3/100 ----------------------------------------------------------- 2.1321 可以看到,这里已经有2.1321G使用了,说明刚开始设置的db_recovery_file_dest_size=2G不足,导致online redo log无法归档,在这里,通过设置db_recovery_file_dest_size参数,增大了flash recovery area来解决这个问题。当然也可以通过删除flash recovery area中不必要的备份来释放flash recovery area空间来解决这个问题:delete obsolete; crosscheck backupset; delete expired backupset; SQL> alter system set db_recovery_file_dest_size=3G scope=both; 系统已更改。 问题解决,数据库终于可以打开了 SQL> alter database open; 数据库已更改。 由于db_recovery_file_dest_size参数和闪回有关,和传统的备份与恢复关系不大,所以 容易被人忽略,但是如果空间满将会导致数据库整体出错,后果也很严重,必须要进NOMOUT模 式下增加db_recovery_file_dest_size的大小才可以,我在我的笔记本上装了10g测试数据库,最近这一段时间来由于准备本集锦做了很多实验,导致该空间增长很快,居然一周出了3次错,所以现实应用中大家应该小心! 大家是否还记得前面我描述过闪回整个数据库找回被truncate的记录,其实依赖的就是这个恢复区,如果这个恢复区的数据被删除的情况下,闪回数据库就不可能实现了,如果恢复 区越大,能闪回的的时间点越早。 4.3. 4.3.1 对数据库比较熟悉的朋友,特别是DBA,在遇到数据性能问题时候,首先想到的是通过某些方 法,找到有问题的SQL,然后对其SQL语句进行分析研究,查出性能上的根本原因,比如执行计划 错误等等。这时候有的心急的DBA就立即动手开始考虑对其表和索引进行信息重新采集等工作,认 定是由于ORACLE没有收集到正确的信息走了错误的执行计划! 对于定位到有问题的SQL,查看后认为其执行计划不正确后,要保持冷静的头脑,不要急于动 手,比如通过查看生产中代码的执行计划,发现某张大表比如计费的INVOICE表,走了全表扫描,但是该表上有选择性很高索引没用上,该怎么办呢?这个时候分析表是可以解决问题,但是要想到, 生产中一张大表要分析,可能会对系统造成比较大的影响,比如analyze会排序,会占用资源,即便不考虑这样的问题,大表分析的时间也要很长,比如要一两个小时采样完毕,那系统不是要过一两 个小时才能恢复正常吗? 除了分析以外,再能想到的是给SQL语句加HINT,不过这个也不可取,因为这个意味着改代 码,正常情况下程序改代码是要经过严格的流程,从自测到外测到申请上线到最后上线 ,所以这样的方式不可取,即便你把你定位出来的SQL语句加了HINT后直接放在命令下执行发现很快,非常 98 有把握也是不允许的。 其实改变执行计划除了分析表重获采样信息,加HINT外,还有第三种方法,就是通过dbms_stats.SET_table_stats这个过程来骗过ORACLE,比如执行如下:EXEC dbms_stats.SET_table_stats('BILL','INVOICE',numrows => 20000000,numblks => 4000000);现在等于告 诉ORACLE,这张invoice表是一张超级大表,全表扫描是不可取的!于是执行计划改变了,问题 解决了!事后,可以选择在晚上的时间将该表做一个重新采样分析,这个时候由于是深夜业务不繁 忙,也不会投诉,上述的担忧就都不会有了,通过分析,ORACLE真正获取正确的采样信息,走了正确的执行计划,一切就都OK了! 总结:数据库故障处理中,如果发现是定位到的SQL语句执行计划有问题,大表需要重新采样的,要冷静,考虑避免在生产中立即进行分析,也不可考虑改代码加HINT,可以考虑用 dbms_stats.SET_table_stats临时骗过ORACLE的方法来灵活的解决,然后在业务低峰期再考虑用分 析表的方法彻底解决问题。 4.3.2 上一小节说了生产中发现大表要重新采样的时候要冷静处理,可以考虑 dbms_stats.SET_table_stats临时骗过ORACLE的方法来灵活的方法来解决问题,但是到了开始分析 表收集采样信息的时候,还是要谨慎处理,因为你数据库环境是由多方面共同决定的,比如一些参 数如optimizer_index_cost_adj、optimizer_index_caching 等等,也许采样分析并不是解决问题的真正原因,你重新采样后可能采样收集的比率不如原来的准确,导致性能更糟糕,这个时候就要考虑将 采样信息恢复回去,这种情况还是比较经常的,我认为DBA在分析采样的时候先备份是非常重要的,这个也是非常容易出错的地方! 下面我用一系列步骤来说明采样的方法和注意点,这个我认为非常重要,是优化的前提条件, 是基础! 4.3.2.1 这里我讲一下如何备份和恢复表的收集信息,现实中很使用,比如你在生产中发现表查询很慢, 这个时候你可能发现是表未分析或者收集的信息太旧了,你要重新分析表,但是有一种情况是,你 分析表后,可能导致系统更慢,和本身的判断错误啊,环境因数啊有关系,因此这个时候最好在做之前 先备份表和索引的分析信息,出了问题后,可以回退到老的收集情况 现在做实验如下,可以考虑模仿操作一遍,加深印象. Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb SQL> drop table test; Table dropped 建测试表TEST SQL> create table test as select * from dba_objects; Table created 发现该表并没有分析过! SQL> select to_char(last_analyzed,'yyyymmdd hh24:mi:ss'),num_rows,blocks,sample_size from user_tables wh ere 2 table_name='TEST'; 99 TO_CHAR(LAST_ANALYZED,'YYYYMMD NUM_ROWS BLOCKS SAMPLE_SIZE ------------------------------ ---------- ---------- ------------------------------------------------------------- 首先进行收集表的信息 SQL> exec dbms_stats.gather_table_stats(ownname => 'LJB',tabname => 'TEST',estimate_percent => 80,cascad e => true); PL/SQL procedure successfully completed 查看收集成功 SQL> select to_char(last_analyzed,'yyyymmdd hh24:mi:ss'),num_rows,blocks,sample_size from user_tables wh ere 2 table_name='TEST'; TO_CHAR(LAST_ANALYZED,'YYYYMMD NUM_ROWS BLOCKS SAMPLE_SIZE ------------------------------ ---------- ---------- ------------------------------------------------------------- 20090305 13:27:11 62714 899 50171 接着将收集的信息放在特定的表中,这里我取名为LJB_COLLECT_STAT_TAB,在输入下面命令后表会自动生成 SQL> exec dbms_stats.create_stat_table(ownname => 'LJB',stattab => 'LJB_COLLECT_STAT_TAB'); PL/SQL procedure successfully completed 再建另一表TEST2,目的何在,在后面讨论 SQL> create table test2 as select * from dba_objects; Table created TEST2也是未分析过 SQL> select to_char(last_analyzed,'yyyymmdd hh24:mi:ss'),num_rows,blocks,sample_size from user_tables wh ere table_name='TEST2'; TO_CHAR(LAST_ANALYZED,'YYYYMMD NUM_ROWS BLOCKS SAMPLE_SIZE ------------------------------ ---------- ---------- ----------- 顺便查看一下我刚才建的LJB_COLLECT_STAT_TAB,发现结构已经生成了,不过尚无记录 SQL> select * from ljb_collect_stat_tab; STATID TYPE VERSION FLAGS C1 C2 C3 C4 C5 N1 N2 N3 N4 N5 N6 N7 N8 N9 N10 N11 N12 D1 R1 R2 CH1 ------------------------------ ---- ---------- ---------- ------------------------------ ------------------------------ ------------------------------ ------------------------------ ------------------------------ ---------- ---------- ---------- ---------- ---------- ------- 执行收集信息的导出命令,其实就是将信息导出到LJB_COLLECT_STAT_TAB表中 SQL> exec dbms_stats.export_table_stats(ownname => 'LJB',tabname => 'TEST',STATTAB =>'LJB_COLLEC T_STAT_TAB'); PL/SQL procedure successfully completed 查看LJB_COLLECT_STAT_TAB表,果然有了记录了 SQL> select count(*) from LJB_COLLECT_STAT_TAB; COUNT(*) ------------------- 15 接着用dbms_stats.delete_table_stats命令删除前面用dbms_stats.gather_table_stats收集到的信息 100 SQL> exec dbms_stats.delete_table_stats(ownname => 'LJB',tabname => 'TEST'); PL/SQL procedure successfully completed 查询果然没有了收集信息了 SQL> select to_char(last_analyzed,'yyyymmdd hh24:mi:ss'),num_rows,blocks,sample_size from user_tables wh ere 2 table_name='TEST'; TO_CHAR(LAST_ANALYZED,'YYYYMMD NUM_ROWS BLOCKS SAMPLE_SIZE ------------------------------ ---------- ---------- ----------- 这个时候可以开始考虑将备份的收集信息还原了 SQL> exec dbms_stats.import_table_stats(ownname => 'LJB',tabname => 'TEST',stattab => 'LJB_COLLECT_S TAT_TAB'); PL/SQL procedure successfully completed 经查看,果然,刚才被删除的收集信息又恢复了! SQL> select to_char(last_analyzed,'yyyymmdd hh24:mi:ss'),num_rows,blocks,sample_size from user_tables wh ere 2 table_name='TEST'; TO_CHAR(LAST_ANALYZED,'YYYYMMD NUM_ROWS BLOCKS SAMPLE_SIZE ------------------------------ ---------- ---------- ----------- 20090305 13:27:11 62714 899 50171 再考虑一下,TEST表的收集信息能否给TEST2表用呢,让证明一下 SQL> exec dbms_stats.import_table_stats(ownname => 'LJB',tabname => 'TEST2',stattab => 'LJB_COLLECT_ STAT_TAB'); PL/SQL procedure successfully completed 查看TEST2表,发现还上老样子,说明特定表的分析信息只能给特定的表使用 SQL> select to_char(last_analyzed,'yyyymmdd hh24:mi:ss'),num_rows,blocks,sample_size from user_tables wh ere 2 table_name='TEST2'; TO_CHAR(LAST_ANALYZED,'YYYYMMD NUM_ROWS BLOCKS SAMPLE_SIZE ------------------------------ ---------- ---------- ----------- LJB_COLLECT_STAT_TAB表记录是否丢失了,还在吗?查看如下,发现记录仍存在 SQL> select count(*) from LJB_COLLECT_STAT_TAB; COUNT(*) ------------------ 15 做完这个步骤后继续思考,那索引呢,是否索引能跟着进行备份,还有,如果要备份整个用户的所有对象的表和索引的收集信息,可以做到吗?让后续再来研究,请听下回分解! 4.3.2.2 前面说到对单表的信息进行收集,做之前对其表信息进行备份很重要,然后详细的描述了如何进行备份的步骤,已经进行恢复的方法。 前面提到了索引可以直接进行统计行吗,这个问题放这里进行研究讨论。 Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 101 Connected as ljb 先删除之前建立的收集统计表ljb_collect_stat_tab SQL> drop table ljb_collect_stat_tab; Table dropped 对test表插入数据 SQL> insert into test select * from test; 62617 rows inserted SQL> insert into test select * from test; 125234 rows inserted 对test增加索引idx_test SQL> create index idx_test on test(object_id); ---这里注意一下,DDL语句就是自动提交,所以前面的 INSERT也就无需提交了,这个要注意! Index created 利用dbms_stats.gather_table_stats来统计收集信息 SQL> exec dbms_stats.gather_table_stats(ownname => 'LJB',tabname => 'TEST',estimate_percent => 10,cascad e => true); PL/SQL procedure successfully completed 查看,表已经分析 SQL> select to_char(last_analyzed,'yyyymmdd hh24:mi:ss'),num_rows,sample_size from user_indexes where ta ble_name='TEST'; TO_CHAR(LAST_ANALYZED,'YYYYMMD NUM_ROWS SAMPLE_SIZE ------------------------------ ---------- -------------------------------------------------------- 20090305 14:49:27 250428 250428 查看索引也已分析 SQL> select to_char(last_analyzed,'yyyymmdd hh24:mi:ss'),num_rows,blocks,sample_size from user_tables wh ere table_name='TEST'; TO_CHAR(LAST_ANALYZED,'YYYYMMD NUM_ROWS BLOCKS SAMPLE_SIZE ------------------------------ ---------- ---------- ------------------------------------------------------------- 20090305 14:49:27 248180 3544 24818 建立收集统计信息的表LJB_COLLECT_STAT_TAB,前面已经先删除了ljb_collect_stat_tab表,否则会提示该表已存在 SQL> exec dbms_stats.create_stat_table(ownname => 'LJB',stattab => 'LJB_COLLECT_STAT_TAB'); PL/SQL procedure successfully completed 查看无记录 SQL> select * from LJB_COLLECT_STAT_TAB; STATID TYPE VERSION FLAGS C1 C2 C3 C4 C5 N1 N2 N3 N4 N5 N6 N7 N8 N9 N10 N11 N12 D1 R1 R2 CH1 ------------------------------ ---- ---------- ---------- ------------------------------ ------------------------------ ------------------------------ ------------------------------ ------------------------------ ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ----------- ---------------------------------------------------------------- ---------------------------------------------------------------- -------------------------------------------------------------------------------- 102 将收集到的TEST信息导到LJB_COLLECT_STAT_TAB表 SQL> exec dbms_stats.export_table_stats(ownname => 'LJB',tabname => 'TEST',stattab => 'LJB_COLLECT_S TAT_TAB'); PL/SQL procedure successfully completed 查看发现表的收集信息已经写进LJB_COLLECT_STAT_TAB表中,但是惊奇的发现索引信息也写进 LJB_COLLECT_STAT_TAB中 SQL> select type,c1 from LJB_COLLECT_STAT_TAB; TYPE C1 ---- ------------------------------ T TEST I IDX_TEST C TEST C TEST C TEST C TEST C TEST C TEST C TEST C TEST C TEST C TEST C TEST C TEST C TEST 15 rows selected 单独执行dbms_stats.export_index_stats命令导索引信息,记录数保持不变 SQL> exec dbms_stats.export_index_stats(ownname => 'LJB',indname => 'IDX_TEST',stattab => 'LJB_COLLE CT_STAT_TAB'); PL/SQL procedure successfully completed SQL> select count(*) from ljb_collect_stat_tab; COUNT(*) ------------------- 15 删除ljb_collect_stat_tab表 SQL> delete from ljb_collect_stat_tab; 15 rows deleted SQL> commit; Commit complete 单独先执行一下dbms_stats.export_index_stats导索引,发现索引信息写进ljb_collect_stat_tab表中, 并且只有一条记录 SQL> exec dbms_stats.export_index_stats(ownname => 'LJB',indname => 'IDX_TEST',stattab => 'LJB_COLLE CT_STAT_TAB'); PL/SQL procedure successfully completed SQL> select c1,count(*) from ljb_collect_stat_tab group by c1; 103 C1 COUNT(*) ------------------------------ --------------------------------- IDX_TEST 1 再次删除ljb_collect_stat_tab SQL> delete from ljb_collect_stat_tab; 1 row deleted SQL> commit; Commit complete 发现dbms_stats.export_table_stats将表信息写进ljb_collect_stat_tab中 SQL> exec dbms_stats.export_table_stats(ownname => 'LJB',tabname => 'TEST',stattab => 'LJB_COLLECT_S TAT_TAB'); PL/SQL procedure successfully completed SQL> select c1,count(*) from ljb_collect_stat_tab group by c1; C1 COUNT(*) ------------------------------ ------------------------------------- TEST 14 IDX_TEST 1 于是结论得出,dbms_stats.export_table_stats这个包可以同时把索引和表的信息都写进 LJB_COLLECT_STAT_TAB表中dbms_stats.export_index_stats 这个包只是将索引信息写进ljb_collect_stat_tab表中 后续再看看恢复吧,复习一遍 删除用户统计信息 SQL> exec dbms_stats.delete_table_stats(ownname => 'LJB',tabname => 'TEST'); PL/SQL procedure successfully completed 发现表和索引的收集信息同时被删除了,并没有执行dbms_stats.delete_index_stats SQL> select to_char(last_analyzed,'yyyymmdd hh24:mi:ss'),num_rows,blocks,sample_size from user_tables wh ere table_name='TEST'; TO_CHAR(LAST_ANALYZED,'YYYYMMD NUM_ROWS BLOCKS SAMPLE_SIZE ------------------------------ ---------- ---------- -------------------------------------------------------------- SQL> select to_char(last_analyzed,'yyyymmdd hh24:mi:ss'),num_rows,sample_size from user_indexes where table_name='TEST'; TO_CHAR(LAST_ANALYZED,'YYYYMMD NUM_ROWS SAMPLE_SIZE ------------------------------ ---------- ------------------------------------------------------- 导LJB_COLLECT_STAT_TAB信息进行恢复 SQL> exec dbms_stats.import_table_stats(ownname => 'LJB',tabname => 'TEST',stattab => 'LJB_COLLECT_S TAT_TAB'); PL/SQL procedure successfully completed 恢复成功,索引和表的收集信息都被恢复了,请注意,这里并没有使用dbms_stats.import_index_stats 包,进一步说明包中带有table的,是可以同时恢复表和索引的统计信息的,假如有收集的话 SQL> select to_char(last_analyzed,'yyyymmdd hh24:mi:ss'),num_rows,sample_size from user_indexes where table_name='TEST'; TO_CHAR(LAST_ANALYZED,'YYYYMMD NUM_ROWS SAMPLE_SIZE 104 ------------------------------ ---------- ----------- 20090305 14:49:27 250428 250428 SQL> select to_char(last_analyzed,'yyyymmdd hh24:mi:ss'),num_rows,blocks,sample_size from user_tables wh ere table_name='TEST'; TO_CHAR(LAST_ANALYZED,'YYYYMMD NUM_ROWS BLOCKS SAMPLE_SIZE ------------------------------ ---------- ---------- ----------- 20090305 14:49:27 248180 3544 24818 讨论到这里,基本上将表和索引信息的收集和备份及恢复的方法讨论完毕了,并明确了带table的包 可以同时收集和恢复索引的统计信息,不过所有的讨论都限制在 单个表和索引的收集信息方面,进一步,可以讨论基于整个用户和数据库层面的讨论,请听下回分 解关于表的收集信息的备份与恢复_3 4.3.2.3 经过前面两部分的描述,应该明白了单表及索引的采集信息的备份与恢复,现在接着描述针对 的收集与恢复。 继续做一遍表的恢复操作,这里有一个很重要的一点前面没说的,这里做个讲解 SQL> drop table LJB_COLLECT_STAT_TAB; Table dropped SQL> exec dbms_stats.create_stat_table(ownname => 'LJB',stattab => 'LJB_CLOLLECT_STAT_TAB'); PL/SQL procedure successfully completed 请注意到这个包中的stattab => 'LJB_COLLECT_STAT_TAB',这里是这样说明的,在统计表和索引信息的时候,如果加上这个地方,就等于取代了 dbms_stats.export_table_stats命令,这个命令的唯一 用途就是将统计信息写进的'LJB_COLLECT_STAT_TAB'表中,现在这个步骤可以在收集的时候就完成了,当然前提是这个表ljb_collect_stat_tab表要先建好,另外还有一个细节是cascade =>true,表示索引 和表都分析,否则只是分析表 SQL> exec dbms_stats.gather_table_stats(ownname => 'LJB',tabname => 'TEST',cascade =>true,stattab => 'LJB_COLLECT_STAT_TAB'); PL/SQL procedure successfully completed SQL> select count(*) from LJB_COLLECT_STAT_TAB; COUNT(*) ------------------- 15 前面复习完了,再仔细看看 SQL> select to_char(last_analyzed,'yyyymmdd hh24:mi:ss'),num_rows,blocks,sample_size from user_tables wh ere table_name='TEST'; TO_CHAR(LAST_ANALYZED,'YYYYMMD NUM_ROWS BLOCKS SAMPLE_SIZE ------------------------------ ---------- ---------- ------------------------------------------------------------- 20090305 17:53:40 250287 3544 56212 这个记录是有的,等会儿我会收集完整个用户的统计信息后,将该表的信息单独删掉,然后恢 复整个用户的采集信息,看是否能看到该表是否恢复 SQL> exec dbms_stats.create_stat_table(ownname => 'LJB',stattab => 'LJB_COLLECT_SCHEMA_TAB'); PL/SQL procedure successfully completed 105 收集用户信息,这里加上stattab => 'LJB_COLLECT_SCHEMA_TAB'和前面有异曲同工之秒,避 免了后续写dbms_stats.export_schema_stats来往'LJB_COLLECT_SCHEMA_TAB'表中写记录,现在下面执行完后,这个'LJB_COLLECT_SCHEMA_TAB'表记录已经不空了 SQL> exec dbms_stats.gather_schema_stats(ownname => 'LJB',stattab => 'LJB_COLLECT_SCHEMA_TAB'); PL/SQL procedure successfully completed SQL> select count(*) from ljb_collect_schema_tab; COUNT(*) ---------------- 1102 SQL> select C1,count(*) from ljb_collect_schema_tab group by C1 having count(*)>30; C1 COUNT(*) ------------------------------ ---------------------------------- TEST111 50 SETTLE_CATALOG_ITEM 224 TESTLJB 50 ADFSADFD 50 SETTLE_CATALOG_ARRAY 421 果然是存储了该用户下各个表信息,为了避免篇幅过大,只选了记录数大于30条的记录。 删除那个单表TEST的统计信息 SQL> exec dbms_stats.delete_table_stats(ownname => 'LJB',tabname => 'TEST'); 查看确实没有收集信息了,被删除了 SQL> select to_char(last_analyzed,'yyyymmdd hh24:mi:ss'),num_rows,blocks,sample_size from user_tables wh ere table_name='TEST'; TO_CHAR(LAST_ANALYZED,'YYYYMMD NUM_ROWS BLOCKS SAMPLE_SIZE ------------------------------ ---------- ---------- ------------------------------------------------------------- 然后开始恢复整个用户级的统计信息 SQL> exec dbms_stats.import_schema_stats(ownname => 'LJB',stattab => 'LJB_COLLECT_SCHEMA_TAB') PL/SQL procedure successfully completed 然后查看test表的收集信息是否恢复,果然恢复如下: SQL> select to_char(last_analyzed,'yyyymmdd hh24:mi:ss'),num_rows,blocks,sample_size from user_tables wh ere table_name='TEST'; TO_CHAR(LAST_ANALYZED,'YYYYMMD NUM_ROWS BLOCKS SAMPLE_SIZE ------------------------------ ---------- ---------- -------------------------------------------------------------- 20090305 17:53:40 250287 3544 56212 : 1、其实不管是备份恢复单表的收集信息还是整个用户的收集信息,dbms_stats.create_stat_table这个 步骤都是必不可少的,这个就是用来存储需要备份数据的动作,建成一张备份的表,用来恢复 2、如果在分析表dbms_stats.gather_table_stats和分析整个用户dbms_stats.gather_schema_stats的动作的时候带上stattab => 'LJB_COLLECT_STAT_TAB' 的选项,那该 'LJB_COLLECT_STAT_TAB'就已经是插入记录了,就可以不要用dbms_stats.export_table_stats动作或者dbms_stats.export_schema_stats动作了 106 3、在PLSQL DEVELOPEMENT 中使用如下命令的时候,输入包后后面的选项会有提示,比较 方便,但是在SQLPLUS无提示,友好性差一下。包里还有很多可选参数,我这里只写部分,其他 次要的,比如degree啊等的,就都不写了,有兴趣自己可以查询 4、以上只是说明最重要的部分,当然还有其他采集信息的采集备份与恢复命令,具体我列如下, 有兴趣可以继续研究,关于表的收集信息的备份与恢复就写到这里,熟练掌握这个还是很有必要性 的,毕竟在关键性业务的行业,做调优前养成先备份采集信息,让将来万一调整的更差后可以恢复 带来了可能。 EXPORT_COLUMN_STATS:导出列的分析信息 EXPORT_INDEX_STATS:导出索引分析信息 EXPORT_SYSTEM_STATS:导出系统分析信息 EXPORT_TABLE_STATS:导出表分析信息 EXPORT_SCHEMA_STATS:导出方案分析信息 EXPORT_DATABASE_STATS:导出数据库分析信息 IMPORT_COLUMN_STATS:导入列分析信息 IMPORT_INDEX_STATS:导入索引分析信息 IMPORT_SYSTEM_STATS:导入系统分析信息 IMPORT_TABLE_STATS:导入表分析信息 IMPORT_SCHEMA_STATS:导入方案分析信息 IMPORT_DATABASE_STATS:导入数据库分析信息 GATHER_INDEX_STATS:分析索引信息 GATHER_TABLE_STATS:分析表信息,当cascade为true时,分析表、列(索引)信息 GATHER_SCHEMA_STATS:分析方案信息 GATHER_DATABASE_STATS:分析数据库信息 GATHER_SYSTEM_STATS:分析系统信息 4.3.3OUTLINE 其实如果应用在测试环境中做过真实模拟压力测试,SQL的执行计划就完全可以固定,并把规 定的执行计划导到生产中去。这也是一个比较不错的方法。另外生产中如果长期运行稳定,也可以 把生产中稳定的执行计划做一个固定,这样就基本上不会出现4.3.1小节中描述的情况,假设对某些 业务的操作非常熟悉,并且在测试环境严格测试的关键业务,完全可以考虑用OUTLINE来固定, 这样就可以避免了前面出现的错误。 下面分两小接简要的描述outline的固定,以下都是我测试的过程,大家可以模拟执行一遍就明 白了其中操作原理了。 4.3.3.1OUTLINE 前面谈了如何搜集和备份、恢复表及索引的统计信息,并从单表及用两个层面进行了考虑,相 信对这块有了比较深刻的认识,继续思考下面一个问题,就是如果表的统计信息都搜集全了,那查 询语句的时候,就能出现正确的执行计划,也就是说,执行计划的正确与否取决与表和索引的信息 收集是否正确,如果搜集正确,ORALCE就会利用搜集到的信息来决定查询是走全表扫描还是索引 107 更块,表连接方式是HASH连接还是嵌套连接等等„„ 分析到这里,也就说明了一个道理,就是表和索引的统计情况非常重要,直接影响了ORALCE 语句的执行计划,而执行计划的正确与否,直接决定了数据库的工作效率,所以才有了前面要备份 和恢复及搜集表索引统计信息的前几个章节说明,现在要考虑的问题是,这个执行计划如果固定了, 是OK了,是否也可以对这个执行计划进行固定,也就是备份的含义。 还可以这样考虑: 1、可以在测试库进行测试 得到相对较优的执行计划 2、然后将该执行计划导入正式库使用 3、这样解决了不方便在正式库调整sql的问题 ORALCEOUTLINE 首先建测试表 SQL> create table emp 2 as 3 select ename, empno from scott.emp group by ename, empno 4 ; Table created SQL> create index idx_emp on emp(empno); Index created 首先查看优化模式 Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb SQL> show parameter opti NAME TYPE VALUE ------------------------------------ ----------- ------------------------------ filesystemio_options string object_cache_optimal_size integer 102400 optimizer_dynamic_sampling integer 2 optimizer_features_enable string 10.2.0.1 optimizer_index_caching integer 0 optimizer_index_cost_adj integer 100 optimizer_mode string ALL_ROWS optimizer_secure_view_merging boolean TRUE plsql_optimize_level integer 2 把系统设置为CHOOSE,目的是我在EMP表建过索引,所以选择CHOOSE模式的情况下,表 没分析过,基于规则,所以执行计划一定能用上索引! SQL> alter session set optimizer_mode=choose; Session altered SQL> explain plan for select * from emp where empno>0; Explained SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT -------------------------------------------------------------------------------- Plan hash value: 306890541 ----------------------------------------------- 108 | Id | Operation | Name | ----------------------------------------------- | 0 | SELECT STATEMENT | | | 1 | TABLE ACCESS BY INDEX ROWID| EMP | |* 2 | INDEX RANGE SCAN | IDX_EMP | ----------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("EMPNO">0) Note ----- - rule based optimizer used (consider using cbo) 18 rows selected 下面切入正题,开始最关键的命令,建立大纲,也就是OUTLINE! SQL> create or replace outline MyOutline 2 for category mycategory 3 on 4 select * from emp where empno>0; Outline created 收集表索引统计计划 SQL> exec dbms_stats.gather_table_stats(ownname => 'LJB',tabname => 'EMP',cascade =>true); PL/SQL procedure successfully completed SQL> explain plan for select * from emp where empno>0; Explained SQL> set linesize 1000 SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT -------------------------------------------------------------------------------- Plan hash value: 3956160932 -------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | -------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 56844 | 555K| 32 (7)| 00:00:01 | |* 1 | TABLE ACCESS FULL| EMP | 56844 | 555K| 32 (7)| 00:00:01 | -------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter("EMPNO">0) 13 rows selected 发现做了表分析后,基于代价,查询走了正确全表扫描,因为这个查询EMPNO全部都是大于 0的,这个索引查询根本就是返回全部的值,加上回表操作,索引查询肯定超过全表扫描的代价 知道,刚才固让定了一个执行计划,就是索引扫描的那个执行计划,虽然说最早那个执行计划是错 的,但是目前只是做实验,看看刚才固定的那个走索引的那个OUTLINE是否能生效,就是让 109 select * from emp where empno>0 这个SQL语句以后一直走刚才OUTLINE下来的那个INDEX RANGE SCAN的执行计划。 以下是关键的一步! SQL> alter session set use_stored_outlines = mycategory; --- Session altered 接着来看执行计划是否不走FULL而走了最早固定的那个INDEX读 SQL> explain plan for select * from emp where empno>0; Explained SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT --------------------------------------------------------------------------------------------------------- Plan hash value: 306890541 --------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Tim --------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 56844 | 555K| 244 (2)| 00: | 1 | TABLE ACCESS BY INDEX ROWID| EMP | 56844 | 555K| 244 (2)| 00: |* 2 | INDEX RANGE SCAN | IDX_EMP | 57344 | | 123 (2)| 00: --------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("EMPNO">0) Note ----- - outline "MYOUTLINE" used for this statement 18 rows selected SQL> 4.3.3.2 果然,走了INDEX RANGE SCAN 的这个执行计划,到这里终于知道了如何固定住执行计划了, 相信这个方法在生产中还是有用的,值得好好去体会! 上讲对OUTLINE做了简要的介绍,通过实验明白了执行计划如何固定,举了 select * from emp where empno>0这个语句的执行计划固定的问题,现在接下来是否会考虑这样一个 问题,现实应用中真是这样的语句吗,empno只能写大于0吗, 如果要<0或是>1呢,是否这样写 select * from emp where empno>1语句也能固定执行计划呢,否则的话这个empno>无数个值,也需 要做无数个OUTLINE吗? 下面带这个这个问题继续进行讨论 Connected to Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 Connected as ljb SQL> analyze table emp compute statistics for table for all indexes for all indexed columns; Table analyzed SQL> explain plan for select * from emp where empno>0; 110 Explained SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT --------------------------------------------------------------------------------------------- Plan hash value: 3956160932 --------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | --------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 229K| 2240K| 120 (8)| 00:00:02 | |* 1 | TABLE ACCESS FULL| EMP | 229K| 2240K| 120 (8)| 00:00:02 | --------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter("EMPNO">0) 13 rows selected SQL> SQL> alter session set use_stored_outlines=mycategory; Session altered SQL> explain plan for select * from emp where empno>0; Explained SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT -------------------------------------------------------------------------------------------------------- Plan hash value: 306890541 -------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Tim -------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 229K| 2240K| 738 (1)| 00: | 1 | TABLE ACCESS BY INDEX ROWID| EMP | 229K| 2240K| 738 (1)| 00: |* 2 | INDEX RANGE SCAN | IDX_EMP | 229K| | 74 (2)| 00: -------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("EMPNO">0) Note ----- - outline "MYOUTLINE" used for this statement 18 rows selected SQL> select name,category,sql_text from user_outlines where name='MYOUTLINE'; NAME CATEGORY SQL_TEXT ------------------------------ ------------------------------ ---------------------------------- MYOUTLINE MYCATEGORY select * from emp where empno>0 SQL> 如果改变为如下写法,当然用不到OUTLINE,如 111 SQL> explain plan for select * from emp where empno>1; Explained SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT --------------------------------------------------------------------------------------------- Plan hash value: 3956160932 --------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | --------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 229K| 2240K| 120 (8)| 00:00:02 | |* 1 | TABLE ACCESS FULL| EMP | 229K| 2240K| 120 (8)| 00:00:02 | --------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter("EMPNO">1) 13 rows selected 那该怎么做呢?让我们继续研究(注意到这里我是用SQLPLUS,前面用的是PLSQL DEVELOPMENT的COMMAND窗口) SQL*Plus: Release 10.2.0.1.0 - Production on 星期六 3月 7 13:57:28 2009 Copyright (c) 1982, 2005, Oracle. All rights reserved. 连接到: Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 - Production With the Partitioning, OLAP and Data Mining options SQL> create or replace outline myoutline for category mycategory on select * from emp where empno> :y; 大纲已创建。 SQL> alter session set cursor_sharing=force; 会话已更改。 SQL> alter session set create_stored_outlines=true; 会话已更改。 SQL> var y number; SQL> exec :y:=8; PL/SQL 过程已成功完成。 SQL> select * from emp where empno>:y; 未选定行 SQL> select count(*) from emp where empno=:y; COUNT(*) ------------------ 229376 SQL> explain plan for select * from emp where empno>:y; 已解释。 SQL> set linesize 1000 SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT ---------------------------------------------------------------------------------------------------------------------------- 112 Plan hash value: 306890541 ---------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ---------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 11469 | 100K| 2 (0)| 00:00:01 | | 1 | TABLE ACCESS BY INDEX ROWID| EMP | 11469 | 100K| 2 (0)| 00:00:01 | |* 2 | INDEX RANGE SCAN | IDX_EMP | 2064 | | 2 (0)| 00:00:01 | ---------------------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): PLAN_TABLE_OUTPUT ---------------------------------------------------------- 2 - access("EMPNO">TO_NUMBER(:Y)) Note ----- - outline "SYS_OUTLINE_09030713555650053" used for this statement 已选择18行。 SQL> select name,category,sql_text from user_outlines where name='SYS_OUTLINE_09030713203295349'; NAME CATEGORY SQL_TEXT ------------------------------ ------------------------------ -------------------------------------------------------------------------------------- SYS_OUTLINE_09030713203295349 DEFAULT select count(*) from emp where empno=:y SQL> select name,category,sql_text from user_outlines where name='SYS_OUTLINE_09030713555650053'; NAME CATEGORY SQL_TEXT ------------------------------ ------------------------------ --------------------------------------------------------------------------------------- SYS_OUTLINE_09030713555650053 DEFAULT select * from emp where empno>:y SQL> explain plan for select count(*) from emp where empno=:y; 已解释。 SQL> select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT ------------------------------------------------------------------------------------------------------------- Plan hash value: 2083865914 ------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 2 | 124 (11)| 00:00:02 | | 1 | SORT AGGREGATE | | 1 | 2 | | | |* 2 | TABLE ACCESS FULL| EMP | 229K| 448K| 124 (11)| 00:00:02 | Predicate Information (identified by operation id): PLAN_TABLE_OUTPUT ------------------------------------------------------------------------------------------------------------- 2 - filter("EMPNO"=TO_NUMBER(:Y)) Note - outline "SYS_OUTLINE_09030713203295349" used for this statement 已选择18行。 113 到这里问题完成了绑定变量的OUTLINE固定,不过我这里可是实验了非常久的时间,大家知 道为什么吗,因为我用了PLSQL DEVELOPMENT的COMMAN窗口,实验不成功,看来是这个工 具的BUG了,进入SQLPLUS后调试成功! 不过这里大家可能发现一个非常奇怪的现象,就是explain plan for select count(*) from emp where empno=:y;这个语句根本就没有设定大纲,create or replace outline myoutline for category mycategory on select * from emp where empno> :y; 但是做走了固定的执行计划,这里是以外收获,说明这里 ORACLE还是会做判定的,具体还有待于下一步研究 4.3.4 sqlsql 这一讲我觉的相当重要!因为太多的人存在认识的误区了! 如何查看SQL的语句效率高低,很多人认为SQL语句的优化是取决于SQL语句怎么写,有的人还根据自己的经验特别的说明了几种情况,比如什么要避免使用not in,尽量要考虑用not exists去替代not in 之类的,甚至很多ORACLE书籍都这么描述。这种认识其实是错的!sql语句执行的性能如何,是否需要优化,真正的是取决于走了什么样的执行计划,当然sql语句写的不同确实是有可能影响了执行计划。大家还记得我在讲述索引的时候1.2.2小节中增加了where id is not null让count(*)用上索引,接着又在1.2.3小节改造了select max(),min() from tab的写法让执行计划由index full scan 改变为index full scan(max/min)。但是本质是取决于执行计划的分析,根据分析的结果改造 的。 不同的sql语句在执行的时候,有可能会出现相同的执行计划,因为cbo会在parse的时候做查询转换,这个时候处理速度就是一样快了!相同的sql语句,处理相同的数据量,在不同的环境执行,有可能因为环境的不同,走了不同的执行计划,这个时候处理的效率也就不一 样了! 子查询可能存在的4种查询转换后的执行计划: 1.unnest后转成semi join或者anti join 2.把子查询转成inline view,然后做连接处理 3.无法做查询转换,执行计划是filter 4.当然某些特殊的情况,同时有索引存在时,产生filter变种后的执行计划 下面构造一个not in比not exists快的例子,因为很多人的定性思维是not in一定比 not exists慢,现在我们就反驳一下! SQL> sqlplus ljb/ljb SQL> drop table ljb_test1; 表已删除。 SQL> create table ljb_test1 as select * from user_objects; 表已创建。 SQL> drop table ljb_test2; 表已删除。 SQL> create table ljb_test2 as select * from user_objects; 表已创建。 SQL> set timing on SQL> set linesize 1000 SQL> set autotrace traceonly 114 查看执行计划,发现not exists走的是filter,说明CBO无法对这个SQL做查询转换,否则应该 是走HASH JOIN ANTI之类的执行计划才更快,另外可以看出consistent gets有1921个,我们的测试 表数据量极小,如果表数据量一大,那将更可怕,我实验用dba_objects构造,有将近2千万个逻辑 读,但是dba_objects表其实数据量也不大。 SQL> select * from ljb_test1 where not exists (select 1 from ljb_test2 where ljb_test1.object_id=ljb _test2.object_id group by object_name having count(*)>1); 已选择304行。 已用时间: 00: 00: 00.15 执行计划 ---------------------------------------------------------- Plan hash value: 1881203664 ---------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ---------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 272 | 43520 | 41 (22)| 00:00:01 | |* 1 | FILTER | | | | | | | 2 | TABLE ACCESS FULL | LJB_TEST1 | 304 | 48640 | 3 (0)| 00:00:01 | |* 3 | FILTER | | | | | | | 4 | HASH GROUP BY | | 3 | 237 | 4 (25)| 00:00:01 | |* 5 | TABLE ACCESS FULL| LJB_TEST2 | 3 | 237 | 3 (0)| 00:00:01 | ---------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter( NOT EXISTS (SELECT /*+ */ 0 FROM "LJB_TEST2" "LJB_TEST2" WHERE "LJB_TEST2"."OBJECT_ID"=:B1 GROUP BY "OBJECT_NAME" HAVING COUNT(*)>1)) 3 - filter(COUNT(*)>1) 5 - filter("LJB_TEST2"."OBJECT_ID"=:B1) Note - dynamic sampling used for this statement 统计信息 ---------------------------------------------------------- 298 recursive calls 0 db block gets 1921 consistent gets 8 physical reads 0 redo size 25643 bytes sent via SQL*Net to client 605 bytes received via SQL*Net from client 22 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 304 rows processed 现在我们实验一下用not in 的情况,发现不再是FILTER,就是说已经发生了查询转换了, 115 走的是前面描述的子查询可能存在的4种查询转换中的第一点。可以看出代价为8比前面的41 小的多,而且逻辑读为103也比前面的1921小的多,执行速度也差异明显! SQL> select * from ljb_test1 where object_id not in (select object_id from ljb_test2 where object_ id is not null group by object_id,object_name having count(*)>1) and object_id is not null; 已选择304行。 已用时间: 00: 00: 00.03 执行计划 ---------------------------------------------------------- Plan hash value: 3700474117 ----------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ----------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 173 | 8 (25)| 00:00:01 | |* 1 | HASH JOIN RIGHT ANTI | | 1 | 173 | 8 (25)| 00:00:01 | | 2 | VIEW | VW_NSO_1 | 305 | 3965 | 4 (25)| 00:00:01 | |* 3 | FILTER | | | | | | | 4 | HASH GROUP BY | | 305 | 24095 | 4 (25)| 00:00:01 | |* 5 | TABLE ACCESS FULL| LJB_TEST2 | 305 | 24095 | 3 (0)| 00:00:01 | |* 6 | TABLE ACCESS FULL | LJB_TEST1 | 304 | 48640 | 3 (0)| 00:00:01 | ----------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - access("OBJECT_ID"="$nso_col_1") 3 - filter(COUNT(*)>1) 5 - filter("OBJECT_ID" IS NOT NULL) 6 - filter("OBJECT_ID" IS NOT NULL) Note - dynamic sampling used for this statement 统计信息 ---------------------------------------------------------- 34 recursive calls 0 db block gets 103 consistent gets 0 physical reads 0 redo size 25643 bytes sent via SQL*Net to client 605 bytes received via SQL*Net from client 22 SQL*Net roundtrips to/from client 3 sorts (memory) 0 sorts (disk) 304 rows processed 不过这里要注意not in必须保证子查询内外的谓词是not null,否则是不会做unnest的,这个在3.7.4 的引申联想中也有提到这第点。开发人员在写sql的时候要特别留意这点,下面我们把where object_id is not null 去掉,发现走了和not exists一模一样的执行计划!证明了我不同的语句有 116 可能走同样的计划的论点! SQL> select * from ljb_test1 where object_id not in (select object_id from ljb_test2 group by obje ct_id,object_name having count(*)>1); 已选择304行。 已用时间: 00: 00: 00.54 执行计划 ---------------------------------------------------------- Plan hash value: 1881203664 ---------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ---------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 272 | 43520 | 41 (22)| 00:00:01 | |* 1 | FILTER | | | | | | | 2 | TABLE ACCESS FULL | LJB_TEST1 | 304 | 48640 | 3 (0)| 00:00:01 | |* 3 | FILTER | | | | | | | 4 | HASH GROUP BY | | 305 | 24095 | 4 (25)| 00:00:01 | | 5 | TABLE ACCESS FULL| LJB_TEST2 | 305 | 24095 | 3 (0)| 00:00:01 | ---------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter( NOT EXISTS (SELECT /*+ */ 0 FROM "LJB_TEST2" "LJB_TEST2" GROUP BY "OBJECT_ID","OBJECT_NAME" HAVING LNNVL("OBJECT_ID"<>:B1) AND COUNT(*)>1)) 3 - filter(LNNVL("OBJECT_ID"<>:B1) AND COUNT(*)>1) Note ----- - dynamic sampling used for this statement 统计信息 ---------------------------------------------------------- 13 recursive calls 0 db block gets 1879 consistent gets 0 physical reads 0 redo size 25643 bytes sent via SQL*Net to client 605 bytes received via SQL*Net from client 22 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 304 rows processed :通过本小节我再次强调一点,SQL语句的写法的是否高效率,是要去靠分析执行计划而不 是去分析SQL语句,离开执行计划而定的SQL语句写法的优化原则是毫无意义的,但是这样的想法却 很普遍,很多人居然把不明真相的快慢归结于sql写法的不同,甚至做为自己的经验总结传播给大家, 117 其实这个认识是错误的!在本小节中我们研究这个not in 快还是not exists快主要是分析了查询转换,是否走合理的查询转换,本例子中由于oracle如果是not exists或exists和类似group by (排 序) 这样的子句连用时cbo不会做查询转换,所以才会更慢的,这时候可以改成not in或者in,或者别 的方式实现。而且我们的心思是放在哪里,是放在根据执行计划常识,走查询转换性能更高,我们 想法子去构造查询转化的机会,而我们是根据查询内外的谓词是not null,才有可能走unnest的知 识,成功的加上not null后走了semi join或者anti join,终于让这个查询变快了。 4.3.5 登陆A窗口 SQL> connect ljb/ljb SQL> create global temporary table ljb_test_tmp on commit preserve rows as select * from dba_objects; 表已创建。 接着登陆B窗口,在另一SESSION收集统计信息 SQL> exec dbms_stats.gather_table_stats(user,'LJB_TEST_TMP'); PL/SQL 过程已成功完成。 回到A窗口操作如下,根据执行计划得出ORACLE认为该表是没有记录的,这个主要是因为 ORACLE临时表的特性,不同的SESSION不可见,因此在另一SESSION收集统计信息的时候当然收集到表的记录数为0 SQL> set linesize 1000 SQL> set autot trace exp SQL> select count(*) from ljb_test_tmp; 执行计划 ---------------------------------------------------------------------------------------------------- Plan hash value: 3572440799 ----------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Cost (%CPU)| Time | ------------------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | 2 (0)| 00:00:01 | | 1 | SORT AGGREGATE | | 1 | | | | 2 | TABLE ACCESS FULL| LJB_TEST_TMP | 1 | 2 (0)| 00:00:01 | ------------------------------------------------------------------------------------------------------------ SQL> set autot off SQL> select blocks,num_rows from user_tables where table_name='LJB_TEST_TMP'; BLOCKS NUM_ROWS ---------- ---------------------------------------- 0 0 实际情况是该临时表的记录数为62756,这里就存在了风险了,我们的SQL语句如果是复杂的关联表,其中有将临时表做为关联查询的条件,那cardinality=1的情况就很可能导致在多表关联的情况下oracle选择了笛卡尔积,或者大表做nest loop的驱动表(比如这个tmp表成为大表了,ORACLE以为是超级小表) SQL> select count(*) from ljb_test_tmp; 118 COUNT(*) -------------------------- 62756 那该怎么办呢?我们有几种方法来处理 1、我们如果可以预测到临时表的数据量的多少,我们可以手动的将临时表设置到某个大小的范 围的值,比如再次登陆B窗口,进行手动统计信息收集 SQL> exec dbms_stats.set_table_stats('LJB','LJB_TEST_TMP',numrows =>62756); PL/SQL 过程已成功完成。 登陆A窗口,现在重新查看执行计划,这下该表的记录数ORACLE认为是62756,就是正确的了。 SQL> set autot trace exp SQL> select count(*) from ljb_test_tmp; 执行计划 ---------------------------------------------------------- Plan hash value: 3572440799 ---------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Cost (%CPU)| Time | --------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 4 (50)| 00:00:01 | | 1 | SORT AGGREGATE | | 1 | | | | 2 | TABLE ACCESS FULL| LJB_TEST_TMP | 62756 | 4 (50)| 00:00:01 | 2、我们可以考虑用dbms_stats包的gather_schema_stats这个过程,其中有一个gather_tmp这个 可选值,默认情况下为fales,设置为true的时候,可以统计on commit preserve rows的全局临时表(不 过这个有个缺点就是无法统计基于事务的全局临时表) 4.5 oracle 在WINDOWS下安装ORACLE数据库想必大多人都有试过,由于有图形向导界面,相对比较 简单,那如果是安装UNIX平台下的数据库该怎么操作呢?很多人认为在UNIX环境下安装数据库 比较麻烦,因为没有图形向导界面。 其实这里要说明一下就是,在UNIX平台上安装,也可以有图形界面,因为有些UNIX平台本 身就是有X窗口的,就是有图形向导界面的,这个就不成问题了。当然为了系统更稳定更干净,生 产中大多UNIX平台是仅有命令行界面的。不过没关系,此时安装数据库可以借助自己的电脑的图 形界面来进行安装。 举个简单例子,下面贴出我在软件园为网管平台安装ORACLE 10g数据库的小部分内容: $ cd 10gr2_sol_install $ ls doc install response runInstaller stage welcome.html $ ./runInstaller Starting Oracle Universal Installer... Checking installer requirements... 119 Checking operating system version: must be 5.8, 5.9 or 5.10. Actual 5.10 Passed Checking Temp space: must be greater than 250 MB. Actual 5486 MB Passed Checking swap space: must be greater than 500 MB. Actual 5675 MB Passed Checking monitor: must be configured to display at least 256 colors >>> Could not execute auto check for display colors using command /usr/openwin/bin/xdpyinfo. Check if the DISPLAY variable is set. Failed <<<< Some requirement checks failed. You must fulfill these requirements before continuing with the installation,at which time they will be rechecked. Continue? (y/n) [n] y 看来安装遇到点问题了。 于是在UNXI环境中增加环境变量如下(192.168.20.147是我笔记本电脑的IP地址),继续操作,情况就改变了,出现图形向导界面后,一切都变的亲切了。 DISPLAY=192.168.20.147:0.0; export DISPLAY 检验: $ echo $DISPLAY 192.168.20.147:0.0 当然,在笔记本电脑上还要安装XMANAGER控制器配合操作。 总结:这里安装内容太多就没必要贴出了,数据库的安装应该严格按照官方文档的步骤进行, 在AIX和SORLIAS等不同操作系统下安装数据库的步骤不完全相同。写这个ORADCLE安装主要是想告诉大家,在UNIX下做好环境的设置,如设好环境变量及XMANAGER,就可像WINDOWS一样在 友好的图形向导界面下安装数据库,有兴趣安装的朋友可以下载官方的安装手册进行研究。除了强 调UNIX下装数据库可以用图形向导界面外,此外还有两点这里强调一下:1、就是其中有个orainstRoot.sh的执行是不可少的,不过这个简单,因为图形向导界面会提示你要执行这个脚本。2、 安装数据库的时候,选择字符集的时候一定要谨慎,如果按默认的选择将不支持中文,一般情况选 择ZHS16GBK。我就吃过亏,由于选择默认的,装好后的数据库不支持中文,不得不将数据重新安 装一遍。 4.6 spfilepfile 这也是比较容易出错的一个地方,有一次我实验将自己测试环境的open_cursors参数修改为1, 这样在重启动数据库后就无法登陆数据库了,因为启动数据库需要至少几次SQL的递归。这个时候我只好用PFILE启动,然后再把PFILE的参数复制给SPFILE,然后重新启动数据库还原参数。不过spfile和pfile的切换要注意两点: 1、默认的spfile的文件路径在database下 2、建立spfile的时候,pfile的路径要指定 下面我们就用实验来证明 SQL> startup pfile='C:\oracle\product\10.2.0\admin\rqrq\pfile\init.ora.25200821237' ORACLE 例程已经启动。 Total System Global Area 289406976 bytes Fixed Size 1248576 bytes Variable Size 92275392 bytes 120 Database Buffers 188743680 bytes Redo Buffers 7139328 bytes 数据库装载完毕。 数据库已经打开。 SQL> create spfile from pfile; 文件已创建。 SQL> shutdown immediate 数据库已经关闭。 已经卸载数据库。 ORACLE 例程已经关闭。 SQL> startup ORA-03113: 通信通道的文件结束 SQL> startup pfile='C:\oracle\product\10.2.0\admin\rqrq\pfile\init.ora.25200821237' ORACLE 例程已经启动。 Total System Global Area 289406976 bytes Fixed Size 1248576 bytes Variable Size 92275392 bytes Database Buffers 188743680 bytes Redo Buffers 7139328 bytes 数据库装载完毕。 数据库已经打开。 SQL> create spfile='C:\oracle\product\10.2.0\db_1\dbs\SPFILERQRQ.ORA' from pfile; 文件已创建。 SQL> startup force ORA-03113: 通信通道的文件结束 所以操作如下,便可以成功 SQL> create spfile from pfile='C:\oracle\product\10.2.0\admin\rqrq\pfile\init.ora.25200821237; 文件已创建。 SQL> startup force ORACLE 例程已经启动。 Total System Global Area 289406976 bytes Fixed Size 1248576 bytes Variable Size 92275392 bytes 121 Database Buffers 188743680 bytes Redo Buffers 7139328 bytes 数据库装载完毕。 数据库已经打开。 SQL> show parameter spfile NAME TYPE VALUE ------------------------------------ ----------- ------------------------------ spfile string C:\ORACLE\PRODUCT\10.2.0\DB_1\ DATABASE\SPFILERQRQ.ORA SQL> 注意点: 1、oracle中如下语法是不支持的 startup spfile =‘xxx‘; 2、如何查看当前是 pfile还是spfile启动的方法 SQL> show parameter pfile NAME TYPE VALUE ------------------------------------ ----------- ------------------------------ spfile string UNIX 当前电信IT项目几乎没有能离开UNIX环境的,将关键性应用放在WINDOWS平台下是难以想象的,所以对UNIX环境的熟悉就显的特重要,我们对其的依赖程度一点也不比数据库少。只要 善于思考会发现,其实操作系统和数据库有着许许多多相似之处。比如OS的内存管理分配和ORACLE的SGA就有着很多类似的地方,在备份恢复方面,现行的UNIX OS和ORALCE都利用到了REDO日志。OS有双机备份,ORACLE中就有与之相似的RAC和DG机制。而在文本文件处理方面,UNIX下的AWK+SED几乎强大到无所不能(至少结合SHELL可以这么说),可以实现类似数据库表中的不少操作,如两表关联查询、COUNT(*)+GROUP BY 、 select 字段1+字段2 from 表 where 的各种条件、 子句中的IN与NOT IN ,这些都可以直接在文本中实现,神奇吧!ORACLE中复杂的逻辑可在过程中实现,而OS层面,复杂的文本处理可考虑放在SHELL中完成,SHELL中可调用AWK+SED让其数据处理能力变的非常强大!我做一个比喻:oracle中的函数就是unix下的awk+sed, 因为都可以直接在命令行使用,oracle中的过程就是unix下的shell,因为都是较复杂的功能加上程序逻辑来封装实现。大家还知道sqlldr吧,这是连接OS文本和数据库表的一座桥梁,不 过sqlldr还只是单向传输,随着ORACLE版本的越来越强大,ORACLE外部表的出现实现了从ORACLE到OS文本的传输,终于实现了双向传输,ORACLE和OS越来越亲近了!此外,研究ORACLE的朋友如果只熟悉ORACLE不熟悉OS,他将会在解决ORACLE故障时在DB层面拼命钻牛角尖却始终无法提升数据库性能,因为最终真正解决问题的方法是处理了OS层面上的一个很占CPU或内存的外部应用,而该应用和数据库无关,它拖跨了整台主机,运行在该主机上的数据库, 当然跟着一起遭殃了! 122 1 1.1mv 用过UNIX的人一般都会使用mv命令,为什么我要说小心mv命令?让我们一起探讨如下: mv命令同时有两种功能,一是挪动文件,二是重命名,当你写命令mv 1.txt ./b/ 的时候,本意是将a.txt移动到b目录中,但是如果b并不在当前路径下,或者说写错名了,此时就不再是移动文 件了,而是将a.txt重命名成b,你会发现原本你写错的b文件其实已经实际成为a.txt了。这还不是最糟的,因为你毕竟东西没有损失,只是被重命令而已,更糟糕的是什么呢?比如你的脚本写成类 似awk方式的,达到的效果是mv 1.txt ./b/ mv 2.txt ./b/ mv 3.txt ./b/ …mv 1000.txt ./b/ .等等,那就完了,当你做完后你会发现你1.txt,2.txt等所有文件都丢了,文件中你只找到了b,你能找回你丢失的最后一个文件1000.txt,因为 cat b 你发现里面的内容正是1000.txt的内容,不过其他的都无可挽回的丢失了! 举例如下 test@nxjs-33:~/ljb/ljb$ls 1. txt 2.txt 3.txt 4.txt 5.txt 我下面写的这个命令其实是在后面会说明为什么要写成这样。 我本意是想把这些文件都mv到abc目录下,但是我粗心了,abc根本不在当前路径下 test@nxjs-33:~/ljb/ljb$find . -type f |awk '{print "mv " $1 " " "./abc/"}' mv ./1.txt ./abc/ mv ./2.txt ./abc/ mv ./3.txt ./abc/ mv ./4.txt ./abc/ mv ./5.txt ./abc/ 加上sh执行后发现大事不妙了! test@nxjs-33:~/ljb/ljb$find . -type f |awk '{print "mv " $1 " " "./abc/"}'|sh test@nxjs-33:~/ljb/ljb$ls abc test@nxjs-33:~/ljb/ljb$ 发现啥都丢了,只剩下一个abc 文件了,里面存储着5.txt的信息(我就吃过这么一次大亏,丢 了计费的漫游上传数据,好在后来恢复了数据) 1.遇到mv语句的时候,仔细检查所要转移的对象的所在路径是否存在,不要写错名。 2.尽量在mv前后都写上全路径,如果是全路径而不是写./的当前路径,mv并不会重命名,而 是提示对象不存在,这里我就不实验了。 1.2 也许大家都有遇到这样的问题,mv或者rm 某个路径下太多文件的时候,则会提示表达式太长 ―parameters list too long‖ 的错误而执行失败,当你要移动或者删除的文件个数超过1万个的时候, 这样的错误一般就会出现,那该怎么办呢? 前一小节的描述中,我使用了awk小程序,让语句转化成一条一条执行,用这个方法就可以解 123 决这个表达式太长的问题。 以下是我的一个简单的shell脚本,这里只提取部分代码,可以看出注释掉的就是因为会执行失败,所以我改动如下。 nxjf68-/cdma/c_data/trans/upbak/normal/shell#cat mv_normal_to_work.sh #! /bin/bash nohup find /cdma/c_data/trans/upbak/normal/ -type f |grep IIC|grep -v normal_2008|grep -v normal_work |grep -v normal_bk|grep -v normal_other|awk '{print "mv " $1 " /cdma/c_data/trans/upbak/normal/normal_work/"}' |sh > /cdma/c_data/trans/upbak/normal/normal_work.log & #mv /cdma/c_data/trans/upbak/normal/IIC* /cdma/c_data/trans/upbak/normal/normal_work 大家在写SHELL代码的时候要注意,有时SHELL中简单的mv * 命令会到某天忽然 执行失败,原因正是本小节描述的这个情况,要对这样的问题有所了解,修改代码,避免错误。 1.3 UNIX 大家是否有见过这样的情况,在UNIX系统中,中文字符集有安装过,但是中文的脚本用vi无 法看到中文,且中文命名的文件也因为复制不出中文无法做任何操作,该如何解决呢? 让我们一起研究如下: zhjs4@nxjs-33:~/settle/shell$cat jf_ftp_to_js.sh #! /bin/bash . ~/.bash_profile echo "测试" 用vi看不了 zhjs4@nxjs-33:~/settle/shell$vi jf_ftp_to_js.sh "jf_ftp_to_js.sh" [Incomplete last line] 86 lines, 1671 characters #! /bin/bash . ~/.bash_profile echo "\304\277\302\274\326\320" 主机为:test@uname -a SunOS nxjs-33 5.8 Generic_108528-23 sun4u sparc SUNW,Netra-T12 具体原因是什么呢? test@:~$env |grep LAN NLS_LANG=SIMPLIFIED CHINESE_CHINA.ZHS16GBK 这个命令看不到LC_*这些变量的结果 可以说明的是:没有设定LANG变量,那么试试这样:LC_ALL=zh_CN.GBK;export LC_ALL, 再用vi打开 test$LC_ALL=zh_CN.GBK test@:$export LC_ALL test@:$env |grep LC_ALL LC_ALL=zh_CN.GBK test@:$ vi jf_ftp_to_js.sh #! /bin/bash 124 . ~/.bash_profile echo "测试" 终于成功了,以后写进环境变量,就可以长期生效了 NLS_LANG和数据库相关的环境变量,不装oracle的话,设置这个变量没意义的。 LC_ALL的设置会覆盖LANG和LC_*(比如LC_CTYPE等)的设置,LC_*又能覆盖LANG的设置,LANG是最没―地位‖ 1.4 这也是一个很常见的问题,经常会遇到由于程序异常或移走备份文件不及时,操作系统文件空 间爆满而最终使应用无法写进磁盘,导致程序挂起,系统故障产生。当然现在的监控系统会对空间 进行监控,在某阀值以内会告警。那收到告警后该如何快速解决呢?分别进每个目录去查看大小的 方法肯定不可取,我处理的方式如下: 比如在文件满的路径的的根目录,查询超过某个大小的文件是多少 test@nxjs-33:~$find . -size +10000000c ./nxftp/bin/接收日志.log ./hxk/ffsc_CaiJi/bin/a.logbak ./zjx/zhjs_param4_20071227.dmp ./zjx/exp_param/nxzhjs_param_20080109.dmp ./settle_version_4.0/bin/CJ_NXIN/接收日志.log ./settle_version_4.0/bin/core ./settle_version_4.0/src/pp_filecheck_20081230_backup省际漫游上传/msc/filecheck/core ./settle_version_4.0/src_090111/pp_filecheck_20081230_backup省际漫游上传/msc/filecheck/core ./settle_version_4.0_backup/bin/CJ_NXIN/接收日志.log ./settle_version_4.0_backup/lib/libparam.a ./settle_version_4.0_backup/src/ffsc_CaiJi/bin/a.logbak 假设这个展现非常多,那我再加一个0,从find . -size +10000000c调整为find . -size +100000000c 这个时候出现的记录就更少了,而此时出现的正是大小多了一个数量级的文件! 如果再加上一个0 ,从find . -size +100000000c 调整为 find . -size +1000000000c 如果还有文件出现,OK,估计就是这个文件导致磁盘暴涨的。 实际操作中加xargs ls –lrt展现,可看清具体大小,代码为find . -size +10000000c|xargs ls –lrt 平时我都是用这样的方法来处理空间满的问题,感觉定位问题相当快,对于只是某个文 件如CORE异常扩大的定位非常方便。 1.5 chmod 777 chmod这个命令应该大多人都知道,我想就不用多介绍了,但是我所见到的周围人大多都直接 输一个chmod 777授权给文件就完事了,这样以后该文件无论是文件所属、组、还是其他用户,都 具备了读写和执行的权限了,在多用户环境系统的UNIX环境下,任何人都可以对被授权777的文件随心所欲进行操作,本来某用户进错路径,删除或者重写了不该属于他的文件时,会因为没有对 应的权限而误操作失败,但是现在777后,误操作成功了,系统变的很不安全了! 我建议废弃chmod 777 的写法,希望大家都习惯写 chmod g+w 某文件(表示对于属于该用户的组的用户授予写的权限),用类似这样的风格去写。当然chmod g+w等类似的写法比 125 chmod 777这样来的复杂些,但是却安全多了,至少写的时候,操作者是具备安全意识的。与之类 似的是数据库的操作,如果你新建一个用户,千万别图省事用grant dba to XXX的方式给用户授权,如此该用户在数据库中就会无所不能,可以在库中随意处理别的用户所有对象,相当危险! 1.6 UNIX 在宁夏结算环境中遇到这样一个问题,在当前工作目录下可以看到文件,cd却提示不存在? zhjs@nxjs-03:/exchange/appdata/settle/ctnbc/send/roam/gaoe/prov$cd normal bash: cd: normal: 无此文件或目录zhjs@nxjs-03:/exchange/appdata/settle/ctnbc/send/roam/gaoe/prov$ 通过执行如下命令的时候发现,normal和后面的/有空格,说明该目录是normal后面带一个空格的。 zhjs@nxjs-03:/exchange/appdata/settle/ctnbc/send/roam/gaoe/prov$find . -type f ./normal /HNIC_20081218951.0064 ./normal /HNIC_20081218951.0065 ./normal /HNIC_20081218951.0066 这个时候我们该如何处理呢?要考虑到使用UNIX的转义命令,将空格转义为UNIX认识 的符号,即‖\ / ― ,请注意这里\和/之间有个空格,执行如下操作 zhjs@nxjs-03:/exchange/appdata/settle/ctnbc/send/roam/gaoe/prov$cd normal\ / You have new mail in /usr/mail/zhjs zhjs@nxjs-03:/exchange/appdata/settle/ctnbc/send/roam/gaoe/prov/normal\ $pwd /exchange/appdata/settle/ctnbc/send/roam/gaoe/prov/normal 果然可以进入该目录,这样的情况产生一般是该目录是在SHELL脚本或者等建立的,不小 心多了一个空格导致,我们可以用以下方式替换修正:mv normal\ / normal ,去掉空格! 为了让大家加深印象,我再举一个同样类型的例子,在后面2.1.2有提过的,用UE写脚本 出现的^M的问题,比如脚本如下 我们vi ljb_test.sh,发现有个^M(请注意这个^M实际上是ctrl+v 和ctrl+m的组合键) #! /bin/bash echo "test1" > /home1/test/ljb/ljb_test1.txt^M 执行完后生成的ljb_test1.txt我们又发现看得见但是无法cat等访问的情况 test@nxjs-33:~/ljb$cat ljb_test1.txt cat: cannot open ljb_test1.txt 该怎么办?大家想想代码中^M就知道该怎么处理了,我们采用\^M/转移掉这个^M就好了。 test@nxjs-33:~/ljb$cat ljb_test1.txt\^M/ test1 总结:UNXI下有很多比如空格,换行啊,回车等等符号,这个时候我们必须要考虑将这些 操作写进\和/之间,这样才能让UNIX明白,本小节举的例子比较典型,现实中我们遇到这样的 情况往往会不知所措,明明看的见文件却又无法操作,希望读完本小节后,能有所了解。 1.7 在UNIX环境下,有时会发现出现这样一种情况,df –k查看发现后台某文件系统路径下空间满 了,但是无论你通过du -ks 还是find . –size 等方式都看不到大文件,导致你不知道到底是什 126 么原因空间暴满,从而无从解决问题。 遇到这样的情况,其实就是遇到了所谓的空间虚增长问题,举例说明:某时候有个程序由于 某种异常在持续不断的往某文件里不停写入,如果文件大到让空间满了,我们也很容易发现这个 大文件而将其清理掉。可是如果该文件在被不断写进的过程被人不小心给删除了(windows下会锁住该文件,提示文件正在使用中而不让删除,但是unix 环境下就能轻易删除掉),由于程序未退 出,写进动作还在进行中。。。。,这时空间就会不断变大,但是我们却找不到这个大文件,原 因很简单,被删除了当然找不到,从而不知道是什么文件异常增大导致的,除非找到这个进程并 强制杀掉这个进程,虚增长的空间才会立即释放,否则空间满问题就无法解决。 由于不好定位出这个进程是什么,现实中我们常用的方法是,用root用户登陆,然后执行sliceclean命令,强制释放这个进程,解决该问题。 总结:这种情况在WINDOWS下不可能发生,因为WINDOWS下在用的文件会锁住,前面大家还记 得我说过的数据库文件被误删,然后我通过利用数据库归档文件将其恢复的描述吧,WINODWS下如果数据库启动了,其dbf数据文件也是会被锁住而不可能被删除的。对于WINDOW和UNIX的这差异大家一定要有深刻的印象。本小节的情况还是比较常见的,因此希望操作人员在删除文件的时候(尤 其在生产环境操作中),再多做一步观察进程的动作,如果查看到引用某文件的进程还没停止,就 暂时不要考虑去删除该文件。 127 1.8 uname -ahostname 由于是常用命令,而且很熟悉,危险性就出来了,我有一次输入hostname -a 结果把主机名字改为 -a, alert.log:Errors in file /u01/admin/baan/udump/baan1_ora_12539.trc: ORA-00600: 内部错误代码,参数: [keltnfy-ldmInit], [46], [1], [], [], [], [], [] Thu Dec 27 17:09:04 2007 Errors in file /u01/admin/baan/udump/baan1_ora_12541.trc: ORA-00600: 内部错误代码,参数: [keltnfy-ldmInit], [46], [1], [], [], [], [], [] Thu Dec 27 17:09:57 2007 Errors in file /u01/admin/baan/udump/baan1_ora_12617.trc: ORA-00600: 内部错误代码,参数: [keltnfy-ldmInit], [46], [1], [], [], [], [], [] Thu Dec 27 17:09:58 2007 Trace dumping is performing id=[cdmp_20071227170958] Thu Dec 27 17:09:58 2007 Errors in file /u01/admin/baan/udump/baan1_ora_12619.trc: ORA-00600: 内部错误代码,参数: [keltnfy-ldmInit], [46], [1], [], [], [], [], [] 2Shell 2.1SHELL 2.1.1 -rw-r--r-- 1 zhjs4 nxjs 96 3月 8 19:11 test.sh zhjs4@nxjs-33:~/settle/shell$test.sh 128 bash: ./test.sh: 权限被否定 这个时候该怎么执行呢? 有两种方式: 1、chmod u+x test.sh 将执行的权限给这个test.sh脚本 zhjs4@nxjs-33:~/settle/shell$chmod u+x test.sh -rwxr--r-- 1 zhjs4 nxjs 96 3月 8 19:11 test.sh 这个时候就可以直接test.sh执行了 2、sh test.sh 或者bash test.sh 这样执行就可以不需要shell脚本有执行权限了。 2.1.2 很多人喜欢写SHELL脚本的时候先在本机用UE将脚本写好,然后用UE的―文件—>FTP—> 另存到FTP‖这样的方式写进后台,其实这个时候要特别注意,有时由于UE客户端字符集问题,写 进的文件会有问题,如果VI查看文件,会发现文件中有^M这样的符号,必须用空格替换掉 如: zhjs4@nxjs-33:~/settle/shell$vi test.sh "test.sh" #! /bin/bash^M mv /jsdata/data4/pas/infile/inter_roam_tmp/* /jsdata/data4/pas/infile/inter_roam 大家有看到这个^M吧 这个时候必须vi进sh脚本,然后采用:g/^M/s///g去掉^M才可以使用,(注意,这个^M是 写成ctrl+v ctrl+m组合而成) 2.2 定时任务要执行,首先必须保证shell 脚本在手动状态下可以执行,如果遇到了前一小节的错误, 手动无法实现,那定时执行就更别提了,在保证本地手动执行没问题的情况下,设置了定时执行任 务失败会是什么原因呢? 这里我举一例,以下是在生产中能正常定时跑的脚本 zhjs4@nxjs-33:~/settle/shell$cat $HOME/settle/shell/create_ods_view.sh #!/bin/bash . ~/.bash_profile #program: # The program will create view for ods #history: #2009/02/11 liangjb sqlplus zhjs_app/zhjs_app << EOF exec p_create_ods_view; EOF 大家请注意这个. ~/.bash_profile,执行该命令时,会将环境变量路径设置读出: PATH=.:/opt/SUNWspro/bin:$ORACLE_HOME/bin:/usr/bin:/usr/ucb:/etc:/usr/openwin/bin:/usr/ccs/bin:/u sr/sbin:$HOME/share/Script:$SETTLE_HOME/bin 129 export PATH 如果读不到路径的环境变量,crontab 就无法成功,大家一定要注意。当然,你可以选择不写 . ~/.bash_profile ,而将bash_profile中涉及shell执行命令的路径写进脚本也行,不过如果是访问数据库,还需要把数据库的相关库文件写进,很麻烦,还是直接执行. ~/.bash_profile方便些。 2.3shelltelnet 可以在shell脚本中实现ftp的功能,代码大致如下 echo "ftp连接计费主机取NIG数据" ftp -n<nx_js.txt " sleep 3 echo " df -k >> nx_js.txt" sleep 3 echo "exit"; ) | telnet 135.161.24.68 我写该脚本曾困惑好久,发现有的命令可以执行有的无法执行,原来是每个命令之间一定 要有sleep,如果漏了一个,紧紧相连的第二个脚本就无法执行。当然如果每个命令完成非常快, sleep不写也可以成立,但是在crontab定时任务中,是无论如何也不能执行下去的,这是多次实验得出的结论。 希望大家记得这SHELL 的TELNET的实用登陆方法,还要记得sleep命令,不要漏 了! 130 2.4SHELL 其实unix shell也是一个很强大的编程工具,可以实现C等编程语言大部分的功能,自然也包括函数,我看到有些朋友的SHELL脚本很复杂,且整个代码中有某处用几行代码实现了一个逻辑功能, 在别处需要使用同样功能的时候却是采用重复编写,我很奇怪问他为什么不考虑函数调用,回答是: ―shell中有函数吗?‖ 下面做一个最简单的shell函数,希望大家在shell中能恰当的使用函数。 test@nxjs-33:~/ljb$more test.sh #! /bin/sh hello () { echo "wabjtam" } echo "let's begin!" hello echo "over" 好了,脚本写好了,相当简洁吧,和C中格式没啥区别,让测试一下效果,成功了! test@nxjs-33:~/ljb$sh test.sh let's begin! wabjtam over 2.5SHELL 项目组有人要在脚本中实现当前月减1,比如当前月份是200903,输入日期必须为200902,也 就是代码需处理上月数据。但是他们实现中,由于转化为数字后,把200903转化成了20092,结果 出错!如果要加上0来并起来的话,200903是转化为200902l ,但是又会把200912转化成2009011 解决方法如下,很简单,加个判断句OK了,见如下测试: test@nxjs-33:~$yy=`date +%Y ` test@nxjs-33:~$echo $yy 2009 test@nxjs-33:~$mm=`date +%m-1|bc` test@nxjs-33:~$echo $mm 2 test@nxjs-33:~$[ $mm -lt 1 ] && (mm=12;yy=`expr $yy -1` ) test@nxjs-33:~$[ $mm -lt 10 ] && mm="0$mm" test@nxjs-33:~$echo $yy$mm 200902 总结: SHELL是个好工具,很简洁,看&&和||表判断真精练!要好好学习这个工具! 131 2.6 SHELL 2.6.1shell 2.6.1.1 shell 经常会把访问数据库并做各种操作的动作写在后台shell中执行,不过会有这样的情况,比如我 在执行中发现写法有问题或者其他原因,要把执行的步骤终止,这个时候该怎么做呢? 很多人就直接在后台找到该shell执行的进程,直接将该进程kill,以为就将执行停止了,其实很多时这个操作是没有用的,因为其实这个时候后台的操作已经到了数据库的层面了,仅仅杀掉shell的进程还不行,要找到数据库的相关进程即v$session中的sid(这里可以通过观察v$session的proam 字段,通过select * from v$session where proam like ?%sqlplus%‘ and status=active的查询语句很快定位到对应的sid),然后alter system kill session ?sid,serial#‘的方式将该session kill掉,当然,这个是需要权限的。另外,你也可以通过关联v$session于v$process两张视图,从v$process中找到spid,也就是对应后台oracle用户下的数据库进程,然后kill该进程,也能将达到终止数据库操作的目的。但 是这个也是需要权限的,因为后台的oracle用户权限是受到很严格的控制的。 总结:平时就有可能出现在oracle的plsql development工具在command下跑脚本,出了问题可以强行终止,但是如果通过shell脚本执行,在数据库层面无权限使用alter system kill session 命令,在后台无权限kill oracle下的进程,导致一直无法终止错误的操作,不得不求救于有权限的类似DBA等人员。所以只具备普通权限的开发人员在把对数据库访问的脚本放在后台shell脚本执行的时候,一定要检查清楚脚本再执行,否则认为有错终止该执行将会是一件很困难的事。我在宁夏出差的时 候就吃过这样的亏,由于没有权限,半夜又不好骚扰有权限的IT中心的管理员,出错的脚本无法终 止,只好任由其跑到结束为止再重新处理,白白浪费了很多宝贵的时间。 2.6.1.2 spoolsql 大家一定有在shell中调用数据库的经验,这里我简单的描述一下,比如某脚本test.sh Cat test.sh 内容为: sqlplus zhjs_app/zhjs_app << EOF set termout off set heading off set term off set echo off set feedback off spool ./test.txt select * From dual; spool off; EOF 该脚本执行完后,输出到/test.txt下,让查看test.txt的输出结果 132 $cat test.txt SQL> select * from dual; D - X 可以看到spool出来的结果中有SQL> select * from dual;同时还有字段值,和set heading off及 set echo off矛盾了,该怎么处理呢? 方法是,在test.sh中写调用test.sql的脚本,输出格式的脚本写在test.sql中,就可以实现了。不 过这里有个细节一定要注意!就是我这里的结束符EOF是写在调用的test.sql 中的,只有这样,才 能实现的目标。 cat test.sh sqlplus test/test <FNR && a[b=substr($1,1,4)] {print $0,a[b]}' 2.txt 1.txt 0011AAA 200.00 20050321 11111 0012BBB 300.00 20050621 22222 0013DDD 400.00 20050622 66666 136 0014FFF 500.00 20050401 55555 此外要另外注意一点,如果执行如下操作会失败: test@nxjs-33:~/ljb$awk 'NR==FNR {a[substr($1,2,5)]=$2} NR>FNR && a[b=substr($1,1,4)] {print $0,a[b]}' 2.txt 1.txt awk: syntax error near line 1 awk: bailing out near line 1 这里使用的是AWK,特别说明,有的版本的AWK并不支持FNR,比如SUN环境下就有这样的问题,这个时候可以考虑使用AWK的亲戚,NAWK来调用 总结:1、NR和FNR很多人搞不清楚含义,这里我做了详细的比较,相信大家应该能完全搞清 楚了。2、由于不同操作系统的限制,有的时候命令无法执行,这里我是用nawk来代替awk才实现了该功能,我是在sorlaris环境,有此限制,不过大家可以多实验一下各种awk,除了awk外还有nawk,gawk等等,试验后可以成立,就可以采用,要有想法解决问题的热情。3、我在UNIX的开篇中有说过,UNIX下的AWK是很强大的,它可以实现数据库中很多类似的操作,这里只是举了最 简单的例子,希望抛砖引玉,大家由此打开兴趣的大门,把后台处理文本的脚本玩的和数据库中SQL操作一样熟练。 2.7.3 举个例子,出题如下, zhjs@nxjs-03:/exchange/transbin/exccheckname/log$下面有很多文件,比如取其中一个文件, BOSSNM.50-30-61-00-00-003.12178.20090110142912,用cat查看内容 cat BOSSNM.50-30-61-00-00-003.12178.20090110142912 2|NOG_20090110951.0058|0|20090110142912 现需要将其中的所的带20090116的文件全部进行统计,把其中类似NOG这样的不同记录(特征为在每条记录的头条,并以‖_‖结束)分类进行统计,并列出统计记录。 很多人采用了将所有文件中的内容用sqlldr导入数据库,然后在数据库中用数据库的sql语句,如count(*) group by 来实现。不是说sqlldr的方式不行,当然入库后会好操作很多,毕竟数 据库的SQL语句功能很强大,但是这里完全可以用更简单的方法来实现,只要善于学习,灵活 运用awk知识,就可以更轻松搞定。 以下是我的统计方法,单条命令很方便的直接在日志中得出了结果: zhjs@nxjs-03:/exchange/transbin/exccheckname/log$awk -F"|" '{print $2}' *20090116*|awk -F"_" '{a[$1]++}END {for (i in a ) print i,a[i]}' NOG 94 NOT 87 IOSMP 86 OBRC 97 我写这个有一定代表性,大家想想,如果你对shell很熟悉,那后台的原始话单等文 件,都可以在入库前,在OS层面就能对数据进行分析,这应该增强了解决问题的能力,比如 话单不能入库了,只要你善于写后台脚本,你就有可能直接从文本中看出话单不能入库的原因。 此外本例中别忘了写END,大家可以实验并思考不写END会是一种什么情况。 137 2.8.4 awk '{gsub(/\$/,"");gsub(/,/,""); if ($4>1000&&$4<2000) c1+=$4; else if ($4>2000&&$4<3000) c2+=$4; else if ($4>3000&&$4<4000) c3+=$4; else c4+=$4; } END {printf "c1=[%d];c2=[%d];c3=[%d];c4=[%d]\n",c1,c2,c3,c4}"' file 判断语句有一定的复杂度,不过相对还是好理解,通过if和else if完成条件语句,希望大家好好体会! 2.8.5 test@nxjs-33:~/ljb$cat 1.txt 0011AAA 200.00 20050321 0012BBB 300.00 20050621 0013DDD 400.00 20050622 0014FFF 500.00 20050401 替换:(请注意这个$2="100"; print $0中间的分号) test@nxjs-33:~/ljb$ test@nxjs-33:~/ljb$awk '{if ($2=="200.00") {$2="100"; print $0}}' 1.txt 结果为:0011AAA 100 20050321 构造: test@nxjs-33:~/ljb$awk '{if ($2=="200.00") {diff=$3-$2; print diff}}' 1.txt 20050121 test@nxjs-33:~/ljb$awk '{if ($2=="200.00") {diff=$3-$2; print $1,$2,$3,diff}}' 1.txt 0011AAA 200.00 20050321 20050121 2.8.6 AWK awk的出错提示不怎么友好,经常报错让摸不着头脑,所以要对它的格式某些地方特别敏感! 这里我根据经验特别说明如下几点: 1、 awk 后面的语句一定要有两个‘‘ 包含起来! 2、一定要有一个文本的输入,可以放在后面,如本例的file,也可以通过管道符如|传给awk 如 ls |awk ?{print $1}‘ 这个时候的输入文件就是把ls的结果输给awk去处理! 3、-F参数是用来控制分割符的,比如 cat ljb.txt |awk –F ―;‖ ?{print $1,$2}‘表示将ljb.txt文件中的内容的分号做为分割符,打印出第一列和第二列的值。这里要特别注意awk –F‖;‖ 命令中,-F和‖;‖ 是没有空格的。 4、awk中的==表示等于,而=表示赋值,如果输错了,结果就完全错了。 比如 cat ljb.txt |awk ?{if($3==6) print $0}‘表示如果ljb.txt中默认以空格分割,假如第三列值为6, 打印出来,如果这里将==写=,那就会打印出所有的值,其中第三列都被更新为6! 138 5、判断语句要有括号 如if($3==6) 6、这里我跳出AWK说一下SHELL,SHELL的[]中往往是判断,类似为 if [ $1=2 ]等,这里有别于awk,shell这个时候其实就是表示等于而不是赋值,如果你把=改写 成两个=的==,那反而出错了,另外SHELL中也要留意,这里[]中的代码不能顶在[]的两边,如: [$1=2]或者是[$1=2 ]和[ $1=2]都是错误的! 7、大家记得2.6.2小节中我整理了shell比较有别于其他编写程语句的一些特定脚本写法, 现将awk 相关部分特别含义写法整理如下: ARGC 命令行参数个数 ARGV 命令行参数排列 ENVIRON 支持队列中系统环境变量的使用 FILENAME awk浏览的文件名 FNR 浏览文件的记录数 FS 设置输入域分隔符,等价于命令行- F选项 NF 浏览记录的域个数 NR 已读的记录数 OFS 输出域分隔符 ORS 输出记录分隔符 RS 控制记录分隔符 139
本文档为【ORACLE+UNIX易錯問題集錦】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
该文档来自用户分享,如有侵权行为请发邮件ishare@vip.sina.com联系网站客服,我们会及时删除。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。
本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。
网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。
下载需要: 免费 已有0 人下载
最新资料
资料动态
专题动态
is_337177
暂无简介~
格式:doc
大小:596KB
软件:Word
页数:0
分类:互联网
上传时间:2018-04-28
浏览量:10