聊一个非常典型的翻车现场月底跑批刚启动业务群里就开始刷屏“写入订单失败”数据库告警栏里躺着一行醒目的错误——ORA-14400: 插入的分区键未找到对应分区。紧跟着磁盘空间告警也亮了登录服务器一看/u01 已经 100%。这时候运维、DBA、应用开发三方同时被拉进会问题却越查越多分区表缺了 8 月份的分区表空间文件又扩不动了因为底层磁盘根本没空间。这个标题所描述的场景就是上面这个事故的浓缩版一方面要“护流程”用前置机制防止分区缺失导致数据插入失败另一方面要提前制定磁盘空间清理和表空间扩展预案关键时刻能快速响应、快速恢复。今天我把这套东西从头到尾梳理一遍覆盖分区路由机制、自动建分区方案、MAXVALUE 兜底设计、空间监控阈值、紧急清理三板斧、表空间扩展预案和故障复盘都是可以直接拿回团队落地的内容。不管是刚接手分区表的初级 DBA还是已经在线上踩过坑的运维老手这篇文章都会有点用。至少下次再遇到 ORA-14400 或者磁盘打满你不会满脑子只剩“重启试试”和“删日志保平安”。1. 先搞清楚为什么分区缺失会让数据写不进去1.1 分区表的写入路由机制分区表对上层应用来说就是一张普通表应用根本不知道自己写进去的数据会被拆到多少个物理段里。但对数据库内核来说每个分区都是一个独立的存储单元有自己的段、自己的空间管理、自己的统计信息。写入数据时优化器会根据分区键的值按照分区定义时的边界规则把这条记录路由到对应的分区。以最常见的范围分区为例CREATE TABLE orders ( order_id NUMBER, order_date DATE, customer_id NUMBER, amount NUMBER ) PARTITION BY RANGE (order_date) ( PARTITION p202501 VALUES LESS THAN (TO_DATE(2025-02-01,YYYY-MM-DD)), PARTITION p202502 VALUES LESS THAN (TO_DATE(2025-03-01,YYYY-MM-DD)), PARTITION p202503 VALUES LESS THAN (TO_DATE(2025-04-01,YYYY-MM-DD)) );这条 SQL 定义得很清楚每个分区负责一个月的订单数据。比如 order_date 是 2025-02-15 的记录数据库会比对分区边界发现它大于等于 2025-02-01 且小于 2025-03-01于是放进 p202502 分区。问题恰恰出在这个“边界匹配”上。如果某条数据的 order_date 是 2025-08-01数据库从上往下扫描所有分区的上界发现没有任何一个分区的 LESS THAN 上界能容纳这条数据同时也找不到 MAXVALUE 分区那这条插入请求就会被直接拒绝。错误码就是最常见的 ORA-14400。可以做一个不严谨但很好懂的生活类比小区信报箱按月份编号已经做好的格子只到 3 月。4 月 1 号邮递员来送报纸发现 4 月的格子不存在又没有“其他月份”的公共格子那这份报纸只能退回去。数据库在处理分区键匹配时比邮递员更严格它不会给你把报纸放在门口而是直接返回错误。1.2 分区缺失的三种典型场景我见过太多因为分区缺失导致写入失败的事故仔细复盘下来真正的原因无非三种。第一种是周期到了但没人提前建分区。这是最常见的情况。很多团队的建分区任务是用 crontab 调一个 shell 脚本脚本里写死了下个月或者下下个月。如果脚本发布新版本时被误删了或者服务器时间跨月后 crontab 没有生效月初第一天业务一开闸立刻就是大面积写入失败。还有一种隐蔽情况脚本里用了 date -d 1 month 来算下个月但在每月 31 号执行时会直接跳到下下个月导致应建的分区没建出来。第二种是应用写入了异常的未来时间。比如前端页面某个下拉框解析出错把一个 2035 年的日期传到了后端或者批处理任务的计算逻辑在跨年时少加了年份把时间算到了明年的同一天。这类数据一旦超过当前所有分区的上界同样会被拒绝。这种问题尤其坑人因为业务代码表面上看完全正常只有到数据库层才能发现分区键值不对劲。第三种是运维变更把分区弄丢了。从生产库导出导入、做表重建、做分区归档清理时只恢复了部分分区或者清理脚本由于边界算错把下个月的分区也删了。我曾见过一个清理脚本用date %m -1算上个月12 月份跑的时候得到的是 11但对应的年份没有减 1结果把明年 11 月的分区当成上个月分区删掉了数据没删但分区结构已经残了。不管哪种原因结果都一样数据写不进去业务停摆。而且这类故障的恢复往往不是加一个分区那么简单如果表上有全局索引你补完分区还得重建索引整个恢复时间会被拉得很长。2. 前置防护让分区永远不缺2.1 固定周期建分区任务先把最基础的工作做扎实用一个可靠的定时任务在上个月或更早的时间点提前把未来两个月的分区全部建好。为什么要提前两个月而不是提前一天因为定时任务本身可能挂发布系统可能出问题DBA 可能在休假。提前两个月意味着即使某一轮调度失败你还有一个月的时间去发现并修复而不是在业务已经报错的时候连夜紧急建分区。这是一个我多次在项目里使用的 Oracle 建分区 shell 脚本核心逻辑非常简单#!/bin/bash # 每月 25 日 02:00 由 cron 触发预创建下个月和下下个月的分区 NEXT1$(date -d 1 month %Y%m) NEXT2$(date -d 2 month %Y%m) sqlplus -s app/app_passwordpdb EOF SET FEEDBACK ON WHENEVER SQLERROR EXIT FAILURE ALTER TABLE app.orders ADD PARTITION p${NEXT1} VALUES LESS THAN (TO_DATE($(date -d 2 month %Y-%m-01), YYYY-MM-DD)); ALTER TABLE app.orders ADD PARTITION p${NEXT2} VALUES LESS THAN (TO_DATE($(date -d 3 month %Y-%m-01), YYYY-MM-DD)); EXIT; EOF注意几个细节WHENEVER SQLERROR EXIT FAILURE 保证 SQL 执行失败时脚本能返回非零退出码crontab 会将失败输出记录到日志中便于后续告警。查询下个月的月初日期时用date -d 2 month %Y-%m-01得到下下个月的 1 号作为下个月分区的上界这样分区边界不会有时间误差。MySQL 没有内置的 interval 分区自动扩展能力Oracle 有 interval partitioning但 MySQL 8.0 的 RANGE COLUMNS 分区仍然需要手工创建新分区所以通常用事件调度器来实现同样的效果CREATE EVENT ev_create_order_partition ON SCHEDULE EVERY 1 MONTH STARTS 2025-07-26 02:00:00 DO BEGIN SET next_month DATE_FORMAT(DATE_ADD(NOW(), INTERVAL 1 MONTH), %Y%m); SET next_date DATE_ADD(DATE_ADD(NOW(), INTERVAL 2 MONTH), INTERVAL -1 DAY); SET sql CONCAT( ALTER TABLE app.orders ADD PARTITION (PARTITION p, next_month, VALUES LESS THAN (TO_DAYS(, DATE_FORMAT(next_date, %Y-%m-%d), ))) ); PREPARE stmt FROM sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; END;这个方案有个隐藏问题EVENT 里面拼接 SQL 如果出错不会自动重试也不一定触发告警。所以无论用哪种方式都要在任务旁加一个监控探针最简单的做法是每天检查目标表最大分区是否已经覆盖到下个月没有就告警。2.2 用“万能分区”兜底给新生数据一个收容所不管你建分区任务多勤快总有万一凌晨 3 点定时任务磁盘满了没跑成新版本发布时间窗口导致任务被跳过或者应用端传进来一个你没预料到的时间。为了不被这些“万一”干趴下最稳妥的做法是给分区表加一个兜底分区。Oracle 叫 MAXVALUE 分区PostgreSQL 叫 DEFAULT 分区MySQL 同样使用 MAXVALUE 分区ALTER TABLE orders ADD PARTITION p_future VALUES LESS THAN (MAXVALUE);加了 MAXVALUE 分区之后任何超出已有分区边界的数据都会落到这个分区里插入永远能找到归宿完全不会有 ORA-14400 或者 MySQL 1526 这类错误。代价是查询性能会有一定影响因为 MAXVALUE 分区像一个黑洞凡是分区键范围条件里包含了可能超出边界的数据优化器会额外访问这个分区分区裁剪失效。更麻烦的是对于真正写脏的异常数据比如 2035 年的时间它们会堆积在这个分区里如果不做清洗这个分区会越涨越大最终变成一头无处安放的巨兽。所以 MAXVALUE 分区只能作为安全网不能当成垃圾场。合理的做法是加一个清洗任务定期将 MAXVALUE 分区中合法的数据拆分到真正的逻辑分区里或者将异常数据搬到一张专门的异常表备份。MySQL 里的操作是通过 REORGANIZE 来实现的ALTER TABLE orders REORGANIZE PARTITION p_future INTO ( PARTITION p202508 VALUES LESS THAN (TO_DAYS(2025-09-01)), PARTITION p_future VALUES LESS THAN (MAXVALUE) );这个操作会在线进行对业务影响相对较小但同样要避开业务高峰。2.3 不同数据库的自动分区方案对比很多团队是多数据库共存的搞清楚各个数据库的“自动分区”能力边界能少踩很多坑。数据库自动建分区能力兜底机制生产推荐做法OracleINTERVAL 分区可在插入新数据时自动创建分区不建议用 MAXVALUE会造成数据堆积和不可控INTERVAL 分区 每月巡检任务定期清理归档MySQL无内置自动建分区功能只能靠 EVENT 或外部调度MAXVALUE 兜底可用但需要清洗任务EVENT 提前建分区 MAXVALUE 兜底 定期 REORGANIZEPostgreSQL无内置自动建分区推荐 pg_partman 扩展DEFAULT 分区兜底pg_partman 定期执行 run_maintenanceSQL Server无自动需要维护作业定期 SPLIT RANGE无严格兜底可预留一个极远期边界定期 SPLIT 对作业结果做监控这张表的重点是不要因为 Oracle 有 INTERVAL 分区就完全托管也不要因为 MySQL 没有自动分区就焦虑。自动化的程度不代表稳定性能监控、能被发现、能快速恢复才是真正的稳定。3. 空间耗尽前的预警与紧急清理3.1 监控阈值怎么定分区只是第一步磁盘空间和表空间管理才是更长期的战场。很多团队只在磁盘亮红的时候才想起来清理这是非常被动的做法。空间管理要建立“两级监控”思维第一级是数据库表空间使用率第二级是操作系统磁盘使用率。两者缺一不可因为表空间使用了 60%不代表磁盘还有 60%——同一块盘上可能还躺着数据库备份、应用日志、归档日志、binlog。阈值设置要分两档85% 是预警线到达后需要生成工单、评估增长速率、确定扩容或清理计划95% 是紧急线到了这个水位说明按当前增长速度已经没有多少缓冲时间了必须立刻介入。下面这条 SQL 是 Oracle 查表空间使用率时最常用的工具SELECT df.tablespace_name, ROUND(df.maxbytes / 1024 / 1024 / 1024, 1) AS max_gb, ROUND((df.maxbytes - fs.free_bytes) / 1024 / 1024 / 1024, 1) AS used_gb, ROUND(fs.free_bytes / 1024 / 1024 / 1024, 1) AS free_gb, ROUND((df.maxbytes - fs.free_bytes) / df.maxbytes * 100, 1) AS pct_used FROM (SELECT tablespace_name, SUM(bytes) maxbytes FROM dba_data_files GROUP BY tablespace_name) df, (SELECT tablespace_name, SUM(bytes) free_bytes FROM dba_free_space GROUP BY tablespace_name) fs WHERE df.tablespace_name fs.tablespace_name ORDER BY pct_used DESC;有一个细节非常关键df.maxbytes 取的是 dba_data_files 里的最大可扩展大小。如果数据文件的 AUTOEXTEND 开了且 MAXSIZE 设了上限按 maxbytes 算出的使用率才能真正反映表空间的极限如果只看当前已分配大小容易产生“明明还有很大空间”的错觉。很多事故恰恰是 AUTOEXTEND 悄悄把数据文件从 32G 撑到了 128Gmaxsize 到了之后才开始报错而监控一直没看出来。再说具体一点增长速率比绝对值重要。假设表空间总大小 2TB当前已用 1.9TB只剩 100GB。每月从 orders 分区表产生约 120GB 的净增长那这个库其实只有不到一个月的时间窗口。这时候不能等 95% 告警再处理85% 时就应该计算出“剩余可用天数”并推动扩容。把剩余可用空间除以日均增长量得到的就是你的安全余量。3.2 紧急清理三板斧当磁盘真的快满了别慌按照优先级做三件事。第一板斧是清日志类文件。应用日志、数据库告警日志、Oracle 归档日志、MySQL binlog、临时导出文件这些是最容易快速释放空间的对象。但清理前必须确认备份链路binlog 和归档日志不是可以无脑删的删除太早会导致从库追不上、PITR 时间窗口缩短。一个相对安全的 MySQL binlog 清理语法是PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 3 DAY);这句话的意思是只保留最近 3 天的 binlog更早的全部删除。具体保留天数要看全量备份频率和从库延迟情况一般至少保留一个全量备份周期再加半天到一天的冗余。第二板斧是清理临时段和表碎片。Oracle 数据库中出现大量排序、hash join 操作后临时表空间可能暴涨而会话结束后临时段不一定会立即释放。遇到这种情况最直接的手段是查当前临时表空间使用情况必要时重建临时表空间或扩容 tempfile。MySQL 侧则要注意临时表空间和 undo 表空间在 8.0 里同样会自动增长很容易被忽略。针对表内部的碎片Oracle 提供了在线收缩方式ALTER TABLE app.orders ENABLE ROW MOVEMENT; ALTER TABLE app.orders SHRINK SPACE CASCADE;执行前必须明确一点SHRINK 会在线产生大量的 undo 和 redo相当于把表重写一遍对 I/O 和 CPU 的压力非常大绝不能放在业务高峰期做。而且很多人容易忽略最后一步——收缩之后的索引可能也需要重建。第三板斧是清理过期分区。这是分区表最大的优势历史数据可以直接通过 TRUNCATE PARTITION 或 DROP PARTITION 来清理操作的是数据字典和段空间速度比 DELETE 记录快几个数量级因为 DELETE 一条条产生 undo/redo而分区级别操作不产生同量级的 redo。例如ALTER TABLE app.orders DROP PARTITION p202401;这里有几个容易翻车的注意点。第一表上有全局索引时DROP/TRUNCATE 分区会导致全局索引失效需要重建本地索引不受影响。第二删除前必须确认该分区的数据已经完成归档或备份。第三清理脚本里的“保留期”要写清楚比如“只能删 6 个月以前的分区”并且脚本要带 where 条件核对分区名是历史的而不是未来的防止误删。3.3 一个可落地的空间巡检脚本把上面的思路落成一个每周巡检脚本是规避重复事故最有效的手段。我的常见做法是写一个 shell 脚本通过 sqlplus 定时把表空间使用率导出到日志再结合钉钉/邮件通知推给值班人员。#!/bin/bash # 每周五 23:30 执行巡检表空间使用率 TOP sqlplus -s / as sysdba EOF SET PAGESIZE 200 SET LINESIZE 200 COL tablespace_name FORMAT A20 COL max_gb FORMAT 9999.9 COL used_gb FORMAT 9999.9 COL free_gb FORMAT 9999.9 COL pct_used FORMAT 999.9 SELECT df.tablespace_name, ROUND(df.maxbytes / 1024 / 1024 / 1024, 1) AS max_gb, ROUND((df.maxbytes - fs.free_bytes) / 1024 / 1024 / 1024, 1) AS used_gb, ROUND(fs.free_bytes / 1024 / 1024 / 1024, 1) AS free_gb, ROUND((df.maxbytes - fs.free_bytes) / df.maxbytes * 100, 1) AS pct_used FROM (SELECT tablespace_name, SUM(bytes) maxbytes FROM dba_data_files GROUP BY tablespace_name) df, (SELECT tablespace_name, SUM(bytes) free_bytes FROM dba_free_space GROUP BY tablespace_name) fs WHERE df.tablespace_name fs.tablespace_name ORDER BY pct_used DESC; EOF脚本本身不复杂关键在于“每周固定跑”这几个字。手工巡检是不可靠的人总会忘记脚本不会。4. 表空间扩展预案的设计与演练4.1 静态扩容加数据文件与 AUTOEXTEND 的正确姿势空间问题迫在眉睫时最直接的方案就是在表空间里增加数据文件。Oracle 的写法是ALTER TABLESPACE app_data ADD DATAFILE /u01/app/oracle/oradata/ORCL/app_data02.dbf SIZE 32G AUTOEXTEND ON NEXT 2G MAXSIZE 128G;这段 SQL 有几个细节值得展开讲。SIZE 32G 是初始大小AUTOEXTEND ON 是允许自动增长NEXT 2G 是每次自动扩展的步长MAXSIZE 128G 是文件上限。为什么 NEXT 不要设置得太小因为自动扩展是串行操作步长太小会导致频繁触发文件扩展数据库会有短暂的停顿。2G 是相对合理的起步值如果文件所在的存储是大容量机械盘或云盘可以设置为 4G 甚至 8G。另一方面MAXSIZE 一定不能省略。我见过一些团队把数据文件设置成无限增长想着“反正存储很大”结果一个失控的 SQL 把磁盘写爆数据库实例直接 hang 住恢复成本极高。无限自动扩展等于没有扩展边界不如设置一个合理的 MAXSIZE同时配套监控触达后再手动扩。MySQL 侧的做法和 Oracle 不太一样。在 innodb_file_per_table 打开的情况下每个 InnoDB 表的数据都在自己的 .ibd 文件里不存在一个统一的“表空间加数据文件”概念。空间不足时通常的做法是给数据目录所在的文件系统扩容云盘直接在控制台扩容后让文件系统识别LVM 环境则用 lvextend 和 resize2fs 在线扩展。MySQL 8.0 的 undo 表空间和临时表空间默认可以自动扩展所以磁盘空间一旦释放数据库一般不需要做额外操作。但这里有一个反直觉的点能加数据文件不代表就应该加。数据文件数量过多会拖慢备份恢复、增加 rman 通道并行复杂度、消耗文件句柄。所以扩展之前先算清楚是不是真的需要扩展这个表空间还是历史数据归档之后空间本来就够用。4.2 动态迁移表空间腾挪与数据重组更高级的扩展预案不是“加文件”而是“做迁移”。当一个表空间里 80% 的数据都是超过一年的历史数据而且业务上已经不需要在线访问时加再多文件也只是给历史数据腾地方。正确思路是归档迁移。第一步是定位大对象。Oracle 用 dba_segments 按字节排序找 TOP 对象MySQL 查 information_schema.tables 里的 data_length 加 index_length。找到大表之后分析它的分区时间范围找出哪些分区已经超过业务要求的保留期。第二步是确认归档方案。如果数据还需要被偶尔查询可以考虑迁移到独立的历史库或者把旧分区逻辑导出后存储到冷备如果数据只是合规留档可以直接 DROP 分区只保留元数据和统计信息。很多团队在做这一步时纠结“万一以后要查怎么办”我的建议是先做“可回滚的归档”把旧分区导出为文件并校验完成再在线上删除分区。文件还在随时可以倒灌回来。第三步是真正执行。以 Oracle 为例可以用 expdp 按分区导出或用 CTAS 把旧分区复制到历史表再接 expdp。MySQL 则通常用CREATE TABLE ... SELECT到目标库或者直接拷贝 .ibd 文件。迁移期间要确保磁盘有双份空间最忌把导出文件放在目标表空间同一块磁盘上边导出边把磁盘写满然后发现恢复都没空间。这套操作熟练之后你面对的第一选择永远不是加文件而是问一句这些数据真的需要留在原来的表空间里吗4.3 预案演练清单预案不能只停留在文档里。一个从没演练过的预案在真实故障面前大概率会卡壳。下面是我在团队内部推行的一份动作清单情景关键操作期望完成时间表空间使用率超过 85%分析增长趋势生成扩容或清理工单72 小时内完成表空间使用率超过 95%执行紧急扩容或紧急清理恢复安全水位30 分钟内解除写入风险分区缺失导致 ORA-14400运行 precreate_partition.sh 或手工补建分区15 分钟内恢复写入补分区后全局索引失效重建对应表的全局索引30 分钟内完成磁盘空间告警清理 binlog/归档、检查大文件、联系存储扩容1 小时内释放足够空间演练的方式最好是故障注入不要做“纸上谈兵”式的讲解。比如故意停掉自动建分区任务两小时看监控和告警是否能在预期时间内发现或者在测试库把表空间刻意填到 95%再演练紧急清理流程。只有真正跑过一遍才知道实际执行时谁会登录哪台机器、SQL 是否有权限、文件目录是否可写。另外一个容易忽略的点是角色分工。预案里必须写清谁是第一响应人、谁是备份响应人、谁来决策是否切换备库、谁来负责对外通知业务方。没有角色分工的预案本质上只是一份技术文档不是预案。5. 故障复盘一次分区缺失加空间不足的连环事故5.1 事故时间线与根因分析分享一次我参与处理的真实案例帮助你把前面的内容串起来。某业务线的订单表 orders 按月份做范围分区每月 1 日 00:05 开始应用会集中写入上一个自然日的订单也就是说每月首小时是写入高峰。结果某月 1 日 00:08 开始应用日志里持续刷出 ORA-14400订单写入全部失败。第一轮处理时DBA 从告警平台看到错误码判断是缺分区于是手动执行了补分区语句写入恢复了。但过了一个小时新的告警又来了磁盘使用率 98%数据库开始出现写入超时。运维登录服务器后发现分区补建本身不占多少空间但之前为了救急有人把表空间里唯一的数据文件 autoextend 打开并设了很大的 maxsize数据文件已经悄悄涨到了接近 200G而底层磁盘只有 220G。也就是说第一次处理只是满足了数据库层没有检查 OS 层和存储层。最后处置方案是从已归档的历史库确认 6 个月前的订单数据已经完成备份直接 DROP 掉线上 5 个历史分区释放了近 90G 空间再补建当月分区重建了受影响的全局索引。整个过程从发现到恢复花费了将近 3 个小时业务侧已经堆积了大量待重试的订单请求。复盘下来暴露的问题至少有四个建分区任务没有配套存活监控任务挂了两周没人发现表空间扩容只做了数据库层没有联动检查 OS 磁盘历史分区清理脚本没有自动核对归档副本紧急预案从未演练过导致处理人员边翻文档边执行。5.2 我们能提前做的 5 个动作第一建分区任务必须带有自身的可观测性。不要只把 crontab 挂在服务器上就算完事要在任务执行成功时更新一张心跳表或者输出一条带时间戳的日志用外部监控定期检查心跳是否在预期时间内被更新一旦超过 26 小时没有新记录立即告警。第二表空间使用率的计算必须基于 maxsize而不是当前文件大小。这样即使 AUTOEXTEND 在跑监控也能提前看到表空间接近极限。同时磁盘监控必须覆盖数据库文件所在的所有文件系统包括数据目录、归档目录、备份目录信息要汇总在同一张看板上。第三历史分区清理脚本必须带“三重校验”当前日期分区是否在保留期内、归档副本是否已存在、目标分区名是否匹配历史前缀。三者缺一脚本就失败退出并告警绝不允许用 DELETE 硬删任何表数据。第四表上有全局索引时涉及分区的维护操作前必须先查索引类型写成标准检查项。最稳的方式是分区维护任务本身就把全局索引重建包含进去不让手工操作来背锅。第五每一个季度做一次 10 分钟的故障注入演练。把自动建分区任务停掉、把某个表空间填充到 90% 以上看看监控能否在 10 分钟内识别出来预案里写的响应人是否真的能联系到。发现问题之后立刻更新预案保证文档里的命令还能跑通。最后再说两句实在话这套东西你听起来可能觉得大多都是常识但每一次线上事故几乎都是常识断链导致的。我现在养成了一个习惯每个月初手动查一次关键分区表的最大分区边界顺手看一眼表空间使用率趋势再确认下月初分区任务的心跳还在更新。这三件事加起来不到五分钟但足够让我在业务高峰之前睡个好觉。建分区、控空间、做预案真正难的从来不是技术而是每一个环节都有人盯着出了异常都有人知道。希望这篇经验能帮你把“盯着”这件事自动化起来别再等到 ORA-14400 刷屏才想起还有分区这回事。
