Oracle split分割槽表引起ORA-01502錯誤
繼上次刪除分割槽表的分割槽遇到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分割槽。接下來再找一找資料,爭取對業務影響最小。