繼上次刪除分區表的分區遇到ORA-01502錯誤后[詳細見鏈接:Oracle分區表刪除分區引發錯誤ORA-01502: 索引或這類索引的分區處於不可用狀態],最近在split分區的時候又遇到了這個問題。這里記錄一下該問題是如何產生的,以及如何去解決。
(一)目的
在生產中,我們的大多數分區表都是按照時間分區的,最常見的是按周或按月分區,對於我們DBA來說,對表分區的創建與刪除都非常好管理,我在2018年10月會將所有表的分區創建到2019年12月,這樣2019年的數據就會進入各個對應月份的分區。
但是也有小部分分區表是按照其它來分區,例如,事物交易編號等,我們將10萬個交易信息存放在一個分區,對於業務,這樣創建分區是合理的,但是存在一定的隱患,每天的交易量是動態變化的,有可能3天使用完1個分區,也有可能1天就使用完一個分區,那么分區什么時候使用完我們是不得而知的。對於這種情況,我會為這類分區表添加max分區,從而保證當數據溢出了我們創建的分區時,會進入到max分區里面。分區表大致形式如下(需要說明的是,實際分區表的分區非常大,這里是為了模擬事故創建的小表):

圖1.表欄位信息

圖2.表分區情況
(二)事故起因
在上周,由於交易量非常大,發現part_max分區已經開始進入數據了,並且進入的數據量還不小,有大概3個partition的數據。擔心大量數據進入part_max分區引起業務查詢緩慢,於是決定實施split part_max分區,split執行的語句為:
ALTER TABLE test01 SPLIT PARTITION part_max AT(1000) INTO(PARTITION part_1000,PARTITION part_max); ALTER TABLE test01 SPLIT PARTITION part_max AT(1100) INTO(PARTITION part_1100,PARTITION part_max); ALTER TABLE test01 SPLIT PARTITION part_max AT(1200) INTO(PARTITION part_1200,PARTITION part_max); ALTER TABLE test01 SPLIT PARTITION part_max AT(1300) INTO(PARTITION part_1300,PARTITION part_max);
通過以上操作,將part_max分區的數據分離到part_1000,part_1100,part_1200,part_1300里面,從而減小part_max數據量。
在執行操作后,過了幾分鍾,業務方面出現了2個問題:
問題1:與該表相關的查詢變得非常緩慢;
問題2:數據插入更新報出了大量的“ORA-01502”錯誤
(三)當時的解決方案
結合上次出現ORA-01502錯誤的經歷,立馬斷定是索引出現問題了。查看索引,果然一部分新分區的局部分區索引失效了。立馬刪除索引,新建索引,將業務給啟動起來。
現在回想起來,解決問題的方式略有不妥。出問題的表size非常的大,有150多GB,創建一個局部分區索引大概需要2.5小時,還好是一部分非關鍵業務,否則都不知道如何處理。
(四)查找原因&實驗驗證
回想了自己當天所做的操作,僅僅對這些表進行了split。那么是不是split引起索引失效呢?我們通過實驗驗證一下。
STEP1:建測試表。創建sales表,以transactionId(交易ID)來分區
create table sales ( transactionId number, goodsId number, goodsName varchar2(30), saleTimekey date, goodsdescrip varchar2(100) ) partition by range(transactionId) ( partition part_100 values less than(100), partition part_200 values less than(200), partition part_300 values less than(300), partition part_400 values less than(400), partition part_500 values less than(500), partition part_600 values less than(600), partition part_700 values less than(700), partition part_800 values less than(800), partition part_900 values less than(900), partition part_max values less than(maxvalue) );
STEP2:創建主鍵約束和局部分區索引。
--6.1 創建主鍵約束,主鍵約束會引入唯一性索引 alter table sales add constraint pk_sales_transactionId primary key(transactionId) using index local online tablespace users; --6.2 創建普通的局部分區索引 create index lijiaman.goodsId on sales(goodsId) local online tablespace users;
STEP3:創建一個自增長序列。該序列用來模擬交易ID的自增長情況
create sequence sq_transactionId start with 1 increment by 1 maxvalue 100000000 nocache;
STEP4:創建一個procedure,用來模擬數據插入
--3.1 創建異常捕獲表 --該表用於捕獲數據插入異常時的異常信息 --drop table sale_exception; create table sale_exception ( timekey date, errcode varchar2(50), errmess varchar2(500) ); --3.2創建插入sales表的pl/sql程序 create or replace procedure p_sales is v_sqlcode number; v_sqlerrm varchar2(4000); begin insert into sales (transactionId, goodsId, goodsName, saleTimekey, goodsdescrip) values (sq_transactionId.Nextval, (select round(dbms_random.value(10000, 100000000)) from dual), (select dbms_random.string('a', 25) from dual), sysdate, (select dbms_random.string('a', 85) from dual)); commit; exception when others then rollback; v_sqlcode := sqlcode; v_sqlerrm := substr(sqlerrm,1,100); insert into sale_exception values(sysdate,v_sqlcode,v_sqlerrm); commit; end p_sales;
STEP5:創建job,定時向sales表插入數據。(多次執行,可以創建多個job向表里插入數據,這里我執行了10次,即由10個job每隔5s向sales表里面插入數據)
declare job1 number; begin sys.dbms_job.submit(job => job1, what => 'p_sales;', next_date => sysdate, interval => 'sysdate + 5/(1440*60)'); --每隔5s向sales表插入一筆隨機數據 commit; end; /
STEP6:查看sales表的數據信息。查看sales表的數據及各個分區的數據
select count(*) from sales; select count(*) from sales partition(part_100); select count(*) from sales partition(part_200); select count(*) from sales partition(part_300); select count(*) from sales partition(part_400); select count(*) from sales partition(part_500); select count(*) from sales partition(part_600); select count(*) from sales partition(part_700); select count(*) from sales partition(part_800); select count(*) from sales partition(part_900); select count(*) from sales partition(part_max);
STEP7:確認索引的狀態
查看dba_indexes,發現index狀態為N/A:
SQL> select owner,table_name,index_name,uniqueness,status from dba_indexes i 2 where i.owner = 'LIJIAMAN' and i.table_name = 'SALES'; OWNER TABLE_NAME INDEX_NAME UNIQUENESS STATUS ------------------------------ ------------------------------ ------------------------------ ---------- -------- LIJIAMAN SALES PK_SALES_TRANSACTIONID UNIQUE N/A LIJIAMAN SALES GOODSID NONUNIQUE N/A
分區索引狀態需要從dba_ind_partitions查看:
SQL> select index_owner,index_name,partition_name,status from dba_ind_partitions i 2 where index_owner = 'LIJIAMAN' and index_name in('PK_SALES_TRANSACTIONID','GOODSID'); INDEX_OWNER INDEX_NAME PARTITION_NAME STATUS ------------------------------ ------------------------------ ------------------------------ -------- LIJIAMAN GOODSID PART_100 USABLE LIJIAMAN GOODSID PART_200 USABLE LIJIAMAN GOODSID PART_300 USABLE LIJIAMAN GOODSID PART_400 USABLE LIJIAMAN GOODSID PART_500 USABLE LIJIAMAN GOODSID PART_600 USABLE LIJIAMAN GOODSID PART_700 USABLE LIJIAMAN GOODSID PART_800 USABLE LIJIAMAN GOODSID PART_900 USABLE LIJIAMAN GOODSID PART_MAX USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_100 USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_200 USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_300 USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_400 USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_500 USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_600 USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_700 USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_800 USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_900 USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_MAX USABLE 20 rows selected
通過最后的STATUS列,可以看到所有局部分區索引都是可用的。
STEP8:再次查看各分區的數據量
SQL> select count(*) from sales; --整個表有1244筆數據 COUNT(*) ---------- 1244 SQL> select count(*) from sales partition(part_max); --part_max分區有375筆數據 COUNT(*) ---------- 375
STEP9:執行split分區操作
在上一步,max分區已經有375筆數據了,如果按照100大小作為一個分區,那么數據可以存放到4個分區里面。執行split分區操作。
alter table sales split partition part_max at (1000) into (partition part_1000,partition part_max); alter table sales split partition part_max at (1100) into (partition part_1100,partition part_max); alter table sales split partition part_max at (1200) into (partition part_1200,partition part_max); alter table sales split partition part_max at (1300) into (partition part_1300,partition part_max); alter table sales split partition part_max at (1400) into (partition part_1400,partition part_max); alter table sales split partition part_max at (1500) into (partition part_1500,partition part_max);
STEP10:再次執行step7,查看分區索引的狀態
SQL> select owner,table_name,index_name,uniqueness,status from dba_indexes i 2 where i.owner = 'LIJIAMAN' and i.table_name = 'SALES'; OWNER TABLE_NAME INDEX_NAME UNIQUENESS STATUS ------------------------------ ------------------------------ ------------------------------ ---------- -------- LIJIAMAN SALES PK_SALES_TRANSACTIONID UNIQUE N/A LIJIAMAN SALES GOODSID NONUNIQUE N/A
查看各個索引分區的狀態:
14:44:42 SQL> select index_owner,index_name,partition_name,status from dba_ind_partitions i 2 where index_owner = 'LIJIAMAN' and index_name in('PK_SALES_TRANSACTIONID','GOODSID'); INDEX_OWNER INDEX_NAME PARTITION_NAME STATUS ------------------------------ ------------------------------ ------------------------------ -------- LIJIAMAN GOODSID PART_100 USABLE LIJIAMAN GOODSID PART_1000 UNUSABLE LIJIAMAN GOODSID PART_1100 UNUSABLE LIJIAMAN GOODSID PART_1200 UNUSABLE LIJIAMAN GOODSID PART_1300 UNUSABLE LIJIAMAN GOODSID PART_1400 UNUSABLE LIJIAMAN GOODSID PART_1500 USABLE LIJIAMAN GOODSID PART_200 USABLE LIJIAMAN GOODSID PART_300 USABLE LIJIAMAN GOODSID PART_400 USABLE LIJIAMAN GOODSID PART_500 USABLE LIJIAMAN GOODSID PART_600 USABLE LIJIAMAN GOODSID PART_700 USABLE LIJIAMAN GOODSID PART_800 USABLE LIJIAMAN GOODSID PART_900 USABLE LIJIAMAN GOODSID PART_MAX USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_100 USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_1000 UNUSABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_1100 UNUSABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_1200 UNUSABLE INDEX_OWNER INDEX_NAME PARTITION_NAME STATUS ------------------------------ ------------------------------ ------------------------------ -------- LIJIAMAN PK_SALES_TRANSACTIONID PART_1300 UNUSABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_1400 UNUSABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_1500 USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_200 USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_300 USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_400 USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_500 USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_600 USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_700 USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_800 USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_900 USABLE LIJIAMAN PK_SALES_TRANSACTIONID PART_MAX USABLE 32 rows selected
從上面可以看到,2個索引的某些分區變為“UNUSABLE”狀態,這些狀態的索引都是新split出來的,但是並不包括全部,如part_1500分區的索引是可用的。上面的索引失效會引起2個問題:
問題1:在查詢失效索引相關的分區時,由於索引不可用,查詢速度會非常慢;
問題2:由於存在主鍵約束(帶有唯一性索性),在失效索引相關的分區上,數據DML時會引發ORA-01502錯誤。我們可以從異常捕獲表sales_exception查看異常信息:

這就明白了,為什么在split分區表后,生產系統中會出現以上2中情況。
小結:什么情況下split會引起index失效?
在測試時,發現在做split后,新split出來的分區,有的相關分區索引失效,而有的分區索引則不會失效。至於為什么會出現這種情況,個人認為是和segment的分裂有關,part_max段在split后,一個表segment分裂為多個,同樣,對應的索引segment也分裂為多個。分裂后,如果一個index分區存放了所有分裂出來的數據,則索引分區與表分區依然可以對應;如果一個index分區存放不下所有數據,則會導致存在數據的索引分區與表分區數據對應不上,索引失效;如果是新分離出來的分區沒有數據,則索引與表依然對應。
經過測試,發現規律:
1.part_max沒有數據時,split操作不會引起local index失效;
2.part_max有數據:
--split出來的第一個分區【可以存放】part_max里面的全部數據,split后part_max為空,則split 【不會】 引起索引失效;
--split出來的第一個分區【不能夠存放】part_max里面的數據,但是后續的分區可以存放下part_max的數據,split后part_max為空,split 【會】 引起索引失效。失效的索引為:新splits出來的有數據的分區,沒有數據的分區不會失效,part_max同樣不會失效;
--split出來的全部分區【不能夠存放】part_max里面的全部數據,split后part_max不為空,split 【會】 引起索引失效。失效的索引為:新split的全部索引和part_max;

圖3.split表分區索引失效梳理
(五)如何對應
方案一:重建不可用的索引
SQL> ALTER INDEX [schema.]index_name REBUILD PARTITION partition_name [ONLINE];
我在出問題時重建了整個表的索引,沒想到可以重建單個分區的索引。
方法小結:
優點:在部分分區的local index不可用后,使用該方法可以快速重建,快速恢復業務;
缺點:用到這種方法,說明部分local index已經不可用,業務已經出現上面2個問題。
方案二:在split時添加update indexes選項
SQL> ALTER TABLE [schema.]table_name SPLIT PARTITION partition_name AT (part_values) INTO (PARTITION part_values, PARTITION part_max) update indexes;
對於這種方法,個人最關心的問題是:
1.會不會導致local index失效;
2.如果不會導致locl index失效,在進行split時,是否存在鎖,導致DML失敗。
經過測試(測試表有2個分區,我們對其中一個分區進行split,該分區數據量有2GB,22800000行數據),發現在進行split時會產生TX鎖,split持續了90s。在這期間DML操作hang住。查看local index的狀態,未出現不可用的索引。
方法小結:
優點:不會造成local index不可用;
缺點:在執行操作期間會造成鎖表,如果表分區較大,持續時間將會很長,在生產中難以接受。
目前來看,對於7*24小時的系統,沒有辦法完美解決分區數據分離的問題,只有隨時關注數據增長,盡量不要讓數據進入part_max分區。接下來再找一找資料,爭取對業務影響最小。
