9i新特性之——線上表格重定義研究在(轉)

zyb200發表於2007-06-04
在Oracle9i出現之前,你只能透過MOVE或匯出和匯入的方式來進行表格重定義,因此表格重定義的過程可能相當漫長或者說是一個離線過程,在此期間應用程式對該表的操作將失敗,特別是大型的分割槽表上的操作(上次coolyl)就問到了這個問題,我們當時是建議他用move partition的辦法解決,但是move可能很耗時並可能引起全域性索引的失效(不過9i已經有了update global indexes)。除了這個,如果用exp,我們也不能保證exp的時候該表的資料沒有改變(除非單使用者),而imp更是一個漫長的過程。[@more@]

為了解決這個問題,Oracle9i在其DBMS_REDEFINITION軟體包中引入了線上表格重定義功能。這個特性對24/7Oracle資料庫來說非常重要,這是由於DBA現在可以在保持表格的可更新性的同時重新組織表格的結構。

然而,線上表格重定義也不是完美無缺的。下面列出了Oracle9i重定義過程的部分限制。
·你必須有足以維護兩份表格複製的空間。
·你不能更改主鍵欄。
·表格必須有主鍵。
·必須在同一個大綱中進行表格重定義。
·在重定義操作完成之前,你不能對新加欄加以NOT NULL約束。
·表格不能包含LONG、BFILE以及使用者型別(UDT)。
·不能重定義連結串列(clustered tables)。
·不能在SYS和SYSTEM大綱中重定義表格。
·不能用具體化檢視日誌(materialized view logs)來重定義表格;不能重定義含有具體化檢視的表格。
·不能在重定義過程中進行橫向分集(horizontal subsetting)。

基本步驟如下

第一步:利用dbms_redefinition.can_redef_table過程檢查該表是否能被線上重定義。

第二步:建立一個與原表類似的空表結構,用於重定義該表,這裡叫做是中間表

第三步:用dbms_redefinition.start_redef_table procedure定義重構開始
該過程可以輸入如下引數資訊
使用者的名稱
原表的名稱
中間表的名稱
需要同步改變的列的名稱


這個過程將回自動執行如下操作:
1、插入所有行從原有表到中間表
2、建立MLOG$_EMP快照與快照日誌,臨時儲存DML語句直到完成。


第四步:與原表一致,在中間表上面建立約束,索引,觸發器
與原表一致(如果需要),中間表的物件許可權被授予給別的物件
任何中間表上的外來鍵約束將被禁止

第五步:用dbms_redefinition.finish_redef_table過程完成表的最終重定義
該過程將自動完成
應用快照日誌中的DML到中間表
互換原表與中間表的名字,包括所有可能出現的資料字典
但是需要注意的是,並不對換約束,索引,觸發器的名稱,這些需要手工修改


第六步:刪除中間表、

第七步:如果是920以上,可以利用ALTER TABLE ... RENAME CONSTRAINT ...語句來修改約束名稱,如果以下版本,就只有刪除並重建了,當然,如果約束名稱並不重要,也就無所謂了

第八步:如果重組織失敗,那麼你就必須採取特殊的步驟來讓它重新開始。由於重定義過程需要建立表格的快照,因此為了重新開始這一過程,你必須呼叫DBMS_REDEFINITION.ABORT_REDEF_TABLE來釋放快照。
詳細過程
1、我們建立一個單獨的測試使用者用來測試整個過程

SQL> create user mytest identified by mytest;
User created
SQL> grant connect,resource to mytest;
Grant succeeded


SQL> connect mytest/mytest;
Connected to Oracle9i Enterprise Edition Release 9.2.0.4.0
Connected as mytest


2、首先,我們建立一個原表,假定是我們需要線上重新定義的表,由於工作需要,我們需要把該表(不是分割槽表)重新定義為一個分割槽表,而且不能影響應用程式的執行。

SQL> create table test(a int,b int) tablespace users;
Table created

模擬一個原表

declare i integer;
begin
for i in 1..100 loop
insert into test values(i,100-i);
end loop;
commit;
end;

插入100條模擬資料

create or replace trigger tr_test
before insert or update or delete on test
for each row
declare
PRAGMA AUTONOMOUS_TRANSACTION;
begin
update audit_test set c=c+1;
commit;
end;

在上面建立一個觸發器,模擬原表可能會有的觸發器,這個觸發器的作用就是如果有任何DML操作,將在audit_test中增加1。

Audit_test現在的資料是:
SQL> select c from audit_test;
100


檢查觸發器是否工作正常
SQL> insert into test values(101,0);
1 row inserted
SQL> commit;


SQL> select c from audit_test;
101

可以看到,觸發器工作正常。

3、檢查該表是否能重定義
SQL> exec dbms_redefinition.can_redef_table('MYTEST', 'TEST');

begin dbms_redefinition.can_redef_table('MYTEST', 'TEST'); end;

ORA-12089: cannot online redefine table "MYTEST"."TEST" with no primary key
ORA-06512: at "SYS.DBMS_REDEFINITION", line 8
ORA-06512: at "SYS.DBMS_REDEFINITION", line 247
ORA-06512: at line 1

可以看到,如果重新定義,需要主鍵,所以我們增加該表的主鍵

我們定義主鍵
SQL> alter table test add constraint pk_test_id primary key(a);
Table altered

SQL> exec dbms_redefinition.can_redef_table('MYTEST', 'TEST');
PL/SQL procedure successfully completed

現在發現,我們可以定義該表了
4、建立我們需要重新定義的中間表,這個是一個分割槽表,以後我們將把原表的所有資料線上轉移到該表上來。

SQL>create table int_test
(a int,b int)
partition by range(a)
(partition p10 values less than(50),
partition p20 values less than(100),
partition p30 values less than(150),
partition p40 values less than(200));
Table created


然後我們檢查所有物件,所有物件共9個

SQL> select object_name,object_type,status,object_id,data_object_id from user_objects;
AUDIT_TEST TABLE VALID 33542 33542
INT_TEST TABLE PARTITION VALID 33558 33558
INT_TEST TABLE PARTITION VALID 33559 33559
INT_TEST TABLE PARTITION VALID 33560 33560
INT_TEST TABLE PARTITION VALID 33561 33561
INT_TEST TABLE VALID 33557
PK_TEST_ID INDEX VALID 33551 33551
TEST TABLE VALID 33541 33541
TR_TEST TRIGGER VALID 33543
9 rows selected


5、執行線上重新定義開始
SQL> execute
DBMS_REDEFINITION.START_REDEF_TABLE('MYTEST','TEST','INT_TEST');

begin SYS.DBMS_REDEFINITION.START_REDEF_TABLE('MYTEST','TEST','INT_TEST'); end;

ORA-01031: insufficient privileges
ORA-06512: at "SYS.DBMS_REDEFINITION", line 8
ORA-06512: at "SYS.DBMS_REDEFINITION", line 146
ORA-06512: at line 1


這裡儲存過程返回一個錯誤,說沒有足夠的許可權,的確是這樣,經查,如果要執行這個儲存過程,起碼需要如下許可權:
* CREATE ANY TABLE
* ALTER ANY TABLE
* DROP ANY TABLE
* LOCK ANY TABLE
* SELECT ANY TABLE

我們可以臨時授予DBA許可權給使用者,完之後取消掉,或轉移到其他有許可權的使用者執行這個操作:

SQL> CONNECT SYSTEM/CHEN
Connected to Oracle9i Enterprise Edition Release 9.2.0.4.0
Connected as SYSTEM
SQL> execute SYS.DBMS_REDEFINITION.START_REDEF_TABLE('MYTEST','TEST','INT_TEST');
PL/SQL procedure successfully completed


這裡可以看到,我們用system執行成功,那麼這個過程到底兩個表執行了那些語句呢?我們可以透過如下的語句來驗證一下

SQL> select sql_text from v$sqlarea where sql_text like '%TEST%';

SQL_TEXT
--------------------------------------------------------------------------------
SELECT CURRENT$."A",CURRENT$."B" FROM (SELECT "TEST"."A" "A","TEST"."B" "B" FROM
select * from "MYTEST"."TEST"
ALTER TRIGGER MYTEST.TR_TEST COMPILE DEBUG
SELECT /*+ NO_MERGE NO_MERGE(LL$) ROWID(MAS$) ORDERED USE_NL(MAS$) NO_INDEX(MAS
DELETE FROM "MYTEST"."INT_TEST" SNAP$ WHERE "A" = :1
truncate table "MYTEST"."INT_TEST" purge snapshot log
delete from "MYTEST"."MLOG$_TEST" where snaptime$$ <= :1
INSERT INTO "MYTEST"."INT_TEST" ("A","B") VALUES (:1,:2)
select sql_text from v$sqlarea where sql_text like '%TEST%'
UPDATE "MYTEST"."INT_TEST" SET "A" = :1,"B" = :2 WHERE "A" = :1
SELECT "TEST"."A" "A","TEST"."B" "B" FROM "MYTEST"."TEST" "TEST"
begin dbms_redefinition.can_redef_table('MYTEST', 'TEST'); end;
select count(*) from snap$ where (vname, sowner) in (('TEST','MYTEST'))
comment on table "MYTEST"."RUPD$_TEST" is 'temporary updatable snapshot log'
begin DBMS_REDEFINITION.START_REDEF_TABLE('MYTEST','TEST','INT_TEST'); end;
comment on table "MYTEST"."MLOG$_TEST" is 'snapshot log for master table MYTEST.
INSERT INTO "MYTEST"."INT_TEST"("A","B") SELECT "TEST"."A","TEST"."B" FROM "MYTE
INSERT /*+ APPEND */ INTO "MYTEST"."INT_TEST"("A","B") SELECT "TEST"."A","TEST".
update "MYTEST"."MLOG$_TEST" set snaptime$$ = :1 where snaptime$$ > to_date('21
SELECT DISTINCT LOG$."A" FROM (SELECT MLOG$."A" FROM "MYTEST"."MLOG$_TEST" MLOG$

20 rows selected


以上的語句我不再解釋,整個內部過程我也不再解釋,只是需要大家明白,其實這裡的主要需要了解的是,就是把原表的資料給中間表複製了一份

我們再檢查所有物件

SQL> select object_name, object_type, status, object_id, data_object_id from user_objects order by 4;
TEST TABLE VALID 33541 33541
AUDIT_TEST TABLE VALID 33542 33542
TR_TEST TRIGGER VALID 33543
PK_TEST_ID INDEX VALID 33551 33551
INT_TEST TABLE VALID 33557
INT_TEST TABLE PARTITION VALID 33558 33564
INT_TEST TABLE PARTITION VALID 33559 33565
INT_TEST TABLE PARTITION VALID 33560 33566
INT_TEST TABLE PARTITION VALID 33561 33567

MLOG$_TEST TABLE VALID 33562 33562
RUPD$_TEST TABLE VALID 33563

11 rows selected

發現比以前多了兩個表物件
這個就是該過程在執行後會產生兩個表
一個是永久表MLOG$_EMP ,這個是一個TEST快照日誌,記錄TEST的在此之後,完成之前的DML語句
另一個就是臨時表RUPD$_EMP

我們檢查一下所有的表資料,已便與下面的結果對比
SQL> select count(*) from test;
101
SQL> select count(*) from MLOG$_test;
0
SQL> select count(*) from rupd$_test;
0
SQL> select count(*) from int_test;
101

可以看到,表的資料已經轉移過來

SQL> select c from audit_test;
101

這裡可以看到,觸發器執行的還是原觸發器

這裡給大家介紹兩個表的來源
SQL> select master,log_table from user_mview_logs;
TEST MLOG$_TEST

SQL> select mview_name,container_name, build_mode from user_mviews;
INT_TEST INT_TEST PREBUILT
6、在新表上建立新的約束與索引,這個步驟其實放在哪裡都可以,但是一般的情況下,我們把主要的資料轉移完之後,再建立索引速度可能會快一些。,為了便於測試,我們將在中間表上面建立一個與原表略有差異的觸發器。

SQL> alter table int_test add constraint int_test_pk primary key(a);
Table altered

請注意該主鍵的名稱,與原表的主鍵名稱的差別。

create or replace trigger tr_int_test
before insert or update or delete on int_test
for each row
declare
PRAGMA AUTONOMOUS_TRANSACTION;
begin
update audit_test set
c=c+2;
commit;
end;
Trigger created


可以看到,這個觸發器的增加是增加2,原表的是增加1。
檢查兩個觸發器,都是正常的。
SQL> select trigger_name, status from user_triggers;
TR_INT_TEST ENABLED
TR_TEST ENABLED


想想,這個時候,如果我們往原表插入資料,會有什麼結果呢?是一個觸發器工作,還是兩個,還是別的可能?
SQL> insert into test values(102,1);
1 row inserted
SQL> commit;
Commit complete

再檢查表的結果

SQL> select count(*) from test;
102
SQL> select count(*) from int_test;
101
SQL> select count(*) from mlog$_test;
1
SQL> select count(*) from rupd$_test;
0
SQL> select c from audit_test;
102
發現,其實原表增加了,中間表的記錄並沒有同步,只是快照日誌增加了一行。觸發器也只執行也原來的觸發器

7、我們來執行表的同步,其實這一步不是必須,如果不執行這一步,在所有的重構完成之後(執行finish過程)也將自動執行以下過程。我們這裡的目的,就是了解該過程怎麼執行。
SQL> execute DBMS_REDEFINITION.SYNC_INTERIM_TABLE('MYTEST','TEST','INT_TEST');
PL/SQL procedure successfully completed


同步之後會有什麼結果呢?我們查詢如下的表

SQL> select count(*) from test;
102
SQL> select count(*) from int_test;
102
SQL> select count(*) from mlog$_test;
0

SQL> select count(*) from rupd$_test;
0
SQL> select c from audit_test;

102
可以看到,其實這個過程是把快照日誌中的資料轉移到了中間表,但是這個過程並不觸發觸發器。

8、我們需要完成最後的一些工作,如授權,檢查TEST以前的許可權,並在中間表上執行同樣的許可權,如果不做,那原表的許可權將不會轉移到新表,以下的測試將說明這個問題。
SQL> select * from USER_TAB_PRIVS_MADE;
SYSTEM TEST MYTEST SELECT NO NO
SYSTEM TEST MYTEST DELETE


為了以後的重構的表與以前一致,我們必須對中間表授原表同樣的物件許可權,這裡,為了測試需要,我們保留一個原來的許可權,還增加一個新的許可權
SQL> grant select on int_test to system;
Grant succeeded
SQL> grant update on int_test to system;
Grant succeeded


完成之後的許可權情況如下,可以與最終重構之後的許可權情況對比。
SQL> select * from USER_TAB_PRIVS_MADE;
SYSTEM INT_TEST MYTEST SELECT NO NO
SYSTEM INT_TEST MYTEST UPDATE
SYSTEM TEST MYTEST SELECT NO NO
SYSTEM TEST MYTEST DELETE
9、所有的工作都準備完成,我們執行重構完成的過程,這個過程將執行表的交換。
SQL> execute DBMS_REDEFINITION.FINISH_REDEF_TABLE('MYTEST','TEST', 'INT_TEST');
PL/SQL procedure successfully completed


我們還是往test表中插入資料,將有什麼結果呢?

SQL> insert into test values(103,1);
1 row inserted

SQL> select count(*) from test;
103

SQL> select count(*) from int_test;
102

SQL> select * from mlog$_test;
select * from mlog$_test
ORA-00942: table or view does not exist

SQL> select * from rupd$_test;
select * from rupd$_test
ORA-00942: table or view does not exist

SQL> select c from audit_test;

104

可以看到,這裡的觸發器執行的是以前在ini_test(中間表)上的觸發器。上一次是102,這次是104

這裡發生了一個很有趣的轉換,其實整個核心就是這裡,ORACLE這裡完成了表test到int_test的表換名的工作,只是所有的約束、索引或觸發器名稱還是保持了原來的名稱

SQL> select t.index_name,t.table_name from user_indexes t;
INT_TEST_PK TEST
PK_TEST_ID INT_TEST


SQL> select t.trigger_name,t.table_name from user_triggers t;
TR_INT_TEST TEST
TR_TEST INT_TEST


我們檢查物件許可權
SQL> select * from USER_TAB_PRIVS_MADE; SYSTEM INT_TEST MYTEST SELECT NO NO
SYSTEM INT_TEST MYTEST DELETE
SYSTEM TEST MYTEST SELECT NO NO
SYSTEM TEST MYTEST UPDATE

這裡可以看到,對於所有資料字典中的這兩個名稱,其實是完成了一個表的名字對換的過程,所以對於原表的特性,我們必須在新表中給予相同的特性,這樣才能真正的轉換到新的表。

10、大功告成,我們可以刪除中間表,並且有可能的話,修改約束,索引,觸發器的名稱與原來一致,如果你認為不重要,可以不予修改。
SQL> drop table int_test;
Table dropped


如果是92以上版本,我們可以對索引,約束換名,但是以下版本,最好就是刪除重新建立了
如果執行了START_REDEF_TABLE 過程開始,我們就必須執行
dbms_redefinition.abort_redef_table('MYTEST','TEST','INT_TEST');來終止整個重構過程,到這裡,就算是完了。

SQL> select object_name, object_type, status, object_id, data_object_id from user_objects order by 4;

OBJECT_NAME OBJECT_TYPE STATUS OBJECT_ID DATA_OBJECT_ID
-------------------------------------------------------------------------------- ------------------ ------- --------- --------------
AUDIT_TEST TABLE VALID 33579 33579
TEST TABLE VALID 33582
TEST TABLE PARTITION VALID 33583 33589
TEST TABLE PARTITION VALID 33584 33590
TEST TABLE PARTITION VALID 33585 33591
TEST TABLE PARTITION VALID 33586 33592

6 rows selected

看到這裡,你也應該相信轉換已經完成了吧,現在是分割槽表了。
總結:
本文不想從大的框架上來說明問題,而是用一些例子來說明問題
這樣可能會更讓大家明白其中發生的一切。

這裡儘量的模擬了實際可能會遇到的情況。
如約束,索引,觸發器,物件許可權,這個在重新定義的時候需要注意什麼,需要我們手工完成什麼

外來鍵約束這裡沒有舉例說明,其實與普通約束一樣,只是在中間表上建立外來鍵約束的時候,最好先禁止掉(DISABLE),等完成之後再開啟。

其實主要過程就是完成了一個表的記錄的複製(透過快照),最後在資料字典中換名的工作。

歡迎大家補充,參考文件
metalink:
Doc ID: Note:177407.1

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/64429/viewspace-917332/,如需轉載,請註明出處,否則將追究法律責任。

相關文章