1. 先想清楚:为什么数据表对象是Oracle 19c学习绕不过去的一关
从第1期走到现在,前面几篇我们把Oracle 19c的体系架构、实例管理、表空间和数据文件都过了一遍。到了第10期,终于轮到最核心的“表”本身。我见过不少初学者,安装包装好了、监听配通了、能连上数据库了,就觉得自己“入门”了——但一问到怎么设计一张像样的业务表,怎么在建表语句里把约束、默认值、注释、存储参数一次性写对,往往就卡壳了。
数据表对象在整个Oracle知识体系里属于最底层的砖块。往上是视图、物化视图、分区、索引,再往上是PL/SQL、存储过程、性能调优。任何一层出了问题,最后回溯过来,大概率都能查到表结构设计不合理、字段类型选错、约束缺失这类根因上。所以这一篇我打算用案例驱动的方式,把Oracle 19c里数据表对象的语法知识点掰开揉碎,从CREATE TABLE开始,到ALTER TABLE、DROP TABLE、约束治理、序列与自增列、19c的分区新特性,全部串起来走一遍。
我自己的学习路径也是这样过来的——先不急着背语法,而是拿着一个真实的业务模型,比如订单系统、用户系统、库存系统,反复建表、改表、删表、重建,把每种DDL语句的每个子句都亲手敲一遍,遇到ORA报错再去翻文档。这个过程比单纯看语法手册有效得多,因为你真正记住了“为什么这样写会报错”,而不是只记住“这样写是对的”。
这篇文章的目标读者是三类人:刚装好Oracle 19c、准备系统学习SQL和对象管理的初学者;已经会用SELECT/INSERT/DML、但对DDL语句和约束设计缺乏体系化认知的开发人员;以及需要快速把一个业务模型落地成物理表结构,回头来补课的数据分析师或运维工程师。我会默认你已经能用SQL*Plus或SQL Developer连接到19c实例,有基本的SQL基础,但不会默认你背得出CREATE TABLE的完整语法。
另外提一句环境准备:我自己是在Linux服务器上安装的19c,RHEL 9.8这种新版本内核配19c时要特别注意Oracle的兼容性补丁。如果你还没装好,建议先去把19c跑起来,这一篇所有的案例SQL都能直接在你自己的环境里复现。不会Linux安装也没关系,Windows下的Oracle 19c同样支持本文的全部语法,只是路径和服务名不同罢了。
2. 数据类型选错,运维跪着还——为什么表字段定义决定了未来三年的姿势
这一章从建表之前的一个关键决策讲起:字段类型。很多人觉得数据类型就是“随便选一个”,实际不是。字段类型直接影响存储空间、查询性能、索引效率、SQL写法和后期迁移成本,是数据表对象设计中最容易埋雷的地方。
2.1 Oracle 19c里字符型的“坑”比你想的多
Oracle的字符型核心就两个:CHAR和VARCHAR2。CHAR是定长,VARCHAR2是变长。这个区别谁都知道,但真要选的时候,很多人还是会随手写成CHAR(20)。我见过一个实际案例,某业务系统把手机号字段定义为CHAR(11),看起来没问题,但因为这个字段参与了大量JOIN和WHERE条件,Oracle在比较时需要做空值处理和尾随空格补齐,索引扫描的计划硬是比用VARCHAR2慢了一截。为什么?CHAR(11)会把你存入的每个值都补齐到11个字符,尾随空格进入索引键值,查询时优化器得做额外处理。
在19c里我建议你养成一个习惯:凡是字符串字段,默认选VARCHAR2,除非你有强业务理由需要定长。另外,19c开始已经把VARCHAR2的最大长度从4000字节扩展到了32767字节,前提是设置了扩展数据类型参数(MAX_STRING_SIZE=EXTENDED)。这个参数一旦开启,VARCHAR2(32767)就可以直接存大文本,很多场景下你甚至不需要动CLOB——当然这只是“能存”,索引和排序的性能你要自己测试权衡。
适合用CHAR的场景也有:固定长度的业务编码,比如订单号、身份证号、银行卡号这类长度恒定的字段。但即便这样,我仍然倾向VARCHAR2,因为一旦将来编码规则改变,CHAR字段会被迫做大量迁移。
2.2 NUMBER和DATE:精度选择是门学问
Oracle的数值类型只有NUMBER(以及19c新增的BINARY_FLOAT/BINARY_DOUBLE浮点类型)。NUMBER(p,s)里的p是精度,s是小数位。我见过最经典的事故是把金额字段定义为NUMBER,不带精度。表面上能存任意数字,但应用层读到的是一个可能带着一堆小数位的值,财务报表一算就出问题。金额字段我建议NUMBER(14,2)起步,如果业务未来可能有跨币种大额交易,直接NUMBER(18,2)。
日期时间类型更值得说。Oracle的DATE本身就包含时分秒,不需要像某些数据库那样DATE和DATETIME分家。TIMESTAMP在DATE基础上加了小数秒,适合需要精确到毫秒的日志类场景。还有一个容易忽略的点:如果字段只需要日期不需要时间,你用DATE也没问题,但查询时如果用了WHERE create_date = TO_DATE('2025-01-15','YYYY-MM-DD'),会因为时间部分不为零导致匹配不到数据。这种情况要么在应用层把时间归零,要么直接写TRUNC(create_date) = TO_DATE(...),但函数套列会让索引失效——这就是你后续要学索引时的经典坑,先记在这里。
2.3 我在实操中踩过的数据类型选择教训
说一个我自己真实踩过的坑。有一张日志表,我图省事把所有描述性字段都建成了CLOB。当时想的是“反正CLOB多大都能存,以后不用怕不够”。结果半年后这张表到了几千万行,做任何聚合分析和报表统计时全表扫描的代价高得吓人,CLOB字段根本没法建普通索引,只能建Oracle Text索引,SQL写法也跟着变复杂,整个团队被迫为一个当初“图省事”的决定买单。
后来我把这张表重构了:真正的大文本单独立表存CLOB,业务查询常用的短文本字段改成VARCHAR2(500),核心关联字段用VARCHAR2(32)或NUMBER。重构之后,报表查询从几分钟降到了十几秒。这里面的教训是:数据类型的选择不只看“能不能存下”,还要看“将来我要怎么用它”。如果字段会频繁出现在WHERE、JOIN、GROUP BY或排序场景里,那就坚决不用大对象类型,宁可拆表、拆字段。
3. CREATE TABLE的各种打开方式——标准建表、CTAS和分区表的取舍
建表这件事,语法看起来就那么几行,但真正到了生产环境,要考虑的事情远不止“把字段列出来”。这一章我们从最标准的CREATE TABLE语法开始,然后讲实用进阶用法,最后聊聊19c里表的存储和并行特性。
3.1 标准CREATE TABLE语法长什么样
Oracle 19c的标准建表语法可以拆成几个部分:表名与列定义、约束定义、存储参数、表空间、并行度。一个最基本的例子是:
CREATE TABLE cux_order ( order_id NUMBER(18) NOT NULL, order_no VARCHAR2(32) NOT NULL, customer_id NUMBER(18) NOT NULL, order_amount NUMBER(14,2) DEFAULT 0 NOT NULL, order_status VARCHAR2(10) DEFAULT 'NEW' NOT NULL, created_date DATE DEFAULT SYSDATE NOT NULL, CONSTRAINT pk_cux_order PRIMARY KEY (order_id) ) TABLESPACE tbs_app_data STORAGE (INITIAL 64K NEXT 64K MAXEXTENTS UNLIMITED);这里每个子句都有自己的门道。NOT NULL约束建议写在列级别,可读性好;主键约束写成表级约束,方便以后要建复合主键或联合唯一键。DEFAULT子句在19c里是一个性能利器——后文ALTER TABLE部分我会专门讲为什么。
TABLESPACE指定表空间,生产环境建议业务数据、索引数据、临时数据分开放,这是后面做存储和IO优化的基础。STORAGE子句里的INITIAL/NEXT在本地管理表空间(LMT)下其实可以不用写,19c默认自动扩展,写了反而容易在未来迁移时报不一致错误。我现在的习惯是:数据文件用AUTOEXTEND,表不手动指定STORAGE,除非碰到需要手工控制段大小的极端场景。
列定义里还有个小细节:字段名和表名建议统一大小写风格。Oracle默认把未加引号的标识符转成大写,你写cux_order,字典里存的是CUX_ORDER。这本身没问题,但如果你用了小写加引号的命名方式,那以后每次查询都得加引号,会折磨死自己和同事。我的建议是:一律不加引号,统一用带下划线的英文名,全大写风格,比如CUX_ORDER、ORDER_ID。
3.2 CTAS建表:一条SQL建出带数据的表
CREATE TABLE AS SELECT(CTAS)是我在实战中经常用的“快建表”手段。它的本质是根据查询结果集来建表,表结构由SELECT的投影列表决定,数据也直接灌进去。语法很简单:
CREATE TABLE cux_order_bak AS SELECT * FROM cux_order WHERE created_date >= SYSDATE - 7;CTAS的优点是快、简单、适合做备份表或中间表。但有几个大坑必须注意:第一,CTAS不会自动继承源表的约束(主键、唯一键、检查约束全部丢失),只会保留NOT NULL约束。如果你CTAS一张业务表然后直接拿去做生产数据同步,没有主键约束就等于给数据质量埋雷。第二,CTAS默认新建表的字段类型和长度与源表一致,但如果源表有虚拟列或基于函数的索引列,可能会报ORA-00998错误。第三,CTAS建出来的表没有注释(COMMENT),源表字段的注释不会跟着过来,你可能需要额外补一遍COMMENT ON语句。
所以我对CTAS的建议是:用在临时表、备份表、报表宽表这类“不需要完整约束”的场景;如果是从一张业务表复制结构并保留约束做新表,用DBMS_METADATA.GET_DDL取出DDL,改个表名再执行,比CTAS靠谱得多。
3.3 建表也能并行和防错:CREATE TABLE的进阶子句
19c里CREATE TABLE支持PARALLEL子句,可以在建表阶段指定表的并行度:
CREATE TABLE cux_big_table ( id NUMBER(18), payload VARCHAR2(4000) ) PARALLEL 4;这里的PARALLEL 4表示这张表后续做全表扫描时,优化器可以建议用4个并行进程。这个参数在生产环境很危险——如果业务系统有几百张表都设了PARALLEL,并发一大,系统资源迅速被打满,DBA会被半夜叫起来。我的个人建议是:OLTP系统默认不要设,只有明确知道这张表会被大规模聚合分析使用再设,而且用完后记得改回NOPARALLEL。
还有个被忽视的子句是ON COMMIT相关的——那属于临时表,后文单讲。另外19c建表时可以指定DEFAULT COLLATION,这个在多语言排序场景下有用,不过绝大多数国内业务用不上,知道名称即可。
关于建表,我再说一个“看起来不起眼但极其影响幸福感”的环节:注释。很多人建表不写注释,半年后连自己都不知道STATUS_FLAG里的值是0还是1、含义是什么。19c里写注释很简单:
COMMENT ON TABLE cux_order IS '订单主表'; COMMENT ON COLUMN cux_order.order_status IS '订单状态: NEW-新建 PAID-已支付 SHIPPED-已发货 DONE-已完成';麻烦是麻烦点,但项目交接、需求变更、故障排查时,这些注释能帮你省下大量的沟通成本。后面第6章的订单系统案例我会把注释也一并写上。
4. ALTER TABLE才是生产环境的重头戏——加列、改类型和约束治理
建表只是开始,业务跑起来之后,表结构的变更才是日常。这一章把ALTER TABLE的常用招数全部过一遍,尤其是19c版本在加默认值列上的性能优化,这个知识点非常值钱。
4.1 加列和改列的语法与实战
向现有表加列,基础语法就一句话:
ALTER TABLE cux_order ADD (remark VARCHAR2(500) DEFAULT '暂无备注');但这句话在19c里有一个隐藏的版本差异。在12cR1以前,如果一张千万级的大表ADD一个有DEFAULT值的列,Oracle会立刻更新所有已有行的该列值,期间表会被锁,DDL可能跑上几十分钟甚至几个小时。而19c开始,加带默认值且DEFAULT是常量(非SYSDATE这类非确定性值)的列,Oracle只会在数据字典里记录默认值,物理上并不重写所有行,查询时Oracle自动补上,这个过程叫“元数据-only的默认值优化”。这带来的体验是:几千万行的表,加一个带DEFAULT的列可以秒级完成。
那我为什么要强调这个?因为很多DBA和开发从旧版本迁移到19c时,不知道这个变化,还在用“先加空列,再写PL/SQL循环回填,最后加NOT NULL”的老套路,白白浪费性能。在19c里你可以胆子大一点,直接ADD带默认值列,但要确认你的DEFAULT是常量且表不是临时表,另一个限制是:如果该列加了NOT NULL约束且表是分区表,某些场景下元数据优化不可用,建议先测后上。
修改字段类型或长度用MODIFY:
ALTER TABLE cux_order MODIFY (order_no VARCHAR2(64));这里有几点务必注意。一是VARCHAR2加长,19c里如果没有开启扩展数据类型,超过4000字节会报ORA-01441;二是缩短长度时,如果表中已存在超长数据,会直接报ORA-01440;三是修改数据类型(比如VARCHAR2改成NUMBER),需要表中该列的值能隐式转换,否则报ORA-01439。真实经验是,生产环境改列前先查一查这一列的最大长度和异常数据,比如:
SELECT MAX(LENGTH(order_no)) FROM cux_order; SELECT COUNT(*) FROM cux_order WHERE NOT REGEXP_LIKE(order_no, '^[0-9]+$');先清掉脏数据再改列,这是最稳妥的姿势。
4.2 约束的增删改查:从混乱到可控
约束是数据表对象里最容易被忽略又最能体现“设计水平”的部分。一个生产表如果连主键都没有,你会逐渐发现数据重复、关联查询混乱、删除时误删一片,最后全成了“跑批脚本里加DISTINCT”的丑陋解法。
19c里约束操作的基础语法:
ALTER TABLE cux_order ADD CONSTRAINT uk_cux_order_no UNIQUE (order_no); ALTER TABLE cux_order ADD CONSTRAINT ck_order_status CHECK (order_status IN ('NEW','PAID','SHIPPED','DONE')); ALTER TABLE cux_order ADD CONSTRAINT fk_cux_order_customer FOREIGN KEY (customer_id) REFERENCES cux_customer(customer_id);主键和唯一约束会自动创建一个唯一索引。外键约束会带来一个隐患:如果子表的外键列没有索引,在主表删除或更新主键时,Oracle会做全表锁和全表扫描来确认有没有子行引用,这在OLTP高并发下是致命伤。所以我的习惯是:每个外键列除了建外键约束,还显式建一个普通索引(或把外键列放进一个复合索引的前缀),这个做法能帮你避开大量软件包冲突。
遇到已经存在但命名混乱的约束,可以用RENAME清理:
ALTER TABLE cux_order RENAME CONSTRAINT SYS_C0012345 TO pk_cux_order;这里有个经验:Oracle自动生成的约束名(SYS_C开头)在报错时非常难定位问题,生产库建议统一约束命名规范——主键PK_表名,唯一键UK_表名_列名,检查约束CK_表名_列名,外键FK_子表_父表。真实案例里,我们曾因为一个自动命名的检查约束在批量导入时疯狂报错,DBA查了半小时才找到是哪张表的哪个字段,后来立了规矩:所有约束必须显式命名。
19c还有一个很实用的子句:ENABLE NOVALIDATE。它的意思是“启用约束,但不去校验已有数据是否符合”。这在你需要给一张历史脏数据表加约束时非常有用——先NOVALIDATE加上,保证后续新数据遵守规则,历史数据找个时间再慢慢清理,而不是被DDL直接卡死。
ALTER TABLE cux_order ADD CONSTRAINT ck_order_amount CHECK (order_amount >= 0) ENABLE NOVALIDATE;注意,ENABLE NOVALIDATE不等于约束没生效,它对新插入和更新的数据依然有效,只是跳过了存量校验。生产环境做大表加约束时,这是比VALIDATE更现实的选择。
4.3 主键索引的选择:NOLOGGING和并行重建
约束和索引经常混在一起。如果表数据量很大,加主键约束时默认会建索引,这个过程会比较慢。19c里可以拆开做:先并行建索引,再加约束。
CREATE UNIQUE INDEX uk_cux_order_order_id ON cux_order(order_id) PARALLEL 4 NOLOGGING; ALTER TABLE cux_order ADD CONSTRAINT pk_cux_order PRIMARY KEY (order_id) USING INDEX uk_cux_order_order_id;NOLOGGING的意思是减少重做日志生成,对新建索引而言能显著提速,但带来的风险是:如果之后数据库异常崩溃,这个索引可能标记为UNUSABLE,需要重建。OLTP系统里查询频繁走这个索引,崩溃后忘重建就是事故。所以我现在的用法是:只在批量建索引或索引重建的窗口期用NOLOGGING,做完立刻切回LOGGING,并纳入监控检查。
顺便说一个很多人问的问题:ALTER TABLE里能不能“一次做多件事”?Oracle官方语法里,ALTER TABLE可以组合多个操作子句,用逗号分隔,比如:
ALTER TABLE cux_order ADD (delivery_addr VARCHAR2(200)) MODIFY (remark VARCHAR2(1000) DEFAULT NULL) ADD CONSTRAINT ck_order_status CHECK (order_status IN ('NEW','PAID','SHIPPED','DONE'));这样一条SQL完成多个DDL变更在19c里是支持的,并且在执行时一致性更好。我的建议是:相关的一组表结构变更尽量合并执行,减少DDL次数,也便于版本回滚脚本里一条条记录清楚。
5. DROP和TRUNCATE一步之差,数据恢复的两重天——删除表对象前你必须知道的闪回底牌
删除表对象是DDL里最刺激的操作。一个DROP下去,表没了,数据没了。好在19c有闪回技术,能救你一部分数据,但前提是你知道它怎么用、边界在哪。
5.1 TRUNCATE和DROP到底删了什么
先看看三种“删除”的区别:
| 操作 | 删除内容 | 是否可回滚 | 是否释放存储 | 是否触发触发器 | 是否记录日志 |
|---|---|---|---|---|---|
| DELETE | 行数据 | 可回滚(事务内ROLLBACK可恢复) | 不释放,高水位线不变 | 是 | 全量写redo |
| TRUNCATE | 行数据 | 不可回滚(但可用Flashback Table恢复部分场景) | 释放段空间,高水位线重置 | 否 | 少量 |
| DROP TABLE | 表结构+数据 | 可闪回(19c默认进入回收站) | 默认不释放,表进回收站 | 否 | 少量 |
这张表是我建议你贴在工位上的。生产环境常见的误操作是TRUNCATE用错了表、DROP的时候没看清用户名,或者本来想DROP临时表结果连正式表一起删了。三类操作里,TRUNCATE和DELETE在“误操作后的心态”上天差地别——DELETE可以用ROLLBACK救回来,TRUNCATE就不行。
5.2 19c里的闪回DROP和回收站机制
19c默认开启回收站(Recyclebin),DROP TABLE并不会真正物理删除,而是把表改名后放进回收站。你可以直接闪回:
FLASHBACK TABLE cux_order TO BEFORE DROP;如果同名表已经重建,会导致系统自动给回收站里的表加一个别名,闪回时需要用原来的“回收站名字”:
SHOW RECYCLEBIN; FLASHBACK TABLE "BIN$xxxxxxxxxxxx$0" TO BEFORE DROP RENAME TO cux_order_dropbak;这里我强烈建议加上RENAME TO子句,免得闪回回来的表名跟现有对象冲突。
回收站也不是无限保险。如果表空间空间不足,Oracle会自动清理回收站里的对象,被清掉的表就再也闪回不回来了。所以两件事必须做:一是定期清理回收站而不是攒着不管;二是对核心大表,DROP之前先备份,而不是指望闪回兜底。执行清理也很简单:
PURGE TABLE cux_order_dropbak; PURGE RECYCLEBIN;5.3 闪回查询和闪回版本查询:表还在,但数据被改错了怎么办
比DROP更常见的是误更新和误删除。比如一个UPDATE语句忘写WHERE,把全表金额改错了。19c的闪回查询能救你:
SELECT * FROM cux_order AS OF TIMESTAMP (SYSTIMESTAMP - INTERVAL '30' MINUTE);这条查询能看到30分钟前的数据。如果知道大概误操作时间点,用AS OF TIMESTAMP TO_TIMESTAMP('2025-01-20 10:30:00','YYYY-MM-DD HH24:MI:SS')更精确。范围更大的还有闪回版本查询,可以看到一个时间段内每一行数据的所有历史版本:
SELECT versions_starttime, versions_endtime, versions_operation, order_amount FROM cux_order VERSIONS BETWEEN TIMESTAMP TO_TIMESTAMP('2025-01-20 10:00:00','YYYY-MM-DD HH24:MI:SS') AND SYSTIMESTAMP;这条SQL能返回每一行在该时间段内被INSERT、UPDATE、DELETE的版本记录。我曾经用它定位过一次“金额莫名变化”的线上问题,最后发现是一个定时任务在特定条件下多执行了一次,而不是数据库逻辑错误。
闪回查询的底层依赖UNDO表空间,UNDO保留时间由UNDO_RETENTION参数决定。19c默认是900秒(15分钟),如果你把UNDO_RETENTION调大到3600甚至86400,闪回窗口就长很多,但UNDO表空间也会膨胀。我的建议是:核心生产库把UNDO_RETENTION设为至少3600,同时给UNDO表空间充足的大小和自动扩展,这笔成本换来的是一次误操作时的“后悔药”,非常值。
6. 19c序列与自增列:别再手动取MAX+1了,IDENTITY列和序列缓存的那些事
自增主键是表设计里的高频需求。早期Oracle没有MySQL那样直观的AUTO_INCREMENT,大家习惯用序列+触发器或序列+应用层取值。19c已经支持GENERATED AS IDENTITY的标识列,用起来很顺手。但“顺手”不等于“没有坑”,序列的缓存参数选错了,同样会引发线上故障。
6.1 IDENTITY列:建表更省事,但灵活性要权衡
19c里的标识列写法:
CREATE TABLE cux_customer ( customer_id NUMBER(18) GENERATED ALWAYS AS IDENTITY PRIMARY KEY, customer_name VARCHAR2(64) NOT NULL, register_date DATE DEFAULT SYSDATE );这里面ALWAYS表示你永远不能手动给这个列插入值,数据库全权管理。如果你想在某些场景下(比如数据迁移时保留原ID)允许手动插入,可以改成GENERATED BY DEFAULT AS IDENTITY。还有一个GENERATED BY DEFAULT ON NULL AS IDENTITY的选项,允许插入NULL时自动生成值,适合ORM框架里经常不传主键值的场景。
标识列在后台会自动创建一个序列。需要留意的坑是:当你TRUNCATE这张表时,序列不会重置,新插入的主键会继续沿着上次的数字往下走。如果业务要求主键从1重新开始,你得手动改序列。这个场景在测试环境特别常见——开发同学反复TRUNCATE、INSERT,最后主键变成几十万,他们还以为自己代码写错了。
6.2 手工序列:CACHE参数是隐藏的炸弹
如果你不用标识列,手工创建序列是常规操作:
CREATE SEQUENCE seq_cux_order_id START WITH 1 INCREMENT BY 1 CACHE 100;CACHE是Oracle预先生成并存到内存里的序列号个数,目的是减少对数据字典的I/O。CACHE设小了性能受影响,设大了……在数据库实例异常重启时会跳号。比如CACHE 100,内存里已经缓存了1-100,还没落盘,实例崩溃后重启,序列会从101开始,于是以后拿到的号可能是“1到100这批全部丢失”之后的状态。如果你的业务对主键连续性有要求(实际上大多数场景没要求,但财务单据场景可能介意),那就要么把CACHE调小,要么接受跳号并提前跟业务方打招呼。
还有一个容易踩的坑:序列与触发器结合使用时,如果触发器在INSERT失败时没做异常处理,序列值照样会消耗掉,于是在批量重试导入时,主键中间会出现大量空洞。这个不会导致功能错误,但如果你看着主键从1跳到1000,心里膈应,可以在触发器里捕获异常后ROLLBACK回滚序列的NEXTVAL——不过Oracle序列不支持回滚,所以你只能接受。我现在的处理是:不在触发器里生成主键,都在INSERT语句里显式SELECT seq.NEXTVAL INTO ...,这样至少逻辑清晰。
6.3 序列的并发和性能调优实测
序列在高并发插入场景下的性能瓶颈主要在“争用”。如果一张核心订单表每秒插上千行,所有会话都在抢同一个序列的缓存,19c里可以通过NOORDER配合大CACHE把争用降到最低,因为NOORDER不保证序列按请求顺序分配,省去了排他锁等待。
实测数据可以参考:CACHE 20时,100个并发会话批量INSERT,序列获取在总耗时里占比约18%;改成CACHE 500,占比降到3%左右。翻倍CACHE对ID跳号风险也翻倍,所以我的建议是:高并发、不要求连续性、ID仅为唯一标识的场景,CACHE设500到1000没问题;财务、票据、合同这类对外可见且用户会抱怨“单号不连续”的场景,CACHE设20,并接受性能上的小幅牺牲。
7. 临时表、分区表与19c新特性:数据表对象的进阶玩法
一张业务表被设计成最普通的堆表(Heap Table),这只是起点。到了数据量上去、查询复杂起来的阶段,你需要了解临时表和分区表,以及19c在这两个方向上的新特性,它们能极大改善性能和可管理性。
7.1 临时表:会话级和事务级的选择
临时表在Oracle里分两类:事务级临时表(ON COMMIT DELETE ROWS)和会话级临时表(ON COMMIT PRESERVE ROWS)。前者的数据在事务结束后自动清空,后者数据在会话结束时才清空。
CREATE GLOBAL TEMPORARY TABLE tmp_etl_stage ( batch_id NUMBER(18), payload VARCHAR2(2000), load_date DATE ) ON COMMIT DELETE ROWS;临时表在报表、ETL、存储过程中间结果集等场景非常实用。它的数据只对当前会话可见,天然隔离,不会污染正式数据。但注意:临时表也是表,也会产生redo日志(只是比普通表少),并且临时表上不能建外键,也不能跨会话共享数据。我见过有人把临时表当成多会话共享缓冲来用,结果查来查去查不到数据,就是这个隔离机制导致的。
7.2 分区表:从范围分区到19c的自动间隔分区
分区表是Oracle大表治理的标配。最常见的范围分区例子:
CREATE TABLE cux_order_part ( order_id NUMBER(18), order_date DATE, order_amount NUMBER(14,2) ) PARTITION BY RANGE (order_date) ( PARTITION p_2024_q1 VALUES LESS THAN (TO_DATE('2025-04-01','YYYY-MM-DD')), PARTITION p_2024_q2 VALUES LESS THAN (TO_DATE('2025-07-01','YYYY-MM-DD')), PARTITION p_max VALUES LESS THAN (MAXVALUE) );分区的核心价值是“分区修剪”——查询条件里带上分区键时,Oracle能跳过无关分区,只扫描目标分区数据。这个特性在归档场景尤其好用:数据按月分区,老数据直接ALTER TABLE ... TRUNCATE PARTITION p_2023_01秒删,而不是一把DELETE删到怀疑人生。
19c里更推荐用间隔分区(Interval Partitioning),它能在数据到达时自动创建新分区,不用你手工维护边界。语法:
CREATE TABLE cux_order_interval ( order_id NUMBER(18), order_date DATE, order_amount NUMBER(14,2) ) PARTITION BY RANGE (order_date) INTERVAL (NUMTOYMINTERVAL(1, 'MONTH')) ( PARTITION p_init VALUES LESS THAN (TO_DATE('2025-02-01','YYYY-MM-DD')) );这个表的含义是:每月自动生成一个分区,比如3月的第一条数据进来,19c自动创建SYS_Pxxx分区存放3月数据。我们当时给订单流水表用了间隔分区后,彻底告别了每月手工建分区的例行工作。
在使用分区表时,我踩过最值钱的一个坑是:分区键千万不要选业务上几乎不会作为查询条件的字段。分区表的价值是“查询条件能落到分片上”,如果查询语句永远不带分区键,分区修剪失效,Oracle只能扫描全部分区,性能可能比普通表还差。比如你可以把订单表按customer_id做HASH分区,但如果所有人的查询都是按order_date过滤,那这个HASH分区就形同虚设。所以建分区表之前,先分析业务SQL最常用的WHERE条件,再决定分区键和分区策略。
7.3 19c的分区维护操作:一键合并、拆分和移动
分区表维护最常用的几个语句:
-- 拆分一个分区 ALTER TABLE cux_order_interval SPLIT PARTITION p_init AT (TO_DATE('2025-01-15','YYYY-MM-DD')) INTO ( PARTITION p_jan_1_15, PARTITION p_jan_16_31 ); -- 合并相邻分区 ALTER TABLE cux_order_interval MERGE PARTITIONS p_jan_1_15, p_jan_16_31 INTO PARTITION p_jan; -- 移动分区到另一个表空间 ALTER TABLE cux_order_interval MOVE PARTITION p_2025_01 TABLESPACE tbs_arc_data;这些运维操作在19c里都支持在线或接近在线完成,大表上的消耗比传统做法小很多,但依然建议在维护窗口操作。还有一点,分区表上的普通索引不会跟着分区走,如果索引是全局索引,TRUNCATE分区时会导致整个全局索引失效,需要加UPDATE GLOBAL INDEXES子句来避免:
ALTER TABLE cux_order_interval TRUNCATE PARTITION p_2024_01 UPDATE GLOBAL INDEXES;这个子句是我用血的教训换来的。有一次直接清一个老分区,忘了加UPDATE GLOBAL INDEXES,结果整个表上的全局索引全部失效,夜间跑批任务全部走全表扫描,第二天早上发现时已经积压了几个小时的批处理。从那以后,凡是对分区表做DDL,第一反应就是检查有没有全局索引、要不要带这个子句。
8. 综合案例:从零建一套订单核心表结构,把本期语法全部串起来
前面几章把语法和参数都过了,这一章用一个完整的订单模块建表案例,把本期内容从头串到尾。我建议你打开自己的SQL*Plus,一步步照着敲,不要复制粘贴,这样才能把语法内化成自己的肌肉记忆。
8.1 需求梳理和表结构设计
案例背景:一个B2C商城要建订单模块,需要四张核心表:客户表、订单主表、订单明细表、订单状态变更日志表。
业务约束如下:
- 客户有唯一客户编号,注册日期默认系统时间;
- 订单有唯一订单号,金额必须大于等于0,状态只能取NEW、PAID、SHIPPED、DONE、CANCELED;
- 订单明细里的商品数量必须大于0,行号在同一个订单内不能重复;
- 状态日志需要记录每次变更的前后状态和操作时间;
- 订单表按月份分区,从而支持历史数据快速归档。
基于这些需求,我把四张表的设计思路先写下来:
-- 客户表 CREATE TABLE cux_customer ( customer_id NUMBER(18) GENERATED BY DEFAULT AS IDENTITY, customer_no VARCHAR2(32) NOT NULL, customer_name VARCHAR2(64) NOT NULL, phone VARCHAR2(20), register_date DATE DEFAULT SYSDATE, status VARCHAR2(10) DEFAULT 'ACTIVE', CONSTRAINT pk_cux_customer PRIMARY KEY (customer_id), CONSTRAINT uk_cux_customer_no UNIQUE (customer_no), CONSTRAINT ck_cux_customer_status CHECK (status IN ('ACTIVE','LOCKED','CLOSED')) ); COMMENT ON TABLE cux_customer IS '客户表'; COMMENT ON COLUMN cux_customer.customer_no IS '客户编号,业务唯一'; COMMENT ON COLUMN cux_customer.status IS '客户状态: ACTIVE正常 LOCKED锁定 CLOSED注销';这里用GENERATED BY DEFAULT,是为了后续数据迁移时能手动保留原客户ID。customer_no业务唯一键建了唯一约束,同时这个约束也会自动生成唯一索引,后续按客户编号查询的速度就有保障。
订单主表用间隔分区:
CREATE TABLE cux_order ( order_id NUMBER(18) GENERATED BY DEFAULT AS IDENTITY, order_no VARCHAR2(32) NOT NULL, customer_id NUMBER(18) NOT NULL, order_amount NUMBER(14,2) DEFAULT 0 NOT NULL, order_status VARCHAR2(10) DEFAULT 'NEW' NOT NULL, created_date DATE DEFAULT SYSDATE, CONSTRAINT pk_cux_order PRIMARY KEY (order_id) USING INDEX TABLESPACE tbs_app_idx, CONSTRAINT uk_cux_order_no UNIQUE (order_no), CONSTRAINT fk_cux_order_customer FOREIGN KEY (customer_id) REFERENCES cux_customer(customer_id), CONSTRAINT ck_cux_order_status CHECK (order_status IN ('NEW','PAID','SHIPPED','DONE','CANCELED')), CONSTRAINT ck_cux_order_amount CHECK (order_amount >= 0) ) PARTITION BY RANGE (created_date) INTERVAL (NUMTOYSMININTERVAL(1, 'MONTH')) ( PARTITION p_init VALUES LESS THAN (TO_DATE('2025-01-01','YYYY-MM-DD')) ); COMMENT ON TABLE cux_order IS '订单主表';注意这里我把主键索引指定到了独立的索引表空间tbs_app_idx,这符合前面讲的基础规范:数据、索引分开存放,便于备份和IO隔离。外键customer_id虽然在物理表里是普通列,但它在业务上会高频关联客户表,我应该单独为它建一个普通索引,防止主表删除或更新客户时触发外键全表扫描。
订单明细表:
CREATE TABLE cux_order_item ( order_id NUMBER(18) NOT NULL, line_no NUMBER(4) NOT NULL, product_id NUMBER(18) NOT NULL, product_name VARCHAR2(128) NOT NULL, quantity NUMBER(12,2) NOT NULL, price NUMBER(14,2) NOT NULL, item_amount NUMBER(14,2) GENERATED ALWAYS AS (quantity * price) VIRTUAL, CONSTRAINT pk_cux_order_item PRIMARY KEY (order_id, line_no), CONSTRAINT fk_cux_order_item_order FOREIGN KEY (order_id) REFERENCES cux_order(order_id), CONSTRAINT ck_cux_order_item_qty CHECK (quantity > 0) );这里我第一次用了虚拟列。item_amount不实际存储,而是通过quantity * price计算得到,好处是查询时直接当普通列用,又不会占用存储空间,也不用担心手工更新和实际计算不一致。
8.2 序列的最小化使用与自增列的小结
这个案例里我没有额外创建手工序列,全都依赖GENERATED BY DEFAULT AS IDENTITY。如果你更习惯序列+触发器的写法,也不冲突,19c都兼容。但我建议新项目优先用标识列,理由有三个:少写代码、约束更清晰、跟Oracle 19c的官方演进方向一致。
要注意,标识列在批量导入场景下会相对麻烦。比如你需要从老库把历史订单原主键迁过来,用GENERATED ALWAYS会直接拒绝你显式插入ID,必须改BY DEFAULT。这也是我在案例里选BY DEFAULT的原因。迁移结束后,如果不想让后面的人动主键,再把它改成GENERATED ALWAYS也可以。
8.3 建表之后立刻要做的三件事
表建完之后,不要急着写业务代码。我个人习惯立刻做三件事:
第一,验证约束和数据完整性。插入一条合法数据、一条非法数据(比如负金额、非法状态值、不存在的外键),确认约束都正确拦截。
-- 正常插入 INSERT INTO cux_customer (customer_no, customer_name) VALUES ('C001', '张三'); INSERT INTO cux_order (order_no, customer_id, order_amount) VALUES ('O001', 1, 99.90); -- 异常插入:金额为负,应该报ORA-02290 INSERT INTO cux_order (order_no, customer_id, order_amount) VALUES ('O002', 1, -5);第二,生成建表DDL归档。用DBMS_METADATA.GET_DDL把四张表的DDL导出来存到版本库,这样后续任何环境重建、结构对比都有据可查。
SELECT DBMS_METADATA.GET_DDL('TABLE', 'CUX_ORDER') FROM DUAL;第三,创建一张表结构版本变更记录表。我自己习惯在项目里建一张tbl_schema_change_log,每次DDL变更都记录:变更时间、变更人、变更内容、影响范围、上线脚本编号。这张表看起来跟业务无关,但真到了审计、追查线上问题时,它就是“救命稻草”。
8.4 给初学者的五个建表自查清单
把这一章的实操做完,我建议你每次自己建表时都走一遍下面这个自查清单,能帮你躲掉90%的常规坑:
- 表名、列名是否遵守了项目的统一命名规范?有没有用中文、保留字或大小写混乱?
- 每个字段的数据类型选得是否合理?字符型是VARCHAR2而不是CHAR?金额是NUMBER(14,2)而不是FLOAT?
- 主键、唯一键、外键、检查约束是否齐全且显式命名?外键列有没有建配套索引?
- 是否需要分区?分区键是不是业务查询最常用的过滤条件?
- 是不是给表和关键字段写了COMMENT?半年后别人(包括你自己)能看懂吗?
这五条里,第二条最容易在“觉得差不多”的时候翻车,第四条最容易被新手忽视,第一条则是在多人协作的项目里最能减少沟通成本。如果你能坚持在每个建表脚本里都对照这个清单,你的表设计水平会明显拉开周围人一档。
9. 本期要点回顾,以及下一期内容的预告
这篇讲的是数据表对象里最核心的DDL操作。按照第10期的内容密度,你真正需要掌握的并不是把所有子句背下来,而是知道每个子句在什么场景下用、为什么用。比如CTAS适合快速造表但不适合要求完整约束的场景,19c加默认值列和间隔分区属于能让你“睡个好觉”的现代能力,闪回则是每个DBA和开发都应该刻进肌肉记忆的底线保障。
我在实际操作中还有一个体会:建表这件事,越往后越不是“会不会写CREATE TABLE”的问题,而是“会不会在设计和运维层面提前想清楚”的问题。约束命名规范、表空间规划、分区键选择、UNDO保留时间、回收站管理,这些都会在你上线半年后的某个深夜变成救命的细节。
下一期我计划把数据表对象的另一半补完:视图、物化视图和同义词。相比普通表,这些对象更像是“给查询和权限管理做的封装”,尤其物化视图在报表库和ODS层的用法,很多项目都踩过刷新策略没选对导致数据不一致的坑。我们下期继续。