文章目录
-
- [1. 表分区的基本概念和原理](#1. 表分区的基本概念和原理)
- [2. MySQL 5.7.18 支持的分区类型与语法](#2. MySQL 5.7.18 支持的分区类型与语法)
-
- [2.1 RANGE 分区](#2.1 RANGE 分区)
- [2.2 LIST 分区](#2.2 LIST 分区)
- [2.3 HASH 分区](#2.3 HASH 分区)
- [2.4 KEY 分区](#2.4 KEY 分区)
- [3. 实际应用示例(结合视频系统表结构)](#3. 实际应用示例(结合视频系统表结构))
-
- [3.1 现有短视频评论表结构(单表)](#3.1 现有短视频评论表结构(单表))
- [3.2 视频评论表:按 `video_id` HASH 分区示例](#3.2 视频评论表:按
video_idHASH 分区示例) - [3.3 视频点赞表:按 `video_id` HASH 分区](#3.3 视频点赞表:按
video_idHASH 分区)
- [4. 分区键的选择原则和最佳实践](#4. 分区键的选择原则和最佳实践)
- [5. 分区维护操作(添加、删除、合并)](#5. 分区维护操作(添加、删除、合并))
-
- [5.1 RANGE 分区维护(视频订单表示例)](#5.1 RANGE 分区维护(视频订单表示例))
- [5.2 HASH 分区维护(视频评论表示例)](#5.2 HASH 分区维护(视频评论表示例))
- [6. 分区与索引的关系及性能影响](#6. 分区与索引的关系及性能影响)
- [7. 使用过程中的重要注意事项和常见陷阱](#7. 使用过程中的重要注意事项和常见陷阱)
- [8. 分区与分表策略的区别和选择建议](#8. 分区与分表策略的区别和选择建议)
- [9. 监控和管理分区表的方法](#9. 监控和管理分区表的方法)
- [10. 性能优化建议和案例分析(高并发视频场景)](#10. 性能优化建议和案例分析(高并发视频场景))
- [11. 如何确认表分区是否生效](#11. 如何确认表分区是否生效)
本文基于 MySQL 5.7.18 ,结合当前项目中的 短视频系统 (数据库 short_video、表如 video_comment、 video_like、 video_collect 等),说明如何在高并发视频场景下正确使用 MySQL 表分区,并给出实用 SQL 示例和性能建议。
1. 表分区的基本概念和原理
-
什么是表分区(Partitioning)
- 逻辑上仍然是一张表:对应用来说,表名不变,SQL 写法不变。
- 物理上拆成多个分区:每个分区类似一张"子表/子文件",数据按某个规则(分区键)分布到不同分区文件中。
-
核心作用
- 提升大表访问性能 :通过 分区裁剪(Partition Pruning),只扫描命中的分区而不是全表。
- 提高维护效率 :可以通过
DROP PARTITION/TRUNCATE PARTITION快速删除或清空一段数据(比如历史数据)。 - 缓解单表数据量过大问题:整体行数不变,但每个分区的数据量更小,索引更小,缓存命中率更高。
-
与索引的关系(关键点)
- 分区后,每个分区有自己的 本地索引,优化器会先"选分区",再在分区内走索引。
- MySQL 5.7 的硬约束:所有唯一索引(包括主键)都必须包含分区键,否则分区表的 DDL 会失败。
2. MySQL 5.7.18 支持的分区类型与语法
2.1 RANGE 分区
-
适用场景:按时间区间或数值区间做冷热数据拆分,例如视频订单、充值记录按日期分区。
-
示例:按订单时间 RANGE 分区
sql
CREATE TABLE video_order (
id BIGINT(20) NOT NULL,
user_id BIGINT(20) NOT NULL,
video_id BIGINT(20) NOT NULL,
order_no VARCHAR(64) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status TINYINT(1) NOT NULL,
order_time DATETIME NOT NULL,
PRIMARY KEY (id, order_time), -- PK 必须包含分区键
KEY idx_user_time (user_id, order_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE (TO_DAYS(order_time)) (
PARTITION p2024 VALUES LESS THAN (TO_DAYS('2025-01-01')),
PARTITION p2025 VALUES LESS THAN (TO_DAYS('2026-01-01')),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
注意:RANGE 分区表达式必须返回整数,常见写法为
TO_DAYS(order_time)、YEAR(order_time)等。
2.2 LIST 分区
-
适用场景 :按 离散枚举值 分区,比如不同的视频业务线、内容类型、地区。
-
示例:按内容类型 LIST 分区
sql
CREATE TABLE video_content_stat (
id BIGINT(20) NOT NULL,
content_type TINYINT(1) NOT NULL, -- 1-短视频,2-图片,3-直播
pv_count BIGINT(20) NOT NULL,
uv_count BIGINT(20) NOT NULL,
stat_date DATE NOT NULL,
PRIMARY KEY (id, content_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY LIST (content_type) (
PARTITION p_short_video VALUES IN (1),
PARTITION p_image VALUES IN (2),
PARTITION p_live VALUES IN (3)
);
2.3 HASH 分区
-
适用场景 :按 哈希均匀打散数据,适合没有明显时间维度、写入非常密集的表,如视频点赞、评论、收藏等。
-
示例:按
video_idHASH 分区的视频评论表
sql
CREATE TABLE video_comment_p (
id BIGINT(20) NOT NULL,
video_id BIGINT(20) NOT NULL,
user_id BIGINT(20) NOT NULL,
parent_id BIGINT(20) DEFAULT 0,
root_id BIGINT(20) DEFAULT 0,
reply_to_user_id BIGINT(20) DEFAULT 0,
content TEXT NOT NULL,
like_count INT(11) DEFAULT 0,
reply_count INT(11) DEFAULT 0,
status TINYINT DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT DEFAULT 0,
PRIMARY KEY (id, video_id), -- 主键包含分区键
KEY idx_video_id (video_id),
KEY idx_user_id (user_id),
KEY idx_parent_id (parent_id),
KEY idx_root_id (root_id),
KEY idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY HASH(video_id)
PARTITIONS 16;
PARTITION BY HASH(expr)中 expr 必须是整数表达式,分区数量PARTITIONS N一般选择 16 / 32 等 2 的幂。
2.4 KEY 分区
-
与 HASH 类似,但使用 MySQL 内置 hash 函数,对字段本身做 hash,不要求是整数。
-
适用场景 :需要对字符串字段分区时,如按
order_no、user_uuid。 -
示例:按订单号 KEY 分区
sql
CREATE TABLE video_order_key (
id BIGINT(20) NOT NULL,
user_id BIGINT(20) NOT NULL,
order_no VARCHAR(64) NOT NULL,
order_time DATETIME NOT NULL,
PRIMARY KEY (id, order_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY KEY (order_no)
PARTITIONS 16;
3. 实际应用示例(结合视频系统表结构)
3.1 现有短视频评论表结构(单表)
当前 short_video 数据库中,video_comment 为单表(见 src/main/resources/init.sql):
sql
CREATE TABLE video_comment (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
video_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
parent_id BIGINT DEFAULT 0,
root_id BIGINT DEFAULT 0,
reply_to_user_id BIGINT DEFAULT 0,
content TEXT NOT NULL,
like_count INT DEFAULT 0,
reply_count INT DEFAULT 0,
status TINYINT DEFAULT 1,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT DEFAULT 0,
INDEX idx_video_id (video_id),
INDEX idx_user_id (user_id),
INDEX idx_parent_id (parent_id),
INDEX idx_root_id (root_id),
INDEX idx_like_count (like_count),
INDEX idx_create_time (create_time),
INDEX idx_video_like_time (video_id, like_count DESC, create_time DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
当数据量上亿级时,单表访问和维护成本会明显升高,可以通过 HASH 分区 等分区策略来解决。
3.2 视频评论表:按 video_id HASH 分区示例
如果只使用 MySQL 分区(不引入或不依赖中间件),可以设计为:
sql
CREATE TABLE video_comment_p (
id BIGINT(20) NOT NULL,
video_id BIGINT(20) NOT NULL,
user_id BIGINT(20) NOT NULL,
parent_id BIGINT(20) DEFAULT 0,
root_id BIGINT(20) DEFAULT 0,
reply_to_user_id BIGINT(20) DEFAULT 0,
content TEXT NOT NULL,
like_count INT(11) DEFAULT 0,
reply_count INT(11) DEFAULT 0,
status TINYINT DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT DEFAULT 0,
PRIMARY KEY (id, video_id), -- PK 包含分区键
KEY idx_video_id (video_id),
KEY idx_user_id (user_id),
KEY idx_parent_id (parent_id),
KEY idx_root_id (root_id),
KEY idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY HASH(video_id)
PARTITIONS 16;
- 访问模式:
- 热门场景是"查看某个视频的评论列表",SQL 通常包含
WHERE video_id = ?,与分区键一致。 - 同一个视频的所有评论都落在同一个分区,统计、排序都在单分区内完成。
- 热门场景是"查看某个视频的评论列表",SQL 通常包含
3.3 视频点赞表:按 video_id HASH 分区
可参考 video_like 的行数预估(见《单库分表优化方案.md》):
sql
CREATE TABLE video_like_p (
id BIGINT(20) NOT NULL,
user_id BIGINT(20) NOT NULL,
video_id BIGINT(20) NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id, video_id), -- PK + 分区键
UNIQUE KEY uk_user_video (user_id, video_id),
KEY idx_video_id (video_id),
KEY idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY HASH(video_id)
PARTITIONS 16;
- 查询示例:
sql
-- 查询某个视频的点赞列表(命中一个分区)
SELECT * FROM video_like_p
WHERE video_id = 12345
ORDER BY create_time DESC
LIMIT 50;
4. 分区键的选择原则和最佳实践
-
必须命中高频查询条件
- 用户维度:
user_id(如"我点赞的视频"、"我收藏的视频")。 - 视频维度:
video_id(如"某个视频的评论/点赞/收藏列表")。
- 用户维度:
-
分布要尽量均匀,避免数据倾斜
user_id % N、video_id % N这类哈希一般比较均匀。- 使用 HASH/KEY 分区时,不要选取值域很小的枚举字段,否则容易集中到少数分区。
-
必须包含在所有唯一索引和主键中
- MySQL 5.7 分区表硬约束:主键和所有唯一索引的列集合必须包含分区键。
- 示例:如果按
video_id分区,主键应为(id, video_id)或(video_id, id),唯一键uk_user_video (user_id, video_id)也包含video_id。
-
避免使用频繁变化的字段
- 分区键不宜为
status、deleted等状态字段,防止频繁跨分区移动数据。
- 分区键不宜为
-
RANGE 分区优先使用时间字段
- 视频订单、充值记录、埋点日志等,适合按
order_time或log_time做 RANGE 分区,方便历史归档。
- 视频订单、充值记录、埋点日志等,适合按
5. 分区维护操作(添加、删除、合并)
5.1 RANGE 分区维护(视频订单表示例)
添加新分区(新增一年)
sql
ALTER TABLE video_order
ADD PARTITION (
PARTITION p2026 VALUES LESS THAN (TO_DAYS('2027-01-01'))
);
合并历史分区
sql
ALTER TABLE video_order
REORGANIZE PARTITION p2024, p2025 INTO (
PARTITION p_history VALUES LESS THAN (TO_DAYS('2026-01-01'))
);
删除历史分区(快速清理旧数据)
sql
ALTER TABLE video_order
DROP PARTITION p_history;
以上操作为 DDL,执行期间会锁表,应放在业务低峰期或在备库上操作后切换。
5.2 HASH 分区维护(视频评论表示例)
增加分区数量
sql
-- 从16分区增加到24分区(追加 8 个分区)
ALTER TABLE video_comment_p
ADD PARTITION PARTITIONS 8;
减少分区数量
sql
-- 从24分区合并回16分区(合并 8 个分区)
ALTER TABLE video_comment_p
COALESCE PARTITION 8;
HASH/KEY 分区的扩缩分区会触发数据重新分布,是重度 DDL,务必提前评估耗时和锁表影响。
6. 分区与索引的关系及性能影响
-
本地索引(Local Index)
- MySQL 5.7 只有本地索引:每个分区内有独立 B+ 树索引,只覆盖本分区数据。
- 唯一约束在"分区键 + 其他字段"维度上保证唯一性。
-
分区裁剪 + 索引结合
- 查询条件包含分区键时,优化器可以先裁剪分区,再在分区内使用索引:
sqlEXPLAIN PARTITIONS SELECT * FROM video_comment_p WHERE video_id = 12345 ORDER BY create_time DESC LIMIT 20;- 如果
partitions列只显示一个分区,说明分区裁剪已生效。
-
缺少分区键会导致扫描所有分区
sql-- 不包含 video_id,可能扫描所有分区 SELECT * FROM video_comment_p WHERE user_id = 1001;- 虽然
user_id有索引,但仍然需要在每个分区的idx_user_id上做索引扫描,总体代价较大。
- 虽然
-
高并发下的效果
- 优点:单分区索引更小,缓存更容易命中;随机 IO 减少。
- 风险:分区过多时,元数据和文件句柄开销增大,一般建议分区数控制在几十级别(16 / 32 / 64)。
7. 使用过程中的重要注意事项和常见陷阱
-
分区键不在主键/唯一索引中(硬错误)
sqlCREATE TABLE t ( id BIGINT NOT NULL PRIMARY KEY, video_id BIGINT NOT NULL, UNIQUE KEY uk_video (video_id) ) PARTITION BY HASH(video_id) PARTITIONS 16;- 上述建表会失败,必须改为:
sqlPRIMARY KEY (id, video_id) -
查询未包含分区键导致全分区扫描
- 一定要在接口规范里要求:访问分区表时,where 条件必须尽量包含分区键(
video_id或user_id)。
- 一定要在接口规范里要求:访问分区表时,where 条件必须尽量包含分区键(
-
对分区键使用函数包装,导致无法裁剪
- 如果分区键为
TO_DAYS(order_time),而查询写成:
sqlWHERE DATE(order_time) = '2025-01-12'- 优化器可能无法根据表达式反推命中分区,进而扫描多个分区。
- 推荐写法:
sqlWHERE order_time >= '2025-01-12 00:00:00' AND order_time < '2025-01-13 00:00:00'; - 如果分区键为
-
在线改分区是重度操作
ALTER TABLE ... PARTITION BY ...往往会重建整表,锁表时间长。- 实战建议:
- 新建分区表 + 双写 + 切流 / rename;
- 或在备库改造后,通过主备切换上线。
8. 分区与分表策略的区别和选择建议
-
MySQL 表分区
- 单库内一张逻辑表拆成多个分区,所有分区都在同一个 MySQL 实例、同一个库中。
- 适合:单机资源还比较充裕,主要问题是单表过大、历史数据清理困难。
-
单库分表策略(拆成多张物理表)
- 将
video_like、video_comment等拆成video_like_0~video_like_15多张物理表,由应用或中间层按照路由规则负责 SQL 路由和结果归并。 - 当前项目的短视频系统已经采用单库分表方案(见《单库分表优化方案.md》),适合:
- 总数据量大(如点赞 3 亿、评论 1 亿);
- 未来有分库扩展需求。
- 将
-
选择建议(结合当前项目)
- 对于核心的 点赞 / 评论 / 收藏 等高并发表,可以优先采用单库分表策略,方便未来平滑扩容到多库。
- 对于 订单、日志、埋点等时间序列数据 ,可以在单库内采用 RANGE 分区 ,利用
DROP PARTITION做快速归档。
9. 监控和管理分区表的方法
- 查看分区元信息
sql
SELECT
TABLE_SCHEMA, TABLE_NAME, PARTITION_NAME,
PARTITION_METHOD, PARTITION_EXPRESSION,
TABLE_ROWS, DATA_LENGTH/1024/1024 AS data_mb
FROM information_schema.PARTITIONS
WHERE TABLE_SCHEMA = 'short_video'
AND TABLE_NAME = 'video_comment_p';
- 查看建表语句
sql
SHOW CREATE TABLE video_comment_p\G
- 分析执行计划命中哪些分区
sql
EXPLAIN PARTITIONS
SELECT * FROM video_comment_p
WHERE video_id = 12345
ORDER BY create_time DESC
LIMIT 20;
10. 性能优化建议和案例分析(高并发视频场景)
-
保持单分区数据量在可控范围
- 参考已有经验:单表/单分区数据量超过 1000 万行后,查询和写入性能会明显下降。
- 视频点赞 3 亿、评论 1 亿等场景下,将数据打散到 16 个分区/分表可以明显缓解单点压力。
-
结合缓存和异步写入
- 热点计数(点赞数、评论数)推荐放在 Redis 中,异步回写 MySQL。
- 批量写入或峰值写入可以通过 MQ 异步落库,减轻单分区的写入压力。
-
案例:视频评论表从单表到 HASH 分区
- 假设单表
video_comment1 亿行,其中某些热门视频有 10 万级评论:- 在单表中,这些数据和其他视频混在一个大索引里,热点视频的查询会和冷门数据争用索引和缓存。
- 使用
PARTITION BY HASH(video_id)后:- 总数据仍为 1 亿,但拆成 16 个分区,每个 ~625 万行;
- 查询
WHERE video_id = ?时只访问一个分区的索引树,延迟大幅下降。
- 假设单表
-
案例:视频订单表使用 RANGE 分区做历史归档
- 清理一年前订单:
-
传统方式:
DELETE FROM video_order WHERE order_time < '2025-01-01',大量 undo/redo、锁表严重; -
使用 RANGE 分区后,只需:
sqlALTER TABLE video_order DROP PARTITION p2024;本质是删除一个分区表空间,速度极快,对线上影响较小。
-
- 清理一年前订单:
11. 如何确认表分区是否生效
从以下三个维度检查,基本可以确认"表是否是分区表 & 查询是否命中分区裁剪":
-
确认表本身已经是分区表
- 方式一:查询
information_schema.PARTITIONS中是否有记录:
sqlSELECT TABLE_SCHEMA, TABLE_NAME, PARTITION_NAME, PARTITION_METHOD, PARTITION_EXPRESSION, TABLE_ROWS FROM information_schema.PARTITIONS WHERE TABLE_SCHEMA = 'short_video' AND TABLE_NAME = 'video_comment_p';- 如果
PARTITION_NAME不为 NULL,且能看到多行记录,说明该表已经按PARTITION_METHOD(如 HASH/RANGE)成功分区。 - 方式二:
SHOW CREATE TABLE中是否有PARTITION BY ...:
sqlSHOW CREATE TABLE video_comment_p\G- 在输出中看到类似:
sqlPARTITION BY HASH (video_id) PARTITIONS 16即可确认该表的 DDL 分区配置已经生效。
- 方式一:查询
-
确认查询是否命中分区裁剪(EXPLAIN PARTITIONS)
- 对典型查询使用
EXPLAIN PARTITIONS查看partitions列:
sqlEXPLAIN PARTITIONS SELECT * FROM video_comment_p WHERE video_id = 12345 ORDER BY create_time DESC LIMIT 20;- 如果
partitions列只显示 一个或少数几个分区名 (例如p0),说明优化器根据video_id成功做了分区裁剪。 - 对比一个没有分区键条件的查询:
sqlEXPLAIN PARTITIONS SELECT * FROM video_comment_p WHERE user_id = 1001;- 如果
partitions列显示为p0,p1,...,p15(或NULL表示所有分区),说明需要扫描所有分区,即使有user_id索引,也没有命中分区裁剪。
- 对典型查询使用
-
用边界数据做简单验证(可选)
- 对于 RANGE 分区(如
video_order按时间分区),可以插入一条位于分区边界附近的数据,然后用EXPLAIN PARTITIONS验证其查询命中哪个分区:
sqlINSERT INTO video_order (id, user_id, video_id, order_no, amount, status, order_time) VALUES (1, 1001, 2001, 'T202601120001', 9.90, 1, '2026-01-12 10:00:00'); EXPLAIN PARTITIONS SELECT * FROM video_order WHERE id = 1 AND order_time = '2026-01-12 10:00:00';- 观察
partitions列应只命中对应年份的分区(如p2026),如果与预期不符,需重新检查PARTITION BY的表达式和各分区的VALUES LESS THAN定义。
- 对于 RANGE 分区(如