postgresql 16中用相关子查询计算移动加权平均性能天然差,不推荐,除非数据量极小;因其对每行执行一次子查询,需依赖复合索引(如transaction_time, id, type)缓解,否则易退化为全表扫描。

PostgreSQL 16 中用子查询算移动加权平均,性能天然差,不推荐——除非数据量极小(SUM(value * weight) OVER w / SUM(weight) OVER w。
为什么子查询在 PostgreSQL 16 里依然慢
相关子查询对每行都重新执行一次独立扫描,即使加了索引,1 万行仍要跑 1 万次范围查询。你看到的“能跑通”,只是小数据下的假象。实际线上表一旦超 5k 行,响应时间就从毫秒跳到秒级;若没建对索引,可能直接超时。
- 子查询必须按时间+唯一键双重排序,否则同时间多笔入库会导致加权和错乱
WHERE t2.transaction_time 这类条件无法高效走索引,除非复合索引包含 <code>transaction_time、id、type三列且顺序匹配- PostgreSQL 16 没优化相关子查询的物化能力,不会自动缓存中间结果
替代方案:必须用窗口函数重写
PostgreSQL 16 原生不提供 MOVING_WEIGHTED_AVG(),但 SUM() OVER () 组合完全够用,语义清晰、执行计划可预测、性能线性增长。
- 核心表达式固定为:
SUM(quantity * unit_price) OVER w / NULLIF(SUM(quantity) OVER w, 0) - 窗口定义必须显式:
WINDOW w AS (PARTITION BY product_id ORDER BY transaction_time, id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) -
NULLIF(..., 0)不可省——出库记录若导致累计 quantity 为 0,除零会报division by zero - 如果只想要“最近 N 笔入库”的移动均价(而非全历史),把
ROWS BETWEEN UNBOUNDED PRECEDING换成ROWS BETWEEN N-1 PRECEDING AND CURRENT ROW
索引与数据准备的关键细节
窗口函数快不快,取决于排序字段是否能走索引。PostgreSQL 16 不会自动为 ORDER BY transaction_time, id 创建隐式索引,你得手动建。
- 必需索引:
CREATE INDEX CONCURRENTLY idx_inventory_time_id_type ON inventory (transaction_time, id, type) WHERE type = 'IN'; - 过滤
type = 'IN'的部分索引比全表索引更小、更高效,且能跳过所有出库记录 - 确保
transaction_time是TIMESTAMP WITH TIME ZONE类型,避免隐式类型转换破坏索引使用 - 如果业务中存在批量导入、时间戳相同的情况,
id必须是严格递增的序列(不能是 UUID 或随机数)
最易被忽略的一点:移动加权平均的“移动”不是指时间滑动窗口,而是按事务发生顺序的累积过程。哪怕你加了 RANGE BETWEEN INTERVAL '7 days' PRECEDING,只要没入库,均价就不会变——这个业务语义,只能靠 ROWS BETWEEN UNBOUNDED PRECEDING + 入库条件过滤来准确表达,子查询反而容易在这里翻车。










