MySQL 8.0+ 创建 RANGE-HASH 复合分区表需先按 RANGE COLUMNS 列分区,再对每个分区统一声明相同数量的 HASH 子分区,子分区键须为确定性整型表达式且 SUBPARTITIONS 数量必须一致。
MySQL 8.0+ 创建 RANGE-HASH 复合分区表的关键写法
mysql 原生不支持 range-hash 这种两级分区语法(比如 partition by range columns(...) subpartition by hash(...) 是合法的,但必须显式写出子分区数量或定义),很多人卡在语法报错或数据只落到第一个分区上。
真正能用的写法是:先按列做 RANGE 分区,再对每个 RANGE 分区手动声明若干 HASH 子分区,且所有子分区总数必须一致。否则会报错 ERROR 1517 (HY000): Duplicate partition name 或直接拒绝建表。
-
PARTITION BY RANGE COLUMNS(created_at)必须指定具体列,不能用表达式;created_at类型要是 DATE/DATETIME/TIMESTAMP,否则分区裁剪失效 - 每个
PARTITION p202401后必须跟SUBPARTITION BY HASH(YEAR(created_at)*100+MONTH(created_at)) SUBPARTITIONS 4—— 注意SUBPARTITIONS 4要统一,不能有的写 4、有的写 8 - 子分区键只能是单列或确定性表达式,
HASH不支持TO_DAYS()这类非整型返回值,会报ERROR 1064
CREATE TABLE orders (
id BIGINT,
created_at DATETIME,
amount DECIMAL(10,2)
) PARTITION BY RANGE COLUMNS(created_at) (
PARTITION p202401 VALUES LESS THAN ('2024-02-01')
SUBPARTITION BY HASH(YEAR(created_at)*100+MONTH(created_at)) SUBPARTITIONS 4,
PARTITION p202402 VALUES LESS THAN ('2024-03-01')
SUBPARTITION BY HASH(YEAR(created_at)*100+MONTH(created_at)) SUBPARTITIONS 4,
PARTITION p_future VALUES LESS THAN (MAXVALUE)
SUBPARTITION BY HASH(YEAR(created_at)*100+MONTH(created_at)) SUBPARTITIONS 4
);
PostgreSQL 没有原生 RANGE-LIST 复合分区,得靠继承 + 触发器模拟
PostgreSQL 12+ 支持声明式分区,但只允许一级分区(PARTITION BY LIST 或 RANGE),没有 SUBPARTITION 关键字。想实现“按年 RANGE、再按地区 LIST”,就得手动建子表 + 触发器路由。
常见翻车点:触发器函数里漏写 RETURN NULL,导致主表也能插数据;或者 INSERT ... SELECT 批量插入时没走触发器,数据全进主表空壳里。
- 主表用
PARTITION BY RANGE (order_date),每个年份建一个子分区表(如orders_2024) - 再对
orders_2024单独执行CREATE TABLE orders_2024_beijing PARTITION OF orders_2024 FOR VALUES IN ('BJ') - 触发器函数里必须用
IF NEW.region = 'BJ' THEN INSERT INTO orders_2024_beijing VALUES (NEW.*); RETURN NULL;—— 少了RETURN NULL,主表还会多存一份 - 批量导入用
COPY或INSERT INTO ... SELECT时,触发器不生效,得改用INSERT ... ON CONFLICT或分批次调用
Oracle 12c+ 的 INTERVAL-RANGE + LIST 子分区实际限制
Oracle 看似最友好:PARTITION BY RANGE (dt) INTERVAL (NUMTOYMINTERVAL(1,'MONTH')) SUBPARTITION BY LIST (region),但真实场景中容易忽略两个硬约束:
- 子分区模板(
SUBPARTITION TEMPLATE)必须提前定义全,比如写了(SUBPARTITION sp_bj VALUES ('BJ'), SUBPARTITION sp_sh VALUES ('SH')),那新生成的每月分区就只会创建这两个子分区,不会自动适配新增的'SZ' -
INTERVAL分区只支持NUMTODSINTERVAL和NUMTOYMINTERVAL,不能用TRUNC(dt, 'MM')这种函数,否则建表直接报ORA-14751 - 查询时如果
WHERE dt BETWEEN ... AND ... AND region = 'SZ',而SZ不在模板里,就会全表扫描所有子分区,性能断崖下跌
补救办法只有手工 ALTER TABLE ... ADD PARTITION ... SUBPARTITIONS,没法靠 INTERVAL 自动扩展子分区维度。
为什么 RANGE-HASH 在 TiDB / OceanBase 上基本不用
TiDB 5.4+ 和 OceanBase 4.x 都明确文档标注:不推荐、不测试、不保证稳定性。它们的分区机制底层依赖 Region 拆分,而 HASH 子分区会打乱数据局部性,导致跨 Region 查询暴增,QPS 掉 30% 以上是常态。
实测发现,即使语法能过(TiDB 允许 PARTITION BY RANGE ... SUBPARTITION BY HASH),执行 EXPLAIN 也会显示 PartitionUnion 扫描全部子分区,等于没分区。
- 真正该用的是
PARTITION BY RANGE COLUMNS单级,配合合理的PRIMARY KEY前缀(比如(order_id, order_date)),让 TiKV Region 按时间天然切分 - OceanBase 推荐用
PARTITION BY KEY替代 HASH,因为 KEY 支持多列且更均匀;RANGE-LIST 组合则必须用LIST COLUMNS显式声明,LIST (region)会因字符集问题报OB_ERR_INVALID_ARGUMENT - 所有分布式数据库里,“复合分区”本质是权衡:换一点 DDL 复杂度,省下后期手动
ALTER TABLE ... REORGANIZE PARTITION的运维成本 —— 但这个成本往往比预想中高得多
最常被跳过的一步:没在业务低峰期验证 SELECT COUNT(*) 是否真的下推到各子分区。只要 EXPLAIN 显示 type: ALL 且没出现 partitions: 列,说明分区策略根本没生效。










