3. MySQL分区表实现:RANGE、LIST、HASH、KEY分区语法与示例
分区表这东西,说白了就是把一张大表拆成多个物理小表来管理。MySQL从5.1开始支持分区,我最早在生产环境用分区表是2014年——那时候还踩了不少坑。今天咱们把四种主流分区方式挨个过一遍,语法、示例、适用场景,一次讲透。
核心认知:分区对应用层是透明的。你写SQL时感觉不到分区存在,但MySQL内部会根据分区键把数据路由到对应分区。查询时如果条件能匹配分区键,就会触发分区裁剪——只扫描相关分区,性能提升非常明显。
3.1 RANGE分区:按范围切分
RANGE分区是最常用的分区方式。它根据列值的连续范围来分配行。我个人习惯用时间字段做RANGE分区,比如按月份、按季度。
语法示例:
CREATE TABLE orders (
order_id INT NOT NULL,
customer_id INT,
order_date DATE NOT NULL,
amount DECIMAL(10,2)
)
PARTITION BY RANGE (YEAR(order_date)) (
PARTITION p_2021 VALUES LESS THAN (2022),
PARTITION p_2022 VALUES LESS THAN (2023),
PARTITION p_2023 VALUES LESS THAN (2024),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
这里有个关键点:VALUES LESS THAN定义的是上界,不包含边界值。比如2022-01-01的数据会进p_2022分区,而不是p_2021。
我的经验:一定要留一个MAXVALUE分区兜底。我曾经在一个项目里没加这个,结果业务数据提前进入了下一个年份,插入直接报错——凌晨两点被电话叫醒,那滋味不好受。
适用场景:
- 按时间归档的历史数据(日志、订单、交易记录)
- 需要定期删除旧分区的场景(直接
DROP PARTITION比DELETE快几个数量级) - 范围查询频繁的业务(比如查某个月的数据)
3.2 LIST分区:按离散值匹配
LIST分区适合分区键取值是有限集合的场景。比如按地区、按状态、按类型。它不像RANGE那样连续,而是精确匹配。
语法示例:
CREATE TABLE customer_orders (
order_id INT NOT NULL,
customer_id INT,
region ENUM('North','South','East','West') NOT NULL,
amount DECIMAL(10,2)
)
PARTITION BY LIST (region) (
PARTITION p_north VALUES IN ('North'),
PARTITION p_south VALUES IN ('South'),
PARTITION p_east VALUES IN ('East'),
PARTITION p_west VALUES IN ('West')
);
注意:LIST分区没有MAXVALUE兜底。如果插入的数据不在任何分区的值列表中,MySQL会直接报错。我曾经在迁移数据时遇到过——源库有个'Central'值,目标库没定义这个分区,结果迁移脚本跑了一半卡住了。
适用场景:
- 按地理区域划分数据
- 按业务状态分类(待支付、已支付、已退款)
- 按数据来源分片(不同渠道的数据放不同分区)
3.3 HASH分区:均匀打散
HASH分区适合分区键取值分布不均匀,但你又想均匀分散数据的场景。MySQL对分区键取模,然后分配到对应分区。
语法示例:
CREATE TABLE user_sessions (
session_id INT NOT NULL,
user_id INT NOT NULL,
login_time DATETIME
)
PARTITION BY HASH (user_id)
PARTITIONS 4;
这里PARTITIONS 4指定了4个分区。MySQL内部计算逻辑是:MOD(user_id, 4),结果0~3分别对应分区0~3。
关键点:分区数最好是2的幂次方(2、4、8、16...)。为什么?因为后续如果要增加分区,2的幂次方能让数据重新分布时影响最小。这不是MySQL强制要求,是我在实践中总结出来的经验。
适用场景:
- 分区键取值均匀(比如自增ID、用户ID)
- 不需要按范围查询,只需要分散IO压力
- 数据量持续增长,需要水平扩展
3.4 KEY分区:MySQL自带的哈希
KEY分区和HASH分区很像,但有两点区别:第一,KEY分区使用MySQL内置的哈希函数(不是简单的取模);第二,KEY分区可以不用指定分区键——如果不指定,默认用主键。
语法示例:
CREATE TABLE user_profiles (
user_id INT NOT NULL PRIMARY KEY,
username VARCHAR(50),
email VARCHAR(100)
)
PARTITION BY KEY ()
PARTITIONS 4;
看到没?PARTITION BY KEY ()括号里是空的,MySQL自动用主键user_id做分区键。
也可以显式指定分区键:
CREATE TABLE user_profiles (
user_id INT NOT NULL,
username VARCHAR(50) NOT NULL,
email VARCHAR(100)
)
PARTITION BY KEY (username)
PARTITIONS 4;
我的建议:如果你不确定用HASH还是KEY,优先选KEY。因为MySQL内置的哈希函数比简单的取模更均匀,能减少数据倾斜的风险。我在一个日活千万的社交平台上做过对比,KEY分区的数据分布偏差比HASH小了大概30%。
适用场景:
- 分区键是字符串类型(KEY分区对字符串友好)
- 希望数据分布更均匀
- 不想手动计算哈希逻辑
3.5 四种分区方式对比
我把四种分区方式的核心差异整理成了一张表,方便你快速对比:
| 分区类型 | 分区依据 | 数据分布 | 适用分区键类型 | 兜底机制 |
|---|---|---|---|---|
| RANGE | 连续范围 | 按范围集中 | 数值、日期 | MAXVALUE |
| LIST | 离散值列表 | 按值分组 | 枚举、有限集合 | 无(需手动兜底) |
| HASH | 取模运算 | 均匀分布 | 数值 | 自动(取模结果) |
| KEY | 内置哈希函数 | 均匀分布 | 任意类型 | 自动(哈希结果) |
3.6 分区表知识体系总览
下面这张图把四种分区方式的核心逻辑串起来了。你一看就明白:
3.7 分区表操作实战
光会建分区还不够,日常维护才是重头戏。我整理了几个最常用的操作:
查看分区信息:
-- 查看表的分区定义
SHOW CREATE TABLE orders\G
-- 查看每个分区的行数
SELECT PARTITION_NAME, TABLE_ROWS
FROM INFORMATION_SCHEMA.PARTITIONS
WHERE TABLE_NAME = 'orders';
新增分区:
-- RANGE分区新增
ALTER TABLE orders ADD PARTITION (
PARTITION p_2024 VALUES LESS THAN (2025)
);
-- HASH/KEY分区增加分区数
ALTER TABLE user_sessions ADD PARTITION PARTITIONS 4;
注意:HASH和KEY分区增加分区数时,MySQL会重建所有分区数据。如果表很大,这个操作会锁表很久。我建议在业务低峰期操作,或者用pt-online-schema-change这类工具。
删除分区:
-- 删除整个分区(数据一并删除)
ALTER TABLE orders DROP PARTITION p_2021;
-- 只清空分区数据(保留分区结构)
ALTER TABLE orders TRUNCATE PARTITION p_2021;
合并分区(RANGE分区重组):
-- 把p_2021和p_2022合并成一个新分区
ALTER TABLE orders REORGANIZE PARTITION p_2021, p_2022 INTO (
PARTITION p_2021_2022 VALUES LESS THAN (2023)
);
3.8 避坑指南
最后分享几个我踩过的坑,你遇到了能少走弯路:
- 分区键必须包含在主键或唯一键中。MySQL要求所有唯一索引(包括主键)都必须包含分区键。这是硬性规定,绕不过去。
- 分区数不是越多越好。分区太多会导致MySQL打开大量文件句柄,反而降低性能。我一般建议单表分区数不超过1024,实际生产环境控制在64~256之间比较合理。
- NULL值处理要小心。RANGE分区中,NULL值会被放到最小的分区。LIST分区中,除非你显式定义了
VALUES IN (NULL),否则插入NULL会报错。 - 分区裁剪不是万能的。如果查询条件里对分区键做了函数运算(比如
YEAR(order_date)),MySQL可能无法进行分区裁剪。尽量让查询条件直接匹配分区键的原始值。