1. 程式人生 > >Oracle split分割槽表引起ORA-01502錯誤

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分割槽。接下來再找一找資料,爭取對業務影響最小。