ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Oracle大表加字段与注释:语法、锁表风险及生产实践

Oracle大表加字段与注释:语法、锁表风险及生产实践 上周接到一个需求线上商城要做用户手机号脱敏展示需要给用户信息表加一个字段存用户的脱敏手机号。我心想这还不简单一条ALTER TABLE加上一条COMMENT ON COLUMN就完事。结果真到生产环境执行的时候才发现加字段和字段注释这件事看起来是两条SQL实际上涉及语法写法、默认值行为、锁表风险、注释查询方式等一系列细节一个不注意就可能把大表卡死或者注释加错地方查不到。这篇就把我在Oracle里加字段和加字段注释的完整经验写出来从最基础的SQL语法到生产环境的避坑操作都覆盖到给正在折腾Oracle表结构变更的朋友做个参考。1. 加字段前先想清楚这次变更到底是哪种场景很多刚接触Oracle的人容易把加字段当成一个纯粹写SQL的动作上来就敲ALTER TABLE。但实际工作中加字段往往伴随着业务逻辑变化不同场景下的操作策略完全不一样先搞清楚这一点后面才不会踩坑。1.1 业务字段变更的几种常见来源我归纳了一下日常遇到的需求基本上逃不出下面这几类新业务属性补充比如用户表要新增一个nickname字段用于展示昵称。这种通常是纯新增老数据没有值新数据写入时才有。老数据回填比如给订单表加一个channel_code字段记录订单来源渠道。加完字段后往往还要跑一段UPDATE脚本把历史订单的渠道值回填进去。逻辑标记字段比如给表加is_deleted、status这类字段通常要求非空、有默认值这样老数据自动落到默认状态。外部系统对接比如需要记录第三方返回的流水号加一个third_party_trans_id字段可能需要唯一约束或者索引。这几种场景对应的加字段SQL难度都不大但后续的处理完全不是一回事。比如老数据回填的场景加字段只是第一步真正的重头戏是回填UPDATE语句怎么写才高效而逻辑标记字段的场景重点在于默认值和非空约束怎么搭配才能既满足业务需求又不锁死大表。1.2 加字段之前必须回答的三个问题我在实际操作中动手写SQL之前一定会先确认三件事第一这个表有多少行数据。几十万行和几千万行的处理方式完全是两个量级。如果表特别大加字段的方式会直接影响执行时间甚至影响线上服务。第二这个字段是否允许为空是否带默认值。这两个属性决定了Oracle在执行ALTER TABLE时是只改数据字典还是需要扫描全表去填充值。第三字段加完之后有没有存量数据要处理。如果只是新字段老数据为NULL那很简单但如果老数据必须有值就需要考虑回填策略。这三个问题想清楚了再去看具体的SQL语法心里就有底了。提示如果你是在开发库或者测试库操作直接执行问题不大。但生产环境的表结构变更尤其是大表建议先确认表行数和数据分布再决定用哪种姿势加字段。2. ALTER TABLE ADD COLUMN标准语法与多字段批量操作先说最基础的。Oracle里加字段的标准语法是ALTER TABLE ... ADD这条语句本身不难但写法上有几个容易忽略的细节我一个个说。2.1 最基础的加字段SQL给表添加一个字段最简单的写法ALTER TABLE user_info ADD mobile_mask VARCHAR2(20);这就给user_info表加了一个mobile_mask字段类型是变长字符串长度20允许为空。执行完之后可以查询字段信息确认是否成功SELECT column_name, data_type, data_length, nullable FROM user_tab_columns WHERE table_name USER_INFO AND column_name MOBILE_MASK;这里要特别注意Oracle在不加引号的情况下表名、列名都会自动转为大写存储。所以查询时写成大写USER_INFO、MOBILE_MASK才能查到写成小写反而查不到。还有一种情况容易被坑如果字段名恰好是Oracle的关键字或保留字直接写会报错。比如想加一个名为comment的字段ALTER TABLE user_info ADD comment VARCHAR2(200);很可能会报ORA-00903: invalid table name或者类似的语法错误。解决办法是给列名加双引号ALTER TABLE user_info ADD comment VARCHAR2(200);但加了双引号之后这个列名就变成了区分大小写的查询的时候也必须用双引号加正确的大小写非常麻烦。我的建议是建表设计的时候主动避开这些保留字别给自己找麻烦。2.2 一次加多个字段的写法与陷阱如果一次要加好几个字段有两种写法。第一种是每个字段一条ALTER TABLE语句ALTER TABLE user_info ADD mobile_mask VARCHAR2(20); ALTER TABLE user_info ADD mobile_bind_time DATE; ALTER TABLE user_info ADD mobile_area_code VARCHAR2(10);第二种是多个字段写在一起用一个括号包起来逗号分隔ALTER TABLE user_info ADD ( mobile_mask VARCHAR2(20), mobile_bind_time DATE, mobile_area_code VARCHAR2(10) );这两种写法在功能上没什么区别但我强烈建议用第二种原因有两点一是减少DDL语句的解析和提交次数。每一条ALTER TABLE都是一次隐式提交多条语句分开执行意味着多次提交。一旦中间某条失败前面的已经生效了回滚起来很麻烦。多条字段放在一条语句里要么全部成功要么全部失败保证了变更的原子性。二是在脚本管理上更清晰。一个表的结构变更脚本尽量用一条DDL表达完整意图后续看变更记录时一目了然。这里有个小细节Oracle的ADD语法中如果只有一个字段可以不加括号直接写如果是多个字段必须加括号。不加括号直接逗号分隔多个字段会报语法错误。这个坑我见过不少人踩过。2.3 默认值与NOT NULL的正确组合方式加字段时最常见的需求是字段不允许为空并且有默认值。比如加一个逻辑删除标记ALTER TABLE user_info ADD is_deleted NUMBER(1) DEFAULT 0 NOT NULL;这条SQL看起来没问题但如果表特别大执行时间会非常长。原因和Oracle版本的实现机制有关我后面专门说。这里先提醒一个写法上的细节DEFAULT和NOT NULL的顺序。标准的写法是DEFAULT 0 NOT NULL把默认值放在前面。有些版本对顺序不敏感但为了保险起见我建议你始终按照类型、默认值、约束这个顺序来写。还有一种情况也要注意如果只加默认值不加NOT NULL约束那么已有的老数据该是NULL还是NULL默认值只对之后插入的新数据生效。很多新手以为加了DEFAULT之后老数据也会自动有值这是个常见误解。-- 只加默认值老数据仍然为NULL ALTER TABLE user_info ADD is_deleted NUMBER(1) DEFAULT 0; -- 验证老数据的is_deleted仍然是NULL SELECT count(*) FROM user_info WHERE is_deleted IS NULL;如果你需要老数据自动填默认值那必须带上NOT NULL约束让Oracle认为所有行都必须有值从而触发全表填充。但这样一来就回到了上面说的性能问题。3. COMMENT ON COLUMN注释这事别嫌麻烦字段加完之后下一步就是加注释。Oracle里给字段加注释用的是COMMENT ON COLUMN语句。很多开发人员觉得注释可加可不加但我个人强烈建议每个字段都加。原因很简单一张表三个月后回头看如果没有注释你根本想不起来某个字段是干嘛的。3.1 加字段注释的标准SQL语法非常直接COMMENT ON COLUMN user_info.mobile_mask IS 脱敏后的手机号;如果表有模式前缀可以把模式名带上COMMENT ON COLUMN scott.user_info.mobile_mask IS 脱敏后的手机号;这里要注意COMMENT ON COLUMN和ALTER TABLE ADD是两条独立的语句Oracle不支持在ALTER TABLE里直接附带字段注释。所以实际执行时总是先加字段再加注释两条SQL一起执行。我在写脚本时习惯把这两条放在一起中间加个注释说明-- 加字段 ALTER TABLE user_info ADD mobile_mask VARCHAR2(20); -- 加字段注释 COMMENT ON COLUMN user_info.mobile_mask IS 脱敏后的手机号;还有个细节注释内容如果是中文要确保客户端和数据库的字符集设置一致否则可能出现乱码。怎么排查呢先查一下数据库的字符集SELECT userenv(language) FROM dual;常见的中文字符集是SIMPLIFIED CHINESE_CHINA.AL32UTF8或ZHS16GBK。然后确认客户端的NLS_LANG环境变量和数据库保持一致。我遇到过好几次注释写进去变成? 的情况基本都是字符集不匹配导致的。3.2 如何验证注释是否加成功加完注释之后可以通过数据字典视图验证。Oracle提供了user_col_comments视图专门存当前用户下所有表的字段注释SELECT table_name, column_name, comments FROM user_col_comments WHERE table_name USER_INFO AND column_name MOBILE_MASK;如果想看所有字段的注释可以去掉column_name条件SELECT column_name, comments FROM user_col_comments WHERE table_name USER_INFO;对于DBA角色可以用all_col_comments查看所有有权限访问的表的注释字段比user_col_comments多一个owner列SELECT owner, table_name, column_name, comments FROM all_col_comments WHERE table_name USER_INFO AND owner SCOTT;如果你用的是PL/SQL Developer或者DBeaver这类图形工具直接在表结构查看器里也能看到注释但脚本验证的方式更严谨适合放进自动化发布流程里做校验。3.3 给表本身加注释既然聊到注释顺带说一句字段注释重要表注释同样重要。给表加注释的语法类似COMMENT ON TABLE user_info IS 用户信息表;查询表注释用SELECT table_name, comments FROM user_tab_comments WHERE table_name USER_INFO;表注释和字段注释是两个维度的信息但配合起来才能让一张表的结构变得可读。我见过不少项目表名起得莫名其妙也没有表注释新同事接手的时候只能靠猜维护成本非常高。既然加注释就几条SQL的事真没必要省。4. 生产环境实战大表加字段的锁表风险与应对前面说的都是语法层面的操作真正让加字段这件事变得有难度的是性能问题。给一张几千万行的大表加字段如果方式不对可能直接拖垮生产库。这块是我最想重点聊的。4.1 11g和12c加默认值字段的本质区别先看一个具体的例子。假设order_info表有5000万行数据现在要加一个is_deleted字段要求非空、默认0ALTER TABLE order_info ADD is_deleted NUMBER(1) DEFAULT 0 NOT NULL;这条SQL在Oracle 11g和Oracle 12c及更高版本上的行为完全不同。在Oracle 11g及更早版本中这条语句会扫描全表对每一行写入默认值0同时生成大量的undo和redo日志。5000万行的表执行时间可能是几分钟到几十分钟期间表会被锁住所有的DML操作都被阻塞。生产环境碰上这种情况基本等于业务中断。在Oracle 12c及以上版本中Oracle引入了metadata-only DEFAULT的优化加带默认值且非空的字段只修改数据字典不实际填充数据。执行时间从几十分钟缩短到秒级。当你查询老数据时Oracle会在内存中自动补上默认值0对应用完全透明。所以如果你用的是Oracle 11g操作大表加非空默认值字段之前一定要评估好窗口时间和锁表风险。如果用的是12c及以上这个优化可以帮你省掉很多麻烦。需要说明的一点是如果你加的是允许为空的字段不管哪个版本ALTER TABLE ... ADD都只是修改数据字典速度很快不涉及全表扫描。真正的分水岭是非空默认值这个组合。4.2 大表加字段的推荐步骤如果很不幸你正在用Oracle 11g并且必须在生产环境大表上加一个非空默认值字段我建议按下面的步骤操作。第一步评估表大小。先确认表有多少行、占多少空间SELECT count(*) FROM order_info; SELECT bytes/1024/1024/1024 AS size_gb FROM user_segments WHERE segment_name ORDER_INFO;第二步选择业务低峰期执行。这个操作会在执行期间锁表任何针对这张表的增删改都会等待。低峰期可以把对业务的影响降到最低。第三步执行前做好备份。加字段本身是DDL不像DML那样好回滚。建议提前用CREATE TABLE order_info_bak_20250101 AS SELECT * FROM order_info备份全表数据万一后续变更有问题可以恢复到备份点。第四步执行变更SQL。在这个过程中随时观察锁等待情况SELECT object_name, session_id, locked_mode FROM v$locked_object WHERE object_name ORDER_INFO;第五步验证结果。确认字段、注释、默认值、数据值都对再对外放开操作。还有个小技巧如果你真的担心一条ALTER TABLE锁太久可以分步走先加允许为空的字段然后写UPDATE分批回填数据最后再MODIFY加非空约束。这种方式对业务的影响小得多但操作步骤更多需要编写回填脚本适合有专门维护窗口的场景。4.3 加字段失败的回滚思路DDL语句本身不带事务回滚能力。也就是说ALTER TABLE order_info ADD xxx;执行成功后如果发现加错了直接执行ROLLBACK是没用的只能重新执行一条ALTER TABLE ... DROP COLUMN把字段删掉。ALTER TABLE order_info DROP COLUMN is_deleted;但删字段同样有风险。大表删字段也是一次全表操作会消耗大量资源。Oracle从11g开始支持DROP COLUMN的标记删除方式可以分批次物理删除ALTER TABLE order_info DROP COLUMN is_deleted CHECKPOINT 1000;这个CHECKPOINT 1000表示每处理1000行做一次检查点减少重做日志的积累压力。我个人的习惯是加字段之前把变更脚本放在一个独立的SQL文件里执行前仔细检查字段名是否拼错、类型长度是否符合业务预期、默认值是否正确。因为DDL的回滚成本太高最好的策略就是执行前多检查几遍宁可慢一点也别上线后才发现问题。5. 从Oracle到MySQL/SQL Server别把语法记混很多团队实际是多种数据库混用的同一个项目在开发环境用MySQL生产却用Oracle或者从Oracle迁移到SQL Server。这时候最容易出问题的就是语法习惯串了。我把三个主流数据库加字段和加注释的语法整理了一下方便对照。5.1 三库加字段语法差异速查操作OracleMySQLSQL Server加单个字段ALTER TABLE t ADD col INT;ALTER TABLE t ADD COLUMN col INT;ALTER TABLE t ADD col INT;加多个字段ALTER TABLE t ADD (col1 INT, col2 VARCHAR2(20));ALTER TABLE t ADD COLUMN (col1 INT, col2 VARCHAR(20));ALTER TABLE t ADD col1 INT, col2 VARCHAR(20);加字段注释COMMENT ON COLUMN t.col IS xx;ALTER TABLE t MODIFY COLUMN col INT COMMENT xx;EXEC sp_addextendedproperty ...非空默认值ALTER TABLE t ADD col INT DEFAULT 0 NOT NULL;ALTER TABLE t ADD COLUMN col INT DEFAULT 0 NOT NULL;ALTER TABLE t ADD col INT DEFAULT 0 NOT NULL;从表格能明显看出来Oracle和SQL Server在加字段时语法比较接近都不需要写COLUMN关键字而MySQL需要写COLUMN。多字段时Oracle要用括号包起来MySQL和SQL Server直接逗号分隔。注释这块差异最大Oracle用COMMENT ONMySQL用MODIFY COLUMNSQL Server则用扩展属性来实现。5.2 注释信息的查询方式对比数据库迁移时注释迁移往往是最容易被忽略的部分。三库查询注释的方式也完全不同。Oracle查注释用前面说的数据字典视图SELECT column_name, comments FROM user_col_comments WHERE table_name USER_INFO;MySQL查表结构时直接带注释SHOW FULL COLUMNS FROM user_info;SQL Server查扩展属性用的是系统存储过程SELECT objname, name, value FROM fn_listextendedproperty(NULL, schema, dbo, table, USER_INFO, column, NULL);如果你做过Oracle到MySQL的迁移就知道Oracle的注释不会自动带过去需要在MySQL里重新执行一遍ALTER TABLE ... MODIFY COLUMN ... COMMENT ...。写迁移脚本时一定要把注释单独列出来否则迁完后表结构光秃秃的维护体验非常差。5.3 迁移脚本的注意点我最近刚好帮朋友把一个系统从Oracle迁到SQL Server踩了一个挺典型的坑原Oracle脚本里用了VARCHAR2SQL Server直接执行会报错因为VARCHAR2是Oracle特有类型SQL Server里对应的是VARCHAR或NVARCHAR。如果是中文场景SQL Server建议用NVARCHAR存储Unicode字符避免乱码。另外Oracle的NUMBER(1)这种布尔语义字段在SQL Server里可以改成BIT类型语义更清晰。DATE类型在Oracle里和SQL Server里的行为也不完全一样SQL Server的DATE不包含时分秒如果业务上需要时间要用DATETIME2。这些差异在加字段时还好主要是写迁移DDL脚本时要注意。我自己的习惯是维护一套标准的DDL转换对照表每次迁移时逐项检查字段类型、默认值、注释语法避免靠脑子记。6. 实际工作中总结的一套加字段标准化动作最后分享一套我自己日常加字段的固定流程。这套流程不一定适合所有人但经过多次线上操作验证帮我避免了不少低级失误。6.1 字段命名与注释规范先行加字段之前我会先确认字段命名是否符合团队的命名规范。比如布尔类型字段统一用is_开头如is_deleted、is_active时间字段统一用_time或_date结尾如create_time、bind_date字符类型字段要确认长度别拍脑袋定一个要评估业务上最大可能的长度留出余量数字类型的精度要明确比如金额字段用NUMBER(10,2)不要直接用NUMBER注释的规范我习惯遵循一句话说清楚这个字段存什么、什么格式、谁在维护。比如COMMENT ON COLUMN user_info.mobile_mask IS 脱敏后的手机号格式138****1234;比单纯的手机号注释信息量大得多。如果是状态字段我还会把枚举值和含义都写进去COMMENT ON COLUMN order_info.order_status IS 订单状态0-待支付1-已支付2-已发货3-已完成4-已取消;这样后来的人不用翻代码就能知道状态值都代表什么。6.2 我的变更脚本模板我一般会用一个统一的SQL文件模板来写变更脚本-- 变更说明用户信息表新增脱敏手机号字段 -- 变更人xxx -- 变更日期2025-01-18 -- 执行环境生产 -- 1. 新增字段 ALTER TABLE user_info ADD mobile_mask VARCHAR2(20); -- 2. 新增字段注释 COMMENT ON COLUMN user_info.mobile_mask IS 脱敏后的手机号格式138****1234; -- 3. 新增表注释如果表还没有注释 COMMENT ON TABLE user_info IS 用户信息表; -- 4. 验证脚本 SELECT column_name, data_type, data_length, nullable FROM user_tab_columns WHERE table_name USER_INFO AND column_name MOBILE_MASK; SELECT comments FROM user_col_comments WHERE table_name USER_INFO AND column_name MOBILE_MASK;脚本头部写清楚变更说明、变更人、日期、执行环境方便追溯。变更内容用编号分步列出执行完第4步验证脚本确认字段和注释都在。这个模板我在多个项目里一直在用每次上线的变更脚本格式统一后续要回溯某个字段是什么时候加的、谁加的直接查脚本记录就行。6.3 执行前核对清单最后把加字段执行前要核对的事项打包成清单贴在下面每次执行前过一遍表名和字段名拼写是否正确字段类型和长度是否符合业务预期默认值写的是否正确会不会影响存量数据是否带非空约束如果带表大小是否允许全表填充字段注释是否已经准备好中文是否会有乱码风险是否处于业务低峰期关键大表的变更是否已通知相关团队执行后验证脚本是否已准备好这套清单看着简单但真的能拦住大部分低级错误。我在一次生产变更中就因为没仔细核对字段长度把VARCHAR2(10)写成了VARCHAR2(5)上线后应用写入直接报ORA-12899: value too large最后只能再跑一次MODIFY改长度多折腾了一轮。从我的经验来看Oracle加字段和字段注释这件事核心不在于SQL语法本身有多难而在于你面对的是什么样的表、什么样的数据量、什么样的业务约束。语法只是基础真正拉开差距的是对DDL执行机制的理解以及对变更风险的评估能力。希望这篇文章里的细节和踩坑经验能帮你少走一些弯路。
返回列表