Oracle表在线重定义操作总结
一、DBMS_REDEFINITION(在线重定义)概述
原理:Oracle 9i引入的内置包,本质上是基于物化视图机制实现的预建快照+增量同步。通过创建一张新的分区表(影子表),在用户持续读写源表的同时,将数据同步至影子表,最后通过原子切换完成转换。
1、DBMS_REDEFINITION包:
- ABSORT_REDEF_TABLE:清理重定义的错误和中止重定义;
- CAN_REDEF_TABLE:检查表是否可以进行重定义,存储过程执行成功代表可以进行重定义;
- COPY_TABLE_DEPENDENTS:同步索引和依赖的对象(包括索引、约束、触发器、权限等);
- FINISH_REDEF_TABLE:完成在线重定义;
- REGISTER_DEPENDENTS_OBJECTS:注册依赖的对象,如索引、约束、触发器等;
- START_REDEF_TABLE:开始在线重定义;
- SYNC_INTERIM_TABLE:增量同步数据;
- UNREGISTER_DEPENDENT_OBJECT:不注册依赖的对象,如索引、约束、触发器等;
2、相关视图
可以通过以下视图查看在线重定义表的状态
DBA_REDEFINITION_ERRORS
DBA_REDEFINITION_OBJECTS
DBA_REDEFINITION_STATUS
3、注意事项
优点:
真正的在线操作,业务几乎无感知
支持回退(ABORT_REDEF_TABLE)
可多次 SYNC_INTERIM_TABLE 控制最终切换时间
支持列映射、列转换等灵活操作
缺点:
需要额外 1 倍表大小的表空间
源表必须有主键或 ROWID 可用
主键列不能被修改
在线重定义无法采用nologging
必须在同一个用户下进行在线重定义。
SYS和SYSTEM用户下的表无法进行在线重定义。
不支持含 LONG、BFILE、域索引的表
不支持含物化视图日志的表(需先删除)
操作步骤较多,复杂度高
重定义期间禁止对源表执行 FLASHBACK TABLE 或 FLASHBACK QUERY
完成后必须手动收集统计信息
表列的默认值,需要提前手工创建
二、具体操作案例
本案例将一个普通表通过在线重定义方式转换为一个分区表
1、创建案例用户和授权(执行DBMS_REDEFINITION在线重定义需要的权限)
--创建用户
create user user1 identified by user1;
--基本授权
grant connect,resource to user1;
grant unlimited tablespace to user1;
--执行DBMS_REDEFINITION在线重定义需要的权限
GRANT EXECUTE ON DBMS_REDEFINITION TO user1;
GRANT ALTER ANY TABLE TO user1;
GRANT DROP ANY TABLE TO user1;
GRANT LOCK ANY TABLE TO user1;
GRANT CREATE ANY TABLE TO user1;
GRANT SELECT ANY TABLE TO user1;
GRANT CREATE ANY TRIGGER TO user1;
GRANT CREATE ANY INDEX TO user1;
2、准备表和测试数据
conn user1/user1
--创建表
create table BIG_TABLE (
id number(10),
created_date date default sysdate,
lookup_id number(10),
data varchar2(50)
);
--准备测试数据
--插入 1000 条模拟数据(时间分布在 2023~2026 年)
INSERT INTO BIG_TABLE (id, created_date, lookup_id, data)
SELECT
ROWNUM+2000,
TO_DATE('2023-01-01', 'YYYY-MM-DD') + DBMS_RANDOM.VALUE(0, 1460),
ROUND(DBMS_RANDOM.VALUE(1, 100)),
DBMS_RANDOM.STRING('X', ROUND(DBMS_RANDOM.VALUE(5, 50)))
FROM DUAL
CONNECT BY LEVEL <= 1000;
commit;
-- 验证数据分布
SELECT
TO_CHAR(created_date, 'YYYY') AS year,
COUNT(*) AS cnt
FROM BIG_TABLE
GROUP BY TO_CHAR(created_date, 'YYYY')
ORDER BY year;
3、创建表上的相关依赖对象(主要用于测试)
--授权
grant select,insert,update,delete on big_table to scott;
-- 创建索引
create index bita_created_date_i on BIG_TABLE(created_date);
create index bita_look_fk_i on BIG_TABLE(lookup_id);
-- 约束1:主键约束
ALTER TABLE BIG_TABLE
ADD CONSTRAINT pk_big_table PRIMARY KEY (id);
-- 约束2:CHECK 约束,限制 lookup_id 范围
ALTER TABLE BIG_TABLE
ADD CONSTRAINT chk_lookup_id CHECK (lookup_id BETWEEN 1 AND 100);
-- 约束3:NOT NULL 约束
ALTER TABLE BIG_TABLE
MODIFY data CONSTRAINT nn_data NOT NULL;
-- 触发器1:插入时自动生成 ID(序列 + 触发器)
CREATE SEQUENCE seq_big_table START WITH 1001 INCREMENT BY 1;
CREATE OR REPLACE TRIGGER trg_big_table_bi
BEFORE INSERT ON BIG_TABLE
FOR EACH ROW
WHEN (NEW.id IS NULL)
BEGIN
SELECT seq_big_table.NEXTVAL INTO :NEW.id FROM DUAL;
END;
/
-- 触发器2:更新时自动记录日志到日志表
CREATE TABLE big_table_audit (
operation VARCHAR2(10),
old_id NUMBER(10),
new_id NUMBER(10),
old_data VARCHAR2(50),
new_data VARCHAR2(50),
changed_by VARCHAR2(30),
changed_at DATE DEFAULT SYSDATE
);
CREATE OR REPLACE TRIGGER trg_big_table_audit
AFTER UPDATE OR DELETE ON BIG_TABLE
FOR EACH ROW
BEGIN
IF UPDATING THEN
INSERT INTO big_table_audit (operation, old_id, new_id, old_data, new_data, changed_by)
VALUES ('UPDATE', :OLD.id, :NEW.id, :OLD.data, :NEW.data, USER);
ELSIF DELETING THEN
INSERT INTO big_table_audit (operation, old_id, new_id, old_data, new_data, changed_by)
VALUES ('DELETE', :OLD.id, NULL, :OLD.data, NULL, USER);
END IF;
END;
/
建立 3 个视图
-- 视图1:按年份汇总统计
CREATE OR REPLACE VIEW v_big_table_yearly AS
SELECT
TO_CHAR(created_date, 'YYYY') AS year,
COUNT(*) AS total_rows,
MIN(created_date) AS first_date,
MAX(created_date) AS last_date,
AVG(lookup_id) AS avg_lookup_id
FROM BIG_TABLE
GROUP BY TO_CHAR(created_date, 'YYYY');
-- 视图2:按 lookup_id 分组统计
CREATE OR REPLACE VIEW v_big_table_lookup AS
SELECT
lookup_id,
COUNT(*) AS cnt,
MIN(data) AS sample_data,
MAX(created_date) AS latest_date
FROM BIG_TABLE
GROUP BY lookup_id;
-- 视图3:审计日志视图(关联原表)
CREATE OR REPLACE VIEW v_big_table_audit AS
SELECT
a.operation,
a.old_id,
a.new_id,
a.old_data,
a.new_data,
a.changed_by,
a.changed_at
FROM big_table_audit a
ORDER BY a.changed_at DESC;
-- 创建存储过程
CREATE OR REPLACE PROCEDURE sp_insert_big_table(
p_count IN NUMBER,
p_year_start IN NUMBER DEFAULT 2023,
p_year_end IN NUMBER DEFAULT 2026
)
AS
v_date_start DATE := TO_DATE(p_year_start || '-01-01', 'YYYY-MM-DD');
v_date_end DATE := TO_DATE(p_year_end || '-12-31', 'YYYY-MM-DD');
v_days NUMBER := v_date_end - v_date_start;
BEGIN
FOR i IN 1 .. p_count LOOP
INSERT INTO BIG_TABLE (id, created_date, lookup_id, data)
VALUES (
NULL,
v_date_start + DBMS_RANDOM.VALUE(0, v_days),
ROUND(DBMS_RANDOM.VALUE(1, 100)),
DBMS_RANDOM.STRING('X', ROUND(DBMS_RANDOM.VALUE(5, 50)))
);
END LOOP;
COMMIT;
DBMS_OUTPUT.PUT_LINE('成功插入 ' || p_count);
END sp_insert_big_table;
/
-- 函数:根据 ID 查询 data 字段
CREATE OR REPLACE FUNCTION fn_get_big_table_data(
p_id IN BIG_TABLE.id%TYPE
)
RETURN BIG_TABLE.data%TYPE
AS
v_data BIG_TABLE.data%TYPE;
BEGIN
SELECT data INTO v_data
FROM BIG_TABLE
WHERE id = p_id;
RETURN v_data;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN NULL;
END fn_get_big_table_data;
/
--创建package包
-- 包头
CREATE OR REPLACE PACKAGE pkg_big_table AS
-- 按年份查询数据
FUNCTION get_by_year(p_year IN NUMBER) RETURN SYS_REFCURSOR;
-- 按 ID 删除数据
PROCEDURE delete_by_id(p_id IN NUMBER);
-- 统计总行数
FUNCTION get_count RETURN NUMBER;
END pkg_big_table;
/
-- 包体
CREATE OR REPLACE PACKAGE BODY pkg_big_table AS
FUNCTION get_by_year(p_year IN NUMBER) RETURN SYS_REFCURSOR
AS
v_cur SYS_REFCURSOR;
BEGIN
OPEN v_cur FOR
SELECT id, created_date, lookup_id, data
FROM BIG_TABLE
WHERE TO_CHAR(created_date, 'YYYY') = TO_CHAR(p_year)
ORDER BY created_date;
RETURN v_cur;
END get_by_year;
PROCEDURE delete_by_id(p_id IN NUMBER)
AS
BEGIN
DELETE FROM BIG_TABLE WHERE id = p_id;
IF SQL%ROWCOUNT = 0 THEN
RAISE_APPLICATION_ERROR(-20001, '未找到 ID = ' || p_id);
END IF;
COMMIT;
END delete_by_id;
FUNCTION get_count RETURN NUMBER
AS
v_cnt NUMBER;
BEGIN
SELECT COUNT(*) INTO v_cnt FROM BIG_TABLE;
RETURN v_cnt;
END get_count;
END pkg_big_table;
/
set linesize 200 pagesize 999
col constraint_name format a20
col constraint_type format a15
col triggering_event format a20
col trigger_name format a20
col view_name format a20
col object_name format a20
col object_type format a20
-- 验证数据量
SELECT COUNT(*) FROM BIG_TABLE;
-- 验证约束
SELECT constraint_name, constraint_type, status
FROM user_constraints
WHERE table_name = 'BIG_TABLE';
-- 验证触发器
SELECT trigger_name, triggering_event, status
FROM user_triggers
WHERE table_name IN ('BIG_TABLE', 'BIG_TABLE_AUDIT');
-- 验证视图
SELECT view_name FROM user_views WHERE view_name LIKE 'V_BIG_TABLE%';
SELECT object_name, object_type, status FROM user_objects WHERE object_name LIKE 'V_BIG_TABLE%';
-- 验证存储过程
SELECT object_name, object_type, status
FROM user_objects
WHERE object_name IN ('SP_INSERT_BIG_TABLE', 'FN_GET_BIG_TABLE_DATA','PKG_BIG_TABLE');
-- 验证包
set serveroutput on
SELECT pkg_big_table.get_count FROM DUAL;
GET_COUNT
----------
1000
4、创建中间表(创建一个包含目标新结构的临时表,注意表列默认值,该案例为分区表)
create table BIG_TABLE2 (
id number(10),
created_date date default sysdate,
lookup_id number(10),
data varchar2(50) )
partition by range (created_date)
(
partition BIG_TABLE_2023 values less than (to_date('01/01/2024', 'dd/mm/yyyy')),
partition BIG_TABLE_2024 values less than (to_date('01/01/2025', 'dd/mm/yyyy')),
partition BIG_TABLE_2025 values less than (maxvalue)
);
5、检查源表是否具备迁移条件
exec dbms_redefinition.can_redef_table('user1', 'BIG_TABLE');
---检查源表是否具备迁移条件
--如果没有主键,报错如下
SQL> exec dbms_redefinition.can_redef_table('user1', 'BIG_TABLE');
BEGIN dbms_redefinition.can_redef_table('user1', 'BIG_TABLE'); END;
*
ERROR at line 1:
ORA-12089: cannot online redefine table "USER1"."BIG_TABLE" with no primary key
ORA-06512: at "SYS.DBMS_REDEFINITION", line 143
ORA-06512: at "SYS.DBMS_REDEFINITION", line 1635
ORA-06512: at line 1
6、开启数据迁移(将源表数据和中间表数据进行迁移同步)
--检查没有问题,开启迁移数据
begin
dbms_redefinition.start_redef_table(
uname => 'user1',
orig_table => 'BIG_TABLE',
int_table => 'BIG_TABLE2');
end;
/
7、第一全量迁移后,模拟增量
模拟增量
INSERT INTO BIG_TABLE (id, created_date, lookup_id, data)
SELECT
ROWNUM+2000,
TO_DATE('2023-01-01', 'YYYY-MM-DD') + DBMS_RANDOM.VALUE(0, 1460),
ROUND(DBMS_RANDOM.VALUE(1, 100)),
DBMS_RANDOM.STRING('X', ROUND(DBMS_RANDOM.VALUE(5, 50)))
FROM DUAL
CONNECT BY LEVEL <= 1000;
commit;
8、增量同步
--手动同步数据到新表,在创建索引之前,避免数据差异过大,即使finish_redef_table也会再一次同步数据
--同步增量数据(可多次执行,减少最终切换的停机时间)
begin
dbms_redefinition.sync_interim_table(
uname => 'user1',
orig_table => 'BIG_TABLE',
int_table => 'BIG_TABLE2');
end;
/
验证是否同步增量(对比两个表数据)
9、再次模拟增量
再次模拟增量
INSERT INTO BIG_TABLE (id, created_date, lookup_id, data)
SELECT
ROWNUM+4000,
TO_DATE('2023-01-01', 'YYYY-MM-DD') + DBMS_RANDOM.VALUE(0, 1460),
ROUND(DBMS_RANDOM.VALUE(1, 100)),
DBMS_RANDOM.STRING('X', ROUND(DBMS_RANDOM.VALUE(5, 50)))
FROM DUAL
CONNECT BY LEVEL <= 1000;
commit;
10、依赖对象迁移(自动复制原表的索引、约束、触发器和权限等到中间表)
set serveroutput on
declare
l_errors number;
begin
dbms_redefinition.copy_table_dependents(
uname => 'user1',
orig_table => 'BIG_TABLE',
int_table => 'BIG_TABLE2',
copy_indexes => dbms_redefinition.cons_orig_params,
copy_triggers => true,
copy_constraints => true,
copy_privileges => true,
ignore_errors => false,
num_errors => l_errors,
copy_statistics => false,
copy_mvlog => false);
dbms_output.put_line('Errors=' || l_errors);
end;
/
11、收集统计信息(正式互换前)
EXEC DBMS_STATS.gather_table_stats(USER, 'BIG_TABLE2', cascade => TRUE);
12、完成重定义
此时会在极短时间内(毫秒级)锁定原表,完成最终的数据一致性检查并切换表名,使新表生效
注:若过程中出现错误或需终止操作,必须调用 ABORT_REDEF_TABLE 清理临时日志和表*
--完成重定义(短暂锁表,交换表名)
begin
dbms_redefinition.finish_redef_table(
uname => 'user1',
orig_table => 'BIG_TABLE',
int_table => 'BIG_TABLE2');
end;
/
备注:(终止重定义方法如下)
BEGIN
DBMS_REDEFINITION.ABORT_REDEF_TABLE(
uname => 'USER1',
orig_table => 'BIG_TABLE',
int_table => 'BIG_TABLE2'
);
END;
/
13、最终验证
--验证表结构,是否转为分区表
SELECT partition_name, high_value
FROM user_tab_partitions
WHERE table_name = 'BIG_TABLE'
ORDER BY partition_position;
set linesize 200 pagesize 999
col constraint_name format a20
col constraint_type format a15
col triggering_event format a20
col trigger_name format a20
col view_name format a20
col object_name format a20
col object_type format a20
-- 验证数据量
SELECT COUNT(*) FROM BIG_TABLE;
-- 验证约束
SELECT constraint_name, constraint_type, status
FROM user_constraints
WHERE table_name = 'BIG_TABLE';
-- 验证触发器
SELECT trigger_name, triggering_event, status
FROM user_triggers
WHERE table_name IN ('BIG_TABLE', 'BIG_TABLE_AUDIT');
-- 验证视图
SELECT view_name FROM user_views WHERE view_name LIKE 'V_BIG_TABLE%';
SELECT object_name, object_type, status FROM user_objects WHERE object_name LIKE 'V_BIG_TABLE%';
-- 验证存储过程
SELECT object_name, object_type, status
FROM user_objects
WHERE object_name IN ('SP_INSERT_BIG_TABLE', 'FN_GET_BIG_TABLE_DATA','PKG_BIG_TABLE');
-- 测试存储过程
SET SERVEROUTPUT ON;
EXEC sp_insert_big_table(100);
-- 测试函数
SELECT fn_get_big_table_data(9000) FROM DUAL;
-- 验证包
set serveroutput on
SELECT pkg_big_table.get_count FROM DUAL;