|
热块概述
当一个会话需要访问一个数据块,而这个数据块正在被另一个用户从磁盘读取到内存中或者这个数据块正在被另一个会话修改时,当前的会话就需要等待,就会产生一个buffer busy waits等待,也伴随着Latch争用。如果太多的会话去访问相同的数据块导致长时间的buffer busy waits等待,通常表现形式为CPU使用率很高,但吞吐量很低。造成热快的原因可能是数据库设置导致或者重复执行的SQL 频繁访问一些相同的数据块导致。热块产生的原因不尽相同,按照数据块的类型,可以分成以下几种热块类型,不同热块类型处理的方式都是不同的:表数据块、索引数据块、索引根数据块、文件头数据块和数据块自身的争用。
表数据块
比如在OLTP系统中,对于一些小表,会出现某些数据块被频繁查询或者修改的操作,这时候这些被频繁访问的数据块就会变成热块,导致内存中Latch的争用。
如果出现这样的热块情况,并且表不太大,一个方法是可以考虑将表数据分布在更多的数据块上,减少数据块被多数会话同时访问的频率。
可以通过下面的命令将每个数据块存放记录的数量减到最少:
alter table minimize records_per_block;
我们也可以通过调大PCTFREE来实现,不过在调整PCTFREE时,我们要注意的是,这个参数虽然是可以动态调整的,但是我们调整PCTFREE参数只能对新的数据插入起作用,对于已经填充过满的老数据块,是无法起作用的,数据分布问题,必须调整参数后,对表进行重组,才能对表中的所有数据块都起作用。对表重组的办法有很多,比如ALTER TABLE ... MOVE或者EXP/IMP等。除了调整PCTFREE外,减少热块冲突的更好的办法是将这张表放在BLOCK_SIZE较小的表空间里。以上所有的方法都是将数据尽可能地分布到多个数据块为指导思想进行实现的。
请看下面的例子,创建两个数据量相同的表,让一个表上每个数据块存放记录数尽可能地小,另一个表保持默认的情况,来看数据块访问情况:
SQL> create table t as select * from dba_objects;
Table created.
SQL> create index t_inx on t(object_id);
Index created.
SQL> exec dbms_stats.gather_table_stats(‘HR','T',cascade=>true);
PL/SQL procedure successfully completed.
SQL> select 'T' tbl_name, rows_per_block,
count(*) number_of_such_blocks from (
2 select dbms_rowid.ROWID_BLOCK_NUMBER
( rowid ), count(*) rows_per_block
3 from t
4 group by dbms_rowid.ROWID_BLOCK_NUMBER( rowid ))
5 group by 'T', rows_per_block;
--- -------------- ---------------------
T 82 16
T 76 18
T 74 22
T 77 16
T 65 3
T 87 2
T 84 5
T 69 76
T 93 2
T 91 1
T 75 20
TB1 ROWS_PER_BLOCK NUMBER_OF_SUCH_BLOCKS
--- -------------- ---------------------
T 68 47
T 72 48
T 94 1
T 88 1
T 73 34
T 81 15
T 79 27
T 92 1
T 62 1
T 83 18
T 71 113
TB1 ROWS_PER_BLOCK NUMBER_OF_SUCH_BLOCKS
--- -------------- ---------------------
T 70 153
T 67 11
T 86 2
T 80 18
T 78 15
T 85 4
T 64 1
29 rows selected.
SQL> create table t1 as select * from dba_objects where rownum select 'T' tb1_name,rows_per_block,count(*) number_of_such_blocks from (select dbms_rowid.rowid_block_number(rowid),count(*) rows_per_block from t1 group by dbms_rowid.rowid_block_number(rowid)) group by 'T',rows_per_block
TB1 ROWS_PER_BLOCK NUMBER_OF_SUCH_BLOCKS
--- -------------- ---------------------
T 2 1
1 rows selected.
SQL> insert into t1 select * from dba_objects;
50833 rows created.
SQL> commit;
Commit complete.
SQL> create index t1_inx on t1(object_id);
Index created.
SQL> exec dbms_stats.gather_table_stats(’HR','t1',cascade=>true);
PL/SQL procedure successfully completed.
SQL> select 'T' tbl_name, rows_per_block,
count(*) number_of_such_blocks from (
2 select dbms_rowid.ROWID_BLOCK_NUMBER
( rowid ), count(*) rows_per_block
3 from t1
4 group by dbms_rowid.ROWID_BLOCK_NUMBER( rowid ))
5 group by 'T', rows_per_block;
6 /
TB ROWS_PER_BLOCK NUMBER_OF_SUCH_BLOCKS
-- -------------- ---------------------
T1 2 25297
T1表上将每个数据块上存放的记录数缩小为2条,这样T1表上有数据的数据块数量要远远多于T表。
SQL> set autotrace traceonly
SQL> select * from t where object_id -------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 954 | 88722 | 25 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID| T | 954 | 88722 | 25 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | T_IDX | 954 | | 4 (0)| 00:00:01 |
-------------------------------------------------------------------------------------
Predicate Information (identified by operation> ---------------------------------------------------
2 - access("OBJECT_ID" 91547 bytes sent via SQL*Net to client
1078 bytes received via SQL*Net from client
65 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
953 rows processed
SQL> select * from t1 where object_id --------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 956 | 88908 | 740 (1)| 00:00:09 |
| 1 | TABLE ACCESS BY INDEX ROWID| T1 | 956 | 88908 | 740 (1)| 00:00:09 |
|* 2 | INDEX RANGE SCAN | T1_INX | 956 | | 4 (0)| 00:00:01 |
--------------------------------------------------------------------------------------
Predicate Information (identified by operation> ---------------------------------------------------
2 - access("OBJECT_ID" 91701 bytes sent via SQL*Net to client
1078 bytes received via SQL*Net from client
65 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
955 rows processed
当我们分别从T,T1表中选出1000条数据时,可以看到两个表的执行计划是相同的,返回的结果集是相同的,但T1表的一致性读的数量是620,要 比T表143大出很多,原因就是T1表中的行分布在更多的数据块上,导致Oracle需要读取更多的数据块来完成查询。从另一个角度看,我们把数据分布到 了更多的数据块上,大大降低了一个数据块被重复读取的概率。
但是这种方式的缺点是显而易见的,就是降低了数据的性能,在这种情况下,访问相同的数据意味着需要读取更多的数据块,性能会有所降低。
不过如果存在这样的情况,比如很多用户只修改1条记录并且各自更新的数据不同,那么这样做的好处就非常明显。在T表中,一个数据块上大约有70条记录,就是说很可能70个用户会同时访问一个数据块,这必然导致热块的出现;当我们让每个数据块只存放2条记录时,最多只会有2个用户会同时访问一个数据 块,这样热块的概率将会被极大地降低。
除了这种方法外我们还可以从data buffer中着手解决,主要思路为划分缓存池和提高SQL命中率。在DATA BUFFER中有如下3种pool
KEEP pool缓冲池将一直保留存储在其中的方案对象的数据,维护一个适当尺寸的KEEP缓冲池可以使Oracle在内存中保留数据库对象而避免I/O操作。在表、聚集、实体化视图或实体化视图日志上,KEEP子句的优先权大于NOCACHE子句,需要显示指定。
Keep Buffer Pool的说明
KeepBuffer Pool 的作用是缓存那些需要经常查询的对象但又容易被默认缓冲区置换出去的对象,按惯例,Keep pool设置为合理的大小,以使其中存储的对象不再age out,也就是查询这个对象的操作不会引起磁盘IO操作,可以极大地提高查询性能。
注意一点,不是设置了keep pool 之后,热点表就一定能够缓存在 keep pool ,keep pool 同样也是由LRU 链表管理的,当keep pool不够的时候,最先缓存到 keep pool的对象会被挤出,不过与default pool 中的LRU的管理方式不同,在keep pool中表永远是从MRU移动到LRU,不会由于你做了FTS(全表扫描)而将表缓存到LRU端,在keep pool中对象永远是先进先出。
在这点上对于DefaultPool就有所不同。默认情况下是nocache,即全表扫描数据在正常情况下是放到LRU的冷端,使其尽快page out(这是default pool的默认策略), 而指定了alter table cache后,该表的全表扫描数据就不是放到LRU的冷端, 而是放到热端(MRU)了,从而使该得数据老化较慢,即保留的时间长。
因为这个原因,如果keep pool空间比table 小,导致不能完全把table keep下,那么在keep pool 中最早使用的数据还是有可能被清洗出去的。还是会产生大量的逻辑读,这样就起不到作用,所以,如果采用keep,就必须全部keep下,要么就不用keep。
综上所述,我们可以总结为:KeepPool改变的是存储位置,Cache改变的是存储方式,Cache则可以认为是无法使用keep的情况下的折中选择。
注意:如果采用Keep Pool,Data 都会cache到内存中,所以会忽略对象本身的Cache 和Nocache。
在Oracle 10g中SGA自动管理,ORACLE并不会管理keep pool,ORACLE只会管理default pool。
默认的情况下db_keep_cache_size=0,未启用,如果想要启用,需要手工设置db_keep_cache_size的值,设置了这个值之后 db_cache_size 会减少。
将表keep到内存中的过程如下
SQL> select * from v$sgainfo;
NAME BYTES RESIZEABL
------------------------- ---------- ---------
Fixed SGA> Redo Buffers 2973696 No
Buffer Cache>
Shared Pool>
Large Pool>
Java Pool>
Streams Pool>
Granule>
Maximum SGA> Startup overhead in Share 37748736 No
d Pool
NAME BYTES RESIZEABL
------------------------- ---------- ---------
Free SGA Memory Available 0
11 rows selected.
SQL> SELECT x.ksppinm NAME,y.ksppstvl VALUE, x.ksppdesc describ FROM SYS.x$ksppi x, SYS.x$ksppcv y WHERE x.indx = y.indx AND x.ksppinm LIKE '%db_cache_size%'
NAME VALUE DESCRIB
------------------------- -------------------- -----------------------------------------------------------------
__db_cache_size 197132288 Actual> s
db_cache_size 0 Size of DEFAULT buffer pool for standard block>
SQL>>
System> 查看db_keep_cache_size和db_cache_size的大小,可以看到db_cache_size变小了
SQL> show parameter db_keep_cache_size
NAME TYPE VALUE
------------------------------------ --------------------------------- ------------------------------
db_keep_cache_size big integer 52M
SQL> SELECT x.ksppinm NAME,y.ksppstvl VALUE, x.ksppdesc describ FROM SYS.x$ksppi x, SYS.x$ksppcv y WHERE x.indx = y.indx AND x.ksppinm LIKE '%__db_cache_size%'
NAME VALUE DESCRIB
------------------------- -------------------- -----------------------------------------------------------------
__db_cache_size 142606336 Actual> s
将表keep到keep pool中
SQL> create table t1_keep as select rownum> Table created.
SQL>>
Table>
SQL> create table t2 storage(buffer_pool keep) as select rownum> Table created.
取消keep
SQL>>
Table> 启用cache
SQL>>
Table> 关闭cache
SQL>>
Table> RECYCLE pool缓冲池将随时清除存储在其中不再被用户需要的数据,一个适当尺寸的RECYCLE缓冲池可以减少默认缓冲池为RECYCLE缓冲池的数据库对象的数量,以避免它们占用不必要的缓冲空间,需要显示指定。设置表使用RECYCLE pool
SQL>>
Table> DEFAULT pool缓冲池中存储的是没有被指定使用其他缓冲池的方案对象的数据,以及被显式地指定使用DEFAULT缓冲池的方案对象的数据。
实际生产环境下,我们可以将被访问次数较多的热块,存放在keep pool中;将被访问次数较较少的冷块,存放在recycle pool中;将被访问次数一般,则保持default pool不变
注意:上述三种数据缓冲池只有标准容量的数据缓存区才具备。非标准容量数据块使用的数据缓冲区中只有 DEFAULT 缓冲池。
关于提高buffer cache提高缓存命中率实际上最有效的办法,是从优化sql入手,不良的sql往往带来大量的不必要的访问,这是造成热点块的根源。比如本该通过全表扫描的查询却走了索引的range scan,这样将带来大量的对块的重复访问。从而形成热点问题。再或者比如不当地走了nested loops的表连接,也可能对非驱动表造成大量的重复访问。那么在这个时候,我们的目标就是找出这些sql来并尝试优化。关于如何找出造成热块的sql可参考
http://czmmiao.iteye.com/blog/1292412
最直接的就是扩大db_cache_size。在10g以后,oracle建议采用sga的自动内存管理,但是我们可以为我们data buffer设置最小值。
SQL>>
System> 设置data buffer的最小值为100MB。
索引数据块
这样的情况通常发生在一个RAC架构里,某个表的索引键值呈现出典型的"右倾"现象,比如一个表的主键使用一个序列来生成键值,那么这个主键在索引数据块上的键值就是以一种顺序递增的方式排列的,比如1,2,3,4,5,…,由于这些键值分布得非常接近,当许多用户在RAC的不同实例来向表中插入主键时,就会出现相同的索引数据块在不同实例的内存中被调用,形成一种数据块的争用,对于这样的情况,使用反向索引可以缓解这种争用。反向索引是将从前的索引键值按照反向的方式排列,在正常的主键B-TREE索引中,键值会按照大小顺序的排列,比如这样:
1234、1235、1236、1237
而反向索引之后,键值变成下面的样子:
4321、5321、6321、7321
这样,本来是放在相同的索引数据块上的键值,现在分布到不同的数据块上,这样用户在RAC不同的实例上插入的主键值因为分布在不同的数据块上,所以不会导致热块的产生,这基本上是反向索引被使用的唯一情况。
反向索引使用场合之所以如此受限,是因为它丢弃了B-TREE索引的一个最重要的功能:
INDEX RANGE SCAN
索引访问方式中,这个方式最为常见,但是反向索引却不能使用这个功能,究其原因就是,反向索引已经把键值的排列顺序打乱,当按照键值顺序地查找一个范围时,在反向索引中,由于键值被反向存储,这些值已经不是连续存放的了。所以INDEX RANGE SCAN的方式在反向索引中没有任何意义。看下面的例子:
SQL> set autotrace trace explain
SQL> select * from t where> Execution Plan
----------------------------------------------------------
Plan hash value: 2152055767
--------------------------------------------------------------------------
|> --------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 765 | 3060 | 3 (0)| 00:00:01 |
|* 1 | INDEX RANGE SCAN| T_INX | 765 | 3060 | 3 (0)| 00:00:01 |
--------------------------------------------------------------------------
Predicate Information (identified by operation> ---------------------------------------------------
1 - access("ID">1234 AND "ID" select * from t1 where> Execution Plan
----------------------------------------------------------
Plan hash value: 3617692013
--------------------------------------------------------------------------
|> --------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 765 | 3060 | 22 (10)| 00:00:01 |
|* 1 | TABLE ACCESS FULL| T1 | 765 | 3060 | 22 (10)| 00:00:01 |
--------------------------------------------------------------------------
Predicate Information (identified by operation> ---------------------------------------------------
1 - filter("ID"1234)
SQL> select /*+ index(t1,t1_inx) */* from t1 where> Execution Plan
----------------------------------------------------------
Plan hash value: 3995001570
---------------------------------------------------------------------------
|> ---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 765 | 3060 | 114 (2)| 00:00:02 |
|* 1 | INDEX FULL SCAN | T1_INX | 765 | 3060 | 114 (2)| 00:00:02 |
---------------------------------------------------------------------------
Predicate Information (identified by operation> ---------------------------------------------------
1 - filter("ID"1234)
我们看到,对于一个在实际应用中非常常见的条件范围的查询,在反向索引中却只能够通过全表扫描或者全索引扫描的方式来实现,这就是反向索引的一个非常严重的缺陷,所以在生产环境下使用它时要十分慎重。
索引根数据块
热块也可能发生在索引的根数据块上。我们知道在B-TREE索引里,当Oracle访问一个索引键值时,首选访问索引的根,然后是索引的分支,最后才是索引的叶块,索引的键值就是存储在叶块上面,下图是B-TREE索引示意图。
当索引的根、枝数据都集中在几个数据块上时,比如D,G所在的枝数据块,当用户访问的范围从A~L时,都会访问这个数据块,如果很多用户频繁地访问这个范围的索引键值,有可能导致这个枝数据块变成热块。
当出现这种现象时,可以考虑对索引做分区,以便于使这些根、枝数据块分布到不同的数据段(分区)上,减少数据块的并行访问的密度。请看下面例子
SQL> create table t(id number(10),object_name varchar2(50))
2 partition by range(id)(
3 partition t_range_p1 values less than(10) tablespace example,
4 partition t_range_p2 values less than(100) tablespace example,
5 partition t_range_p3 values less than(200) tablespace example,
6 partition t_range_p4 values less than(300) tablespace example,
7 partition t_range_p5 values less than(500) tablespace example,
8 partition t_range_p6 values less than(600) tablespace example,
9 partition t_range_p7 values less than(700) tablespace example,
10 partition t_range_p8 values less than(800) tablespace example,
11 partition t_range_p9 values less than(900) tablespace example,
12 partition t_range_p10 values less than(1000) tablespace example,
13 partition t_range_p11 values less than(1100) tablespace example,
14 partition t_range_p12 values less than(1200) tablespace example,
15* partition t_range_max values less than(maxvalue) tablespace example)
Table created
SQL> insert into t
2 select rownum> 50333 rows created.
SQL> commit;
Commit complete.
SQL> create index t_inx on t(id) local;
Index created.
SQL> exec dbms_stats.gather_table_stats('HR','T',cascade=>true);
PL/SQL procedure successfully completed.
SQL> create table t1 as select rownum> Table created.
SQL> create index t1_inx on t1(id);
Index created.
SQL> exec dbms_stats.gather_table_stats('HR','T1',cascade=>true);
PL/SQL procedure successfully completed.
SQL>>
Session>
SQL> select /*+ index(t t_inx) */ * from t where> .....................
999 rows selected.
SQL> select /*+ index(t1 t1_inx) */ * from t1 where> .....................
999 rows selected.
SQL>>
Session> trace文件中的t表执行计划如下
select /*+ index(t t_inx) */ *
from
t where> call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 2 0.00 0.00 0 0 0 0
Execute 2 0.00 0.00 0 0 0 0
Fetch 136 0.02 0.02 0 308 0 1998
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 140 0.02 0.02 0 308 0 1998
Misses in library cache during parse: 1
Optimizer mode: ALL_ROWS
Parsing user> Rows Row Source Operation
------- ---------------------------------------------------
999 PARTITION RANGE ITERATOR PARTITION: 1 10 (cr=154 pr=0 pw=0 time=38191 us)
999 TABLE ACCESS BY LOCAL INDEX ROWID T PARTITION: 1 10 (cr=154 pr=0 pw=0 time=27015 us)
999 INDEX RANGE SCAN T_INX PARTITION: 1 10 (cr=77 pr=0 pw=0 time=10564 us)(object> trace文件中的t1表执行计划如下
select /*+ index(t1 t1_inx) */ *
from
t1 where> call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 1 0.00 0.00 0 0 0 0
Execute 1 0.00 0.00 0 0 0 0
Fetch 68 0.03 0.02 0 141 0 999
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 70 0.03 0.02 0 141 0 999
Misses in library cache during parse: 1
Optimizer mode: ALL_ROWS
Parsing user> Rows Row Source Operation
------- ---------------------------------------------------
999 TABLE ACCESS BY INDEX ROWID T1 (cr=141 pr=0 pw=0 time=166354 us)
999 INDEX RANGE SCAN T1_INX (cr=71 pr=0 pw=0 time=51109 us)(object> 从结果中可以看到,T1表上的SQL扫过了71个索引数据块,而T表扫过了77个索引块,T表索引的访问方式为分区索引的范围扫描。尽管两个SQL扫过相同的索引键值,但是T表却比T1表多扫过13+6=19个数据块,原因就是T的索引分布在不同的分区上,Oracle需要多次通过索引根数据块才能访问 到键值,所以就会多出一些数据块来。
上面的例子说明了当把一个索引创建为分区索引时,需要将索引的根数据块会分布到更多的数据块上,具体可通过控制pctfree参数来实现。通过这种方式,可以避免由于索引根、枝数据块太集中导致热块产生。
段头数据块
在Oracle 9i之前,数据块的空间使用情况需要手工来管理,在每个数据段的段头有个(或者几个)Free List列表,用于存放段中哪些数据块可以使用。当数据块里面的数据达到数据块总容量的一个比例时,数据块就会从Free List列表中剔除,这个数据块就不能够再插入数据。这个比例由参数PCT_FREE来确定。当数据块的空间减少到一个比例时,数据块就会被放到Free List列表中,这些数据块可以被用来插入数据。这个比例由参数PCT_USED确定。
在OLTP系统数据库中,一些数据段的Free List可能会是一个访问很频繁的数据块,比如这个数据库上有些表有很多删除、插入的动作,很多会话需要不断访问这些数据块的Free List列表,以便获得需要的数据块信息。此时这个数据块(称作段头数据块)就会变成一个热块,此时内存中就会出现比如cache buffer chain这样的Latch等待事件;当出现这个问题时,一个常见的解决方法是增加Free List的数量,以便于分散会话访问数据块的密度,比如有10个用户并行地向一个表中插入数据,我们设置Free Lists的值为10,就可以避免出现等待,这在一定程度上能够缓解段头的热块问题。
从Oracle 9i开始,引入了一个自动段管理的技术ASSM(Automatic Segment Space Management,ASSM),它让Oracle自动管理"Free Lists"。实际上在ASSM里面,已经没有Free List这样的列表结构,Oracle使用位图方式来标记数据块是否可用,这种数据块的空间管理方式比用一个列表来管理更加高效。当然段头位图端也有可能出现等待,如果等待集中在BMB 上,那么一般来说只能通过使用分区表或者调整应用等手段来解决了。
对于一个OLTP系统,表的DML操作非常密集,对于这些表,使用ASSM方式来管理会比人工管理更加方便和准确,能有效地避免段头变成热块;而对于OLAP系统数据库,这个参数并没有太大的实际意义,因为在这样的数据库中,很少有表发生频繁修改,OLAP系统数据库主要的工作是报表和海量数据的批量加载。
数据块自身的争用
HASH 分区表和HASH 簇表 是解决热块冲突的一种较为常用的办法,对于表数据量较大的情况,可以考虑采用 HASH 分区表。比如有一张表,主键是通过 SEQUENCE 产生的,那么在没有使用 HASH 分区表的情况下,同一个时间点产生的记录存储在同一个数据块中的可能性很大。而这些数据随后又被其他应用使用,这样产生热块的机会就很高了。如果我们将这张表根据主键设计成 HASH 分区表,那么同一个时刻产生的记录就被 HASH 算法分布到不同的表分区中去了,访问这些数据的时候就可以从多个数据块中读取,从而缓解了热块冲突。
任何事物都有两面性, HASH 分区也是。 HASH 分区 表解决了热块冲突的问题,但是带来了另外一个问题,如果我们的应用总是通过主键来访问这张表的数据,那么这种方式确实是最好的。但是如果我们还有大量的应用需要根据主键进行范围扫描,或者按照记录的生成日期进行范围扫描,那么 HASH 分区表的弱点就显现出来了。原本放在同一个数据块中的数据被 HASH 算法分散开了,这同时意味着我们对这些数据做范围扫描的时候需要扫描更多的数据块。这就是 HASH 分区表的弱点,增加了范围扫描的成本。
在实际的生产环境中,我们可能不总是那么幸运,我们肯定会碰到两方面的需求。一方面是热块冲突必须解决,另外一方面可能我们还存在一定的应用要对这些数据做范围扫描。在这种情况下,我们必须进行综合的评估,到底哪种需求是主要需求。如果我们优化范围扫描对系统更为有利,那么我们就必须放弃 HASH 分区;如果解决热块冲突更为重要,那么我们就必须牺牲范围扫描。
参考至:《让Oracle跑得更快》谭怀远著
http://blog.sina.com.cn/s/blog_7414f6640100uouf.html
http://www.oraclefans.cn/forum/showblog.jsp?rootid=17841
http://www.ordba.net/Tutorials/Seg4.htm
http://justdba.blogbus.com/logs/65743832.html
http://blog.csdn.net/tianlesoftware/article/details/6581159
本文原创,转载请注明出处、作者
如有错误,欢迎指正
邮箱:czmcj@163.com
原文链接: http://czmmiao.iteye.com/blog/1474472
|
|