2009年12月31日星期四

MySQL字符集III

今天继续研究字符集.

上次似乎的出了结论,但是后来测试又发现一些怪现象,所以今天继续测试.就是关于客户端字符集的,因为每一个客户端都有自己的字符集,当其与设置的character_set_client以及character_set_results不匹配的时候,其中的转换关系是否如我所预期的呢?

 

我使用我机器上的windows命令行,因为我确定其客户端使用的字符集是gbk

Create database tim character set utf8;

Use tim;

Create table test_gbk(t varchar(100)) character set gbk;

Create table test_utf8(t varchar(100)) character set utf8;

insert test_gbk values('阳光'),('太阳'),('济南'),('光明');

insert test_utf8 values('阳光'),('太阳'),('济南'),('光明');

select * from test_gbk;

select * from test_utf8;

都能正常显示中文

Select length(t) from test_gbk; 4

Select length(t) from test_utf8; 6

Select char_length(t) from test_gbk; 2

Select length(t) from test_utf8; 2

也是正常的存储了对应的gbk编码以及utf8编码

Set character_set_connection=’ascii’;

奇怪的事情出现了

Select * from test_gbk; select * from test_utf8;

都能够正常显示中文,并没有如我预期的一样从gbk,utf8转换到ascii出现数据丢失.

但是insert into test_gbk values(‘中国’);  insert into test_utf8 values(‘中国’);

Select * from test_gbk; select * from test_utf8;

我们刚插入的一行显示的是问号?

也就是说在insert的时候,是有一个character_set_clientcharacter_set_connect然后再到数据库字符集的转换过程的,但是在select的时候,这个转化过程是没有的.

 

然后我们来看一下character_set_clientcharacter_set_results起的作用

在一个utf8的客户端,set names gbk;

INSERT INTO test_gbk VALUES ('南方');

INSERT INTO test_utf8 VALUES ('南方');

然后在设置正常的命令行查看,得到的结果均是���

LENGTH函数查看,前者得到的结果是6,后者得到结果是9.

我们分析一下过程,因为是utf8的客户端,所以实际得到的均是utf8编码的南方

然而因为我们set names gbk, 也就是说character_set_clients, character_results, character_connection均是gbk,clientconnection不需要转换,存储到数据库的时候,当存储到test_gbk的时候,不需要转换,所以存储的是6个字节的utf8编码南方”;

当存储到test_utf8的时候,执行从gbkutf8的转换,所以引擎讲utf8编码的南方视为gbk编码的3个汉字, 也就是���”,然后再以utf8编码来编码这3个汉字,所以就变成了9个字节.

 

我前面谈到过phpmyadmin的字符集,也曾经在phpmyadminset names,结果不起作用.

经研究,发现phpmyadmin在连接mysql数据库的时候,character set connection是可以设置的,就是通过选择MySQL connection collation来设置的,character_set_client, character_set_results目前尚不清楚是怎样决定的,其客户端字符集则是跟登陆时候选择的语言所对应的php文件编码有关系.

我使用的phpmyadmin3.2.4, phpmyadminlibrary目录下有一个select_lang.lib.php的文件,其中定义了所有我们在登陆时候可以选择的语言列表:

return array(

        'af-utf-8'    => array('af|afrikaans', 'afrikaans-utf-8', 'af', ''),

        'ar-utf-8'    => array('ar|arabic', 'arabic-utf-8', 'ar', 'العربية'),

        'az-utf-8'    => array('az|azerbaijani', 'azerbaijani-utf-8', 'az', 'Azərbaycanca'),

        'bn-utf-8'    => array('bn|bangla', 'bangla-utf-8', 'bn', ''),

……

其中每一条都有类似于”afrikaans-utf-8”,”arabic-utf-8”,”azerbaijani-utf-8”,”bangla-utf-8”的字符串,phpmyadmin根据选择语言所对应的这一串字符串来寻找想对应的界面配置文件,这些文件存储在phpmyadminlang目录下,而这个文件中第一行定义的变量$charset则决定了登陆以后的页面字符集,也可以通过查看页面源码文件中的字符集来查看.

MySQL字符集II

最近发现RD发过来我这边checkSQL中有些数据是日文的, 但是我check了一下我们的production数据库, 发现数据库设的字符集是latin1, 但是我询问了下RD, 以前也一直这样, 但是从来没有报过错, 而且对应的系统上面也能正常显示出日文来.

我就开始纳闷了,latin1不是明明不能存储日文吗? 怎么这边就不会出问题呢.

 

研究之下, 还是有些发现的, 尽管我原来就知道MySQL中关于字符集的几个变量有些作用, 但是一直没有深究其中每一个的具体作用.

character_set_client指的是客户端的字符集, 一般在下SQL语句的时候, 最好设置成与客户端一致.

character_set_results指的是select结果显示的字符集, 一般情况下也设置成与客户端一致.

character_set_connection指的是连接的字符集.

然后剩下的就是数据库端的字符集了, 数据库端的自个字符集变量之间的关系这里先不讲, 因为数据库端字符集划分的最小单位是column, 所以我们这里指column的字符集.

 

我们在与MySQL进行交互的时候, 一种是客户端向数据库做请求. 这种交互第一步是character_set_clientcharacter_set_connection之间的转换, 如果不一致, 就需要进行转换. 而如果源字符集不是目标字符集的超集, 就可能出现数据丢失. 第二步就是character_set_connection与数据库之间字符集的转换, 同样, 如果不一致, 就有可能出现数据丢失.

 

而在我碰到的这个例子里面, 变量设置如下:

character_set_client latin1

character_set_connection utf8

character_set_database latin1

character_set_filesystem binary

character_set_results latin1

character_set_server latin1

character_set_system utf8

我们可以看到, 这中间经过了两层转换, 首先是character_set_clientcharacter_set_connection, 然后是character_set_connectioncharacter_set_database的转换, 但是可以看到这两种转换都不会损失数据.

也就是说我们下SQL的时候输入的是什么, 最终存入数据库的就是什么. 这也就是为什么我们的系统一直没有出现问题, 一直能够正常显示其中的日文.

从这个例子我们也可以看出, 只要我们保证这两个过程不需要进行字符集转换或者转换不会丢失数据, 我们的系统的数据就不会出现问题.

 

但是会出现问题的是COLLATE, 我这边做了如下测试:

CASE1: 数据表字符集gbk,set names 'utf8',客户端是utf8,

INSERT INTO test VALUES ('济南'),('大山'),('光明'),('阳光');

然后查看排序结果, 发现能够正常排序,结果为

大山

光明

济南

阳光

  

CASE2: 数据表字符集gbk,set names 'gbk',客户端是utf8

INSERT INTO test VALUES ('济南'),('大山'),('光明'),('阳光');

发现虽然还是能够正常看到了结果中文, 但是排序乱掉了, 变成了

阳光

光明

济南

大山

 

我们分析下

CASE1:clientconnection,不需要转换,connectiondatabase,需要进行转换,但因为我本来存储的是中文, gbk可以存储中文,所以能够正常转换,存入的也是gbk编码的中文,所以排序不会乱.

CASE2:clientconnection,不需要转换,connectiondatabase,也不需要进行转换. 所以没有数据丢失, 但是为什么排序就乱掉了呢, 原来我们存入的不是gbk编码的中文,而是我们客户端的utf8的中文, 而我们数据库的collate是中文排序,当然就不能正常排序啦.


2009年12月22日星期二

INNODB存储引擎中的AUTO_INCREMENT列

INNODB存储引擎中表AUTO_INCREMENT列的下一个值会存储在informationf_schem.TABLES表中, 每次往这个表中新插入数据, 如果对应列值比当前最大值要大, MYSQL都会更新informationf_schem.TABLES中的AUTO_INCREMENT, 使其等于最大值+1, 无论insert语句是在事务内还是自动COMMIT.

但是当事务内的INSERT语句ROLLBACK, informationf_schem.TABLES中的列值不会回滚. 也就是说这样就会造成跳号”.

 

也就是说, 我们可以通过查找informationf_schem.TABLES来确定某一TABLE AUTO_INCREMENT列的下一个值, 但是这一个值并不一定总是等于当前TABLE中最大值+1, 因为可能被其他connect连接所用, 但是还没有被COMMIT.

2009年12月17日星期四

有关auto_increment

MYSQL5.1中, 在MYISAM存储引擎下, 当auto_increment列与其他列组合作为主键, 且auto_increment列不作为第一列的时候, 能够实现auto_increment值根据第一列分组产生.

但是在INNO DB存储引擎下, 不允许这么设置, auto increment列必须为唯一主键或者组合主键的第一列.

2009年12月14日星期一

MySQL关于INNO DB参数

有用参数:

innodb_additional_mem_pool_size

存储数据字典的cache

innodb_autoextend_increment

auto extend table space file自增长幅度

innodb_buffer_pool_size

数据缓存

innodb_data_file_path

数据文件

innodb_data_home_dir

数据文件目录

innodb_fast_shutdown

0do a full purge and an insert buffer merge before a shutdown

1fast shutdown

2flush logs and then shut down cold

innodb_flush_log_at_trx_commit

0 每秒log buffer写到log file, 并更新数据文件

1 每次commit log buffer写到log file, 并更新数据文件

2 每次commit log buffer写到log file, 每秒更新数据文件

以上两个参数感觉对performance至关重要, 但其后台到底做了什么动作, 暂时还不晓得, 留待以后研究!!

innodb_log_buffer_size

innodb_log_file_size

innodb_log_files_in_group

innodb_log_group_home_dir

2009年12月8日星期二

MYSQL的字符集问题

MySQL的字符集支持(Character Set Support)有两个方面:
     字符集(Character set)和排序方式(Collation)
对于字符集的支持细化到四个层次:
     服务器(server),数据库(database),数据表(table)和连接(connection)

 

查看MYSQL支持的字符集:information_schema.CHARACTER_SETS

MySQL默认字符集

  MySQL对于字符集的指定可以细化到一个数据库,一张表,一列,应该用什么字符集。

但是,传统的 程序在创建数据库和数据表时并没有使用那么复杂的配置,它们用的是默认的配置,那么,默认的配置从何而来呢?

    (1)编译MySQL 时,指定了一个默认的字符集,这个字符集是 latin1
    (2)安装MySQL 时,可以在配置文件 (my.ini) 中指定一个默认的的字符集,如果没指定,这个值继承自编译时指定的;
    (3)启动mysqld 时,可以在命令行参数中指定一个默认的的字符集,如果没指定,这个值继承自配置文件中的配置,此时 character_set_server 被设定为这个默认的字符集;
    (4)当创建一个新的数据库时,除非明确指定,这个数据库的字符集被缺省设定为character_set_server
    (5)当选定了一个数据库时,character_set_database 被设定为这个数据库默认的字符集;
    (6)在这个数据库里创建一张表时,表默认的字符集被设定为 character_set_database,也就是这个数据库默认的字符集;
    (7)当在表内设置一栏时,除非明确指定,否则此栏缺省的字符集就是表默认的字符集;

简单的总结一下,如果什么地方都不修改,那么所有的数据库的所有表的所有栏位的都用 latin1 存储,不过我们如果安装 MySQL,一般都会选择多语言支持,也就是说,安装程序会自动在配置文件中把 default_character_set 设置为 UTF-8,这保证了缺省情况下,所有的数据库的所有表的所有栏位的都用 UTF-8 存储。

乱码问题一般是由于以上几个变量设置错误照成的,很多人在请教乱码问题的时候,一般都会得到一个这样的答案:你先set names一下

那么set names是什么呢? set names实际上就是同时设置了character_set_client, character_set_connection, character_set_results这三个系统变量。

 

因为character_set_client,character_set_connection这两个变量仅用与保证与 character_set_database编码的一致,而character_set_results则用与保证SELECT返回的结果与程序的编码 一致。

如,你的数据库(character_set_database)用的是utf8的字符集,那么你就要保证 character_set_client,character_set_connection也是utf8的字符集。而你的程序也许采用的并不是 utf8,比如你的程序用的是gbk,那么你若把character_set_results也设置为utf8的话就会出现乱码问题。此时你应该把 character_set_results设置为gbk。这样就能保证数据库返回的结果与你的程序的编码一致。

 

 

2009年12月3日星期四

关于MySQL FLOAT数据类型的误差

最近刚接触MySQL, 在设计表结构的时候大量的使用了FLOAT类型, 这个时候新公司的同事过来说不能用FLOAT, FLOAT的时候会带来误差, 并且举了个例子, 就是用FLOAT(8,3)存储99999.993的时候会变成99999.992, 我测试了一下, 果然.

我就纳闷了, 为什么会产生这种情况呢? 这么容易产生误差!!! 我在玩Oracle的时候使用NUMBER类型可从来没有出现过这种问题. 于是下决心研究以下这个问题, 找出问题的究竟.

通过翻阅资料以及自己的测试, 终于找出了结果.

自从 MySQL 3.23 版本以后,MySQL 的浮点型 FLOAT DOUBLE 就已经遵循了IEEE-754的单精度和双精度标准。MySQL FLOAT 类型占用4个字节,DOUBLE 类型占用8个字节.

IEEE 标准中,浮点数是将特定长度的连续字节的所有二进制位分割为特定宽度的符号域,指数域和尾数域三个域,其中保存的值分别用于表示给定二进制浮点数中的符 号,指数和尾数。这样,通过尾数和可以调节的指数(所以称为"浮点")就可以表达给定的数值了。具体的格式参见下面的图例:

第一个域为符号域。其中 0 表示数值为正数,而 1 则表示负数。

第二个域为指数域,对应 于我们之前介绍的二进制科学计数法中的指数部分。其中单精度数为 8 位,双精度数为 11 位。以单精度数为例,8 位的指数为可以表达 0 255 之间的 255 个指数值。但是,指数可以为正数,也可以为负数。为了处理负指数的情况,实际的指数值按要求需要加上一个偏差(Bias)值作为保存在指数域中的值,单精 度数的偏差值为 127,而双精度数的偏差值为 1023。比如,单精度的实际指数值 0 在指数域中将保存为 127;而保存在指数域中的 64 则表示实际的指数值 -63 偏差的引入使得对于单精度数,实际可以表达的指数值的范围就变成 -127 128 之间(包含两端)。我们不久还将看到,实际的指数值 -127(保存为 0)以及 +128(保存为全 1)保留用作特殊值的处理。这样,实际可以表达的有效指数范围就在 -127 127 之间。

第三个域为尾数域,其中 单精度数为 23 位长,双精度数为 52 位长。除了我们将要讲到的某些特殊值外,IEEE 标准要求浮点数必须是规范的。这意味着尾数的小数点左侧必须为 1,因此我们在保存尾数的时候,可以省略小数点前面这个 1,从而腾出一个二进制位来保存更多的尾数。这样我们实际上用 23 位长的尾数域表达了 24 位的尾数。比如对于单精度数而言,二进制的 1001.101(对应于十进制的 9.625)可以表达为 1.001101 × 23,所以实际保存在尾数域中的值为 00110100000000000000000,即去掉小数点左侧的 1,并用 0 在右侧补齐。

好了, 以上是背景知识介绍, 下面就来看我们这个例子:

FLOAT(8,3) 存储99999.993

我们首先规格化:

BIN(99999) = 11000011010011111

也就是说99999需要17, 省略掉第一个1, 需要耗费16, 也就是说剩下来的只有7位可以存储小数点后面的0.993

BIN(0.993 * POW(2,7))=1111111

也就是说最后规格化的结果为1.10000110100111111111111 * 216

但是我们来看一下这个存储的结果

CONV(110000110100111111111111,2,10) / POW(2,7)= 99999.9921875

也就是说我们存储的99999.993实际存储的是99999.9921875

我们知道FLOAT(M,D)中的D并不会影响, 只会映像查看的结果, 所以在SELECT的时候就四舍五入变成了99999.992

那我们来测试下,实际上是不是这样子的

CREAT TABLE TEST(T FLOAT(20,12));

INSERT INTO TEST VALUES(9999.993);

SELECT * FROM TEST;

果然, 结果是99999.9921875

我们接下来看下一系列的

INSERT INTO TEST VALUES

(99999.991),

(99999.992),

(99999.993),

(99999.994),

(99999.995),

(99999.996),

(99999.997),

(99999.998),

(99999.999),

我们发现从99999.991 - 99999.996存储的都是99999.9921875, 99999.997 - 99999.999我们QUERY出来的结果是100000

可是细心一点会发现, 我们在规格化小数部分的时候, 使用的是BIN(0.993 * POW(2,7)), 而如果按照这样子

Bin(0.991 * pow(2,7)) = bin(126.848) = 1111110

Bin(0.992 * pow(2,7)) = bin(126.976) = 1111110

Bin(0.993 * pow(2,7)) = bin(127.104) = 1111111

Bin(0.994 * pow(2,7)) = bin(127.232) = 1111111

Bin(0.995* pow(2,7)) = bin(127.36) = 1111111

Bin(0.996 * pow(2,7)) = bin(127.488) = 1111111

Bin(0.997 * pow(2,7)) = bin(127.616) = 1111111

Bin(0.998 * pow(2,7)) = bin(127.744) = 1111111

Bin(0.999 * pow(2,7)) = bin(127.872) = 1111111

我们存储99999.991, 99999.992的时候,后面7为就应该是1111110, 而不是1111111, 最终结果就应该是99999.984375

99999.997, 99999.998, 99999.999 3个数存储的也应该是99999.9921875 而不是100000.

这是为什么呢? 四舍五入? 前面两个入成127, 后面两个入成128?

我们来测试下, 我们存储99999.935

0.935 * POW(2,7) = 119.68 ≈ 120

后面7位就应该是BIN(120) = 1111000

CONV(110000110100111111111000,2,10) / POW(2,7)= 99999.9375

我们看以下存储结果, 果然是99999.9375.

现在, FLOAT型之所以产生误差的原因就已经清楚了, 是因为存储格式的局限性造成的. DOUBLE因为可以用来存储尾数的位更多, 所以相对来说会更精确一点, 但也是有误差的. 所以如果要严格的要求精确, 就只能采用DECIMAL.

2009年5月13日星期三

深度分析数据库的热点块问题[转]

作者:biti_rainy (出处)

热点块的定义

数据库的热点块,从简单了讲,就是极短的时间内对少量数据块进行了过于频繁的访问。定义看起来总是很简单的,但实际在数据库中,我们要去观察或者确定热点 块的问题,却不是那么简单了。要深刻地理解数据库是怎么通过一些数据特征来表示热点块的,我们需要了解一些数据库在这方面处理机制的特性。

数据缓冲区的结构

我们都知道,当查询开始的时候,进程首先去数据缓冲区中查找是否存在查询所需要的数据块,如果没有,就去磁盘上把数据块读到内存中来。在这个过程中,涉及 到数据缓冲区中LRU链的管理(8i开始以接触点计数为标准衡量buffer冷热从而决定buffer是在LRU的冷端还是热端),关于这部分内容,从 oracle concepts 中就能得到详尽的文档,我不准备去论述这部分内容,这也不是本文的重点。现在我们的重点是,到底进程是如何地去快速定位到自己所想要的block的,或者 如何快速确定想要的block不在内存中而去进行物理读的。

我们仔细想一想,随着硬件的发展,内存越来越大,cache buffer也越来越大,我们如何才能在大量的内存中迅速定位到自己想要的block?总不能去所有buffer中遍历吧!在此数据库引出了hash的概 念(oracle中快速定位信息总是通过hash算法的,比如快速定位sql是否在shared pool size中存在就是通过hash value来定位的,也就是说shared pool size中对象也是通过hash table来管理的),了解一点数据结构的基本知识就知道,hash 的一大重要功能就是快速地查找。举个最简单的例子,假设我们有一个hash table 就是一个二维数组a[200][100],现在有1000个无序数字,我们要从这1000个数字里面查找某个值是否存在,或者说当我们接收到某个数字的时 候必须判断是否已经存在,当然,我们可以遍历这1000个数字,但这样的效率就很低。但现在我们考虑这样一种方法,那就是把1000个数字除以200,根 据其余数,放在a[200][100]里面(假设相同余数的最大数量不超过100),余数就是数组的下标。这样,平均来说一个数组a[i]里面可能有5个 左右的数字。当我们要去判别一个数字是否存在的时候,对这个数字除以200(这就是一个最简单的hash算法),根据余数i作为下标去数组a[i]中查 找,大约进行5次查找就能判别是否已经存在,这样通过开辟内存空间a[200][100]来换取了时间(当然hash 算法的选取和hash table的大小是一个很关键的问题)。

明白了基本的hash原理之后,我们再来看oracle的block的管理。数据库为这些block也开辟了hash table,假设是a,则在一维上的数量是由参数_db_block_hash_buckets 来决定的,也就是存在hash table a[_db_block_hash_buckets ],从oracle8i开始,_db_block_hash_buckets =db_block_buffers*2。而一个block被放到哪个buckets里面,则是由block的文件编号、块号(x$bh.dbarfl、 x$bh.dbablk对应了block的文件属于表空间中的相关编号和block在文件中的编号,x$bh是所有cache buffer的header信息,通过表格的形式可以查询)做hash 算法决定放到哪个bucket的,而bucket里面就存放了这些buffers的地址。这样当我们要访问数据的时候,可以获得segment的 extent(可以通过dba_extents查到看,详细的信息来源这里不做探讨),自然知道要访问的文件编号和block编号,根据文件和block 编号可以通过hash算法计算出hash bucket,然后就可以去hash bucket里面去找block对应的buffer。

除此之外,为了维护对这些block的访问和更改,oracle还提供了一种latch来保护这些block。因为要避免不同的进程随意地径直并发修改和 访问这些block,这样很可能会破坏block的结构的。latch是数据库内部提供的一种维护内部结构的一种低级锁,latch的生存周期极短(微秒 以下级别),进程加latch后快速的进行某个访问或者修改动作然后释放latch(关于latch不再过多的阐述,那可能又是需要另一篇文章才能阐述清 楚)。这种latch数量是通过参数_db_block_hash_latches 来定义的,一个latch对应的保护了多个buckets。从8i开始,这个参数的default规则为:

当cache buffers 少于2052 buffers

_db_block_hash_latches = power(2,trunc(log(2, db_block_buffers - 4) - 1))

当cache buffers多于131075 buffers

_db_block_hash_latches = power(2,trunc(log(2, db_block_buffers - 4) - 6))

当cache buffers位于2052与131075 buffers之间

_db_block_hash_latches = 1024

通过这个规则我们可以看出,一个latch大约可以维护128个左右的buffers。由于latch使得对block的操作的串行化(9i中有改进,读 与读可以并行,但读与写、写与写依然要串行),很显然我们可以想到一个道理,如果大量进程对相同的block进程进行操作,必然在这些latch上造成竞 争,也就是说必然形成latch的等待。这在宏观上就表现为系统级的等待。明白了这些原理,为我们下面的在数据库中的诊断奠定了基础。

如何确定热点对象

如果我们经常关注statspack报告,会发现有时候出现cache buffer chains的等待。这个cache buffer chains就是_db_block_hash_latches所定义的latch的总称,通过查询v$latch也可得到:

select">sys@OCN>select latch#,name,gets,misses,sleeps from v$latch where name like 'cache buffer%';

LATCH# NAME GETS MISSES SLEEPS
---------- ------------------------------ ---------- ---------- ----------
93 cache buffers lru chain 54360446 21025 238
98 cache buffers chains 6760354603 1680007 27085
99 cache buffer handles 554532 6 0

在这个查询结果里我们可以看到记录了数据库启动以来的所有cahce buffer chains的latch的状况,gets表示总共有这么多次请求,misses表示请求失败的次数(加锁不成功),而sleeps 表示请求失败休眠的次数,通过sleeps我们可以大体知道数据库中latch的竞争是否严重,这也间接的表征了热点块的问题是否严重。由于 v$latch是一个聚合信息,我们并不能获得哪些块可能存在频繁访问。那我们要来看另一个view信息,那就是 v$latch_children,v$latch_children.addr记录的就是这个latch的地址。

select">sys@OCN>select addr,LATCH#,CHILD#,gets,misses,sleeps from v$latch_children
2 where name = 'cache buffers chains' and rownum < 21;

ADDR LATCH# CHILD# GETS MISSES SLEEPS
-------- ---------- ---------- ---------- ---------- ----------
91B23B74 98 1024 10365583 3957 33
91B23374 98 1023 5458174 964 25
91B22B74 98 1022 4855668 868 15
91B22374 98 1021 5767706 923 22
91B21B74 98 1020 5607116 934 31
91B21374 98 1019 9389325 1111 25
91B20B74 98 1018 5060207 994 31
91B20374 98 1017 18204581 1145 18
91B1FB74 98 1016 7157081 920 23
91B1F374 98 1015 4660774 922 22
91B1EB74 98 1014 6954644 976 32
91B1E374 98 1013 4881891 970 19
91B1DB74 98 1012 5371135 971 28
91B1D374 98 1011 5154497 990 26
91B1CB74 98 1010 5013796 936 18
91B1C374 98 1009 5667446 939 25
91B1BB74 98 1008 4673421 883 14
91B1B374 98 1007 4589646 986 17
91B1AB74 98 1006 10380781 1020 20
91B1A374 98 1005 5142009 1110 19

20 rows selected.


到此我们可以根据v$latch_child.addr关联到对应的x$bh.hladdr(这是buffer header中记录的当前buffer所处的latch地址),通过x$bh可以获得块的文件编号和block编号。

select">sys@OCN>select dbarfil,dbablk
from x$bh
where hladdr in
(select addr
from (select addr
from v$latch_children
order by sleeps desc)
where rownum < 11);

DBARFIL DBABLK
---------- ----------
4 6498
40 14915
15 65564
28 34909
40 17987
1 24554
8 21404
39 29669
28 46173
28 48221

……………………

由此我们就打通了cache buffers chains和具体block之间的关系,那再继续下来,知道了block,我们需要知道究竟是哪些segment。这个可以通过dba_extents来获得。

select distinct a.owner,a.segment_name from
dba_extents a,
(select dbarfil,dbablk
from x$bh
where hladdr in
(select addr
from (select addr
from v$latch_children
order by sleeps desc)
where rownum < 11)) b
where a.RELATIVE_FNO = b.dbarfil
and a.BLOCK_ID <= b.dbablk and a.block_id + a.blocks > b.dbablk;


OWNER SEGMENT_NAME SEGMENT_TYPE
------------------------------ ------------------------------ ------------------
ALIBABA BIZ_SEARCHER TABLE
ALIBABA CMNTY_USER_MESSAGE TABLE
ALIBABA CMNTY_VISITOR_INFO_PK INDEX
ALIBABA COMPANY_AMID_IND INDEX
ALIBABA COMPANY_DRAFT TABLE
ALIBABA FEEDBACK_POST TABLE
ALIBABA IM_BLACKLIST_PK INDEX
ALIBABA IM_GROUP TABLE
ALIBABA IM_GROUP_LID_IND INDEX
ALIBABA MEMBER TABLE
ALIBABA MEMBER_PK INDEX
ALIBABA MLOG$_SAMPLE TABLE

……………………

我们还有另外一种方式

select object_name
from dba_objects
where data_object_id in
(select obj
from x$bh
where hladdr in
(select addr
from (select addr
from v$latch_children
order by sleeps desc)
where rownum < 11)) ;


OBJECT_NAME
------------------------------------
I_CCOL2
RESOURCE_PLAN$
DUAL
FGA_LOG$
AV_TRANSACTION
COMPANY_DRAFT
MEMBER
SAMPLE
SAMPLE_GROUP
VERTICAL_COMPONENT
MEMBER_PK
SAMPLE_GROUP_PK
IM_BLACKLIST_PK
IM_CONTACT
IM_GROUP
CMNTY_USER_MESSAGE
CMNTY_VISITOR_INFO_PK
IM_OFFLINEMSG_TID_IND
OFFER
OFFER_PK
OFFER_EMAIL_IND
OFFER_DRAFT
CMNTY_USER_MESSAGE_TD_BSM_IND
CMNTY_MESSAGE_NUM_PK
BIZ_EXPRESS_MEMBER_ID_IND

……………………

到这里我们基本能找到热点块对对应的对象。但实际上还有另外一个途径来获取这些信息,那就是和x$bh.tch 相关的一种方法。对于8i开始oracle提供了接触点(touch count)来作为block是冷热的标志,在一定条件满足的情况下block被进程访问一次touch count 增加一,到某个标准之后被移动到LRU热端(关于touch count 在这里不做详细介绍,那又将是一大篇文章)。那在短时间内从某种意义上讲,touch count 大的block可能暗示着在当前某个周期内被访问次数比较多。

select distinct a.owner,a.segment_name,a.segment_type from
dba_extents a,
(select dbarfil,dbablk
from (select dbarfil,dbablk
from x$bh order by tch desc) where rownum < 11) b
where a.RELATIVE_FNO = b.dbarfil
and a.BLOCK_ID <= b.dbablk and a.block_id + a.blocks > b.dbablk;

OWNER SEGMENT_NAME SEGMENT_TYPE
------------------------------ ------------------------------ ------------------
ALIBABA CMNTY_USER_MESSAGE TABLE
ALIBABA MEMBER_PK INDEX
ALIBABA OFFER_DRAFT_GMDFY_IND INDEX

同上面一样还有这个方法

select object_name
from dba_objects
where data_object_id in
(select obj
from (select obj
from x$bh order by tch desc) where rownum < 11) ;
OBJECT_NAME
---------------------------------------------------
DUAL
MEMBER_PK
SAMPLE_GROUP_PK
CMNTY_USER_MESSAGE_TD_BSM_IND
OFFER_DRAFT_MID_GMDFY_IND
OFFER_MID_GPOST_IND
OFFER_DRAFT_PK
MEMBER_GLLOGIN_IND
OFFER_MID_STAT_GEXPIRE_IND
SAMPLE_MID_STAT_IND

10 rows selected.

到这里,我们寻找热点块和热点对象的工作算是完成了,但我们还并没有解决问题。

热点问题的解决

热点块和热点对象我们都找到了,但是我们该怎么来解决这个问题呢?一般来说,热点块会导致cache buffers chains竞争等待,但并不是说cache buffer chains一定是因为热点块而起,在特别情况下有可能是因为latch数量的问题导致的,也就是一个latch管理的buffers数量太多而导致竞争 激烈。但是latch数量我们一般是不会轻易去设置的,这是oracle的隐藏参数。

实际上最有效的办法,是从优化sql入手,不良的sql往往带来大量的不必要的访问,这是造成热点块的根源。比如本该通过全表扫描的查询却走了索引的 range scan,这样将带来大量的对块的重复访问。从而形成热点问题。再或者比如不当地走了nested loops的表连接,也可能对非驱动表造成大量的重复访问。那么在这个时候,我们的目标就是找出这些sql来并尝试优化。在statspack报告中,根 据报告中sql列表,我们如果是通过dba_extents确定的热点对象而不是通过dba_objects确定的,则可以通过查找出的热点 segment转换为对应的表,对于非分区的索引,index_name就是segment_name,通过dba_indexes很容易的找到对应的 table_name,对于分区表和分区索引也能通过和dba_tab_partition和dba_ind_partitions找到segment和 table的对应关系。通过这些table到statspack报告中去找相关的sql。

select sql_text
from stats$sqltext a,
(select distinct a.owner,a.segment_name,a.segment_type from
dba_extents a,
(select dbarfil,dbablk
from (select dbarfil,dbablk
from x$bh order by tch desc) where rownum < 11) b
where a.RELATIVE_FNO = b.dbarfil
and a.BLOCK_ID <= b.dbablk and a.block_id + a.blocks > b.dbablk) b
where a.sql_text like '%'||b.segment_name||'%' and b.segment_type = 'TABLE'
order by a.hash_value,a.address,a.piece;

SQL_TEXT
----------------------------------------------------------------
SELECT SEQ_SMS_TRANSACTION.nextval FROM DUAL
SELECT SEQ_BIZ_EXPRESS.nextval FROM DUAL
SELECT bizgroup.seq_grp_post.NextVal FROM DUAL
SELECT SEQ_SAMPLE.nextval FROM DUAL
SELECT bizgroup.seq_grp_user.NextVal FROM DUAL
SELECT SEQ_BIZ_SEARCHER.nextval FROM DUAL
SELECT SEQ_OFFER_DRAFT.nextval FROM DUAL
select seq_Company_Draft.NextVal from DUAL
SELECT SEQ_SAMPLE_GROUP.nextval FROM DUAL
SELECT SEQ_CMNTY_USER_MESSAGE.nextval FROM DUAL
SELECT SYSDATE FROM DUAL
select seq_News_Forum.NextVal from DUAL
SELECT SEQ_SMS_USER.nextval FROM DUAL
select seq_Biz_Member.NextVal from DUAL
select seq_Pymt_Managing.NextVal from DUAL
E= '+08:00' NLS_DUAL_CURRENCY = '$' NLS_TIME_FORMAT = 'HH.MI.SSX
SELECT SEQ_COMPANY_DRAFT.nextval FROM DUAL
SELECT 1 FROM DUAL
select seq_offer_draft.NextVal from DUAL
select seq_Biz_Express_Category.NextVal from DUAL

20 rows selected.

当然这里是从statspack搜集的stats$sqltext中去找的(你可以在statspack的文本报告中去找),实际上,我们可以直接在当前数据库中的v$sqlarea或者v$sqltext里面去找到这些sql,然后来尝试优化。

select sql_text
from v$sqltext a,
(select distinct a.owner,a.segment_name,a.segment_type from
dba_extents a,
(select dbarfil,dbablk
from (select dbarfil,dbablk
from x$bh order by tch desc) where rownum < 11) b
where a.RELATIVE_FNO = b.dbarfil
and a.BLOCK_ID <= b.dbablk and a.block_id + a.blocks > b.dbablk) b
where a.sql_text like '%'||b.segment_name||'%' and b.segment_type = 'TABLE'
order by a.hash_value,a.address,a.piece;
SQL_TEXT
----------------------------------------------------------------
SELECT NULL FROM DUAL FOR UPDATE NOWAIT
SELECT SEQ_SMS_TRANSACTION.nextval FROM DUAL
SELECT SEQ_BIZ_EXPRESS.nextval FROM DUAL
SELECT SEQ_IM_GROUP.nextval FROM DUAL
SELECT SEQ_SAMPLE.nextval FROM DUAL
='DD-MON-RR HH.MI.SSXFF AM TZR' NLS_DUAL_CURRENCY='$' NLS_COMP='
SELECT SEQ_BIZ_SEARCHER.nextval FROM DUAL
SELECT SEQ_OFFER_DRAFT.nextval FROM DUAL
SELECT SEQ_SAMPLE_GROUP.nextval FROM DUAL
DD-MON-RR HH.MI.SSXFF AM TZR' NLS_DUAL_CURRENCY='$' NLS_COMP='BI
SELECT SEQ_CMNTY_USER_MESSAGE.nextval FROM DUAL
SELECT SYSDATE FROM DUAL
SELECT SEQ_SMS_USER.nextval FROM DUAL
IMESTAMP_TZ_FORMAT='DD-MON-RR HH.MI.SSXFF AM TZR' NLS_DUAL_CURRE
SELECT SEQ_COMPANY_DRAFT.nextval FROM DUAL
SELECT 1 FROM DUAL
SELECT USER FROM DUAL
SELECT DECODE('A','A','1','2') FROM DUAL

18 rows selected.


除了优化sql外,当然对于热点的表或者索引来说,如果小的话,我们可以考虑cache在内存中,这样可能降低物理读提高sql运行速度(这并不会减少 cache buffer chains的访问次数),对于序列,我们可以对序列多设置一些cache。如果是并行服务器环境中的索引对象,并且这个索引是系列递增类型,我们可以考 虑反向索引(关于反向索引这里就不过多地做介绍了)。

热点块的其他相关症状

在数据库中还可能存在一些其他方面的热点块症状,通过v$waitstat的等待可以看出一些端倪,v$waitstat是根据数据缓冲区中各种block的类型(x$bh.class)而分类统计的等待状况。

select">sys@OCN>select * from v$waitstat;

CLASS COUNT TIME
------------------ ---------- ----------
data block 1726977 452542
sort block 0 0
save undo block 0 0
segment header 40 11
save undo header 0 0
free list 0 0
extent map 0 0
1st level bmb 611 112
2nd level bmb 42 13
3rd level bmb 0 0
bitmap block 0 0
bitmap index block 0 0
file header block 13 92
unused 0 0
system undo header 111 28
system undo block 7 0
undo header 2765 187
undo block 633 156


比如在ASSM表空间出现之前,由于freelist的存在,如果表经常被并发的进程DML,则可能存在大量的data block的等待,或者有free list的等待。那么这个时候我们发现这样的segment之后需要考虑增加freelist数量。再比如经常发生长时间的DML的表被频繁地访问,这样 将会造成过多的对回滚段中块的访问,这时可能undo block 的等待会比较多。那么我们可能需要控制DML的时间长度或者想办法从应用程序入手来解决问题。如果是undo header的等待比较多,没使用undo tablespace 之前,可能需要考虑增加回滚段的数量。

总结

本文从热点块的原理入手,详细地由oracle的内部结构特征开始介绍了热点块的产生和表现特征。进而阐述了诊断热点对象和找出造成热点对象的sql的方法。并从解决热点问题方面提供了解决方向。

2009年5月11日星期一

latch: cache buffer chain事件

线上反应系统反应缓慢, statpack报表中显式latch: cache buffer chain过多.
使用以下SQL查找当前session等待的latch:
SELECT SID, P1RAW, P2, P3, SECONDS_IN_WAIT, WAIT_TIME, STATE
FROM V$SESSION_WAIT
WHERE EVENT LIKE 'latch%'
ORDER BY P2, P1RAW;

P1为latch地址, P2为latch类别, 可以从v$latch中找到对应的latch类别名称.
如果是latch buffer chain, 则可以从x$bh中找到对应latch管理的object_id, 然后从dba_object中找到相对应的object.

如果第一步找到的P1RAW中存在大量重复的latch地址, 则很有可能是热点块问题.
热点块有以下解决方法:
1、加大PCTFREE
2、减小BLOCK SIZE(9i或者更新的版本)
3、使用hash 簇表
4、使用HASH 分区
5、使用反转键索引(针对索引)

v$latchholder可以看到当前有哪些latch被持有.

另外设置隐含参数_db_block_hash_latches的值大于当前hash latches的值也能在一定程度上缓解当前cache buffer chain latch的争用. 我们也可以通过查看_db_block_hash_buckets的值来查看当前hash buckets的数量.
但最根本的方法还是找出哪些SQL导致了latch的争用, 减少逻辑读.

2009年5月7日星期四

STREAMS配置(DownStream,ArchiveLog,异构DB, 不同schema)

由于上次直接使用MAINTAIN_SCHEMA配置异构DB出现问题, 后来测试发现, 如果将出错的script手动按照顺序在各DB执行, 是可以通过的. 而且测试streams也正常工作.
因为我的最终目标是将多个生产库使用streams合并到一个DATA CENTER中去, 而各生产库的schema存在相同名字的情况, 所以需要配置到将一个schema的数据apply到不同name的schema中.
我的做法是将maintain_schema产生的script手动拉出来, 并做适当修改, 手动执行, 数据库还是前面的3个数据库, HISP(Source),DC(DownStream),DEV4(Destination):
1. HISP: Add supplemental log group
BEGIN
   EXECUTE IMMEDIATE 'ALTER TABLE "HIS"."XX_TIM_TEST" ADD SUPPLEMENTAL LOG DATA (PRIMARY KEY, FOREIGN KEY, UNIQUE INDEX) COLUMNS';
EXCEPTION
   WHEN OTHERS
   THEN
      IF SQLCODE = -32588
      THEN
         NULL;                                    -- Logging attribute exists
      ELSE
         RAISE;
      END IF;
END;
2. HISP: Dictionary build
BEGIN
  dbms_capture_adm.build;
END;
3. HISP: Prepare schema instantiation
BEGIN
  dbms_capture_adm.prepare_schema_instantiation(
    schema_name => '"HIS"');
END;
4. DC: Set up queue "STRMADMIN"."HISP$CAPQ"
BEGIN
  dbms_streams_adm.set_up_queue(
    queue_table => '"STRMADMIN"."HISP$CAPQT"',
    storage_clause => NULL,
    queue_name => '"STRMADMIN"."HISP$CAPQ"',
    queue_user => '');
END;
5. DC:  PROPAGATE changes for schema HIS
DECLARE
  version_num            NUMBER := 0;
  release_num            NUMBER := 0;
  pos                    NUMBER;
  initpos                NUMBER;
  q2q                    BOOLEAN;
  stmt                   VARCHAR2(100);
  ver                    VARCHAR2(30);
  compat                 VARCHAR2(30);  

BEGIN
  BEGIN
    stmt := 'BEGIN dbms_utility.db_version@DEV4.LDI.COM(:ver, :compat); END;';
    EXECUTE IMMEDIATE stmt USING OUT ver, OUT compat;
    -- Extract version number
    initpos := 1;
    pos := INSTR(compat, '.', initpos, 1);
    IF pos > 0 THEN
      version_num := TO_NUMBER(SUBSTR(compat, initpos, pos - initpos));
      initpos := pos + 1;

      -- Extract release number
      pos := INSTR(compat, '.', initpos, 1);
      IF pos > 0 THEN
        release_num := TO_NUMBER(SUBSTR(compat, initpos,
                                   pos - initpos));
        initpos := pos + 1;
      ELSE
        release_num := TO_NUMBER(SUBSTR(compat, initpos));
      END IF;
    ELSE
      version_num := TO_NUMBER(SUBSTR(compat, initpos));
    END IF;

    -- use q2q propagation if compatibility >= 10.2
    IF version_num > 10 OR
       (version_num = 10 AND release_num >=2) THEN
      q2q := TRUE;
    ELSE
      q2q := FALSE;
    END IF;

  EXCEPTION WHEN OTHERS THEN
    q2q := FALSE;
  END;


  dbms_streams_adm.add_schema_propagation_rules(
    schema_name => '"HIS"',
    streams_name => '',
    source_queue_name => '"STRMADMIN"."HISP$CAPQ"',
    destination_queue_name => '"STRMADMIN"."HISP$APPQ"@DEV4.LDI.COM',
    include_dml => TRUE,
    include_ddl => TRUE,
    include_tagged_lcr => TRUE,
    source_database => 'HISP.LDI.COM',
    inclusion_rule => TRUE,
    and_condition => NULL,
    queue_to_queue => q2q);
END;
6. DC: Disable propagation. Enable after destination has been setup
DECLARE
  q2q       VARCHAR2(10);
  destn_q   VARCHAR2(65);
BEGIN
  SELECT queue_to_queue INTO q2q
  FROM dba_propagation
  WHERE source_queue_owner = 'STRMADMIN' AND
        source_queue_name = 'HISP$CAPQ' AND
        destination_queue_owner = 'STRMADMIN' AND
        destination_queue_name = 'HISP$APPQ' AND
        destination_dblink = 'DEV4.LDI.COM';

  IF q2q = 'TRUE' THEN
    destn_q := '"STRMADMIN"."HISP$APPQ"';
  ELSE
    destn_q := NULL;
  END IF;

  dbms_aqadm.disable_propagation_schedule(
    queue_name => '"STRMADMIN"."HISP$CAPQ"',
    destination => 'DEV4.LDI.COM',
    destination_queue => destn_q);
EXCEPTION WHEN OTHERS THEN
  IF sqlcode = -24065 THEN NULL;  -- propagation already disabled
  ELSE RAISE;
  END IF;
END;
7. DC: Create downstream capture HISP$CAP
DECLARE
  cnt       NUMBER;
  first_scn NUMBER;
BEGIN
  SELECT COUNT(*) INTO cnt
  FROM dba_capture
  WHERE capture_name = 'HISP$CAP';

  -- Create capture only if the process does not exist.
  IF cnt = 0 THEN
    SELECT MAX(first_change#) INTO first_scn
    FROM v$archived_log@HISP.LDI.COM
    WHERE dictionary_begin = 'YES' AND
          first_change# <= (SELECT MAX(first_change#)
                            FROM v$archived_log@HISP.LDI.COM
                            WHERE dictionary_end  = 'YES');

    dbms_capture_adm.create_capture(
      queue_name=> '"STRMADMIN"."HISP$CAPQ"',
      capture_name=> '"HISP$CAP"',
      use_database_link => FALSE,
      first_scn => first_scn,
      source_database=> 'HISP.LDI.COM');
  END IF;
END;
8. DC: CAPTURE changes for schema HIS
DECLARE
  compat         VARCHAR2(512);
  initpos        NUMBER;
  pos            NUMBER;
  version_num    NUMBER;
  release_num    NUMBER;
  compat_func    VARCHAR2(65);
  get_compatible VARCHAR2(4000);
BEGIN
  SELECT value INTO compat
  FROM v$parameter
  WHERE name = 'compatible';

  -- Extract version number
  initpos := 1;
  pos := INSTR(compat, '.', initpos, 1);
  IF pos > 0 THEN
    version_num := TO_NUMBER(SUBSTR(compat, initpos, pos - initpos));
    initpos := pos + 1;

    -- Extract release number
    pos := INSTR(compat, '.', initpos, 1);
    IF pos > 0 THEN
      release_num := TO_NUMBER(SUBSTR(compat, initpos, pos - initpos));
      initpos := pos + 1;
    ELSE
      release_num := TO_NUMBER(SUBSTR(compat, initpos));
    END IF;
  END IF;

  IF version_num < 10 THEN
    compat_func := 'dbms_streams.compatible_9_2';
  ELSIF version_num = 10 THEN
    IF release_num < 2 THEN
      compat_func := 'dbms_streams.compatible_10_1';
    ELSE
      compat_func := 'dbms_streams.compatible_10_2';
    END IF;
  ELSE
    compat_func := 'dbms_streams.compatible_10_2';
  END IF;

  get_compatible := ':lcr.get_compatible() <= '||compat_func;


  dbms_streams_adm.add_schema_rules(
    schema_name => '"HIS"',
    streams_type => 'CAPTURE',
    streams_name => '"HISP$CAP"',
    queue_name => '"STRMADMIN"."HISP$CAPQ"',
    include_dml => TRUE,
    include_ddl => TRUE,
    include_tagged_lcr => TRUE,
    source_database => 'HISP.LDI.COM',
    inclusion_rule => TRUE,
    and_condition => get_compatible);
END;
9. HISP: Datapump SCHEMA MODE EXPORT
DECLARE
  h1                NUMBER;       -- data pump job handle
  schema_expr_list  VARCHAR2(32767); -- for metadata_filter
  cnt               NUMBER; -- temp variable
  object_owner      dbms_utility.uncl_array; -- obj owners
  job_state         VARCHAR2(30); -- job state
  status            ku$_Status; -- data pump status
  job_not_exist     exception;
  pragma            exception_init(job_not_exist, -31626);
BEGIN

  object_owner(1) := 'HIS';
  FOR idx IN 1..1 LOOP
    -- schema does not exist locally, need instantiation
    IF schema_expr_list IS NULL THEN
      schema_expr_list := '(';
    ELSE
      schema_expr_list := schema_expr_list ||',';
    END IF;
    schema_expr_list := schema_expr_list||''''||object_owner(idx)||'''';
  END LOOP;
  IF schema_expr_list IS NOT NULL THEN
    schema_expr_list := schema_expr_list || ')';
  ELSE
    COMMIT;
    RETURN;
  END IF;

  h1 := dbms_datapump.open(operation=>'EXPORT',job_mode=>'SCHEMA',
    remote_link=>'',
    job_name=>NULL, version=>'COMPATIBLE');

  dbms_datapump.metadata_filter(
    handle=>h1,
    name=>'SCHEMA_EXPR',
    value=>'IN'||schema_expr_list);

  dbms_datapump.add_file(
    handle=>h1,
    filename=>'expdat113.dmp',
    directory=>'STREAMS_DIR',
    filetype=>dbms_datapump.ku$_file_type_dump_file);
  dbms_datapump.add_file(
    handle=>h1,
    filename=>'expdat113.dlg',
    directory=>'STREAMS_DIR',
    filetype=>dbms_datapump.ku$_file_type_log_file);

  dbms_datapump.start_job(h1);

  job_state := 'UNDEFINED';
  BEGIN
    WHILE (job_state != 'COMPLETED') AND (job_state != 'STOPPED') LOOP
      status := dbms_datapump.get_status(
        handle => h1,
        mask => dbms_datapump.ku$_status_job_error +
                dbms_datapump.ku$_status_job_status +
                dbms_datapump.ku$_status_wip,
        timeout => -1);
      job_state := status.job_status.state;
      dbms_lock.sleep(10);
    END LOOP;
  EXCEPTION WHEN job_not_exist THEN
    dbms_output.put_line('job finished');
  END;

  -- Transfer dump file to the destination directory
  dbms_file_transfer.put_file(
    source_directory_object => '"STREAMS_DIR"',
    source_file_name => 'expdat113.dmp',
    destination_directory_object => '"STREAMS_DIR"',
    destination_file_name => 'expdat113.dmp',
    destination_database => 'DEV4.LDI.COM');

  COMMIT;
EXCEPTION WHEN OTHERS THEN
  ROLLBACK;
  RAISE;
END;
10. DC:  Start capture process HISP$CAP
BEGIN
  dbms_capture_adm.start_capture(
    capture_name => '"HISP$CAP"');
EXCEPTION WHEN OTHERS THEN
  IF sqlcode = -26666 THEN NULL;  -- CAPTURE process already running
  ELSE RAISE;
  END IF;
END;
11. DEV4: Datapump SCHEMA MODE IMPORT
DECLARE
   H1               NUMBER; -- data pump job handle
   SCHEMA_EXPR_LIST VARCHAR2(32767); -- for metadata_filter
   CNT              NUMBER; -- temp variable
   OBJECT_OWNER     DBMS_UTILITY.UNCL_ARRAY; -- obj owners
   JOB_STATE        VARCHAR2(30); -- job state
   STATUS           KU$_STATUS; -- data pump status
   JOB_NOT_EXIST EXCEPTION;
   PRAGMA EXCEPTION_INIT(JOB_NOT_EXIST, -31626);
BEGIN

   OBJECT_OWNER(1) := 'HIS';
   FOR IDX IN 1 .. 1 LOOP
      -- schema does not exist locally, need instantiation
      IF SCHEMA_EXPR_LIST IS NULL THEN
         SCHEMA_EXPR_LIST := '(';
      ELSE
         SCHEMA_EXPR_LIST := SCHEMA_EXPR_LIST || ',';
      END IF;
      SCHEMA_EXPR_LIST := SCHEMA_EXPR_LIST || '''' || OBJECT_OWNER(IDX) || '''';
   END LOOP;
   IF SCHEMA_EXPR_LIST IS NOT NULL THEN
      SCHEMA_EXPR_LIST := SCHEMA_EXPR_LIST || ')';
   ELSE
      COMMIT;
      RETURN;
   END IF;

   H1 := DBMS_DATAPUMP.OPEN(OPERATION   => 'IMPORT',
                            JOB_MODE    => 'SCHEMA',
                            REMOTE_LINK => '',
                            JOB_NAME    => NULL,
                            VERSION     => 'COMPATIBLE');

   DBMS_DATAPUMP.ADD_FILE(HANDLE    => H1,
                          FILENAME  => 'expdat114.dmp',
                          DIRECTORY => 'STREAMS_DIR',
                          FILETYPE  => DBMS_DATAPUMP.KU$_FILE_TYPE_DUMP_FILE);
   DBMS_DATAPUMP.ADD_FILE(HANDLE    => H1,
                          FILENAME  => 'expdat114.dlg',
                          DIRECTORY => 'STREAMS_DIR',
                          FILETYPE  => DBMS_DATAPUMP.KU$_FILE_TYPE_LOG_FILE);

   DBMS_DATAPUMP.METADATA_REMAP(HANDLE    => H1,
                                NAME      => 'REMAP_SCHEMA',
                                OLD_VALUE => 'HIS',
                                VALUE     => 'HIS2_HISP');

   DBMS_DATAPUMP.METADATA_REMAP(HANDLE    => H1,
                                NAME      => 'REMAP_TABLESPACE',
                                OLD_VALUE => 'HIS',
                                VALUE     => 'HIS2_HISP');


   DBMS_DATAPUMP.START_JOB(H1);

   JOB_STATE := 'UNDEFINED';
   BEGIN
      WHILE (JOB_STATE != 'COMPLETED') AND (JOB_STATE != 'STOPPED') LOOP
         STATUS    := DBMS_DATAPUMP.GET_STATUS(HANDLE  => H1,
                                               MASK    => DBMS_DATAPUMP.KU$_STATUS_JOB_ERROR +
                                                          DBMS_DATAPUMP.KU$_STATUS_JOB_STATUS +
                                                          DBMS_DATAPUMP.KU$_STATUS_WIP,
                                               TIMEOUT => -1);
         JOB_STATE := STATUS.JOB_STATUS.STATE;
         DBMS_LOCK.SLEEP(10);
      END LOOP;
   EXCEPTION
      WHEN JOB_NOT_EXIST THEN
         DBMS_OUTPUT.PUT_LINE('job finished');
   END;
   COMMIT;
EXCEPTION
   WHEN OTHERS THEN
      ROLLBACK;
      RAISE;
END;
12: DEV4: Set up queue "STRMADMIN"."HISP$APPQ"
BEGIN
  dbms_streams_adm.set_up_queue(
    queue_table => '"STRMADMIN"."HISP$APPQT"',
    storage_clause => NULL,
    queue_name => '"STRMADMIN"."HISP$APPQ"',
    queue_user => '');
END;
13. DEV4: APPLY changes for schema HIS
DECLARE
  compat         VARCHAR2(512);
  initpos        NUMBER;
  pos            NUMBER;
  version_num    NUMBER;
  release_num    NUMBER;
  compat_func    VARCHAR2(65);
  get_compatible VARCHAR2(4000);
BEGIN
  SELECT value INTO compat
  FROM v$parameter
  WHERE name = 'compatible';

  -- Extract version number
  initpos := 1;
  pos := INSTR(compat, '.', initpos, 1);
  IF pos > 0 THEN
    version_num := TO_NUMBER(SUBSTR(compat, initpos, pos - initpos));
    initpos := pos + 1;

    -- Extract release number
    pos := INSTR(compat, '.', initpos, 1);
    IF pos > 0 THEN
      release_num := TO_NUMBER(SUBSTR(compat, initpos, pos - initpos));
      initpos := pos + 1;
    ELSE
      release_num := TO_NUMBER(SUBSTR(compat, initpos));
    END IF;
  END IF;

  IF version_num < 10 THEN
    compat_func := 'dbms_streams.compatible_9_2';
  ELSIF version_num = 10 THEN
    IF release_num < 2 THEN
      compat_func := 'dbms_streams.compatible_10_1';
    ELSE
      compat_func := 'dbms_streams.compatible_10_2';
    END IF;
  ELSE
    compat_func := 'dbms_streams.compatible_10_2';
  END IF;

  get_compatible := ':lcr.get_compatible() <= '||compat_func;


  dbms_streams_adm.add_schema_rules(
    schema_name => '"HIS"',
    streams_type => 'APPLY',
    streams_name => '',
    queue_name => '"STRMADMIN"."HISP$APPQ"',
    include_dml => TRUE,
    include_ddl => TRUE,
    include_tagged_lcr => TRUE,
    source_database => 'HISP.LDI.COM',
    inclusion_rule => TRUE,
    and_condition => get_compatible);
END;
14: DEV4: Set apply rule
CREATE OR REPLACE FUNCTION SOURCE2DESTINATION(IN_ANY IN ANYDATA)
   RETURN ANYDATA IS
   LCR SYS.LCR$_DDL_RECORD;
   RC  NUMBER;
   SEQ NUMBER;
   DDL CLOB;
BEGIN
   -- Check if the object type is SYS.LCR$_DDL_RECORD
   IF IN_ANY.GETTYPENAME = 'SYS.LCR$_DDL_RECORD' OR
      IN_ANY.GETTYPENAME = 'SYS.LCR$_PROCEDURE_RECORD' THEN
      -- Put the row LCR into lcr
      RC := IN_ANY.GETOBJECT(LCR);   
      -- Transform the LCR
      IF LCR.GET_OBJECT_OWNER = 'HIS' THEN
         LCR.SET_OBJECT_OWNER('HIS2_HISP');
      END IF;
      -- Return the transformed LCR
      RETURN ANYDATA.CONVERTOBJECT(LCR);
   END IF;
END;

BEGIN
   DBMS_STREAMS_ADM.SET_RULE_TRANSFORM_FUNCTION(RULE_NAME          => 'HIS77',
                                                TRANSFORM_FUNCTION => 'source2destination');
   DBMS_STREAMS_ADM.RENAME_SCHEMA(RULE_NAME        => 'HIS76',
                                  FROM_SCHEMA_NAME => 'HIS',
                                  TO_SCHEMA_NAME   => 'HIS2_HISP');
END;

15: DEV4: Get tag value to be used for Apply
DECLARE
  found            BINARY_INTEGER := 0;
  tag_num          NUMBER;
  apply_nm         VARCHAR2(30);
  apply_nm_dqt     VARCHAR2(32);
BEGIN
  SELECT apply_name INTO apply_nm
  FROM dba_apply_progress
  WHERE source_database = 'HISP.LDI.COM';

  apply_nm_dqt := '"' || apply_nm || '"';
  -- Use the apply object id as the tag
  SELECT o.object_id INTO tag_num
  FROM dba_objects o
  WHERE o.object_name= apply_nm AND
        o.object_type='APPLY';
  LOOP
    BEGIN
      found := 0;
      SELECT 1 INTO found FROM dba_apply
      WHERE apply_name != apply_nm AND
            apply_tag = hextoraw(tag_num);
    EXCEPTION WHEN no_data_found THEN
      EXIT;
    END;
    EXIT WHEN (found = 0);
    tag_num := tag_num + 1;
  END LOOP;
  -- alter apply
  dbms_apply_adm.alter_apply(
    apply_name => apply_nm_dqt,
    apply_tag => hextoraw(tag_num));
END;
16. DEV4: Start apply process applying changes from HISP.LDI.COM
DECLARE
  apply_nm VARCHAR2(32);
  apply_nm_dqt VARCHAR2(32);
BEGIN
  SELECT apply_name INTO apply_nm
  FROM dba_apply_progress
  WHERE source_database = 'HISP.LDI.COM';

  apply_nm_dqt := '"' || apply_nm || '"';
  dbms_apply_adm.start_apply(
    apply_name => apply_nm_dqt);
EXCEPTION WHEN OTHERS THEN
  IF sqlcode = -26666 THEN NULL;  -- APPLY process already running
  ELSE RAISE;
  END IF;
END;
17. DC: Enable propagation schedule for "STRMADMIN"."HISP$CAPQ"
DECLARE
  q2q       VARCHAR2(10);
  destn_q   VARCHAR2(65);
BEGIN
  SELECT queue_to_queue INTO q2q
  FROM dba_propagation
  WHERE source_queue_owner = 'STRMADMIN' AND
        source_queue_name = 'HISP$CAPQ' AND
        destination_queue_owner = 'STRMADMIN' AND
        destination_queue_name = 'HISP$APPQ' AND
        destination_dblink = 'DEV4.LDI.COM';

  IF q2q = 'TRUE' THEN
    destn_q := '"STRMADMIN"."HISP$APPQ"';
  ELSE
    destn_q := NULL;
  END IF;

  dbms_aqadm.enable_propagation_schedule(
    queue_name => '"STRMADMIN"."HISP$CAPQ"',
    destination => 'DEV4.LDI.COM',
    destination_queue => destn_q);
EXCEPTION WHEN OTHERS THEN
  IF sqlcode = -24064 THEN NULL; -- propagation already enabled
  ELSE RAISE;
  END IF;
END;