本文沿用第 1 篇的订单系统案例,聚焦预计算与大表生命周期管理。

先判断问题是重复计算还是数据规模

报表慢,先确认成本来自哪里:若同一批源数据被反复扫描、连接和聚合,应考虑把结果预先计算;若难点是查询只需要近期数据、旧数据却难以归档或删除,应考虑按访问范围管理数据。物化视图和汇总表解决前一个问题,分区解决后一个问题。

两者可以同时使用,但不能互相替代。分区不会自动消除跨月聚合,物化视图也不会让历史明细的清理变得便宜。先用执行计划、调用频率和保留策略确认瓶颈,再选择维护机制。

物化视图适合重复聚合

当相同的大范围聚合被报表、定时任务和接口反复执行,而且业务允许结果延迟时,可以保存聚合结果。下面按上海时区汇总每日已支付订单,避免 timestamptz 转日期时依赖会话时区:

CREATE MATERIALIZED VIEW daily_order_summary AS
SELECT
  (created_at AT TIME ZONE 'Asia/Shanghai')::date AS order_date,
  count(*) AS order_count,
  sum(total_amount) AS total_amount
FROM orders
WHERE status = 'paid'
GROUP BY (created_at AT TIME ZONE 'Asia/Shanghai')::date
WITH NO DATA;

CREATE UNIQUE INDEX idx_daily_order_summary_date
ON daily_order_summary (order_date);

-- 第一次填充不能使用 CONCURRENTLY。
REFRESH MATERIALIZED VIEW daily_order_summary;

WITH NO DATA 将定义和初次计算拆开,但未填充的物化视图不能查询。第一次刷新后,接口读取 daily_order_summary,不必重复扫描订单明细。代价是结果只新鲜到最近一次成功刷新,源数据修正也要纳入重算策略。

普通视图、物化视图与汇总表

方案更新方式适合场景主要代价
普通视图查询时执行底层 SQL封装查询并始终读取当前数据不减少底层计算
物化视图显式完整刷新可容忍延迟的重复聚合刷新消耗资源,存在陈旧窗口
业务汇总表应用、CDC 或任务增量维护更低延迟、需要按区间补算一致性、幂等、补偿和回放更复杂

PostgreSQL 原生刷新会替换物化视图的完整内容,不是通用的增量刷新方案。若源表很大而刷新间隔很短,完整刷新可能成为新的 I/O 与 CPU 高峰;这时应评估按日期维护的汇总表,而不是只把刷新调得更频繁。

并发刷新与数据时效

填充后可以在刷新期间保留并发读取:

REFRESH MATERIALIZED VIEW CONCURRENTLY daily_order_summary;

并发刷新要求物化视图至少有一个只使用普通列、覆盖全部行的唯一索引;部分唯一索引和表达式唯一索引不满足要求。它不能与 WITH NO DATA 同用,且同一物化视图同一时刻只能执行一次刷新。

调度前要定义可接受延迟、刷新超时、失败告警与重试、旧数据标识,以及刷新耗时超过调度间隔时的互斥。CONCURRENTLY 主要保护读取可用性,并不消除刷新本身的计算成本。

分区适合数据生命周期管理

分区把一张逻辑表路由到多个物理子表。持续追加且按时间保留的订单事件,适合按月范围分区:

CREATE TABLE order_events (
  id bigint GENERATED ALWAYS AS IDENTITY,
  order_id bigint NOT NULL REFERENCES orders (id),
  event_type text NOT NULL,
  payload jsonb NOT NULL DEFAULT '{}'::jsonb,
  created_at timestamptz NOT NULL,
  PRIMARY KEY (created_at, id)
) PARTITION BY RANGE (created_at);

CREATE TABLE order_events_2026_07
PARTITION OF order_events
FOR VALUES FROM ('2026-07-01 00:00:00+00')
         TO   ('2026-08-01 00:00:00+00');

CREATE TABLE order_events_2026_08
PARTITION OF order_events
FOR VALUES FROM ('2026-08-01 00:00:00+00')
         TO   ('2026-09-01 00:00:00+00');

CREATE INDEX idx_order_events_order_created
ON order_events (order_id, created_at DESC);

在分区父表上创建索引,会为现有分区创建匹配的子索引,后续新建或挂载的分区也遵循该结构。运维流程还要提前创建未来分区,并监控落不到任何分区的写入错误。

旧数据可以先从在线表分离,再导出、校验或删除:

ALTER TABLE order_events
DETACH PARTITION order_events_2026_07;

DROP TABLE order_events_2026_07;

分离后子表仍存在,因此归档与最终删除可以拆开;操作仍涉及锁、依赖与并发查询,需要先在接近生产的数据量上验证。

分区裁剪与分区内索引

查询条件与分区键的边界匹配时,规划器或执行器可以裁剪无关分区:

EXPLAIN (ANALYZE, BUFFERS)
SELECT order_id, event_type, created_at
FROM order_events
WHERE created_at >= TIMESTAMPTZ '2026-07-10 00:00:00+00'
  AND created_at <  TIMESTAMPTZ '2026-07-11 00:00:00+00';

裁剪依据分区边界,不依赖子表是否有普通索引;索引负责在保留下来的分区内部继续缩小扫描范围。上线前应使用真实参数检查计划实际访问的分区数量,并确认 enable_partition_pruning 没有被关闭。

分区不是普通表的性能开关

表不大、索引已经能快速定位,或常用查询不包含分区键时,分区往往只增加规划、元数据和维护成本。分区过多还会扩大建表、索引、备份与统计信息管理的工作量。

分区表上的主键或唯一约束必须包含全部分区键列,所以示例使用 (created_at, id)。序列仍可生成跨分区不重复的 id,但各分区的局部索引不能单独提供一个不含分区键的全局唯一约束。改造已有订单主表前,还要评估外键、迁移窗口和应用查询形态。

选型决策表

观察到的问题优先方案决策证据
只想复用 SQL,结果必须实时普通视图底层查询成本可接受
同一聚合反复执行,可接受分钟级或小时级延迟物化视图读取收益高于完整刷新成本
聚合要求更低延迟或按时间段增量修正汇总表团队能承担幂等、补偿与重算逻辑
查询常带分区键,旧数据按时间归档或删除分区裁剪范围与保留策略都清晰
既有重复聚合又有长期明细分区加预计算分别验证生命周期收益与刷新收益

最终判断不是“表大就分区”或“报表慢就建物化视图”,而是明确要减少的是重复计算,还是在线数据范围与生命周期操作成本。

参考资料