Oracle 在线重定义实战:不停业务,把普通表改造成分区表

Oracle 在线重定义实战:不停业务,把普通表改造成分区表

大家好,我是睿。

最近遇到一个比较典型的表结构改造需求:一张普通业务日志表,数据量已经接近 4000 万行,表段约 34GB,并且里面还有 CLOB 字段。随着数据持续增长,后续查询、归档和空间维护都会越来越麻烦,所以这次要把它改造成按时间字段分区的分区表。

如果直接停业务、建新表、导数据、改名,思路很简单,但生产环境往往不允许这么干。表越大,停机窗口越难谈,风险也越高。

这类场景我更倾向于用 Oracle 自带的 DBMS_REDEFINITION 做在线重定义:先建一张目标结构的中间表,Oracle 在线把数据和增量同步过去,最后在业务低峰期做一次短暂切换。

今天把这次普通表在线转分区表的完整流程整理出来,所有用户名、表名、字段名和业务含义都已经做了脱敏处理,大家可以按自己的环境替换。

一、场景和目标

本次示例对象如下:

-- 原表

APP_USER.BIZ_LOG

-- 在线重定义中间表

APP_USER.BIZ_LOG_TMP

-- 分区键

LOG_TIME

目标很明确:把普通表 APP_USER.BIZ_LOG 在线改造成按 LOG_TIME 做 RANGE 分区的分区表。

本次表的特点:

  1. 表段约 34GB,实际操作时还要考虑 LOB 段、索引段、UNDO、REDO 和归档空间。
  2. 表中包含 CLOB 字段,重定义耗时和日志量都不能按普通小表估算。
  3. 数据主要分布在 2022 年到 2026 年。
  4. 业务希望尽量不停机,只接受最终切换阶段的短暂锁表。

先说结论:在线重定义的命令本身不复杂,真正要花时间的是前期检查。主键有没有、分区键能不能用、空间够不够、依赖对象能不能复制,这些比执行命令更关键。

二、改造前先看表到底有多大

第一步先看表段大小:

select owner,

segment_name,

segment_type,

round(sum(bytes) / 1024 / 1024 / 1024, 2) gb

from dba_segments

where owner = 'APP_USER'

and segment_name = 'BIZ_LOG'

group by owner, segment_name, segment_type;

本次示例结果:

OWNER SEGMENT_NAME SEGMENT_TYPE GB


APP_USER BIZ_LOG TABLE 34.39

这里有个细节一定要注意:这个 SQL 只看到了表段本身。如果表里有 CLOB、BLOB 之类的大字段,还要继续看 LOB 段和 LOB 索引段。

可以通过下面的 SQL 补充确认:

select s.owner,

s.segment_name,

s.segment_type,

round(s.bytes / 1024 / 1024 / 1024, 2) gb

from dba_lobs l

join dba_segments s

on s.owner = l.owner

and s.segment_name in (l.segment_name, l.index_name)

where l.owner = 'APP_USER'

and l.table_name = 'BIZ_LOG'

order by s.segment_type, s.segment_name;

我的习惯是:凡是做在线重定义,不只看原表大小,还要把中间表、索引、LOB、UNDO、REDO 和归档空间一起算进去。否则执行到一半空间爆了,后面处理起来会很被动。

三、确认是否有主键

DBMS_REDEFINITION 常见有两种方式:按主键方式和按 ROWID 方式。

生产环境里,如果原表具备主键条件,我一般优先选择主键方式,也就是 DBMS_REDEFINITION.CONS_USE_PK。

先检查原表有没有主键或唯一约束:

select owner,

table_name,

constraint_name,

constraint_type,

status

from dba_constraints

where owner = 'APP_USER'

and table_name = 'BIZ_LOG'

and constraint_type in ('P', 'U');

本次初始检查没有发现主键和唯一约束。

这种情况下不要马上开干,先看是否有字段能作为主键候选列。比如这张表里有一个业务日志 ID 字段 LOG_ID,先检查是否重复:

select log_id

from APP_USER.BIZ_LOG

group by log_id

having count(*) >= 2;

如果没有返回行,说明 LOG_ID 没有重复值。

再继续检查是否存在空值:

select count(*) total_rows,

count(log_id) not_null_log_id,

count(*) - count(log_id) null_log_id,

count(distinct log_id) distinct_log_id

from APP_USER.BIZ_LOG;

判断标准很简单:

null_log_id = 0

并且

total_rows = distinct_log_id

如果这两个条件都满足,LOG_ID 就具备主键条件。

生产环境中我更建议先建唯一索引,再用这个索引加主键约束:

create unique index APP_USER.UK_BIZ_LOG_LOG_ID

on APP_USER.BIZ_LOG(LOG_ID)

tablespace APP_IDX_TS

online;

alter table APP_USER.BIZ_LOG

add constraint PK_BIZ_LOG

primary key (LOG_ID)

using index APP_USER.UK_BIZ_LOG_LOG_ID;

注意,是否能使用 online、是否会产生明显锁等待,要结合数据库版本、业务并发和对象状态测试确认。生产上不要把这一步当成一个无感操作。

四、检查分区字段是否适合做分区键

这次准备用 LOG_TIME 作为分区键。分区字段一定要先查清楚最小值、最大值和空值情况:

select count(*) total_rows,

min(log_time) min_log_time,

max(log_time) max_log_time,

sum(case when log_time is null then 1 else 0 end) null_log_time

from APP_USER.BIZ_LOG;

本次示例结果:

TOTAL_ROWS : 约 3900 万

MIN_LOG_TIME : 2022-01-01

MAX_LOG_TIME : 2026-07-30

NULL_LOG_TIME : 0

再按年份看一下数据分布:

select nvl(to_char(log_time, 'YYYY'), 'NULL') yyyy,

count(*)

from APP_USER.BIZ_LOG

group by nvl(to_char(log_time, 'YYYY'), 'NULL')

order by yyyy;

示例结果如下:

YYYY COUNT(*)


2022 约 667 万

2023 约 758 万

2024 约 848 万

2025 约 975 万

2026 约 700 万

这里可以得出几个判断:

  1. LOG_TIME 没有空值,可以直接按时间范围分区。
  2. 历史数据从 2022 年开始,第一个分区从 P2022 开始即可。
  3. 当前最大时间在 2026 年,可以预建 2027 年到 2030 年分区。
  4. 最后保留一个 PMAX 分区,承接未来没有提前建分区的数据。

如果你的表里分区键存在空值或者异常历史日期,一定要提前处理。不要让脏数据悄悄落到第一个正常业务分区里,后面维护时会很难受。

五、检查是否支持在线重定义

主键处理完后,先用 CAN_REDEF_TABLE 做可行性检查:

begin

dbms_redefinition.can_redef_table(

uname => 'APP_USER',

tname => 'BIZ_LOG',

options_flag => dbms_redefinition.cons_use_pk

);

end;

/

如果执行无报错,说明这张表可以按主键方式进行在线重定义。

这个检查不要省。它可以提前暴露一些对象限制问题,比你执行到 START_REDEF_TABLE 再失败要舒服得多。

六、创建分区中间表

接下来创建中间表 APP_USER.BIZ_LOG_TMP。

这里的关键点是:中间表字段结构要和原表对齐,包括字段类型、默认值、CLOB 字段、表空间参数等。实际生产中建议先通过 DBMS_METADATA.GET_DDL 抽取原表 DDL,再在这个基础上增加分区定义,不建议纯手写。

下面是脱敏后的示例 DDL:

create table APP_USER.BIZ_LOG_TMP

(

log_id number(12),

operator_id number(6),

log_time date default sysdate,

log_type number(1) default 1,

log_content varchar2(2000),

business_id number(12) default 0,

detail_type number(3),

detail_text clob,

archive_count number(2),

client_host varchar2(32),

access_channel varchar2(32),

node_user_id number(9),

node_time date

)

partition by range (log_time)

(

partition P2022 values less than (date '2023-01-01') tablespace APP_TAB_TS,

partition P2023 values less than (date '2024-01-01') tablespace APP_TAB_TS,

partition P2024 values less than (date '2025-01-01') tablespace APP_TAB_TS,

partition P2025 values less than (date '2026-01-01') tablespace APP_TAB_TS,

partition P2026 values less than (date '2027-01-01') tablespace APP_TAB_TS,

partition P2027 values less than (date '2028-01-01') tablespace APP_TAB_TS,

partition P2028 values less than (date '2029-01-01') tablespace APP_TAB_TS,

partition P2029 values less than (date '2030-01-01') tablespace APP_TAB_TS,

partition P2030 values less than (date '2031-01-01') tablespace APP_TAB_TS,

partition PMAX values less than (maxvalue) tablespace APP_TAB_TS

)

tablespace APP_TAB_TS

pctfree 10

initrans 1

maxtrans 255

enable row movement;

我会保留 ENABLE ROW MOVEMENT。后续如果业务更新了分区键,并且新值需要跨分区移动,没开行移动就容易报错。

字段注释也建议补齐,尤其是后续需要把这张表交给开发或运维同事继续维护时:

comment on column APP_USER.BIZ_LOG_TMP.log_type

is '日志类型';

comment on column APP_USER.BIZ_LOG_TMP.detail_text

is '日志明细文本';

comment on column APP_USER.BIZ_LOG_TMP.client_host

is '客户端主机信息';

comment on column APP_USER.BIZ_LOG_TMP.node_time

is '节点业务时间';

七、开始在线重定义

中间表建好后,开始执行在线重定义:

begin

dbms_redefinition.start_redef_table(

uname => 'APP_USER',

orig_table => 'BIZ_LOG',

int_table => 'BIZ_LOG_TMP',

options_flag => dbms_redefinition.cons_use_pk

);

end;

/

这一步会把原表数据复制到中间分区表。因为本次表约 34GB,而且包含 CLOB 字段,所以执行时间可能比较长。

这里不要只盯着会话有没有跑完,还要同步观察表空间、UNDO、REDO、归档和等待事件。尤其是归档空间,很多重定义任务不是 SQL 写错了,而是归档把磁盘打满了。

八、复制依赖对象

数据复制完成后,需要复制索引、触发器、约束、权限和统计信息等依赖对象:

set serveroutput on

declare

l_errors number;

begin

dbms_redefinition.copy_table_dependents(

uname => 'APP_USER',

orig_table => 'BIZ_LOG',

int_table => 'BIZ_LOG_TMP',

copy_indexes => dbms_redefinition.cons_orig_params,

copy_triggers => true,

copy_constraints => true,

copy_privileges => true,

ignore_errors => true,

num_errors => l_errors,

copy_statistics => true

);

dbms_output.put_line('copy errors=' || l_errors);

end;

/

执行后一定要看 l_errors。如果不是 0,就查 DBA_REDEFINITION_ERRORS:

select object_name,

base_table_name,

ddl_txt

from dba_redefinition_errors

where base_table_name in ('BIZ_LOG', 'BIZ_LOG_TMP');

这里的错误不能一眼带过。索引、约束、触发器、权限任何一个没复制好,切换后都可能变成生产事故。

九、同步增量数据

在线重定义期间,原表仍然可能有业务 DML。切换前需要同步增量:

begin

dbms_redefinition.sync_interim_table(

uname => 'APP_USER',

orig_table => 'BIZ_LOG',

int_table => 'BIZ_LOG_TMP'

);

end;

/

这个过程可以执行多次。

我的习惯是:正式切换前先执行一次同步,确认没有异常;临近切换时再执行一次同步,尽量缩短 FINISH_REDEF_TABLE 阶段的锁表时间。

十、完成在线重定义

最后一步建议放在业务低峰期执行:

begin

dbms_redefinition.finish_redef_table(

uname => 'APP_USER',

orig_table => 'BIZ_LOG',

int_table => 'BIZ_LOG_TMP'

);

end;

/

这一步会短暂锁表。它不是完全无锁,只是把长时间的数据复制过程放到了在线阶段,最终切换仍然需要一个很短的窗口。

执行完成后,原表 APP_USER.BIZ_LOG 就已经变成分区表。

十一、验证结果

切换后不要急着宣布成功,先做验证。

查看原表是否已经变成分区表:

select owner,

table_name,

partitioned

from dba_tables

where owner = 'APP_USER'

and table_name = 'BIZ_LOG';

查看分区信息:

select table_owner,

table_name,

partition_name,

tablespace_name

from dba_tab_partitions

where table_owner = 'APP_USER'

and table_name = 'BIZ_LOG'

order by partition_position;

查看索引状态:

select owner,

index_name,

status,

partitioned

from dba_indexes

where owner = 'APP_USER'

and table_name = 'BIZ_LOG';

查看约束状态:

select owner,

table_name,

constraint_name,

constraint_type,

status

from dba_constraints

where owner = 'APP_USER'

and table_name = 'BIZ_LOG';

查看对象是否失效:

select owner,

object_name,

object_type,

status

from dba_objects

where owner = 'APP_USER'

and status <> 'VALID';

如果发现索引失效,需要及时重建:

alter index APP_USER.IDX_BIZ_LOG_01 rebuild online;

具体是否能在线重建,也要结合版本、索引类型和现场业务情况确认。

十二、收集统计信息

结构变化完成后,建议重新收集统计信息:

begin

dbms_stats.gather_table_stats(

ownname => 'APP_USER',

tabname => 'BIZ_LOG',

cascade => true

);

end;

/

分区表上线后,统计信息很重要。否则优化器还拿着旧的统计信息做判断,可能出现执行计划波动。

十三、清理中间表

业务验证无误后,再清理中间表:

drop table APP_USER.BIZ_LOG_TMP purge;

注意,我不建议刚切换完就立刻删除。最好等业务侧验证、核心 SQL 验证、对象状态检查都通过后,再做清理。

十四、异常处理:遇到 ORA-23539 怎么办

如果在线重定义过程中因为空间不足、会话异常或其他原因中断,再次执行时可能会报:

ORA-23539: table is currently being redefined

这种情况说明 Oracle 认为这张表仍处于重定义状态。需要先中止本次重定义:

begin

dbms_redefinition.abort_redef_table(

uname => 'APP_USER',

orig_table => 'BIZ_LOG',

int_table => 'BIZ_LOG_TMP'

);

end;

/

中止之后,先把空间、权限、对象状态等问题处理干净,再重新执行在线重定义流程。

这里不要急着重复跑命令。很多问题如果不先定位原因,只会从失败一次变成失败两次。

十五、总结与实战建议

这次普通表在线转分区表,流程可以概括成一句话:

检查主键和分区键 -> 创建分区中间表 -> 开始在线重定义 -> 复制依赖对象 -> 同步增量 -> 低峰切换 -> 验证和收尾

我的实战建议:

  1. 不要只看表段大小,LOB、索引、UNDO、REDO 和归档空间都要一起评估。
  2. 能用主键方式就优先用主键方式,前提是主键列必须唯一且非空。
  3. 分区键一定要查最小值、最大值、空值和数据分布,不要凭感觉设计分区。
  4. 中间表 DDL 尽量从原表 DDL 改造,避免漏字段、默认值、LOB 参数和注释。
  5. COPY_TABLE_DEPENDENTS 执行后一定要查错误表,不要只看 PL/SQL 执行成功。
  6. SYNC_INTERIM_TABLE 可以多执行几次,最终切换前再同步一次。
  7. FINISH_REDEF_TABLE 仍然会短暂锁表,生产环境建议放在业务低峰期。
  8. 切换完成后要验证分区、索引、约束、失效对象和统计信息。

在线重定义不是为了让变更"没有风险",而是把风险拆开:前面大部分工作在线完成,最后只留下一个尽量短的切换窗口。

表分区改造这类工作,看起来是 DDL,实际考验的是 DBA 对对象结构、空间、业务窗口和回退方案的整体把控。

好了,今天这篇就到这里。你在生产环境中有没有做过普通表在线转分区表?是用 DBMS_REDEFINITION,还是通过 CTAS、导入导出或者停机窗口处理?欢迎在评论区聊聊你的经验。

------ 睿 | Oracle性能优化老司机

专注硬核干货,欢迎一起卷技术!

相关推荐
隔窗听雨眠3 小时前
OceanBase旁路导入与自增主键冲突深度解析:原理、排查与解决方案
java·服务器·数据库
ZCBUS实时计算3 小时前
政务海量分库分表汇聚实战:基于 3 节点 ZCBUS 集群完成医保 10TB 数据实时整合
大数据·数据库·数据仓库·sql·dba·etl·政务
名不经传的养虾人3 小时前
从0到1:企业级AI项目迭代日记 Vol.81|工具调用前,先过一道审批门
数据库·人工智能·ai编程·ai工作流·企业ai
Databend3 小时前
Databend 产品更新:从 Spatial Index Join 到 Eval 数据管道
大数据·数据库·sql
数据知道3 小时前
SQL 注入从入门到精通:手工注入 + sqlmap 自动化
网络·数据库·sql·网络安全·自动化
lilian2334 小时前
Harmony os 技术实战|拼豆制图06:收藏 ID、生成记录与重启恢复怎么不打架
android·java·数据库·harmonyos
2601_955759624 小时前
ClaudeAPI成本中心与业务标签设计指南
android·java·数据库
仙人球部落 揞殺4 小时前
Sql Server查询性能优化之走出索引的误区
数据库·性能优化
Bryce学亮4 小时前
FileLine,基于 Qt 6 + QML 构建的跨平台文件传输与即时通讯工具
数据库·c++·人工智能·python·qt·github
轻揉小乔 真新人4 小时前
T-SQL查询进阶—理解SQL Server中的锁
服务器·数据库·sql