1. 一张不断膨胀的大表,逼我认真认识分区表
做数据库运维这些年,我最怕听到的一句话不是“库挂了”,而是“这张表已经几个亿了,查一下慢得要死”。有个线上业务表,存的是用户操作日志,半年时间涨到 3 亿多行,单表磁盘占用将近 60GB。SELECT 带时间范围倒是能用上索引,但一旦涉及清理历史数据,DELETE 一次跑十几分钟,把主从延迟直接拉满,DBA 群里一片哀嚎。
当时第一个反应是上归档任务,把三个月前的数据搬到历史库。但 DELETE 的代价摆在那里:每删一行都要走事务、记 binlog、维护二级索引,删 1000 万行的时间够我喝三杯咖啡。有人提议“直接 DROP 掉那些老分区数据”,这句话点醒了我——业务表如果当初建成了分区表,清理数据压根不用 DELETE,一个 DROP PARTITION 就是秒级的事。
MySQL 分区表(Partitioning Table)说白了就是:逻辑上是一张表,物理上拆成多个独立的存储片段。查询的时候,优化器可以只扫描需要的那几个片段,而不是整表撸一遍。官方文档叫 partition pruning(分区裁剪),记住这个词,后面所有性能讨论都围着它转。
这篇文章我会把分区表的原理、四种分区类型怎么选、分区键怎么定、实际运维怎么操作、以及哪些场景会被“分区表”三个字忽悠瘸,一口气讲透。适合被大表折磨过、正在考虑给现有表做分区的同学,也适合刚学 MySQL 想系统理解分区机制的初学者。
1.1 先还原一个真实的表膨胀场景
假设你手上有一张订单表:
CREATE TABLE order_record ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL, user_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL, create_time DATETIME NOT NULL, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_create_time (create_time) ) ENGINE=InnoDB;业务跑了一年,数据量破亿。这时候你发现三个问题:
第一,索引越来越大。二级索引的 B+ 树体积跟着数据量膨胀,内存 Buffer Pool 里能缓存的索引页占比越来越小,随机 IO 变多,查询响应时间从几十毫秒慢慢爬到几百毫秒。
第二,旧数据清理困难。产品说订单数据要保留两年,但用户的日常查询基本只看最近三个月。你想把一年前的数据删掉,DELETE 语句的 WHERE 条件如果命中 create_time,即便有索引,删除过程中索引也要同步更新,且事务过大还会造成锁竞争、主从延迟。1500 万行 DELETE 下去,从库延迟直接报了 800 秒。
第三,备份恢复的粒度太粗。整库备份 60GB,恢复一次少说 40 分钟。可实际上你关心的活跃数据可能只有最近几个月的 10GB。
分区表解决的就是这类问题。把表按 create_time 做 RANGE 分区,每个月一个分区,历史数据的清理变成:
ALTER TABLE order_record DROP PARTITION p202301;删一个分区等于删掉一个物理文件片段,不产生大量行级操作,主从延迟几乎为零,备份也可以通过只备份活跃分区来缩小范围。这个收益是立竿见影的。
1.2 分区表到底是什么:一张表分成了 N 个物理小家伙
在 MySQL 8.0 里,InnoDB 引擎的分区表在存储引擎层仍然是多个独立的 B+ 树,每个分区有自己独立的 .ibd 文件(前提是 innodb_file_per_table 开启,默认就是开启的)。从客户端连接来看,你操作的还是同一张表,普通的 SELECT、INSERT、UPDATE、DELETE 语法完全不用改。
每个分区有一个名字,对应一段数据范围。比如按月份分了 12 个分区,数据写入时存储引擎根据分区表达式自动决定这一行该进哪个分区。你在 SQL 里根本感知不到底层的路由逻辑,感知不到不代表不重要——如果查询条件里不带分区键,MySQL 可能把所有分区都扫一遍,这就是“分区表反而更慢”的经典来源。
打个比方:分区表就像把一个超大型仓库按货架区域拆成 A 区、B 区、C 区,每个区有独立的库管员。你找货时只要报上区域编号,库管员直接去对应区域找;如果不报编号,那就得让所有区域的库管员一起翻一遍,费力不讨好。分区键就是那个区域编号,查询必须带上它,收益才成立。
2. 分区键没选好,还不如不分区——最容易被忽视的根基
我见过太多人兴致勃勃地把表改成分区表,结果过了两周发现查询更慢了,最后又花了一个通宵把分区拆掉。问题几乎都出在分区键上。分区键的选择不是“选一个字段这么简单”,它直接决定两个硬性约束和一个核心性能假设。
2.1 第一条铁律:主键和唯一键必须包含分区键
MySQL 对分区表有一个在别的数据库里不太常见的限制:建分区表时,表上的所有主键列和唯一键列,都必须包含分区键。
什么意思?比如上面那张订单表,主键是 id,你想按 create_time 分区,MySQL 直接报错:
ERROR 1503 (HY000): A PRIMARY KEY must include all columns in the table's partitioning function因为主键在全局要求唯一性,而分区表的数据分布在各分区文件中,MySQL 需要保证主键索引和分区键能在同一个个分区内完成唯一性校验。如果主键只包含 id,不包含 create_time,那么插入时为了校验 id 不重复,理论上得去所有分区查一遍,代价不可控,所以 MySQL 直接禁止。
解决办法通常是两种:
- 把分区键加入主键,做成联合主键:
PRIMARY KEY (id, create_time); - 或者取消主键,只保留普通索引(不推荐,主键对 InnoDB 太重要了)。
我看到过不少线上表,为了分区把主键语义强行改掉,业务代码里拿 id 当唯一键用的逻辑全部推翻重来。这就是为什么我建议大家在表设计阶段就要想好分区需求,等数据量上来再改,会很痛。如果你真的要对已有主键表改分区,第一步先检查所有唯一键。
2.2 第二条铁律:查询条件里不带分区键,性能直接打回原形
分区裁剪是分区表唯一的性能核心。所谓裁剪,就是优化器根据 WHERE 条件中的分区键范围,把不相关的分区直接跳过,只扫描命中的分区。
还是订单表的例子,如果按 create_time 分区,下面这条查询:
SELECT * FROM order_record WHERE create_time >= '2024-08-01' AND create_time < '2024-09-01';MySQL 可以直接定位到 p202408 这一个分区,扫描的数据量从数十 GB 缩小到几 GB,效果立竿见影。
但如果你按 user_id 查:
SELECT * FROM order_record WHERE user_id = 123456;优化器无法从 create_time 上推断出该用户的数据分布在哪个分区,只能全部 12 个分区挨个扫一遍。表面上你建了分区,实际上查询很可能是全分区扫描,再加上一次回表,性能甚至不如普通单表加索引。
第二个约束由此而来:分区键必须是你最核心、最高频的那条查询路径上的过滤条件。对订单表来说,绝大多数业务查询都要带 create_time 范围,所以它是天然的分区键;如果你的表平时主要按 user_id 查,那分区键就应该选 user_id,或者干脆别分区,用普通索引更省事。
2.3 分区键的另一个隐藏要求:筛选性要好
筛选性指的是分区键的取值离散程度。如果分区键只有“是/否”两个取值,那么最多分成两个有意义的区,其余全是摆设。RANGE 分区希望键值能自然分段(比如时间、数值区间);HASH 和 KEY 分区希望键值均匀散列,避免数据倾斜。
数据倾斜是个很隐蔽的问题。比如按 user_id 做 HASH 分区,如果业务里 80% 的订单都来自几个大客户,那么这几个值散列到的分区数据量会明显大于其他分区,“木桶效应”会让查询性能被最热分区拖垮。分区表不是灵丹妙药,它只是把大表切碎,切完之后每片是否匀称,取决于分区键和分区类型的配合。
3. 四种分区类型,各回各家——语法与适用场景对照
MySQL 支持 RANGE、LIST、HASH、KEY 四种基础分区方式。虽然日常 90% 的场景都在用 RANGE,但理解四种类型的差别仍然很重要,选错了后面运维全是泪。
3.1 RANGE 分区:最常用的时间分区方案
RANGE 分区按连续区间划分数据,最经典的就是按时间分区:
CREATE TABLE order_record ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL, user_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL, create_time DATETIME NOT NULL, PRIMARY KEY (id, create_time), KEY idx_user_id (user_id) ) ENGINE=InnoDB PARTITION BY RANGE (TO_DAYS(create_time)) ( PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-02-01')), PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-03-01')), PARTITION p202403 VALUES LESS THAN (TO_DAYS('2024-04-01')), PARTITION p202404 VALUES LESS THAN (TO_DAYS('2024-05-01')), PARTITION p_future VALUES LESS THAN MAXVALUE );分区边界是“小于”语义:p202401存放 2024-01-01 零点到 2024-01-31 二十四点的数据,2024-02-01零点这行会进p202402。p_future吸收所有超出已知范围的数据,防止插入报错。
这里的TO_DAYS()是一个时间处理函数,返回天数数值。也可以用YEAR()做按年分区,但粒度太粗,月度分区是电商、日志类业务比较常见的折中。
RANGE 分区非常适合时间范围查询 + 历史数据归档的场景。归档操作前面说了,DORP PARTITION 秒删,合并历史分区用REORGANIZE PARTITION,把连续几个月的数据合并成一个大分区。
3.2 LIST 分区:适合枚举式分发
LIST 分区按枚举值列表划分,比如按地区、按业务类型:
CREATE TABLE user_login_log ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, login_channel TINYINT NOT NULL COMMENT '1:App 2:Web 3:小程序', login_time DATETIME NOT NULL, PRIMARY KEY (id, login_channel) ) ENGINE=InnoDB PARTITION BY LIST (login_channel) ( PARTITION p_app VALUES IN (1), PARTITION p_web VALUES IN (2), PARTITION p_mini VALUES IN (3), PARTITION p_other VALUES IN (0, 4, 5) );LIST 分区的场景相对窄,但有个典型优势:可以按渠道单独做数据统计或清理。比如小程序渠道因为合规要求只保留 90 天,其他渠道保留一年,直接 TRUNCATE 对应分区就行,比 DELETE 加条件高效得多。
需要注意:LIST 分区的分区键如果是 NULL,且没有任何分区的 VALUES IN 列表包含 NULL,插入时直接报错。这点和 RANGE 不一样,RANGE 会把 NULL 丢进最小分区,LIST 更严格。
3.3 HASH 分区和 KEY 分区:散列均匀,但运维复杂
HASH 分区对分区键取模,把数据尽量均匀地打散到 N 个分区:
CREATE TABLE user_session ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, session_data TEXT, last_active_time DATETIME NOT NULL, PRIMARY KEY (id, user_id) ) ENGINE=InnoDB PARTITION BY HASH (user_id) PARTITIONS 8;KEY 分区和 HASH 类似,区别在于 KEY 可以指定多个列,并且分区函数由 MySQL 内部哈希实现,不要求只能是整数列:
PARTITION BY KEY (user_id, create_time) PARTITIONS 8;HASH 和 KEY 分区的核心价值是写入均匀分布,对按分区键等值查询的小查询有一定帮助。但它的短板也很明显:按时间范围归档数据时,你无法把“某段时间”单独圈出来删掉,因为同一时间段的数据可能散在 8 个分区里。想清理历史数据,只能按普通 DELETE 的方式处理,分区带来的归档优势就没了。所以我会说,没有明确的数据生命周期管理需求时,HASH 分区反而不如 RANGE 实用。
3.4 四种分区方式怎么选:一张对照表
| 分区类型 | 适用场景 | 分区键建议 | 归档删除 | 查询裁剪效果 |
|---|---|---|---|---|
| RANGE | 时间范围、数值区间 | 日期、自增 ID、金额区间 | 极好,DROP PARTITION | 范围查询收益明显 |
| LIST | 枚举值、业务分类 | 渠道、地区、状态 | 可以按枚举清理 | 等值查询收益明显 |
| HASH | 键值均匀写入 | user_id、订单号 | 较差,无法按范围删 | 等值查询有一定收益 |
| KEY | 类似 HASH,支持多列 | 多列组合 | 较差 | 等值查询有一定收益 |
4. 从建表到归档:一套完整可复制的分区表操作
这一节我们把实际操作串一遍。考虑到前面说的主键限制,如果你是从零开始设计,建表时就要把分区键塞进主键或唯一键里。下面我以一个订单流水表为例,走一遍建表、业务写入、分区维护、数据归档的完整流程。
4.1 建表:从零创建一个按月份分区的订单表
CREATE TABLE trade_record ( id BIGINT NOT NULL AUTO_INCREMENT, trade_no VARCHAR(64) NOT NULL, user_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL, trade_status TINYINT NOT NULL, create_time DATETIME NOT NULL, PRIMARY KEY (id, create_time), UNIQUE KEY uk_trade_no (trade_no, create_time), KEY idx_user_id (user_id) ) ENGINE=InnoDB PARTITION BY RANGE (TO_DAYS(create_time)) ( PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-02-01')), PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-03-01')), PARTITION p202403 VALUES LESS THAN (TO_DAYS('2024-04-01')), PARTITION p202404 VALUES LESS THAN (TO_DAYS('2024-05-01')), PARTITION p_future VALUES LESS THAN MAXVALUE );注意两处设计细节:
一是唯一键uk_trade_no变成了(trade_no, create_time)联合唯一。这是为了满足“唯一键必须包含分区键”的硬性要求。trade_no 本身业务上就是唯一的,加上 create_time 唯一性不变,但业务代码里如果依赖 trade_no 做“唯一冲突检测”,需要同步调整判断逻辑。
二是保留p_future分区。一旦业务数据的时间超过了 2024-04-30,比如 2024-05-02 的订单,如果没有 MAXVALUE 分区就会直接插入失败。保留这个“兜底分区”可以有效避免线上故障,代价是兜底分区里的数据在查询时裁剪不干净,需要定期把它拆成新的月度分区。
INSERT 语句和普通表完全一样,MySQL 根据TO_DAYS(create_time)自动路由:
INSERT INTO trade_record (trade_no, user_id, amount, trade_status, create_time) VALUES ('T20240115001', 10001, 299.00, 1, '2024-01-15 10:30:00');4.2 分区维护:加分区、删分区、合并拆分
当p_future里开始积累当月数据时,你需要把下一个月独立拆出来:
ALTER TABLE trade_record REORGANIZE PARTITION p_future INTO ( PARTITION p202405 VALUES LESS THAN (TO_DAYS('2024-06-01')), PARTITION p_future VALUES LESS THAN MAXVALUE );这个操作会把 MAXVALUE 分区里的数据重新分发到新分区和剩下的兜底分区,执行时间取决于p_future里积压的数据量。所以最佳实践是提前拆分,比如每个月底就把下个月的分区建好,别等数据真的涌进来再动刀。
删除历史分区:
ALTER TABLE trade_record DROP PARTITION p202401;这一步物理删除对应分区文件内的所有数据,释放磁盘空间,速度远快于 DELETE。我见过有人用 DELETE 清一年前的数据,跑了 1 个多小时;换成 DROP PARTITION 不到 5 秒。
只清空某个分区的数据但保留分区定义,用:
ALTER TABLE trade_record TRUNCATE PARTITION p202402;把多个小分区合并成一个大分区,比如把上半年的 6 个月分区合并成半年分区:
ALTER TABLE trade_record REORGANIZE PARTITION p202401, p202402, p202403, p202404, p202405, p202406 INTO ( PARTITION p2024_h1 VALUES LESS THAN (TO_DAYS('2024-07-01')) );这里有个小细节:REORGANIZE PARTITION的目标分区范围必须和源分区的范围完全一致,不能多也不能少,否则 MySQL 报错。实际运维中我是写个存储过程按月自动生成下月分区,避免手工维护。
4.3 数据归档:从 DELETE 到 DROP PARTITION 的质变
分区表最有说服力的收益在归档场景。假设产品要求订单数据保留两年,按月分区就有 24 个分区加一个兜底分区。到了 2026 年初,2024 年 1 月的数据已经超期,直接:
ALTER TABLE trade_record DROP PARTITION p202401;这个操作不会产生几百 MB 的 binlog(DELETE 会产生每一行的 binlog 事件),也不会触发二级索引的大规模更新,对主从复制的影响几乎可以忽略。
如果只是希望把旧数据搬走而不是删掉,流程是:先CREATE TABLE trade_record_202401 LIKE trade_record,再ALTER TABLE trade_record_202401 REMOVE PARTITIONING去掉分区,然后INSERT INTO trade_record_202401 SELECT * FROM trade_record PARTITION (p202401),最后 DROP 原分区。这样旧数据变成一张独立普通表,可以单独备份或转移到低成本存储。
5. 分区表的性能真相:哪些收益被高估了
分区表常被当作“性能银弹”,但实际上它的收益面比很多人以为的要窄。我必须把这段话写明白:分区表最大的价值是数据管理效率,其次才是查询性能。如果你只是为了“查询快点”而不做数据生命周期管理,分区表大概率会让你失望。
5.1 分区裁剪:性能收益的唯一来源
分区裁剪是分区表性能提升的唯一机制,没有之一。它发生在优化器阶段:MySQL 根据 WHERE 条件里的分区键范围,把不满足条件的分区直接排除,只对剩余分区执行扫描。
你可以用 EXPLAIN 验证裁剪效果:
EXPLAIN SELECT * FROM trade_record WHERE create_time >= '2024-03-01' AND create_time < '2024-04-01';在 8.0 里,EXPLAIN 输出会有一个partitions列,上面这条语句应该显示p202403,表示只扫描了这一个分区。如果显示p202401,p202402,p202403,p202404,p_future这种完整列表,说明条件没有触发裁剪,查询实际上是全分区扫描。
一个值得注意的细节:分区裁剪对函数包裹的列不生效。比如WHERE DATE(create_time) = '2024-03-15'这种写法,因为函数把索引列变成了表达式,优化器无法推导分区范围,裁剪失效。正确的做法是写成范围条件:
WHERE create_time >= '2024-03-15 00:00:00' AND create_time < '2024-03-16 00:00:00'5.2 什么时候分区表反而更慢
分区表比普通表更慢,常见原因有三个。
第一,查询条件不带分区键。前面已经反复强调,这里用一个例子量化:如果有 12 个分区,每次查询理想扫描量是 1/12 的数据量,但分区裁剪失效时扫描量是 100%,还要加上分区的额外路由开销,所以比普通表更慢。
第二,每个分区独立维护二级索引。假设分区表有 12 个分区,每个分区内部都有一棵 idx_user_id 的 B+ 树。你在查询时即使只命中一个分区,也要在这个分区的索引树上额外走一次定位;如果用普通表,只需要在一棵大索引树上定位。对单点查询来说,分区表在索引层没有优势,甚至因为区与区之间无法共享索引缓存,冷分区频繁被访问时 Buffer Pool 命中率可能下降。
第三,统计信息不准导致执行计划跑偏。分区表在收集统计信息时是按分区收集的,某些情况下优化器对分区行数的估算会偏差较大,可能选错索引。这一点在 MySQL 8.0 里比 5.7 好一些,但仍然不能掉以轻心。遇到“SQL 突然变慢”的排查,记得先用 EXPLAIN 看partitions列和rows估算值。
5.3 和索引的关系:分区不能替代索引
这是一个很常见的误解:以为建了分区,索引就可以不用建了。错得离谱。分区决定的是“数据存放的位置”,索引决定的是“在分区内部怎么快速定位数据”。两者正交。
比如前面订单表的idx_user_id,它的作用是让你在某个分区内部能按 user_id 快速查。如果不建这个索引,按 user_id 的查询在命中的分区里仍然要做全分区扫描。正确姿势是:分区键解决范围裁剪,二级索引解决分区内的精确查找,两者配合才有最佳效果。
不过二级索引也要注意选择性。分区表里如果二级索引的唯一性很差(比如 status 字段只有 0/1 两个值),索引扫描加回表的开销可能比全分区扫描更大,MySQL 优化器一般会根据统计信息自己判断,但你对这个逻辑要有数。
6. 实践中踩过的坑:NULL、主键限制与 ALTER TABLE 的代价
最后这部分,是我真实动手改分区表时踩过的坑。有些坑官方文档写得清楚,但不到出问题时你根本不会注意到;有些坑属于“文档说归说,实际遇到才知道多痛”。
6.1 NULL 值分区:一个容易踩的边界
RANGE 分区中,如果分区键是 NULL,MySQL 会把它放进最小分区。也就是说,一行 create_time 为 NULL 的数据,会被写到p202401(最小边界)里。这在语义上不算错,但如果你的分区和业务强相关,比如要对 p202401 做归档删除,那这些 NULL 数据会被一并清理掉,可能会造成业务数据丢失。
解决办法:在业务层保证分区键必填,或者建表时给 create_time 加NOT NULL约束。最稳妥的方案是分区键字段一律NOT NULL,从源头堵住。
LIST 分区的 NULL 处理更严格,NULL 不在任何 VALUES IN 列表里时插入直接报错。HASH 和 KEY 分区则把 NULL 当作 0 处理。对同一张表,不同分区类型对 NULL 的容忍度不一样,这个差异容易在新手手里变成线上事故。
6.2 分区数量:不是越多越好
MySQL 8.0 中一张分区表最多支持 8192 个分区(包含子分区)。但这只是上限,实际远不该用这么多。分区数过多会带来多重问题:
- 打开表时,MySQL 需要初始化所有分区的文件句柄,表分区过多会让
SHOW TABLE STATUS、DDL 操作明显变慢; - 每个分区都要独立收集统计信息,
ANALYZE TABLE的耗时随分区数线性增长; - 如果每个分区数据量太小,比如一个分区只有几千行,分区裁剪省下的扫描开销还抵不过分区路由和文件切换的开销,收益为负。
我的经验值:月度分区保留 24 个月,加上 1 个兜底分区,总共 25 个分区,运维和性能比较均衡。如果数据量实在大到月度分区仍超过 20GB,可以考虑按周分区甚至按天分区,但随之而来的是更频繁的分区维护,建议配合自动化脚本。
6.3 ALTER TABLE 的隐性代价与在线 DDL
对已有大表改成分区表,不是一条 ALTER 语句瞬间完成的事。下面的操作:
ALTER TABLE order_record PARTITION BY RANGE (TO_DAYS(create_time)) (...);在 InnoDB 里通常会触发全表重建,执行期间可能需要拷贝数据、重建所有索引,一个 60GB 的表改分区,跑几个小时很正常。虽然 8.0 支持在线 DDL 的 ALGORITHM=INPLACE,但分区变更的很多场景仍会退化为 COPY 算法,表现是表被锁住,业务读写被卡死。
所以我的建议是:彻底放弃“线上大表原地改分区”的想法。靠谱的路径是:新建一张分区表,通过INSERT INTO ... SELECT ...分批次迁移数据,同时用触发器或应用双写保证增量数据同步,最后在低峰期做表名切换。流程繁琐,但可控。
另一个容易被忽略的代价是:分区表的ALTER TABLEDDL 在复制架构中会传递到从库,如果从库性能较差,一个加分区的操作也可能拖慢从库。所以重要分区的维护操作,建议放到业务低峰期,并且先在测试环境跑一遍确认耗时。
6.4 UNIQUE KEY 的连带影响:比你想的更隐蔽
前面提到唯一键必须包含分区键,这个限制还带了一个更隐蔽的问题:唯一键的语义发生变化,可能影响业务幂等逻辑。
我遇到过一个实际案例:业务方原来用trade_no做幂等,重复提交订单时INSERT会因唯一键冲突而失败。改成联合唯一键(trade_no, create_time)之后,同一毫秒内相同 trade_no 的两次请求,如果 create_time 不同,唯一约束就失效了,线上出现重复订单。最后被迫在应用层加了分布式锁才解决。
如果你正在设计分区表,务必和业务开发对齐:所有用到唯一键做幂等或去重的逻辑,都要重新审视分区键加入后的语义变化。这一点文档不会提醒你,但线上事故会。
6.5 时区问题:UNIX_TIMESTAMP 作分区键的坑
有人喜欢用UNIX_TIMESTAMP(create_time)作为 RANGE 分区的表达式,因为秒级时间戳方便按固定秒数切分。但有个隐蔽的坑:UNIX_TIMESTAMP的返回值受 MySQL 会话时区影响。如果数据库时区从+08:00调整成+00:00,同一个create_time算出的时间戳不同,分区边界和实际数据分布可能错乱,查询裁剪也可能失效。
稳妥的做法是基于日期的TO_DAYS()或YEAR(),它们直接解析日期时间值,与时区无关。如果业务场景要求按时间戳粒度分区,也要保证数据库时区规范统一,且不随意变更。
最后分享两个我常用的分区表运维小技巧
第一个,提前建分区,别靠兜底分区扛。兜底分区(MAXVALUE)的目的是防止插入报错,不是让你把数据堆在里面。我习惯在每个月的 25 号左右自动执行一个存储过程,预创建下下个月的分区,这样即使业务提前写入下月数据,也有独立分区可以裁剪,不会积压在兜底分区里影响性能。
第二个,监控里加上分区裁剪失效的 SQL 发现机制。在慢查询日志和监控报表里,重点关注那些partitions列显示全分区扫描的语句。这类 SQL 往往是后来新增的业务查询,开发没有意识到表是分区表、没带分区键。发现一条就推动业务优化一条,长期下来分区表的红利才能真正兑现。
分区表不是银弹,它是数据管理思维下的产物。想清楚你的数据生命周期,选对分区键和分区类型,它能让大表运维轻松一个量级;但如果只是为了“别人都分区了我也要分”,那大概率是给自己制造新的麻烦。希望这篇文能帮你少走几步弯路。