SQL优化案例:使用分区表优化SQL查询性能

文章目录

环境

系统平台:N/A

版本:9.0,4.5

文档用途

本文介绍通过使用分区表提升SQL查询性能。

详细信息

1. 场景

  1. 分区裁剪
    使用分区键查分区表,访问数据量下降,提升性能。
  2. 批量删除
    删除分区操作代替DELETE操作,增强可维护性。

2. 分区建议

几种分区观点

  1. 大于1000万行或表大小超过100G
  2. 历史数据清理频繁,DELETE产生大量死元组,VACUUM跟不上,DROP分区代替DELETE

3. 选择合适的分区类型

分区类型 适用场景 数据分布方式
RANGE 时间序列、数值区间 按范围映射,无重叠
LIST 地理区域、业务分类 按离散值映射
HASH 均匀打散,无天然分区键 根据哈希值分配

3.1 RANGE 分区 --- 最常用

适合时间序列数据(日志、订单、交易流水)。

sql 复制代码
-- 按年月分区
CREATE TABLE orders (
    id         BIGSERIAL,
    order_date DATE NOT NULL,
    amount     NUMERIC(10,2),
    user_id    INT
) PARTITION BY RANGE (order_date);

-- 创建分区
CREATE TABLE orders_2025_01 PARTITION OF orders
    FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');

CREATE TABLE orders_2025_02 PARTITION OF orders
    FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');

CREATE TABLE orders_2025_03 PARTITION OF orders
    FOR VALUES FROM ('2025-03-01') TO ('2025-04-01');

注意:RANGE 分区必须连续且无重叠。插入超出范围的数据会报错;预留"默认"分区兜底。

3.2 LIST 分区 --- 离散值

适合按区域、状态、类型分类的数据。

sql 复制代码
-- 按区域分区
CREATE TABLE users (
    id      BIGSERIAL,
    name    TEXT,
    region  TEXT NOT NULL
) PARTITION BY LIST (region);

CREATE TABLE users_asia PARTITION OF users
    FOR VALUES IN ('CN', 'JP', 'KR', 'SG');

CREATE TABLE users_eu PARTITION OF users
    FOR VALUES IN ('DE', 'FR', 'UK', 'IT');

CREATE TABLE users_others PARTITION OF users
    DEFAULT;

DEFAULT 分区兜底所有未匹配的值。如果有大量数据落入 DEFAULT,说明分区策略需要调整。

3.3 HASH 分区 --- 均匀打散

适合没有合适分区键但需提升性能的场景,数据均匀分布,但分区裁剪效果最弱(只有等值查询才能裁剪)。

sql 复制代码
-- 按 user_id 哈希分为 4 个分区
CREATE TABLE sessions (
    id         BIGSERIAL,
    user_id    INT NOT NULL,
    login_at   TIMESTAMPTZ
) PARTITION BY HASH (user_id);

CREATE TABLE sessions_p0 PARTITION OF sessions
    FOR VALUES WITH (MODULUS 4, REMAINDER 0);

CREATE TABLE sessions_p1 PARTITION OF sessions
    FOR VALUES WITH (MODULUS 4, REMAINDER 1);

CREATE TABLE sessions_p2 PARTITION OF sessions
    FOR VALUES WITH (MODULUS 4, REMAINDER 2);

CREATE TABLE sessions_p3 PARTITION OF sessions
    FOR VALUES WITH (MODULUS 4, REMAINDER 3);

HASH 分区无法做范围裁剪,查询 WHERE user_id = 42 才会只扫一个分区。范围查询 WHERE user_id > 100 会扫所有分区。

4. 主键或唯一约束必须包含分区键

sql 复制代码
highgo=# CREATE TABLE measurement (
    city_id         int,
    logdate         date not null,
    peaktemp        int,
    unitsales       int,
PRIMARY KEY (city_id)
) PARTITION BY RANGE (logdate);
ERROR:  unique constraint on partitioned table must include all partitioning columns
DETAIL:  PRIMARY KEY constraint on table "measurement" lacks column "logdate" which is part of the partition key.
highgo=# CREATE TABLE measurement (
    city_id         int,
    logdate         date not null,
    peaktemp        int,
    unitsales       int,
PRIMARY KEY (city_id, logdate)
) PARTITION BY RANGE (logdate);
CREATE TABLE

5. 限制分区个数

虽然没有对分区数量设置硬性上限,但实际使用时必须限制分区个数,核心原因在于查询规划(Query Planning)时间会随分区数量线性增长。优化器在生成执行计划时,需要为每个分区分别生成最优的访问路径,再将这些路径并联起来作为整张分区表的执行计划。当分区数过多时,即使查询最终只访问少数几个分区,规划阶段的耗时也可能高达数秒,相比普通表的毫秒级规划时间相差数百倍。

此外,每个分区都需要独立的统计信息和元数据缓存,分区过多会显著增加内存消耗,在长连接场景下甚至可能触发OOM; 同时,当SQL未携带分区键条件需要全分区扫描时,一些高效的执行路径(如 HashAgg、HashJoin、Merge Join 等)也可能无法被支持。

因此,在设计分区策略时要合理选择分区数量,在数据局部性收益与规划开销之间取得平衡。

6. 查询条件必带分区键

6.1 不带分区键等于全分区扫描

不带分区键等于全分区扫描,比普通表要慢。

sql 复制代码
-- 错误:不带分区键,扫描所有分区
SELECT * FROM orders WHERE user_id = 42;

-- 正确:带上分区键,只扫描 1/12 的分区
SELECT * FROM orders
WHERE user_id = 42
  AND order_date >= '2025-01-01'
  AND order_date <  '2025-02-01';

6.2 分区裁剪的两种模式

模式 触发时机 条件
静态裁剪(Static Pruning) 查询规划阶段 分区键使用常量
运行时裁剪(Runtime Pruning) 查询执行阶段 分区键使用参数(PREPARE / 子查询结果)

静态裁剪

sql 复制代码
-- 规划阶段就能确定目标分区
EXPLAIN SELECT * FROM orders 
WHERE order_date = '2025-01-15';
-- 输出:
--  Seq Scan on orders_2025_01 orders  (cost=0.00..35.50 rows=10 width=24)
--    Filter: (order_date = '2025-01-15'::date)
--   只扫描 orders_2025_01

-- 范围查询也能裁剪
EXPLAIN SELECT * FROM orders 
WHERE order_date >= '2025-01-01' AND order_date < '2025-03-01';
-- 只扫描 1 月和 2 月的分区

运行时裁剪

sql 复制代码
-- 使用 PREPARE,参数值在执行时才确定
PREPARE find_orders (date) AS
    SELECT * FROM orders WHERE order_date = $1;
EXPLAIN EXECUTE find_orders('2025-02-10');
-- 运行时裁剪:只扫描目标分区

-- 子查询也能触发运行时裁剪
EXPLAIN SELECT * FROM orders
WHERE order_date = (SELECT max(order_date) FROM recent_updates);

6.3 裁剪失效

分区键被函数包裹

sql 复制代码
-- 裁剪失效:函数包裹分区键
SELECT * FROM orders WHERE date_trunc('month', order_date) = '2025-01-01';

-- 改为范围查询
SELECT * FROM orders
WHERE order_date >= '2025-01-01' AND order_date < '2025-02-01';

隐式类型转换

sql 复制代码
-- 裁剪可能失效:字符串与 date 比较
SELECT * FROM orders WHERE order_date = '2025-01-15';

-- 显式类型转换,确保裁剪生效
SELECT * FROM orders WHERE order_date = '2025-01-15'::date;

总结

分区类型:时序用 RANGE,离散值用 LIST,均匀打散用 HASH,超大用复合

查询必带分区键:否则分区无意义,甚至更慢;注意函数包裹和类型转换陷阱

日常监控:定期检查分区数、分区大小、裁剪是否生效

相关推荐
SelectDB1 小时前
15 分钟搭建 PostgreSQL + Apache Iceberg + Apache Doris 的湖仓分析平台
数据库
SelectDB2 小时前
科大讯飞可观测性底座:从 ES/Loki 到 Apache Doris 的可观测存储分析升级实践
数据库
青禾8372 小时前
Linux 系统信息、权限管理与 Python 并发编程(协程与线程)完全指南
linux·python
SelectDB2 小时前
Cisco WebEx 数据平台统一改造:基于 Apache Doris / SelectDB 替换 Trino、Pinot、Iceberg 及 Kyuubi
数据库
2301_800954992 小时前
Linux 常用系统信息与进程管理命令速查
linux·运维·服务器
数据技术说2 小时前
MySQL 和 PostgreSQL 的 CDC 方案到底有什么区别?
数据库·架构
SelectDB2 小时前
灵犀科技统一数据服务平台:基于 Apache Doris / SelectDB 实现存储降本 60%、计算提效 10 倍
数据库
让学习成为一种生活方式2 小时前
KMC 3.2.4 安装与使用--生信工具108
数据库
今天AI了吗2 小时前
从聊天到委派:AI Agent 如何推进长期任务
数据库·人工智能·python·sql·rust