數據庫從PostgreSQL遷移至Oracle指導書(二)


前言:近日,公司的一套使用 postgresql 數據庫的應用軟件要兼容oracle。本文系統性地整理了PostgreSQL 和 Oracle的一些差異點,和應用程序中的改動點。

 

3 SQL腳本的改造

應用程序的每個子系統有自己的數據庫初始化腳本。下面我們介紹初始化腳本中涉及到的PostgreSQL與Oracle的差異點及改造點。

3.1 數據類型

下面是PostgreSQL 中的各種數據類型與對應的Oracle數據類型。

 

 

PostgreSQL 11

Oracle 19c

備注

短整型

smallint / int2

number(5)

 

整型

integer / int / int4

number(10)

 

長整型

bigint / int8

number(20)

 

字符串(4000字節以下)

varchar(n)

varchar2(n char)

pg的varchar(n)表示n個字符;Oracle的varchar(n)表示 n 字節;varchar(n char) 表示 n 個字符;

 

pg中varchar最多可存放1GB內容,Oracle中varchar最多存放4000字節的內容

長字符串(4000字節及其以上)

varchar / text / varchar(2000) 及其以上長度

clob

Oracle 中,4000字節以內的字符串,用 varchar(4000);4000字節以上的,用clob

數組類型

有原生的數組,可作為字段類型和變量。例如:

 

create table tb_test

(

    attributes varchar(32)[],

);

用varchar或clob類型表示,格式為json數組。例如:

create table tb_test

(

attributes varchar(512),

CHECK (attribute  IS JSON)

);

 

json

有原生類型json和jsonb。例如:

 

create table tb_test

(

     json_column jsonb,

);

用varchar或clob表示。例如

 

create table tb_test

(

 json_column clob,

CHECK (attribute  IS JSON)

);

 

自增序列類型

create sequence seq_tb_test_id minvalue 1 maxvalue 2147483647;

 

create table tb_test

(

    id int default nextval('seq_tb_test_id')

);

(推薦)

 

或者

create table tb_test

(

    id serial

); (不推薦)

create sequence seq_tb_test_id minvalue 1 maxvalue 2147483647;

 

create table tb_test

(

    id int default seq_tb_test_id.nextval

);

postgresql聲明一個字段類型是serial,實質是創建一個序列(sequence),並將其綁定到這個字段上。

bool類型

boolean

不支持bool類型,可以用 number(5)類型代替。1表示true,0表示false

 

幾何類型

geometry

st_geometry

 

ip地址類型

inet

不支持inet,用字符串替代

 

 

 

3.2 常用 sql 語法

下面是數據庫腳本中一些需要改造的語法:

 

 

PostgreSQL 11

Oracle

備注

創建擴展插件

create extension pgcrypto;

不支持

將postgresql的 sql腳本改造為oracle版本時,刪除這些語句

創建/刪除數據庫對象之前先判斷

if exists/ if not exists

不支持if exists/ if not exists。

將sql腳本改造為基於oracle版本時,刪除if exists/ if not exists。

批量插入

insert into tb01 (id) values(1),(2),(3);

insert into tb01 (id) values(1);

insert into tb01 (id) values(2);

insert into tb01 (id) values(3);

Oracle中,需要逐行插入數據

設置字段默認值

alter table tb_region alter column region_id set default gen_random_uuid();

alter table tb_region modify region_id default sys_guid();

 

NULL和''

NULL和''不同

ORACLE認為''等同於NULL

NULL和''

字段定義時 not null 和default 的順序

create table test (

……,

status default 0 not null

); (推薦)

create table test ( ……,

 status not null  default 0

);

create table test (

    ……,

    status default 0 not null

);

 

 

 

3.3 常用 sql 函數

下面是PostgreSQL和Oracle中,對應的常用的數據庫函數和運算符:

 

 

PostgreSQL 11

Oracle 19c

備注

類型轉換

value :: type

CAST(value AS type)

CAST(value AS type)

 

序列取值

nextval('sequence_name')

sequence_name.nextval

注意:在pg中,nextval('sequence_name') 在同一行中每調用一次,數值會增加1;

在oracle中,sequence_name.nextval 在同一行中多次調用,值是相同的。

 

例如:

-- postgresql

# select nextval('sequence_name'),nextval('sequence_name');

result: 1,2

 

# select sequence_name.nextval,sequence_name.nextval;

result : 1,1

獲取uuid

gen_random_uuid() 或者uuid_generate_v1mc()

sys_guid()

 

獲取當前時間

獲取事務開始時間戳:

select now();

select current_timestamp;

 

獲取當前命令執行的時間戳:

select clock_timestamp();

獲取事務開始時間戳:

 

獲取當前命令執行的時間戳:

select current_timestamp from dual;

注意,兩個數據庫中  current_timestamp 的含義的差別

3.4 索引

下面是數據庫由PostgreSQL 遷移到Oracle時,索引的改造點:

 

 

PostgreSQL 11

Oracle 19c

備注

創建btree索引

create index idx_tb_event_event_id on tb_event [using btree] (event_id);

create index idx_tb_event_event_id on tb_event (event_id);

btree 是默認的索引類型。不建議明確指出索引類型是 btree。

PostgreSQL的 pg_trgm

PostgreSQL的擴展,用於模糊匹配

不支持,改造時直接刪除這個索引

 

PostgreSQL的 gin索引

PostgreSQL的索引類型

不支持,可根據索引具體用途,判斷是刪除這個索引還是改造它。

 

PostgreSQL的 gin_trgm_ops

PostgreSQL的索引訪問方法,用於模糊匹配

 

create index indx_tb_card_card_no_like on tb_card using gin (card_no gin_trgm_ops);

不支持,改造時直接刪除這個索引。

 

PostgreSQL的 text_pattern_ops

PostgreSQL的索引訪問方法,用於后模糊匹配 'abc%'

 

create index idx_tb_region_region_path on tb_region (region_path text_pattern_ops);

Oracle 不需要此方法,默認的btree即支持后模糊匹配 'abc%':

 

create index idx_tb_region_region_path on tb_region (region_path);

 

PostgreSQL的 brin索引

PostgreSQL的索引類型

不支持

在Oracle中用 默認btree索引 替代

PostgreSQL的 bloom 索引

PostgreSQL的索引類型

不支持

在Oracle中用 默認btree索引替代

為幾何類型  geometry的字段創建索引

create index idx_tb_map_point_the_geom on tb_map_point using gist (the_geom);

CREATE INDEX idx_tb_map_point_the_geom  ON tb_map_point (the_geom)

   INDEXTYPE IS MDSYS.SPATIAL_INDEX;

 

為JSON 字段(存放JSON對象)的某些鍵創建索引

create table tb_test

(

    id int,

    json_column jsonb,

);

 

create index idx_tb_test_json_column on tb_test ((json_column ->> 'parentId'));

create table tb_test

(

    id int,

     json_column clob,

    CHECK (json_column  IS JSON)

);

 

create index idx_tb_test_json_column on tb_test (json_value(json_column, '$.parentId'                                 RETURNING varchar(64)));

 

為數組字段創建索引

1. 字段類型為array

create table tb_test

(

    id int,

    codes integer [],

);

 

create index idx_tb_test_codes on tb_test using gin (codes);

 

2. 字段類型為jsonb

create table tb_test

(

    id int,

    codes jsonb

);

 

create index idx_tb_test_codes on tb_test using gin (codes);

不支持創建包含數組中的所有元素的索引

 

 

3.5 序列

下面是SQL腳本中與的序列相關的改造點:

 

 

PostgreSQL 11

Oracle 19c

備注

獲取序列當前值

currval('seqence_name');

seqence_name.currval

 

獲取序列下一個值

nextval('seqence_name');

seqence_name.nextval

注意:在pg中,nextval('sequence_name') 在同一行中每調用一次,數值會增加1;

在oracle中,sequence_name.nextval 在同一行中多次調用,值是相同的。

 

例如:

-- postgresql

# select nextval('sequence_name'),nextval('sequence_name');

result: 1,2

 

# select sequence_name.nextval,sequence_name.nextval;

result : 1,1

修改序列的值

setval('seqence_name')

不支持

 

設置序列不循環

create sequence seqence_name NO CYCLE;

create sequence seqence_name NOCYCLE;

 

創建序列的語法差異

CREATE SEQUENCE seq_1

  INCREMENT [ BY ] 1

  MINVALUE 0

  MAXVALUE 10000

  START [ WITH ] 1

  CACHE  [ 1 | 2 | 3 | ...]

CREATE SEQUENCE seq_1

  INCREMENT BY 1

  MINVALUE 0

  MAXVALUE 10000

  START WITH 1

  CACHE [ 2 | 3 | ...]

postgresql創建序列語句中,“BY”,“with” 是可選的。

PostgreSQL 序列緩存值可以等於1,而 Oracle 的序列緩存值必須大於1。

 

3.6 自定義函數、存儲過程和觸發器

PostgreSQL 的函數內部支持DDL(create, drop, alter)和DML(insert.delete,update),而Oracle 的函數只支持DML,存儲過程則支持DDL和DML。因此,對於我們在PostgreSQL 中定義的函數,那些內部沒有DDL 語句,且運行時會不進行DDL操作的,應該被改造為Oracle的函數;而那些內部有DDL 語句(create, drop, alter),或者運行時會進行DDL操作的,應該被改造為Oracle存儲過程;

 

PostgreSQL 允許函數/存儲過程重載,即有多個同名函數;而Oracle不允許。

 

下面是這兩種數據庫中,自定義函數,存儲過程和觸發器相關的語法差異:

 

 

PostgreSQL 11

Oracle 19c

備注

 

函數

函數

存儲過程

 

是否支持在內部執行 DDL 語句

 

內部執行 DDL 語句的方法

1. 直接在函數內部執行DDL語句

begin

create table test(id int);

end;

 

2. 執行sql字符串;

begin

execute 'create table test(id int)';

end;

不支持

執行sql 字符串

begin

execute immediate 'create table test(id number(10))';

end;

 

存儲過程中,執行sql字符串

begin

execute ' insert into test(id int) values (1);';

end;

begin

execute immediate ' insert into test(id int) values (1)';

end;

begin

execute immediate ' insert into test(id int) values (1)';

end;

注意:在postgresql中,execute 執行的sql字符串內可以分號結尾;而在oracle中,execute immediate 執行的sql 字符串不允許以分號結尾

函數中賦值符號

:= 或者 =

:=

:=

在變量賦值時,需要注意

字符串作為函數參數

varchar或 varchar(n)

 

例如:

func(a varchar(32), b varchar)

varchar,不帶長度

 

例如:

func(a varchar, b varchar)

varchar,不能長度

 

例如:

proc(a varchar, b varchar)

 

創建並調用有參函數/存儲過程

create or replace function func(a int, a2 varchar(32))                    

returns int as

$$

declare

  b int;

[declare] c varchar(32);

begin

      PL/SQL statements...

      return 0;

end;

$$  language plpgsql;

(推薦)

 

select func(1);

create or replace function func(a number, a2 varchar)

return int  {as | is}

    b int;

    c varchar(32);

begin

      PL/SQL statements...

      return 0;

end;

 

select func(1) from dual;

create or replace procedure proc(a number, a2 varchar)

 {as | is}

    b int;

    c varchar(32);

begin

       PL/SQL statements...

end [proc];

 

 call proc(1);

 

創建和調用無參函數/存儲過程

create or replace function func()                    

returns int as

$$

declare

  b int;

[declare] c varchar(32);

begin

    PL/SQL statements...

    return 0;

end;

$$  language plpgsql;

 

select func();

create or replace function func

return int  {as | is}

    b int;

    c varchar(32);

begin

    PL/SQL statements...

    return 0;

end;

 

select func() from dual;

create or replace procedure proc

 {as | is}

    b int;

    c varchar(32);

begin

       PL/SQL statements...

end [proc];

 

 

 call proc();

Oracle數據庫的無參函數在定義時函數名后面沒有括號,在調用時,函數名后面有括號。

創建和調用返回集合的函數(示例)

create type tp_id_name as (id int,name varchar(128))

 

create or replace function func_get_id_name()                   

returns setof tp_id_name as

$$

begin

  return query execute 'select id, name from man'; 

end;

$$  language plpgsql;

 

select * from func_get_id_name();

create type tp_id_name as object(id int,name varchar(128));

create type table_tp_id_name as table of tp_id_name;

 

CREATE OR replace function funcunc_get_id_name RETURN table_tp_id_name

as

  v_table_tp_id_name table_tp_id_name;

    sql_text VARCHAR(1000);

BEGIN

    sql_text:='select tp_id_name(id,name) from man';

    execute immediate sql_text bulk collect into v_table_tp_id_name;

 return v_table_tp_id_name;

end func_get_id_name;

 

select * from table(func_get_id_name());

 

需要定義一個類型,表示集合中的元組

刪除函數/存儲過程

drop function f(a int);

drop function f;

drop procedure f;

 

觸發器

create or replace function trigger_function()                    

returns trigger as

$$

declare ...;

begin

    PL/SQL statements...

end;

$$  language plpgsql;

 

create [or replace] tigger trigger_name {before| after | instead of } event

on table_name

[for each row] execute trigger_function() ;

create [or replace] tigger trigger_name {before| after | instead of } event

on table_name

[for each row]

begin

   PL/SQL statements...

end;

 在postgresql中,觸發器執行的語句需要被定義為一個函數。而在oracle中,觸發器執行的語句位於觸發器的定義內部。

 

3.7 分區表

3.7.1 分區字段是以毫秒為單位的utc時間

這里以事件表 tb_event 為例,我們是這樣設計它的的分區方案的:

1. 這個表是根據 event_time_utc (事件發生UTC時間,以毫秒為單位),以月為間隔來分區的。從主表創建之日起,每個月為這張表創建一張分區,用來存放對應月份的數據。

2. 分區表有默認的分區,存放的是有獨立月分區的數據以外的數據。在PostgreSQL 11中,我們創建一個默認分區tb_event_default,在Oracle中,創建默認分區  tb_event_default_future,存放有獨立分區的最新的那個月以后的數據。

3. 在程序啟動和每個月即將結束時,創建存放這個月和下一(幾)個月的數據的分區表(如果他們不存在),並將默認分區中屬於這些月份的數據遷移至對應的分區表中,最后修改默認分區的范圍。

 

我們單獨講解與分區表相關的一些sql語句的改造:

 

  1. 創建分區表

下面是創建分區表的語法差異。可以看出,在Oracle中創建分區表時,必須至少創建一個分區。而在PostgreSQL中創建分區表時,則不能創建分區。

 

PostgreSQL 11

Oracle 19c

create table tb_event

(

    event_id varchar(36) not null,

    event_type varchar(32),

    event_time timestamp,

    event_time_difference varchar(32) not null,

    event_time_utc bigint not null

) partition by range (event_time_utc);

create table tb_event

(

    event_id varchar(36) not null,

    event_type varchar(32),

    event_time timestamp,

    event_time_difference varchar(32) not null,

    event_time_utc number(20) not null,

    constraint pk_tb_event primary key (event_id)

) partition by range (event_time_utc)

(

    partition tb_event_default_future values less than (maxvalue)

);

 

  1. 創建主鍵,唯一鍵.

PostgreSQL要求全局主鍵、唯一鍵必須包含分區字段。如果你執行這樣的sql語句:

create table tb_event

(

        event_id varchar(36) not null,

        event_type varchar(32),

        event_time timestamp,

        event_time_difference varchar(32) not null,

      event_time_utc bigint not null,

      constraint pk_tb_event primary key (event_id)

) partition by range (event_time_utc);

 

則會報錯:

ERROR:  insufficient columns in PRIMARY KEY constraint definition

DETAIL:  PRIMARY KEY constraint on table "tb_event" lacks column "event_time_utc" which is part of the partition key.

 

對於PostgreSQL,你可以為每個分區創建局部主鍵或局部唯一鍵,局部主鍵列可以不包含分區字段。例如:

alter table tb_event_201909 add constraint pk_tb_event_201909 primary key (event_id) ;

 

而Oracle則支持不包含分區字段的全局主鍵:

 

create table tb_event

(

             event_id varchar(36) not null,

            event_type varchar(32),

            event_time timestamp,

           event_time_difference varchar(32) not null,

           event_time_utc number(20) not null,

           constraint pk_tb_event primary key (event_id)

) partition by range (event_time_utc)

(

         partition tb_event_default_future values less than (maxvalue)

);

 

  1. 為下一個月的數據創建分區,並調整默認分區的范圍

這里以2019年9月為例,下面是在PostgreSQL和Oracle中的方法。

 

PostgreSQL 11

Oracle 19c

alter table tb_event detach PARTITION  tb_event_default;

 

create table tb_event_201909 partition of tb_event for values from (1567267200000) to (1569859200000);

 

insert into tb_event select * from tb_event_default where event_time_utc >= 1567267200000 and event_time_utc <1569859200000;

 

delete from tb_event_default where event_time_utc >= 1567267200000 and event_time_utc < 1569859200000;

 

alter table tb_event attach partition tb_event_default default;

方法是拆分分區后重建索引

 

ALTER TABLE tb_event

  SPLIT PARTITION tb_event_default_future AT (1569859200000)

  INTO (PARTITION tb_event_201909,

        PARTITION tb_event_default_future);

 

begin

  for c1 in (select t.index_name, t.partitioned from user_indexes t where table_name = upper('tb_event'))

  loop

    if c1.partitioned='NO' then

      -- rebuild global index directly

      execute immediate 'alter index ' || c1.index_name || ' rebuild';

    else

      -- rebuild every unusable partition for partitioned index

      for c2 in (select partition_name from user_ind_partitions where index_name=c1.index_name and status = 'UNUSABLE')

      loop

        execute immediate 'alter index ' || c1.index_name || ' rebuild partition ' || c2.partition_name;

      end loop;

    end if;

  end loop;

end;

 

 

  1. 刪除2019年8月的分區,並調整默認分區的范圍

PostgreSQL 11

Oracle 19c

drop table drop table tb_event_201908;

 

--默認分區范圍會自動調整

刪除分區后重建全局索引

 

alter table tb_event drop partition tb_event_201908;

 

begin

  for c1 in (select t.index_name, t.partitioned from user_indexes t where table_name = upper('tb_event'))

  loop

    if c1.partitioned='NO' then

      -- rebuild global index directly

      execute immediate 'alter index ' || c1.index_name || ' rebuild';

    else

      -- rebuild every unusable partition for partitioned index

      for c2 in (select partition_name from user_ind_partitions where index_name=c1.index_name and status = 'UNUSABLE')

      loop

        execute immediate 'alter index ' || c1.index_name || ' rebuild partition ' || c2.partition_name;

      end loop;

    end if;

  end loop;

end;

 

 

 

  1. 分區表上創建索引

PostgreSQL 11

Oracle 19c

create index idx_tb_event_event_type on tb_event(event_type);

第一種:

create index idx_tb_event_event_type on tb_event (event_type) local;

 (局部索引,容易維護,推薦)

 

第二種:

create index idx_tb_event_event_type on tb_event (event_type) global;

 

 (全局索引,不移維護,不推薦)

 

 

  1. 創建和刪除分區表的存儲過程編寫。

 

針對tb_event,我們編寫創建分區的存儲過程如下:

存儲過程的三個參數的含義如下:

p_partition_unit:分區的單位。取值范圍是month,day和quarter

p_partiton_cnt: 希望創建的分區的數量

p_start_utc_time: 第一個新建分區的對應時間范圍內的任意時刻,以毫秒為單位。

 

/*

-- procedure: Create partitions for table tb_event

 

-- parameters:

-- p_partition_unit: Partition unit. Values: month, day, quarter

-- p_partiton_cnt: Count of partitions to be created when this procedure is called. The default value is 6.

-- p_start_utc_time: One UTC timestamp value that is in the range of the first partition's partition-key, in milliseconds. The default value is now.

 

*/

 

 

create or replace procedure proc_create_partition_for_tb_event(p_partition_unit varchar default 'month', p_partiton_cnt number default 6,

p_start_utc_time number default (CAST(SYS_EXTRACT_UTC(current_timestamp) AS date) - to_date('1970-01-01', 'YYYY-MM-DD'))*86400*1000)

as

    v_start_time timestamp;  -- 要新建的第一個分區的開始時間

    v_partitioned_tbname varchar(64);

    v_partition_name varchar(64);

    v_partition_to_split varchar(64);

    v_time_format varchar(64);

    v_start_time_of_this timestamp;

    v_end_time_of_this timestamp;

    v_high_value_of_this number(20);  --當前新建分區的分區字段的上限值(不含)

    v_high_value_text varchar(64);

begin

    v_partitioned_tbname := 'tb_event';

 

    v_time_format := (case p_partition_unit

        when 'month' then 'YYYYMM'

        when 'day' then 'YYYYMMDD'

        when 'quarter' then 'YYYY"q"Q'

        end);

 

    -- 倫敦時間的字面值再加上 時區偏移量

    SELECT trunc(to_timestamp('1970-01-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS')  + (p_start_utc_time/1000/86400) + TO_NUMBER(SUBSTR(TZ_OFFSET(sessiontimezone),1,3))/24, 'month') INTO v_start_time FROM dual;

 

    if p_partition_unit = 'month' then

        v_start_time_of_this := v_start_time;

            v_end_time_of_this := v_start_time_of_this + interval '1' month;

    elsif p_partition_unit = 'day' then

        v_start_time_of_this := v_start_time;

            v_end_time_of_this := v_start_time_of_this + interval '1' day;

    elsif p_partition_unit = 'quarter' then

        v_start_time_of_this := v_start_time;

            v_end_time_of_this := v_start_time_of_this + interval '3' month;

    end if;

 

    --當前新建分區的分區字段的上限值(不含)

    v_high_value_of_this := (CAST(SYS_EXTRACT_UTC(v_end_time_of_this) as date) - to_date('1970-01-01', 'YYYY-MM-DD'))*86400*1000;

    v_partition_name := v_partitioned_tbname || '_' || to_char(v_start_time_of_this, v_time_format);

 

    if p_partition_unit in ('month', 'day', 'quarter') then

        for i in 0..(p_partiton_cnt - 1) loop

            for c in (select table_name, HIGH_VALUE, partition_name from USER_TAB_PARTITIONS where table_name = upper(v_partitioned_tbname) order by partition_position) loop

                v_high_value_text := c.HIGH_VALUE; -- HIGH_VALUE is of type 'Long', and can only be converted into varchar in this way

 

                -- if this partition to be create already exists

                if v_high_value_text = cast(v_high_value_of_this as varchar) then

                    exit;

                -- if this partition does not exist and its partition-key's range is included by another partition, we need to split that parition

                elsif (v_high_value_text = 'MAXVALUE' or cast(v_high_value_text as number) > v_high_value_of_this)

                then

                    v_partition_to_split := c.partition_name;

                    execute immediate 'alter table ' || v_partitioned_tbname || ' split PARTITION ' || v_partition_to_split || ' AT (' || v_high_value_of_this || ') INTO (PARTITION ' || v_partition_name || ', PARTITION ' || v_partition_to_split || ')';

                    exit;

                end if;

            end loop;

 

            if p_partition_unit = 'month' then

                v_start_time_of_this := v_start_time_of_this + interval '1' month;

                v_end_time_of_this := v_end_time_of_this + interval '1' month;

            elsif p_partition_unit = 'day' then

                v_start_time_of_this := v_start_time_of_this + interval '1' day;

                v_end_time_of_this := v_end_time_of_this + interval '1' day;

            elsif p_partition_unit = 'quarter' then

                v_start_time_of_this := v_start_time_of_this + interval '3' month;

                v_end_time_of_this := v_end_time_of_this + interval '3' month;

            end if;

 

            v_high_value_of_this := (CAST(SYS_EXTRACT_UTC(v_end_time_of_this) as date) - to_date('1970-01-01', 'YYYY-MM-DD'))*86400*1000;

            v_partition_name := v_partitioned_tbname || '_' || to_char(v_start_time_of_this, v_time_format);

        end loop;

    end if;

 

    for c1 in (select t.index_name, t.partitioned from user_indexes t where table_name = upper(v_partitioned_tbname))

    loop

        if c1.partitioned = 'NO' then

            -- rebuild global index directly

            execute immediate 'alter index ' || c1.index_name || ' rebuild';

        else

            -- rebuild every unusable partition for partitioned index

            for c2 in (select partition_name from user_ind_partitions where index_name=c1.index_name and status = 'UNUSABLE')

            loop

                execute immediate 'alter index ' || c1.index_name || ' rebuild partition ' || c2.partition_name;

            end loop;

        end if;

    end loop;

end;

 

調用時,執行命令:

 call proc_create_partition_for_tb_crosshistory('month',6);

即可為從本月開始的6個月創建分區。

 

為tb_event刪除分區的存儲過程如下:

存儲過程的兩個函數含義如下:

p_partition_unit:分區的單位。取值范圍是month,day和quarter。

p_keep_days: 數據保留的天數。超過這么多天以前的數據會被刪除;

 

 

create or replace procedure proc_clean_partition_for_tb_event(p_partition_unit varchar default 'month', p_keep_days number default 90)  as

    v_partitioned_tbname varchar(64);

    v_sql varchar(64);

    v_clean_time timestamp;

    v_clean_utctime number;

    v_partiton_colname varchar(64);

    v_time_format varchar(64);

    v_high_value_text varchar(64);

begin

 

    v_sql := 'select current_timestamp - interval ''' || p_keep_days ||  ''' day from dual';

    execute IMMEDIATE v_sql into v_clean_time;

    v_clean_utctime := (CAST(SYS_EXTRACT_UTC(v_clean_time) AS date) - to_date('1970-01-01', 'YYYY-MM-DD'))*86400*1000;

 

    v_partitioned_tbname := 'tb_event';

    v_partiton_colname := 'event_time_utc';

 

    v_time_format := (case p_partition_unit

        when 'month' then 'YYYYMM'

        when 'day' then 'YYYYMMDD'

        when 'quarter' then 'YYYY"q"Q'

        end);

 

    if p_partition_unit in ('month', 'day', 'quarter') then

        for c in (select table_name, HIGH_VALUE, partition_name from USER_TAB_PARTITIONS where table_name = upper(v_partitioned_tbname) order by partition_position) loop

            v_high_value_text := c.HIGH_VALUE; -- HIGH_VALUE is of type 'Long', and can only be converted into varchar in this way

            if v_high_value_text <> 'MAXVALUE' AND CAST(v_high_value_text AS number) <= v_clean_utctime then

                execute immediate 'alter table ' || v_partitioned_tbname || ' drop partition ' || c.partition_name;

            end if;

        end loop;

    end if;

    execute immediate 'delete from ' || v_partitioned_tbname || ' where  ' || v_partiton_colname || ' < ' || v_clean_utctime;

 

    for c1 in (select t.index_name, t.partitioned from user_indexes t where table_name = upper(v_partitioned_tbname))

    loop

        if c1.partitioned = 'NO' then

            -- rebuild global index directly

            execute immediate 'alter index ' || c1.index_name || ' rebuild';

        else

            -- rebuild every unusable partition for partitioned index

            for c2 in (select partition_name from user_ind_partitions where index_name=c1.index_name and status = 'UNUSABLE')

            loop

                execute immediate 'alter index ' || c1.index_name || ' rebuild partition ' || c2.partition_name;

            end loop;

        end if;

    end loop;

end;

 

調用時,執行命令:

 call proc_clean_partition_for_tb_event ('month',90);

即可刪除90天以前的數據。

3.7.2 分區字段是 timestamp 類型的

我們以tb_record為例。

1. 這個表是根據 event_time,以月為間隔來分區的。從主表創建之日起,每個月為這張表創建一張分區,用來存放對應月份的數據。

2. 分區表有默認的分區,存放的是有獨立月分區的數據以外的數據。在PostgreSQL 11中,我們創建一個默認分區tb_record_default,在Oracle中,創建默認分區  tb_record_default_future,存放有獨立分區的最新的那個月以后的數據。

3. 在程序啟動和每個月即將結束時,創建存放這個月和下一(幾)個月的數據的分區表(如果他們不存在),並將默認分區中屬於這些月份的數據遷移至對應的分區表中,最后修改默認分區的范圍。

 

針對tb_record,我們編寫創建分區的存儲過程如下:

  1. 創建分區的存儲過程

存儲過程的三個參數的含義如下:

p_partition_unit:分區的單位。 取值范圍是month,day和quarter。

p_partiton_cnt: 希望創建的分區的數量

p_start_time: 第一個新建分區的對應時間范圍內的任意時刻。

 

/*

-- procedure: Create partitions for table tb_record

 

-- parameters:

-- p_partition_method: Partition unit. Values: month, day, quarter

-- p_partiton_cnt: Count of partitions to be created when this funciton is called. The default value is 7.

-- p_start_time: One timestamp value that is in the range of the first partition's partition-key. The default value is now.

 

*/

 

create or replace procedure proc_create_partition_for_tb_record(p_partition_unit varchar default 'month', p_partiton_cnt number default 6,

p_start_time timestamp with time zone default current_timestamp)

as

    v_start_time timestamp;  -- 要新建的第一個分區的開始時間

    v_partitioned_tbname varchar(64);

    v_partition_name varchar(64);

    v_partition_to_split varchar(64);

    v_time_format varchar(64);

    v_start_time_of_this timestamp;

    v_end_time_of_this timestamp;

    v_high_value_of_this timestamp;  --當前新建分區的分區字段的上限值(不含)

    v_high_value_text varchar(64);

begin

    v_partitioned_tbname := 'tb_record';

 

    v_time_format := (case p_partition_unit

        when 'month' then 'YYYYMM'

        when 'day' then 'YYYYMMDD'

        when 'quarter' then 'YYYY"q"Q'

        end);

 

    SELECT trunc(p_start_time, 'month') INTO v_start_time FROM dual;

 

    if p_partition_unit = 'month' then

        v_start_time_of_this := v_start_time;

            v_end_time_of_this := v_start_time_of_this + interval '1' month;

    elsif p_partition_unit = 'day' then

        v_start_time_of_this := v_start_time;

            v_end_time_of_this := v_start_time_of_this + interval '1' day;

    elsif p_partition_unit = 'quarter' then

        v_start_time_of_this := v_start_time;

            v_end_time_of_this := v_start_time_of_this + interval '3' month;

    end if;

 

    --當前新建分區的分區字段的上限值(不含)

    v_high_value_of_this := v_end_time_of_this;

    v_partition_name := v_partitioned_tbname || '_' || to_char(v_start_time_of_this, v_time_format);

 

    if p_partition_unit in ('month', 'day', 'quarter') then

        for i in 0..(p_partiton_cnt - 1) loop

            for c in (select table_name, HIGH_VALUE, partition_name from USER_TAB_PARTITIONS where table_name = upper(v_partitioned_tbname) order by partition_position) loop

                v_high_value_text := REPLACE(replace(c.HIGH_VALUE, 'TIMESTAMP'' ', ''), ''''); -- HIGH_VALUE is of type 'Long', and can only be converted into varchar in this way

 

                -- if this partition to be create already exists

                if v_high_value_text = to_char(v_high_value_of_this, 'YYYY-MM-DD HH24:MI:SS') THEN

                    exit;

                -- if this partition does not exist and its partition-key's range is included by another partition, we need to split that parition

                elsif (v_high_value_text = 'MAXVALUE' or v_high_value_text > to_char(v_high_value_of_this, 'YYYY-MM-DD HH24:MI:SS'))

                then

                    v_partition_to_split := c.partition_name;

                    execute immediate 'alter table ' || v_partitioned_tbname || ' split PARTITION ' || v_partition_to_split || ' AT ( to_timestamp(''' || to_char(v_high_value_of_this, 'YYYY-MM-DD HH24:MI:SS') || ''', ''YYYY-MM-DD HH24:MI:SS'')) INTO (PARTITION ' || v_partition_name || ', PARTITION ' || v_partition_to_split || ')';

                    exit;

                end if;

            end loop;

 

            if p_partition_unit = 'month' then

                v_start_time_of_this := v_start_time_of_this + interval '1' month;

                v_end_time_of_this := v_end_time_of_this + interval '1' month;

            elsif p_partition_unit = 'day' then

                v_start_time_of_this := v_start_time_of_this + interval '1' day;

                v_end_time_of_this := v_end_time_of_this + interval '1' day;

            elsif p_partition_unit = 'quarter' then

                v_start_time_of_this := v_start_time_of_this + interval '3' month;

                v_end_time_of_this := v_end_time_of_this + interval '3' month;

            end if;

            v_high_value_of_this := v_end_time_of_this;

            v_partition_name := v_partitioned_tbname || '_' || to_char(v_start_time_of_this, v_time_format);

        end loop;

    end if;

 

    for c1 in (select t.index_name, t.partitioned from user_indexes t where table_name = upper(v_partitioned_tbname))

    loop

        if c1.partitioned = 'NO' then

            -- rebuild global index directly

            execute immediate 'alter index ' || c1.index_name || ' rebuild';

        else

            -- rebuild every unusable partition for partitioned index

            for c2 in (select partition_name from user_ind_partitions where index_name=c1.index_name and status = 'UNUSABLE')

            loop

                execute immediate 'alter index ' || c1.index_name || ' rebuild partition ' || c2.partition_name;

            end loop;

        end if;

    end loop;

end;

 

 

調用時,執行命令:

   call proc_create_partition_for_tb_record('month');

即可為從本月開始的6個月創建分區。

 

 

  1. 刪除分區的存儲過程

為tb_record刪除分區的存儲過程如下:

存儲過程的兩個函數含義如下:

p_partition_unit:分區的單位。取值范圍是month,day和quarter

p_keep_days: 數據保留的天數。超過這么多天以前的數據會被刪除;

 

/*

-- procedure: Clean old data for tb_record

 

-- parameters:

-- p_partition_unit: Partition unit. Values: month, day, quarter

-- p_keep_days: Duration of data retention, in days. The default value is 90.

 

*/

 

create or replace procedure proc_clean_partition_for_tb_record(p_partition_unit varchar default 'month', p_keep_days number default 90)  as

    v_partitioned_tbname varchar(64);

    v_sql varchar(64);

    v_clean_time timestamp;

    v_partiton_colname varchar(64);

    v_time_format varchar(64);

    v_high_value_text varchar(64);

begin

 

    v_sql := 'select current_timestamp - interval ''' || p_keep_days ||  ''' day from dual';

    execute immediate v_sql into v_clean_time;

 

    v_partitioned_tbname := 'tb_record';

    v_partiton_colname := 'event_time';

 

    v_time_format := (case p_partition_unit

        when 'month' then 'YYYYMM'

        when 'day' then 'YYYYMMDD'

        when 'quarter' then 'YYYY"q"Q'

        end);

 

    if p_partition_unit in ('month', 'day', 'quarter') then

        for c in (select table_name, HIGH_VALUE, partition_name from USER_TAB_PARTITIONS where table_name = upper(v_partitioned_tbname) order by partition_position) loop

            v_high_value_text := replace(replace(c.HIGH_VALUE, 'TIMESTAMP'' ', ''), ''''); -- HIGH_VALUE is of type 'Long', and can only be converted into varchar in this way

            if  v_high_value_text <> 'MAXVALUE' AND v_high_value_text <= to_char(v_clean_time, 'YYYY-MM-DD HH24:MI:SS') then

                execute immediate 'alter table ' || v_partitioned_tbname || ' drop partition ' || c.partition_name;

            end if;

        end loop;

    end if;

    execute immediate 'delete from ' || v_partitioned_tbname || ' where  ' || v_partiton_colname || ' < to_timestamp(''' || to_char(v_clean_time, 'YYYY-MM-DD HH24:MI:SS') || ''', ''YYYY-MM-DD HH24:MI:SS'')';

 

    for c1 in (select t.index_name, t.partitioned from user_indexes t where table_name = upper(v_partitioned_tbname))

    loop

        if c1.partitioned = 'NO' then

            -- rebuild global index directly

            execute immediate 'alter index ' || c1.index_name || ' rebuild';

        else

            -- rebuild every unusable partition for partitioned index

            for c2 in (select partition_name from user_ind_partitions where index_name=c1.index_name and status = 'UNUSABLE')

            loop

                execute immediate 'alter index ' || c1.index_name || ' rebuild partition ' || c2.partition_name;

            end loop;

        end if;

    end loop;

end;

 

調用時,執行命令:

 call proc_clean_partition_for_tb_record('month',90);

即可刪除90天以前的數據。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM