数据库基础体系 · 第 87/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。
MySQL 分区表:Range、List、Hash、裁剪、维护和适用边界
分区表(partitioned table)是把一张逻辑表的数据,按照分区表达式映射到多个物理分区中。应用仍然通过同一个表名读写数据,MySQL 在执行访问和维护操作时,根据分区定义决定访问哪些分区。
分区表解决的主要问题是:
- 让大表可以按时间、类别等规则分段管理;
- 让查询能够通过分区裁剪跳过不相关的数据;
- 让归档、删除历史数据等操作从“逐行删除”变成“删除或交换分区”;
- 将单个表的数据和索引划分为多个更小的管理单元。
它没有解决以下问题:
- 不会自动把数据分布到多个 MySQL 实例;
- 不会自动提供跨实例路由;
- 不等同于分库分表或 Vitess;
- 不保证查询一定更快;
- 不替代合理的索引和数据模型设计。
本文以 MySQL 8.4、InnoDB、单个 MySQL 实例为主要边界。不同存储引擎,尤其是 NDB Cluster,可能具有不同的分区语义和限制。
一、先建立分区表的基本模型
设表中每一行是 ,分区表达式为 ,分区集合为:
分区定义决定了一个映射函数:
插入一行时,MySQL 先计算分区表达式,再将该行写入对应分区。
以按日期范围分区为例:
p202401: created_at < 2024-02-01
p202402: created_at < 2024-03-01
pmax: created_at >= 2024-03-01
如果一行的 created_at 是 2024-02-18,它会进入 p202402。
分区不是多个独立的逻辑表。对用户而言,仍然存在一个表:
SELECT * FROM orders;
INSERT INTO orders (...);
UPDATE orders SET ...;
但是在存储和执行层面,分区表通常拥有每个分区自己的数据和索引结构。对 InnoDB 而言,分区表仍然受事务、锁、MVCC、崩溃恢复等 InnoDB 机制管理;“分区”不是事务边界。
1. 分区键和唯一键的约束
对于 InnoDB 分区表,每一个唯一键,包括主键,都必须包含分区表达式使用的列。
例如,下面的设计是可行的:
CREATE TABLE orders (
order_id BIGINT NOT NULL,
created_at DATETIME NOT NULL,
customer_id BIGINT NOT NULL,
amount DECIMAL(12, 2) NOT NULL,
PRIMARY KEY (order_id, created_at),
KEY idx_customer_created (customer_id, created_at)
)
PARTITION BY RANGE COLUMNS (created_at) (
PARTITION p202401 VALUES LESS THAN ('2024-02-01'),
PARTITION p202402 VALUES LESS THAN ('2024-03-01'),
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
这里主键包含了 created_at,因此满足约束。
下面这种设计通常会被拒绝:
CREATE TABLE orders (
order_id BIGINT NOT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (order_id)
)
PARTITION BY RANGE COLUMNS (created_at) (
PARTITION p202401 VALUES LESS THAN ('2024-02-01'),
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
原因不是 order_id 不能作为主键,而是唯一性检查必须能够在分区范围内完成。MySQL 的分区表没有通用的全局唯一索引来跨所有分区检查 order_id。
因此,分区键进入主键后,主键的实际访问形态也会改变:
WHERE order_id = 100
未必能直接定位分区,因为缺少 created_at 条件。数据库可能需要检查多个分区。
2. 分区键不改变逻辑唯一性
如果主键是:
PRIMARY KEY (order_id, created_at)
则唯一性是这两个列的组合唯一,而不是 order_id 在整个表中单独唯一。
如果业务要求 order_id 全局唯一,有几种常见选择:
- 将
order_id设计为包含分区键信息的全局唯一值; - 使用应用层或独立序列生成器;
- 不对这张表做这种分区;
- 采用支持全局唯一索引的其他架构。
不能仅因为列声明了 UNIQUE(order_id),就认为分区表会提供跨分区的全局唯一约束。
3. 分区表与外键
MySQL InnoDB 分区表在外键支持方面存在限制。生产设计中不能假设分区表可以像普通 InnoDB 表一样自由地作为外键的父表或子表。建立表结构前,应以目标 MySQL 版本对分区表外键的明确支持范围为准;对于 MySQL 8.4 的常规 InnoDB 分区设计,通常采用应用层维护引用完整性,或把有强外键要求的表保持为非分区表。
二、Range:按连续范围分区
Range 分区按照有序边界划分区间。常见用途是:
- 按月份或季度保存事件、订单、日志;
- 保留最近若干个月的数据;
- 删除完整的历史时间段;
- 对连续数值区间进行管理。
1. RANGE 与 RANGE COLUMNS
RANGE 使用一个表达式:
PARTITION BY RANGE (TO_DAYS(created_at))
分区边界必须使用整数值:
CREATE TABLE events (
event_id BIGINT NOT NULL,
created_at DATETIME NOT NULL,
payload JSON,
PRIMARY KEY (event_id, created_at)
)
PARTITION BY RANGE (TO_DAYS(created_at)) (
PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-02-01')),
PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-03-01')),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
RANGE COLUMNS 直接按照一个或多个列的值比较,适合日期、字符串或多列范围:
CREATE TABLE events_by_date (
event_id BIGINT NOT NULL,
created_at DATE NOT NULL,
payload JSON,
PRIMARY KEY (event_id, created_at)
)
PARTITION BY RANGE COLUMNS (created_at) (
PARTITION p202401 VALUES LESS THAN ('2024-02-01'),
PARTITION p202402 VALUES LESS THAN ('2024-03-01'),
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
对于单列日期,RANGE COLUMNS(created_at) 通常比 RANGE(TO_DAYS(created_at)) 更直观,也避免在表达式中重复编码日期转换逻辑。
2. 边界是左闭右开
Range 分区的边界遵循:
前一个边界 <= value < 当前边界
上例中:
p202401: created_at < 2024-02-01
p202402: 2024-02-01 <= created_at < 2024-03-01
因此:
created_at |
分区 |
|---|---|
2024-01-31 |
p202401 |
2024-02-01 |
p202402 |
2024-02-29 |
p202402 |
2024-03-01 |
pmax |
这是设计月份分区时最常见的边界规则。不要把 VALUES LESS THAN ('2024-02-01') 理解成“截至 2 月 1 日”,它表示严格小于 2 月 1 日。
3. MAXVALUE 的作用和限制
MAXVALUE 表示大于等于其他边界的所有值:
PARTITION pmax VALUES LESS THAN (MAXVALUE)
它可以防止未来日期插入时报“没有匹配分区”,但会带来维护要求:不能直接在 pmax 后面追加新的分区,因为 pmax 已经覆盖了所有剩余值。
错误思路:
ALTER TABLE events_by_date
ADD PARTITION (
PARTITION p202403 VALUES LESS THAN ('2024-04-01')
);
如果已有 pmax,这个操作无法按预期追加到 pmax 后面。正确方式是重新组织 pmax:
ALTER TABLE events_by_date
REORGANIZE PARTITION pmax INTO (
PARTITION p202403 VALUES LESS THAN ('2024-04-01'),
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
其逻辑是:
- 找到原来的
pmax; - 按新边界把其中的数据拆成两部分;
- 建立新的
p202403; - 保留新的
pmax; - 重新建立或调整相关索引结构。
如果原 pmax 中的数据已经包含很多历史数据,这个操作可能很重。因此,定期提前创建未来分区,或者控制 pmax 的数据量,比临时拆分一个巨大的 pmax 更容易维护。
4. NULL 的边界行为
Range 分区中,NULL 会被视为小于其他可比较值,因此通常会落入第一个 Range 分区。
例如:
PARTITION p0 VALUES LESS THAN ('2024-01-01')
可能接收 created_at IS NULL 的行。
如果业务上 created_at 不允许为空,应直接声明:
created_at DATETIME NOT NULL
不要把 NULL 是否进入某个“异常分区”当成数据质量控制方案。RANGE、RANGE COLUMNS、LIST 对 NULL 的处理细节并不应混用推断,使用前应在目标版本中用实际 DDL 和插入测试确认。
三、List:按离散值集合分区
List 分区不是比较连续范围,而是把明确列出的值归入某个分区。
例如,按地区分区:
CREATE TABLE customers (
customer_id BIGINT NOT NULL,
region VARCHAR(16) NOT NULL,
name VARCHAR(100) NOT NULL,
PRIMARY KEY (customer_id, region)
)
PARTITION BY LIST COLUMNS (region) (
PARTITION p_cn VALUES IN ('cn'),
PARTITION p_jp VALUES IN ('jp'),
PARTITION p_us VALUES IN ('us'),
PARTITION p_other VALUES IN ('uk', 'de', 'fr')
);
插入:
INSERT INTO customers VALUES
(1, 'cn', 'Alice'),
(2, 'jp', 'Bob'),
(3, 'de', 'Carol');
对应关系为:
cn -> p_cn
jp -> p_jp
de -> p_other
1. LIST 与 LIST COLUMNS
LIST 使用表达式:
PARTITION BY LIST (region_code)
LIST COLUMNS 直接使用列值,并支持更自然的字符串、日期或多列值匹配:
PARTITION BY LIST COLUMNS (country_code, business_type) (
PARTITION p_a VALUES IN (('cn', 'retail'), ('jp', 'retail')),
PARTITION p_b VALUES IN (('cn', 'enterprise')),
PARTITION p_other VALUES IN (('us', 'retail'))
);
多列 List COLUMNS 中,列表元素是元组,元组中的列顺序必须与分区列顺序一致。
2. List 分区要求覆盖所有允许值
如果没有匹配的 List 分区,插入会失败。比如上例中插入:
INSERT INTO customers VALUES (4, 'au', 'Dave');
如果没有定义包含 au 的分区,会得到类似:
ERROR 1526 (HY000): Table has no partition for value ...
这和 Range 分区中的 MAXVALUE 不同。List 没有“自动接收其他值”的通配写法。通常有三种处理方式:
- 提前把所有允许值写入某个分区;
- 增加一个包含已知“其他值”的分区,并在业务层限制枚举;
- 如果值集合持续增长,改用 Range、Hash,或重新评估是否需要分区。
3. List 适合什么数据
List 适合离散、稳定、数量有限的分类:
地区、租户等级、业务类型、数据状态
它不适合高基数、持续增长的值集合。例如按用户 ID 为每个用户定义一个 List 分区,会造成大量 DDL 和分区管理成本,通常也超过 MySQL 分区本身适合的范围。
四、Hash:按哈希结果分散数据
Hash 分区将分区表达式计算为整数,再根据哈希规则将数据分散到多个分区。
CREATE TABLE session_data (
session_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
created_at DATETIME NOT NULL,
data JSON,
PRIMARY KEY (session_id, user_id)
)
PARTITION BY HASH (user_id)
PARTITIONS 8;
它的目标不是表达业务范围,而是尽可能把数据均匀分散到多个分区。
1. 普通 HASH
普通 Hash 分区基于表达式结果进行分区。对整数表达式,直觉上可以理解为:
其中:
- 是分区表达式结果;
- 是哈希结果;
- 是分区数量;
- 是分区编号。
实际实现细节由 MySQL 定义,不能把所有类型都简单等同于应用层的 % 运算。
2. KEY 分区
KEY 分区由 MySQL 对指定列执行内部哈希,适合不想自行构造整数表达式的情况:
CREATE TABLE accounts (
account_id CHAR(36) NOT NULL,
tenant_id BIGINT NOT NULL,
email VARCHAR(255) NOT NULL,
PRIMARY KEY (account_id, tenant_id)
)
PARTITION BY KEY (tenant_id)
PARTITIONS 8;
KEY 和 HASH 的语义不同:
HASH(expr)由用户提供表达式;KEY(col1, col2)由 MySQL 负责对列值进行哈希;KEY对字符等类型更方便;- 改变分区数量可能导致大量数据重新分布。
3. LINEAR HASH 和 LINEAR KEY
线性 Hash:
CREATE TABLE metrics (
metric_id BIGINT NOT NULL,
device_id BIGINT NOT NULL,
value DECIMAL(12, 4) NOT NULL,
PRIMARY KEY (metric_id, device_id)
)
PARTITION BY LINEAR HASH (device_id)
PARTITIONS 8;
线性算法使用逐步扩展的分区编号计算方式,设计目标之一是让增加或减少分区时的数据迁移量相对普通 Hash 更小。
但这不意味着调整分区数量没有成本。重分布仍可能涉及大量数据和索引操作,而且查询裁剪能力也不会因为使用线性 Hash 而变强。
4. Hash 的主要边界
Hash 分区适合:
- 需要把数据均匀分散;
- 查询经常带有分区键等值条件;
- 不需要按时间快速删除整个连续区间。
Hash 不适合:
DELETE FROM metrics
WHERE created_at < '2024-01-01';
如果分区键是 device_id,这个时间条件无法对应到少数 Hash 分区,MySQL 仍可能访问全部分区。
Hash 还会削弱按时间归档的管理能力。对于日志和事件表,Range 通常比 Hash 更符合维护目标。
五、分区裁剪:为什么有时只访问几个分区
分区裁剪(partition pruning)是优化器根据查询条件,排除不可能包含结果的分区。
设 Range 分区为:
p202401: [2024-01-01, 2024-02-01)
p202402: [2024-02-01, 2024-03-01)
p202403: [2024-03-01, 2024-04-01)
pmax: [2024-04-01, +∞)
查询:
SELECT *
FROM events_by_date
WHERE created_at >= '2024-02-10'
AND created_at < '2024-02-20';
查询区间是:
与分区区间相交的只有 p202402,因此可以裁剪成:
访问:p202402
跳过:p202401、p202403、pmax
查询:
SELECT *
FROM events_by_date
WHERE created_at >= '2024-02-20';
无法排除 p202402、p202403 和 pmax,所以至少需要访问这三个分区。
查询:
SELECT *
FROM events_by_date
WHERE customer_id = 100;
如果 customer_id 不是分区键,且没有由条件推导出 created_at 的约束,通常不能依靠分区裁剪减少分区数量。此时是否高效,主要取决于每个分区上的索引和扫描代价。
1. 用 EXPLAIN 验证裁剪
可以使用:
EXPLAIN
SELECT *
FROM events_by_date
WHERE created_at >= '2024-02-10'
AND created_at < '2024-02-20';
在支持的输出格式中,可以看到 partitions 列,例如:
table partitions type
events_by_date p202402 range
也可以显式指定 JSON:
EXPLAIN FORMAT=JSON
SELECT *
FROM events_by_date
WHERE created_at >= '2024-02-10'
AND created_at < '2024-02-20';
EXPLAIN 显示的是优化器计划,不是对每一行实际执行过程的逐行日志。遇到复杂条件时,应结合实际执行计划、表统计信息和查询耗时判断。
2. 哪些写法容易阻碍裁剪
分区键条件最好保持为可推导的范围或等值条件:
WHERE created_at >= '2024-02-01'
AND created_at < '2024-03-01'
下面的写法可能使优化器更难根据分区边界推导:
WHERE DATE(created_at) = '2024-02-18'
这条条件的逻辑含义是清楚的,但它把函数应用在列上。更明确的写法是:
WHERE created_at >= '2024-02-18 00:00:00'
AND created_at < '2024-02-19 00:00:00'
对于 RANGE (TO_DAYS(created_at)),查询条件是否能被优化器准确转换,也应通过 EXPLAIN 验证;不能仅凭“条件中出现了日期列”就断定发生了裁剪。
以下情况也可能降低裁剪效果:
- 对分区键使用复杂函数;
- 对分区键进行隐式类型转换;
- 使用无法在优化阶段确定的表达式;
- 条件通过非确定性函数生成;
OR条件覆盖多个不连续范围;- 参数化查询在特定执行路径下无法提前确定参数值。
3. 裁剪不是索引
分区裁剪先决定访问哪些分区;索引再决定在每个已选分区内如何查找。
例如:
WHERE created_at >= '2024-02-10'
AND created_at < '2024-02-20'
AND customer_id = 100
可能发生:
- 根据
created_at裁剪到p202402; - 在
p202402的customer_id索引上查找; - 返回满足完整条件的行。
如果只有分区而没有合适索引,裁剪后仍可能扫描整个 p202402。如果有合适索引但查询没有分区键条件,则可能要在多个分区中分别使用索引。
六、分区维护:增加、拆分、删除和交换
分区维护的价值,主要体现在“按整段数据管理”,而不是让所有 DML 自动变快。
1. 查看分区定义和数据量
查看表定义:
SHOW CREATE TABLE events_by_date\G
查看分区元数据:
SELECT
PARTITION_NAME,
PARTITION_ORDINAL_POSITION,
PARTITION_METHOD,
PARTITION_EXPRESSION,
PARTITION_DESCRIPTION,
TABLE_ROWS
FROM INFORMATION_SCHEMA.PARTITIONS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'events_by_date'
ORDER BY PARTITION_ORDINAL_POSITION;
TABLE_ROWS 对 InnoDB 通常是估算值,不应当当作精确行数。它适合观察趋势,不适合直接作为精确审计结果。
2. 删除历史分区
如果表按月份分区,可以删除整个月份:
ALTER TABLE events_by_date
DROP PARTITION p202401;
这会删除该分区中的全部数据。它不是带 WHERE 条件的可回滚单行删除操作,必须把它当作破坏性 DDL 处理。
优点是:
- 不需要逐行生成大量删除记录;
- 不需要长时间扫描目标行;
- 可以直接移除整个分区的数据和索引。
风险是:
- 分区名或边界选错会删除错误数据;
- 相关 DDL 会获取元数据锁;
- 执行期间可能阻塞或等待其他事务;
- 数据删除后不能依赖普通事务回滚来恢复。
执行前应确认:
SELECT PARTITION_NAME, PARTITION_DESCRIPTION
FROM INFORMATION_SCHEMA.PARTITIONS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'events_by_date';
并准备可验证的备份或归档副本。
3. 用 REORGANIZE PARTITION 拆分和合并
把 pmax 拆成新月份和新的 pmax:
ALTER TABLE events_by_date
REORGANIZE PARTITION pmax INTO (
PARTITION p202403 VALUES LESS THAN ('2024-04-01'),
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
合并两个相邻 Range 分区:
ALTER TABLE events_by_date
REORGANIZE PARTITION p202401, p202402 INTO (
PARTITION p2024q1 VALUES LESS THAN ('2024-04-01')
);
拆分前,原分区中的每一行必须能够落入新的某个分区;否则操作会失败。合并时,新分区边界必须覆盖原分区中的所有值。
这类操作可能需要重建数据和索引,执行时间取决于分区大小、索引数量、磁盘速度和并发负载。不要把 REORGANIZE 当作常数时间的目录操作。
4. TRUNCATE PARTITION
如果要清空一个分区但保留分区定义:
ALTER TABLE events_by_date
TRUNCATE PARTITION p202401;
这适用于临时数据或需要重复使用分区结构的场景。它同样是破坏性操作,不应与“删除满足条件的部分行”混淆。
5. EXCHANGE PARTITION
EXCHANGE PARTITION 可以在分区与普通表之间交换数据归属,常用于归档或快速装载。
示例:
CREATE TABLE events_archive LIKE events_by_date;
但这里不能直接把另一个分区表当作交换对象。交换对象通常应是一个非分区表,并且表结构、索引等必须满足交换要求。
一个更完整的流程如下:
CREATE TABLE events_202401 LIKE events_by_date;
ALTER TABLE events_by_date
EXCHANGE PARTITION p202401
WITH TABLE events_202401
WITH VALIDATION;
WITH VALIDATION 会验证交换表中的行是否都符合目标分区边界。例如,p202401 只能接收:
2024-01-01 <= created_at < 2024-02-01
如果普通表里有越界行,交换会失败。
WITHOUT VALIDATION 可以跳过验证,减少执行成本,但前提是操作者已经独立证明数据满足边界。否则会破坏分区表的数据约束,后续查询和维护可能出现严重问题。生产环境不应为了“更快”而盲目使用它。
交换完成后,原分区中的数据会出现在普通表中,普通表原有的数据会进入分区。这个操作不是复制数据,而是改变数据归属,但仍可能受元数据锁、结构兼容性和版本能力限制。
七、按时间分区的完整维护示例
下面建立一个按月份分区的订单表:
CREATE TABLE orders (
order_id BIGINT NOT NULL,
created_at DATETIME NOT NULL,
customer_id BIGINT NOT NULL,
status VARCHAR(20) NOT NULL,
amount DECIMAL(12, 2) NOT NULL,
PRIMARY KEY (order_id, created_at),
KEY idx_customer_created (customer_id, created_at),
KEY idx_status_created (status, created_at)
)
ENGINE = InnoDB
PARTITION BY RANGE COLUMNS (created_at) (
PARTITION p202401 VALUES LESS THAN ('2024-02-01'),
PARTITION p202402 VALUES LESS THAN ('2024-03-01'),
PARTITION p202403 VALUES LESS THAN ('2024-04-01'),
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
插入测试数据:
INSERT INTO orders
(order_id, created_at, customer_id, status, amount)
VALUES
(1, '2024-01-31 23:59:59', 10, 'paid', 100.00),
(2, '2024-02-01 00:00:00', 11, 'paid', 200.00),
(3, '2024-03-15 12:00:00', 12, 'new', 300.00);
查看行所在分区,可以使用分区选择语法:
SELECT * FROM orders PARTITION (p202402);
预期能看到 order_id = 2 的行,而 order_id = 1 和 order_id = 3 不在该分区。
查询二月份数据:
EXPLAIN
SELECT order_id, customer_id, amount
FROM orders
WHERE created_at >= '2024-02-01'
AND created_at < '2024-03-01';
预期计划中的 partitions 主要显示 p202402。如果实际显示多个分区,需要进一步检查:
- 日期列类型和常量类型是否发生转换;
- 分区定义是否与查询边界一致;
- 使用的是否是目标表和目标索引;
- 优化器统计信息和执行计划是否发生变化。
增加四月份分区:
ALTER TABLE orders
REORGANIZE PARTITION pmax INTO (
PARTITION p202404 VALUES LESS THAN ('2024-05-01'),
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
归档并删除一月份:
CREATE TABLE orders_202401 LIKE orders;
ALTER TABLE orders
EXCHANGE PARTITION p202401
WITH TABLE orders_202401
WITH VALIDATION;
交换后,应先验证归档表:
SELECT COUNT(*) FROM orders_202401;
SELECT MIN(created_at), MAX(created_at)
FROM orders_202401;
确认归档数据完整后,再考虑删除分区:
ALTER TABLE orders
DROP PARTITION p202401;
这里的顺序很重要:交换、校验、备份确认、删除。直接 DROP PARTITION 不具备归档能力。
八、分区数量、DDL 和并发风险
分区数量不是越多越好。分区过多会增加:
- 表定义和元数据规模;
- 优化器枚举和裁剪成本;
- 每个分区的索引和统计信息管理成本;
- DDL、备份、恢复、检查和监控复杂度;
- 查询访问多个分区时的执行协调开销。
MySQL 对分区数量有上限,InnoDB 的具体限制应以目标版本文档和实际配置为准;即使没有达到硬上限,数千个分区也可能已经带来明显管理成本。
分区 DDL 还涉及元数据锁(MDL):
ALTER TABLE orders
DROP PARTITION p202401;
执行时可能等待正在运行的事务释放表相关元数据锁。反过来,DDL 持有或等待 MDL 时,也可能阻塞新的查询或事务。
生产执行前至少应检查:
SHOW PROCESSLIST;
以及当前版本支持的锁等待、事务和 Performance Schema 相关视图,确认:
- 是否存在长事务;
- 是否有长时间运行的查询;
- 是否有其他 DDL;
- 是否处于高峰流量;
- 是否有足够磁盘空间完成重建或临时文件操作。
“分区操作看起来只是调整边界”并不代表一定是轻量操作。DROP PARTITION、TRUNCATE PARTITION、REORGANIZE PARTITION、EXCHANGE PARTITION 的数据移动和验证成本不同,必须按具体操作评估。
九、常见误解和失败表现
1. “分区后查询自动变快”
不成立。
只有同时满足以下条件时,分区裁剪才可能减少读取范围:
- 查询谓词约束了分区表达式或分区列;
- 优化器能够从谓词推导出分区范围;
- 被保留的分区确实比全部分区少;
- 分区内还有合适的索引或扫描成本可接受。
全表查询:
SELECT COUNT(*) FROM orders;
仍然需要处理所有分区。
2. “分区可以替代索引”
不成立。
按月份分区的订单表,如果查询是:
WHERE customer_id = 100
且没有日期条件,通常无法裁剪到某一个月份。此时仍需要在多个分区中使用 customer_id 索引,或者扫描多个分区。
3. “分区就是分库分表”
不成立。
分区表仍属于同一个 MySQL 实例,通常共享:
- 实例资源;
- 连接和线程资源;
- Buffer Pool;
- 日志和后台线程;
- 事务管理;
- 故障域。
分片则需要应用或中间件根据分片键把请求路由到不同实例,可能还要处理跨分片事务、全局 ID、重分片和聚合查询。分区表不能自动提供这些能力。
4. “删除历史行应该一直用 DELETE”
如果目标正好是完整分区,例如删除整个 2024 年 1 月:
DELETE FROM orders
WHERE created_at >= '2024-01-01'
AND created_at < '2024-02-01';
这会逐行删除,并产生相应的 undo、redo、锁和二级索引维护成本。
如果数据已经按该边界单独分区:
ALTER TABLE orders DROP PARTITION p202401;
通常更符合生命周期管理模型。但它删除的是整个分区,条件验证和恢复准备必须由运维流程负责。
5. “增加分区不会影响业务”
不一定。
REORGANIZE PARTITION 可能重建大量数据;DDL 可能等待 MDL;在磁盘空间紧张时还可能失败。执行结果应通过以下方式验证:
SHOW CREATE TABLE orders\G
SELECT
PARTITION_NAME,
PARTITION_DESCRIPTION,
TABLE_ROWS
FROM INFORMATION_SCHEMA.PARTITIONS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'orders';
如果操作失败,应检查错误日志、DDL 状态、锁等待和磁盘空间,而不是直接重复执行。
十、什么时候选择哪种分区方式
选择 Range
优先考虑 Range 或 Range COLUMNS 的情况:
- 数据天然具有时间或数值顺序;
- 查询大量使用范围条件;
- 需要按连续时间段归档和删除;
- 可以预先规划边界。
典型表:
订单、支付流水、访问日志、监控指标、审计事件
选择 List
选择 List 的条件:
- 分类值是离散集合;
- 集合规模有限且变化可控;
- 希望对类别独立维护;
- 查询经常带有该分类条件。
典型表:
区域、业务线、数据等级
如果分类集合经常增加,List 的 DDL 维护会变得频繁。
选择 Hash 或 Key
选择 Hash 或 Key 的条件:
- 需要较均匀地分散数据;
- 分区键通常出现在等值查询中;
- 没有明显的时间归档需求;
- 可以接受调整分区数量时的重新分布成本。
典型表:
按用户、租户、设备 ID 分散的会话或指标数据
如果主要目标是跨 MySQL 实例扩展容量和吞吐,应该评估分片或 Vitess,而不是仅仅增加分区数量。
十一、最终判断:分区是访问路径和生命周期工具
分区设计至少要回答四个问题:
-
分区表达式是什么?
它是否稳定、可计算,并且能表达真实的数据生命周期? -
查询能否裁剪?
主要查询是否带有分区键的等值或范围条件?应使用EXPLAIN验证,而不是凭感觉判断。 -
唯一性和关联关系是否仍然成立?
主键、唯一键必须满足分区键约束;外键能力也必须按目标版本确认。 -
数据如何维护和恢复?
是否能按分区新增、归档、交换、清空和删除?DDL 锁等待、重建成本、备份与误删恢复是否可控?
Range 更偏向时间生命周期,List 更偏向稳定分类,Hash/Key 更偏向均匀分散。三者都只是同一 MySQL 实例内的数据组织方式。只有当分区规则同时符合查询条件、唯一键约束和维护流程时,分区表才是合理的数据建模选择;否则,普通表加索引,或者跨实例分片,可能更符合问题本身。
系列导航与关联阅读
- 系列入口:数据库完整学习路线:从关系模型、事务索引到分布式与向量检索
- 上一篇:MySQL Buffer Pool 与内存结构:页缓存、刷脏、Change Buffer 和 AHI
- 下一篇:MySQL SQL 实战:分页、批量、Upsert、JSON、窗口函数和锁定读
- 延伸:MySQL 表设计:主键、行格式、NULL、生成列、分区与归档
- 延伸:MySQL 分片与 Vitess:路由、VSchema、重分片和跨分片事务
官方资料
本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。

评论
0 条讨论