数据库从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天以前的数据。

posted @ 2020-08-13 10:43  草色青青送马蹄  阅读(2303)  评论(0编辑  收藏  举报