Postgres中如何将WHERE条件推入OUTER JOIN的最优方法?
我需要统计两张大表中特定值的出现次数并返回单一结果集。理论上,先分别统计各表再连接结果,与先连接表再统计的逻辑等价,但实际中先连接会降低数据库优化效率。尤其是PostgreSQL 16.3的查询优化器无法将WHERE col IN (1,2,3,...)这类条件推入OUTER JOIN分支,导致无法利用索引,必须手动为每个被连接的表(子查询或视图)应用过滤条件。
是否有办法避免手动为每个被连接表添加过滤?如果没有,最佳实践或惯用方法是什么?
示例环境搭建
DROP TABLE IF EXISTS t1 CASCADE; DROP TABLE IF EXISTS t2 CASCADE; CREATE TABLE t1 (id INT); CREATE TABLE t2 (id INT); -- t1包含负ID,t2不包含 INSERT INTO t1 SELECT (SELECT (- random()*20)::int + i % 10000 LIMIT 1) FROM generate_series(1,1000000) i; INSERT INTO t2 SELECT (SELECT (random()*20)::int + i % 10000 LIMIT 1) FROM generate_series(1,1000000) i; CREATE INDEX "t1_idx" ON t1 USING btree ("id"); CREATE INDEX "t2_idx" ON t2 USING btree ("id"); -- 统计视图 CREATE VIEW c1 AS ( SELECT id, count(1) as cnt1 FROM t1 GROUP BY id ); CREATE VIEW c2 AS ( SELECT id, count(1) as cnt2 FROM t2 GROUP BY id );
低效查询示例
以下查询在真实数据库中极慢,执行计划显示过滤条件在全表扫描和连接后才应用:
EXPLAIN ANALYZE SELECT * FROM c1 NATURAL FULL OUTER JOIN c2 WHERE id IN (-5, 11);
执行计划:
QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------- Hash Full Join (cost=27979.91..28107.22 rows=5011 width=20) (actual time=630.949..631.763 rows=2 loops=1) Hash Cond: (t2.id = t1.id) Filter: (COALESCE(t1.id, t2.id) = ANY ('{-5,11}'::integer[])) Rows Removed by Filter: 10038 -> Finalize HashAggregate (cost=13881.60..13981.90 rows=10030 width=12) (actual time=306.364..307.877 rows=10020 loops=1) Group Key: t2.id Batches: 1 Memory Usage: 1169kB -> Gather (cost=11675.00..13781.30 rows=20060 width=12) (actual time=287.054..294.920 rows=30056 loops=1) Workers Planned: 2 Workers Launched: 2 -> Partial HashAggregate (cost=10675.00..10775.30 rows=10030 width=12) (actual time=276.957..280.021 rows=10019 loops=3) Group Key: t2.id Batches: 1 Memory Usage: 1169kB Worker 0: Batches: 1 Memory Usage: 1169kB Worker 1: Batches: 1 Memory Usage: 1169kB -> Parallel Seq Scan on t2 (cost=0.00..8591.67 rows=416667 width=4) (actual time=0.008..73.377 rows=333333 loops=3) -> Hash (cost=13973.39..13973.39 rows=9993 width=12) (actual time=321.427..321.465 rows=10020 loops=1) Buckets: 16384 Batches: 1 Memory Usage: 598kB -> Finalize HashAggregate (cost=13873.46..13973.39 rows=9993 width=12) (actual time=318.759..320.219 rows=10020 loops=1) Group Key: t1.id Batches: 1 Memory Usage: 1169kB -> Gather (cost=11675.00..13773.53 rows=19986 width=12) (actual time=300.414..306.962 rows=30057 loops=1) Workers Planned: 2 Workers Launched: 2 -> Partial HashAggregate (cost=10675.00..10774.93 rows=9993 width=12) (actual time=291.697..297.355 rows=10019 loops=3) Group Key: t1.id Batches: 1 Memory Usage: 1169kB Worker 0: Batches: 1 Memory Usage: 1169kB Worker 1: Batches: 1 Memory Usage: 1169kB -> Parallel Seq Scan on t1 (cost=0.00..8591.67 rows=416667 width=4) (actual time=0.013..73.148 rows=333333 loops=3) Planning Time: 0.298 ms Execution Time: 632.066 ms (32 rows)
可以看到Filter: (COALESCE(t1.id, t2.id) = ANY ('{-5,11}'::integer[]))是在全表扫描和连接后才执行,完全没有利用索引。
手动优化后的高效查询
将过滤条件复制到每个连接的子查询中,优化器会将条件下推并使用索引仅扫描:
EXPLAIN ANALYZE SELECT * FROM ( SELECT * FROM c1 WHERE id IN (-5, 11) ) NATURAL FULL OUTER JOIN ( SELECT * FROM c2 WHERE id IN (-5, 11) );
执行计划:
QUERY PLAN --------------------------------------------------------------------------------------------------------------------------------- Merge Full Join (cost=0.85..33.58 rows=198 width=20) (actual time=0.051..0.068 rows=2 loops=1) Merge Cond: (t1.id = t2.id) -> GroupAggregate (cost=0.42..15.33 rows=198 width=12) (actual time=0.029..0.045 rows=2 loops=1) Group Key: t1.id -> Index Only Scan using t1_idx on t1 (cost=0.42..12.35 rows=200 width=4) (actual time=0.012..0.029 rows=178 loops=1) Index Cond: (id = ANY ('{-5,11}'::integer[])) Heap Fetches: 0 -> GroupAggregate (cost=0.42..15.29 rows=197 width=12) (actual time=0.020..0.020 rows=1 loops=1) Group Key: t2.id -> Index Only Scan using t2_idx on t2 (cost=0.42..12.32 rows=199 width=4) (actual time=0.009..0.015 rows=64 loops=1) Index Cond: (id = ANY ('{-5,11}'::integer[])) Heap Fetches: 0 Planning Time: 0.165 ms Execution Time: 0.110 ms (14 rows)
注意:当IN子句只有单一值时(如WHERE id IN (11)),优化器能自动识别为等值条件并下推,两种查询效率一致。
优化方案与最佳实践
1. 使用参数化函数封装逻辑
创建带参数的PL/pgSQL函数,将过滤条件统一传入,内部自动应用到每个子查询,客户端只需调用函数即可:
CREATE OR REPLACE FUNCTION get_id_counts(p_target_ids INT[]) RETURNS TABLE(id INT, cnt1 BIGINT, cnt2 BIGINT) AS $$ BEGIN RETURN QUERY SELECT COALESCE(c1.id, c2.id), c1.cnt1, c2.cnt2 FROM ( SELECT id, count(1) AS cnt1 FROM t1 WHERE id = ANY(p_target_ids) GROUP BY id ) c1 NATURAL FULL OUTER JOIN ( SELECT id, count(1) AS cnt2 FROM t2 WHERE id = ANY(p_target_ids) GROUP BY id ) c2; END; $$ LANGUAGE plpgsql STABLE;
调用方式:
SELECT * FROM get_id_counts('{-5, 11}'::INT[]);
这种方式既隐藏了复杂的子查询逻辑,又保证了优化器能下推条件利用索引。
2. 改用UNION ALL + 聚合替代全外连接
通过UNION ALL合并两张表的统计结果,再进行聚合,优化器同样能下推过滤条件,逻辑与全外连接等价:
EXPLAIN ANALYZE SELECT id, SUM(cnt1) AS cnt1, SUM(cnt2) AS cnt2 FROM ( SELECT id, count(1) AS cnt1, 0::BIGINT AS cnt2 FROM t1 WHERE id IN (-5, 11) GROUP BY id UNION ALL SELECT id, 0::BIGINT AS cnt1, count(1) AS cnt2 FROM t2 WHERE id IN (-5, 11) GROUP BY id ) combined GROUP BY id;
该查询的执行计划同样会使用索引仅扫描,且结果与全外连接一致。
3. 升级PostgreSQL版本
PostgreSQL的优化器一直在迭代,后续版本(如17+)可能修复了多值IN条件无法下推到全外连接分支的问题,升级后可能无需手动优化即可获得高效执行计划。
补充说明(2024-10-15更新)
当IN子句仅含单一值时,优化器能自动将COALESCE(c1.id, c2.id) IN (11)条件下推到连接分支,例如:
EXPLAIN ANALYZE SELECT * FROM c1 FULL OUTER JOIN c2 ON c1.id = c2.id WHERE COALESCE(c1.id, c2.id) IN (11);
执行计划:
QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------- Merge Full Join (cost=0.85..12.88 rows=1 width=24) (actual time=0.057..0.059 rows=1 loops=1) -> GroupAggregate (cost=0.42..6.43 rows=1 width=12) (actual time=0.034..0.035 rows=1 loops=1) -> Index Only Scan using t1_idx on t1 (cost=0.42..6.17 rows=100 width=4) (actual time=0.015..0.023 rows=107 loops=1) Index Cond: (id = 11) Heap Fetches: 0 -> Materialize (cost=0.42..6.44 rows=1 width=12) (actual time=0.021..0.021 rows=1 loops=1) -> GroupAggregate (cost=0.42..6.43 rows=1 width=12) (actual time=0.018..0.018 rows=1 loops=1) -> Index Only Scan using t2_idx on t2 (cost=0.42..6.17 rows=100 width=4) (actual time=0.009..0.014 rows=65 loops=1) Index Cond: (id = 11) Heap Fetches: 0 Planning Time: 0.174 ms Execution Time: 0.105 ms (12 rows)
这是因为IN (11)等价于=11,优化器能识别这种等值条件并完成下推,但多值IN的场景在16.3版本中仍需手动优化。
内容的提问来源于stack exchange,提问作者paperskilltrees

