一、分区 Partitioning
分区表是将一张大表在物理上分成多个分区(文件),但逻辑上仍是一张表,由MySQL内部管理。写入表的数据会通过MySQL的分区函数来决定存储在哪个分区中。
1. Range分区
RANGE 分区就是按某个列的值的连续区间 ,把数据分到不同的物理分区里。它最适合用在日期/时间字段上,比如按年、按月拆分历史数据,查询和清理数据都会快很多。可以使用 MINVALUE 和 MAXVALUE 设置范围的最小和最大值。
sql
CREATE TABLE employees (
id INT NOT NULL,
store_id INT NOT NULL
) PARTITION BY RANGE (store_id) (
PARTITION p0 VALUES LESS THAN (6), -- 存 store_id 1~5
PARTITION p1 VALUES LESS THAN (11), -- 存 store_id 6~10
PARTITION p2 VALUES LESS THAN (16), -- 存 store_id 11~15
PARTITION p3 VALUES LESS THAN MAXVALUE -- 存 >= 16 的所有数据
);
- 顺序要求:分区必须按从小到大的顺序定义,不能乱序。
- 上限排除 :
LESS THAN是严格小于,边界值本身不属于该分区。 - 兜底分区 :建议加一个
VALUES LESS THAN MAXVALUE分区,防止超出范围的数据插入时报错。
2. List分区
LIST分区是按离散的枚举值列表 来分配数据的,每个分区对应一组明确指定的值,而不是连续的区间。注意:RANGE按连续区间分,LIST按值列表分。
sql
CREATE TABLE employees (
id INT NOT NULL,
store_id INT
) PARTITION BY LIST (store_id) (
PARTITION pNorth VALUES IN (3, 5, 6, 9, 17),
PARTITION pEast VALUES IN (1, 2, 10, 11, 19, 20),
PARTITION pWest VALUES IN (4, 12, 13, 14, 18),
PARTITION pCentral VALUES IN (7, 8, 15, 16)
);
- 分区键必须是整数 :LIST分区只支持整型列或返回整型的表达式,非整型字段要先转换(比如用
YEAR(hired)对日期取年份)。 - 没有"兜底"分区 :不像RANGE有
MAXVALUE,LIST必须覆盖所有可能值。插入不在任何枚举列表中的值会直接报错:Table has no partition for value 3。 - NULL值处理:如果NULL不在枚举列表里,插入NULL也会失败。建议分区列设为非NULL。
- 分区字段必须在主键里:如果表有主键或唯一索引,分区字段必须包含在其中,否则创建失败。
3. HASH分区
HASH分区主要用于把数据均匀地分散到预定数量的分区里 ,你不需要手动指定每条数据存哪个分区,MySQL会自动根据分区键的哈希值来分配。HASH分区的核心是取模运算 :分区编号 N = MOD(expr, num)
sql
CREATE TABLE employees (
id INT NOT NULL,
fname VARCHAR(30),
lname VARCHAR(30),
hired DATE NOT NULL DEFAULT '1970-01-01',
store_id INT
) PARTITION BY HASH(store_id)
PARTITIONS 4;
- 表达式要求 :
expr必须是返回非负整数 的表达式,可以是整数列名(如store_id)或基于列的函数(如YEAR(hired))。表达式必须确定且非随机,每次插入、更新(可能还有删除)时都会重新计算,太复杂的表达式会影响批量写入性能。 - 分区数量 :
PARTITIONS num中的num是正整数,不写默认为 1 ;写了PARTITIONS但没跟数字会报语法错误。 - 唯一键限制 :如果表有主键或唯一键,分区表达式涉及的所有列必须包含在每个唯一键中 (包括主键)。如果唯一键里不包含分区键,那么插入一条新数据时,MySQL 不知道它该去哪个分区找重复值,只能扫描所有分区来确保不重复------这就让分区失去了性能意义,所以 MySQL 直接从语法层面禁止了这种行为。
- NULL值处理 :HASH分区把 NULL 当作 0 处理。
4. KEY分区
KEY分区使用MySQL服务器提供的内部哈希函数。这个函数会处理各种数据类型(如字符串、日期等),确保它们能被均匀地映射到各个分区。
与HASH分区的区别:HASH分区让你自己决定"如何计算哈希值",而KEY分区则把这个任务交给了MySQL服务器。
| 特性 | HASH 分区 | KEY 分区 |
|---|---|---|
| 哈希函数 | 用户定义的表达式 | MySQL服务器提供的内部哈希函数 |
| 分区键 | 一个整数表达式或列 | 一个或多个列名 ,且列不限于整数或NULL值 |
| 语法关键字 | PARTITION BY HASH (expr) |
PARTITION BY KEY (column_list) |
| 与主键/唯一键关系 | 分区表达式中的列必须包含在表的所有唯一键(包括主键)中 | 分区键必须是表主键的一部分或全部;若未指定列,则默认使用主键 |
sql
CREATE TABLE k1 (
id INT NOT NULL PRIMARY KEY,
name VARCHAR(20)
) PARTITION BY KEY() PARTITIONS 2; -- 不指定分区键,自动用主键
二、垂直分表(冷热数据分离)
MySQL的垂直分表本质就是冷热数据分离:按字段访问频率,把一张大表拆成"热表"和"冷表"两张一对一的小表,通过主键关联。
举例:有一张订单表(单裤单表),字段如下:
sql
-- 表结构(只列关键字段)
orders (
order_id BIGINT AUTO_INCREMENT PRIMARY KEY, -- 自增主键
user_id INT,
order_no VARCHAR(32),
amount DECIMAL(10,2),
create_time DATETIME,
remark TEXT, -- 买家备注(很长,但很少查)
...
)
- 原则:把访问频率低、字段很长的列,放到另一张"附属表"里。
- 操作 :把
remark(备注)、收货地址详情等长字段,迁移到orders_ext表中。 - 拆后表现:
orders表:留下高频字段,行数据变"窄"了。现在单行只有几百字节,一页内存(16KB)能存更多行,查询缓存命中率大大提升,IO 开销降低。orders_ext表:存冷数据,通过order_id一对一关联。
sql
-- 主表(热数据,频繁查询)
orders ( order_id, user_id, order_no, amount, create_time )
-- 扩展表(冷数据,偶尔查)
orders_ext ( order_id, remark, delivery_address )
注意事项:
- 关联查询:拆表后如果业务要同时查两边字段,需要应用层二次加工或冗余,不能完全依赖JOIN。
- 数据一致性:写入时最好用事务保证主表和副表同时成功,否则容易出现数据对不上。
三、垂直分库(业务拆分)
垂直分库就是按业务模块把不同的表拆到不同的数据库里。比如把用户相关的表放进用户库,商品相关的表放进商品库,订单相关的表放进订单库,每个库各管一摊业务。
优缺点:
- 优点:业务职责清晰、隔离性好,不同业务的数据可以分级管理,也方便独立扩展资源。
- 缺点:跨库 JOIN 会变得很复杂甚至无法直接执行,通常得靠应用层做数据聚合;跨库事务需要引入分布式事务方案,开发复杂度和维护成本明显上升。
四、水平分表
水平分表是将一张大表的数据按行拆分到多个结构相同的子表中,用于解决单表数据量过大导致的性能瓶颈 。逻辑上仍视为一张表,但物理存储分散 。
一句话定义 :把一张"胖大"的逻辑表,按行拆成多张结构完全相同的"瘦小"物理表,存放在同一个数据库实例(同一台服务器)中。
-
具体操作 :表名从
order变成order_0、order_1、order_2......order_7。 -
解决的核心痛点 :单表数据量过大(比如超过千万级)。
-
数据量大了,B+Tree 索引层级变高(3层变4层),磁盘I/O次数增加。
-
执行
ALTER TABLE加字段时,会锁表锁很久,影响业务。
-
-
保留的痛点 :依然在同一个 MySQL 实例上,磁盘空间、CPU、内存、网络带宽依然是这台的,没变。
五、水平分库
一句话定义 :把一张"胖大"的逻辑表,按行拆成多张结构完全相同的物理表,并分布到不同的数据库实例(多台服务器)上。
-
具体操作 :表名可能还叫
order,但库名从db_order变成db_order_0、db_order_1......放在 IP 不同的 4 台物理机上。 -
解决的核心痛点 :单台服务器的硬件极限。
-
磁盘写满了?分库后写到了4块盘上。
-
CPU 打满了?分库后 4 颗 CPU 一起扛。
-
数据库连接数不够了?分库后总连接数翻了4倍。
-
-
新增的代价:事务变得复杂(跨库分布式事务),连表查询(JOIN)基本废了。
六、🛒 实战案例:订单表 order 的分库分表
背景 :我们的订单表每天新增 50 万条,一年接近 2 亿条。查询基本都是 WHERE user_id = xxx。
架构设计 :我们规划 4 个库 (物理机),每个库里 4 张表,总共 16 张物理表。
1. 分片键 :选择 user_id(用户ID)。因为 99% 的查询都是"我的订单"。
2. 路由算法 :采用 user_id % 4(取模)。
(简单起见,库序号和表序号用同一个规则,即 user_id=1 的数据全进 库1 的 表1)
3. 写入演示(INSERT)
用户 user_id = 10086 下了一笔新订单。
-
中间件计算 :
10086 % 4 = 2。 -
SQL 改写 :
INSERT INTO order ...被改写成INSERT INTO order_2 ...。 -
路由结果 :这条数据被精准写入
第 2 台数据库服务器上的order_2这张表里。
4. 查询演示(SELECT)
该用户点击"我的订单":SELECT * FROM order WHERE user_id = 10086 ORDER BY create_time DESC LIMIT 10。
-
中间件计算 :依然是
10086 % 4 = 2。 -
查询定位 :中间件直接去第 2 台机器,查
order_2表。 -
结果:只查 1 台机器、1 张表(仅 500 万数据),速度飞起!
5. 翻车现场(不带分片键)
运营后台要查:SELECT * FROM order WHERE create_time > '2026-01-01'。
因为没有 user_id,中间件不知道去哪查。它会向 4 个库的 16 张表同时发起查询 ,拿到结果后在内存里归并排序。这就是分库分表最怕的场景,会导致查询极慢甚至内存溢出。
七、总结对比
| 对比维度 | 水平分库 | 水平分表 | 垂直分库 | 垂直分表 | 分区(Partition) |
|---|---|---|---|---|---|
| 一句话定义 | 按行将数据拆分到不同数据库实例(多台服务器) 的相同表中 | 按行将数据拆分到同一数据库实例的多张相同结构表中 | 按业务模块,将不同表拆分到不同数据库实例 | 按字段访问频率,将一张宽表拆成多张窄表(同一库) | 按行将数据拆分到同一张逻辑表下的多个物理文件片段中 |
| 拆分依据 | 数据行(基于分片键如 user_id) |
数据行(基于分片键如 user_id) |
业务表(如订单表、用户表、商品表) | 字段列(冷热字段分离) | 数据行(基于分区表达式如 HASH(id)) |
| 存储位置 | 跨服务器(多台物理机/虚拟机) | 单服务器(同一个MySQL实例) | 跨服务器(不同业务库在不同机器) | 单服务器(同一个库中) | 单服务器(同一实例的同一数据库目录下) |
| 对应用透明性 | 不透明 需中间件或应用层改写SQL路由 | 不透明 需中间件或应用层改写表名 | 不透明 需应用层管理多数据源,无法跨库JOIN | 半透明 需应用层知道要关联查扩展表 | 完全透明 应用写的还是原表名,MySQL内部自动路由 |
| 解决的核心痛点 | 突破单机物理极限 (磁盘、CPU、内存、连接数、IOPS) | 降低单表数据量 (压减B+Tree层级,提升索引效率) | 业务解耦 (微服务化,资源隔离,互不抢占) | 缩小行宽度 (提高热点数据缓存命中率,减少IO) | 方便管理海量冷数据 (可快速删除分区文件)且查询时可裁剪分区 |
| 主要代价/痛点 | 分布式事务难; 跨库JOIN失效; 深分页极慢; 扩容复杂 | 表总数膨胀; 仍需解决单机资源瓶颈; 跨表查询需中间件聚合 | 无法跨库联表查询; 需在应用层组装数据; 事务一致性难保证 | 查询常需关联两张表; 增加了代码复杂度 | 单机资源瓶颈依然存在 ; 对主键/唯一键有致命限制(分区列必须在所有唯一键中) |
| 典型适用场景 | 高并发C端核心表(如订单、支付、用户) | 单表数据量巨大但并发尚可,且暂时不想引入多机房分库 | 中大型微服务系统(订单库、库存库、用户库分离) | 行数据包含大字段(如 TEXT、BLOB 的备注、详情) |
日志流水表、历史归档表(按天/月分区,便于快速清理) |
| 扩容能力 | 水平扩展(强) 加机器即可提升吞吐量 | 垂直扩展(弱) 仅能换更强硬件,无法加机器 | 水平/垂直混合 可按业务分配不同性能机器 | 几乎无 仍依赖单机性能 | 无 无法突破单机物理上限 |
| 主键/唯一键约束 | 较宽松 中间件通常支持全局ID(如雪花算法),不强制要求分片键包含在唯一键中(视中间件具体实现而定) | 较宽松 同上(通常依赖中间件管理唯一性) | 无特殊限制 库不同,主键可独立自增 | 无特殊限制 依然是单表,主键自增正常 | 极其严格 若表有主键/唯一键,分区表达式涉及的所有列必须包含在每一个唯一键中 |