前言:近日,公司的一套使用 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語句的改造:
- 創建分區表
下面是創建分區表的語法差異。可以看出,在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) ); |
- 創建主鍵,唯一鍵.
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)
);
- 為下一個月的數據創建分區,並調整默認分區的范圍
這里以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; |
- 刪除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;
|
- 分區表上創建索引
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;
(全局索引,不移維護,不推薦) |
- 創建和刪除分區表的存儲過程編寫。
針對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,我們編寫創建分區的存儲過程如下:
- 創建分區的存儲過程
存儲過程的三個參數的含義如下:
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個月創建分區。
- 刪除分區的存儲過程
為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天以前的數據。