数据库基础体系 · 第 87/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。

MySQL 分区表:Range、List、Hash、裁剪、维护和适用边界

分区表(partitioned table)是把一张逻辑表的数据,按照分区表达式映射到多个物理分区中。应用仍然通过同一个表名读写数据,MySQL 在执行访问和维护操作时,根据分区定义决定访问哪些分区。

分区表解决的主要问题是:

  • 让大表可以按时间、类别等规则分段管理;
  • 让查询能够通过分区裁剪跳过不相关的数据;
  • 让归档、删除历史数据等操作从“逐行删除”变成“删除或交换分区”;
  • 将单个表的数据和索引划分为多个更小的管理单元。

它没有解决以下问题:

  • 不会自动把数据分布到多个 MySQL 实例;
  • 不会自动提供跨实例路由;
  • 不等同于分库分表或 Vitess;
  • 不保证查询一定更快;
  • 不替代合理的索引和数据模型设计。

本文以 MySQL 8.4、InnoDB、单个 MySQL 实例为主要边界。不同存储引擎,尤其是 NDB Cluster,可能具有不同的分区语义和限制。


一、先建立分区表的基本模型

设表中每一行是 rr,分区表达式为 f(r)f(r),分区集合为:

P={p0,p1,,pn1}P = \{p_0, p_1, \ldots, p_{n-1}\}

分区定义决定了一个映射函数:

g(f(r))=pig(f(r)) = p_i

插入一行时,MySQL 先计算分区表达式,再将该行写入对应分区。

以按日期范围分区为例:

p202401: created_at < 2024-02-01
p202402: created_at < 2024-03-01
pmax:    created_at >= 2024-03-01

如果一行的 created_at2024-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. RANGERANGE 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)
);

其逻辑是:

  1. 找到原来的 pmax
  2. 按新边界把其中的数据拆成两部分;
  3. 建立新的 p202403
  4. 保留新的 pmax
  5. 重新建立或调整相关索引结构。

如果原 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 是否进入某个“异常分区”当成数据质量控制方案。RANGERANGE COLUMNSLISTNULL 的处理细节并不应混用推断,使用前应在目标版本中用实际 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. LISTLIST 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 分区基于表达式结果进行分区。对整数表达式,直觉上可以理解为:

p=h(x)modNp = h(x) \bmod N

其中:

  • xx 是分区表达式结果;
  • h(x)h(x) 是哈希结果;
  • NN 是分区数量;
  • pp 是分区编号。

实际实现细节由 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;

KEYHASH 的语义不同:

  • HASH(expr) 由用户提供表达式;
  • KEY(col1, col2) 由 MySQL 负责对列值进行哈希;
  • KEY 对字符等类型更方便;
  • 改变分区数量可能导致大量数据重新分布。

3. LINEAR HASHLINEAR 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';

查询区间是:

[2024-02-10, 2024-02-20)[2024\text{-}02\text{-}10,\ 2024\text{-}02\text{-}20)

与分区区间相交的只有 p202402,因此可以裁剪成:

访问:p202402
跳过:p202401、p202403、pmax

查询:

SELECT *
FROM events_by_date
WHERE created_at >= '2024-02-20';

无法排除 p202402p202403pmax,所以至少需要访问这三个分区。

查询:

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

可能发生:

  1. 根据 created_at 裁剪到 p202402
  2. p202402customer_id 索引上查找;
  3. 返回满足完整条件的行。

如果只有分区而没有合适索引,裁剪后仍可能扫描整个 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 = 1order_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 PARTITIONTRUNCATE PARTITIONREORGANIZE PARTITIONEXCHANGE PARTITION 的数据移动和验证成本不同,必须按具体操作评估。


九、常见误解和失败表现

1. “分区后查询自动变快”

不成立。

只有同时满足以下条件时,分区裁剪才可能减少读取范围:

  1. 查询谓词约束了分区表达式或分区列;
  2. 优化器能够从谓词推导出分区范围;
  3. 被保留的分区确实比全部分区少;
  4. 分区内还有合适的索引或扫描成本可接受。

全表查询:

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,而不是仅仅增加分区数量。


十一、最终判断:分区是访问路径和生命周期工具

分区设计至少要回答四个问题:

  1. 分区表达式是什么?
    它是否稳定、可计算,并且能表达真实的数据生命周期?

  2. 查询能否裁剪?
    主要查询是否带有分区键的等值或范围条件?应使用 EXPLAIN 验证,而不是凭感觉判断。

  3. 唯一性和关联关系是否仍然成立?
    主键、唯一键必须满足分区键约束;外键能力也必须按目标版本确认。

  4. 数据如何维护和恢复?
    是否能按分区新增、归档、交换、清空和删除?DDL 锁等待、重建成本、备份与误删恢复是否可控?

Range 更偏向时间生命周期,List 更偏向稳定分类,Hash/Key 更偏向均匀分散。三者都只是同一 MySQL 实例内的数据组织方式。只有当分区规则同时符合查询条件、唯一键约束和维护流程时,分区表才是合理的数据建模选择;否则,普通表加索引,或者跨实例分片,可能更符合问题本身。


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。