原创

MySQL 按月分区清理前,先核对唯一键、外键和补写边界

每个月保留一份事件记录,过期时执行 DROP PARTITION,看起来比逐批 DELETE 省事。这个选择真正困难的地方,却经常在建表前:业务编号还需要全表唯一吗?其他表要不要通过外键引用它?旧月份的数据还能不能补写?

MySQL 8.4 的 InnoDB 用户分区表有明确约束。按日期分区,所有唯一键都必须包含分区表达式使用的列。把日期补进主键能让 DDL 通过,也同时改变了唯一性的含义。如果这一步没有经过业务确认,清理操作越方便,错误模型留得越久。

先看一个能建成、却不再约束编号唯一的表

下面使用专用实验库,按 DATE 列做 RANGE COLUMNS 分区。前置分区单独接收2025年以前的数据,两个自然月各有边界,最后保留未来分区。这里不依赖服务器会话时区。

CREATE DATABASE IF NOT EXISTS partition37_lab
  CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE partition37_lab;

CREATE TABLE event_archive (
    event_id BIGINT UNSIGNED NOT NULL,
    business_day DATE NOT NULL,
    message VARCHAR(200) NOT NULL,
    PRIMARY KEY (event_id, business_day),
    KEY idx_day_id (business_day, event_id)
) ENGINE=InnoDB
PARTITION BY RANGE COLUMNS (business_day) (
    PARTITION p_before VALUES LESS THAN ('2025-01-01'),
    PARTITION p202501 VALUES LESS THAN ('2025-02-01'),
    PARTITION p202502 VALUES LESS THAN ('2025-03-01'),
    PARTITION p_future VALUES LESS THAN (MAXVALUE)
);

INSERT INTO event_archive VALUES
(7,'2025-01-10','same id in January'),
(7,'2025-02-10','same id in February'),
(8,'2025-01-20','another January event');

SELECT * FROM event_archive WHERE event_id=7 ORDER BY business_day;

两条编号7都能存在。主键约束的是 (event_id,business_day) 组合,数据库没有全局唯一的 event_id 约束。补一个 UNIQUE(event_id) 也行不通,因为它缺少分区列。

ALTER TABLE event_archive ADD UNIQUE KEY uk_event_id (event_id);

这一步应失败,原因是唯一键必须包括分区列。不能把失败简单归因于“现有样本刚好有重复值”,然后删完重复数据再试;即使表为空,这个键与当前分区方案也不兼容。

如果业务原本允许每天独立编号,这个组合键完全合理。若编号在对外接口、消息去重和关联查询中一直代表全局唯一事件,就不能只为了通过建表而把日期塞进主键。应用随机生成编号可以降低碰撞概率,但它没有保留数据库原本提供的相同强度约束。

外键约束不是换一个列顺序就能留下

InnoDB 用户分区表不支持外键。分区表自身不能带外键,其他 InnoDB 表也不能用外键引用这种分区表。以下完整子表定义,在本例中属于故意失败的操作:

CREATE TABLE event_note (
    note_id BIGINT UNSIGNED NOT NULL,
    event_id BIGINT UNSIGNED NOT NULL,
    business_day DATE NOT NULL,
    note_text VARCHAR(200) NOT NULL,
    PRIMARY KEY (note_id),
    KEY idx_event (event_id, business_day),
    CONSTRAINT fk_note_event
      FOREIGN KEY (event_id, business_day)
      REFERENCES event_archive(event_id, business_day)
) ENGINE=InnoDB;

父表的主键列顺序已经对齐,子表索引也齐全,仍无法绕过存储引擎对用户分区表的外键限制。继续换索引名字、关闭 foreign_key_checks,都不是恢复这项能力的方法。

可以把事件主记录留在普通表中,分区表只保存可独立过期的明细或归档副本;也可以接受由业务流程维护关联完整性,但要明确删除前后怎样处理引用。两者都需要重新设计数据边界,不能视为一次透明的物理优化。

删除分区以后,月份写入边界也变了

实验中的一月有两条记录。只在这个实验库执行:

SELECT COUNT(*) AS january_rows
FROM event_archive PARTITION (p202501);

ALTER TABLE event_archive DROP PARTITION p202501, ALGORITHM=INPLACE;

SELECT * FROM event_archive ORDER BY business_day,event_id;

这里显式固定 INPLACE,才把实验行为限制在删除分区数据的路径。COPY 算法或旧式 ALTER 路径可能重建整张分区表,并把待删除分区中的数据迁往仍然兼容的分区;不能在两种算法之间只比较速度,却沿用同一份数据去向假设。

一月分区里的两条记录被移除,二月编号7仍在。DROP PARTITION 属于结构变更,会触及元数据锁,并会造成隐式提交;同一连接此前尚未提交的业务修改也不能指望在后面统一撤回。它不是放进普通业务事务后,最后反悔执行 ROLLBACK 就能恢复的数据删除。是否保留备份、何时禁止写入、怎样确认清理对象,都应在执行前完成。

更容易误判的是后续补写。删掉一个 RANGE 分区,不能据此认为对应月份从此成为数据库拒绝的“空洞”。剩余分区仍按边界值决定归属。本例删完一月后,二月分区的上界仍是2025-03-01,它会承接前一个保留边界到这个上界之间的值。

INSERT INTO event_archive VALUES
(9,'2025-01-31','late January event');

SELECT * FROM event_archive PARTITION (p202502)
WHERE event_id=9;

这条一月数据会落入当前的 p202502。这个结论来自 RANGE 的剩余边界定义,不能用分区名字里写了“202502”来推断数据库只接收二月。分区名是标签,不是额外的日期约束。

如果已封账月份必须禁止补写,应在写入合同中校验封账状态,或者采用经过验证的表结构约束;如果业务允许迟到数据,就要决定它们何时再次清理。每天显示“清理任务成功”,不能证明旧日期数据永远不会回来。

哪种表值得支付这些限制

按日期追加、到期整段丢弃、独立明细查询带时间条件的表,通常更容易与分区模型对齐。全局编号唯一、被许多关系表引用、随时回填历史数据的核心业务表,则需要更谨慎。分区是否合适,取决于这些真实操作,而不是某一天表到了一个笼统的行数阈值。

分区也不会自动替查询创建索引。idx_day_id 与主键分别提供不同访问顺序;时间条件能否裁剪分区、在剩下的分区里怎样找行,仍然需要看查询与执行计划。只按 event_id 查一条数据,没有日期条件时,可能需要探查多个分区,并不会因为每个分区小一些就天然更省事。

本例保留 MAXVALUE 分区,未来日期能写入,但不能简单在它后面追加一个上界更小的月份分区。维护新的月份边界时,需要按 RANGE 的有序边界规则重组末尾分区,并评估当中已有数据的移动。今天 DROP 很简单,不代表未来所有分区维护都同样便宜。

决定采用这套方案以前,先用真实的唯一键和外键需求建一次完整 DDL,再用历史补写样本检查剩余边界。模型能成立,分区清理才是收益;模型已经改变,就应该先把改变交代清楚。

正文到此结束
评论插件初始化中...
Loading...