【发布时间】:2021-07-16 11:10:22
【问题描述】:
当加入一个表然后过滤(例如 LIMIT 30)时,Postgres 将对所有行应用一个 JOIN 操作,即使这些行中的列仅用于返回的列,而不是作为过滤谓词。
这对于 INNER JOIN(PG 必须知道是否返回该行)或没有唯一约束的 LEFT JOIN(PG 必须知道是否返回多行)是可以理解的,但是对于 UNIQUE 列上的 LEFT JOIN,这似乎很浪费:如果查询匹配 10k 行,则将执行 10k 连接,然后只返回 30。
尽可能“延迟”或推迟连接似乎更有效,这是我在其他一些查询中看到的情况。
将其拆分为子查询 (SELECT * FROM (SELECT * FROM main WHERE x LIMIT 30) LEFT JOIN secondary) 可以通过确保在加入主表之前只返回 30 个项目,但感觉就像我遗漏了一些东西,以及查询的“标准”形式也应该应用相同的优化。
但是,查看 EXPLAIN 计划,我可以看到加入的行数始终是总行数,而不是“提前退出”,例如,在运行带有 LIMIT 的 Seq Scan 时5.
具有main 表和secondary 1 的示例架构:只会返回辅助列,从不过滤。
drop table if exists secondary;
drop table if exists main;
create table main(id int primary key not null, main_column int);
create index main_column on main(main_column);
insert into main(id, main_column) SELECT i, i % 3000 from generate_series( 1, 1000000, 1) i;
create table secondary(id serial primary key not null, main_id int references main(id) not null, secondary_column int);
create unique index secondary_main_id on secondary(main_id);
insert into secondary(main_id, secondary_column) SELECT i, (i + 17) % 113 from generate_series( 1, 1000000, 1) i;
analyze main;
analyze secondary;
查询示例:
explain analyze verbose select main.id, main_column, secondary_column
from main
left join secondary on main.id = secondary.main_id
where main_column = 5
order by main.id
limit 50;
这是编写查询最“明显”的方式,在我的计算机上平均需要大约 5 毫秒。
解释:
Limit (cost=3742.93..3743.05 rows=50 width=12) (actual time=5.010..5.322 rows=50 loops=1)
Output: main.id, main.main_column, secondary.secondary_column
-> Sort (cost=3742.93..3743.76 rows=332 width=12) (actual time=5.006..5.094 rows=50 loops=1)
Output: main.id, main.main_column, secondary.secondary_column
Sort Key: main.id
Sort Method: top-N heapsort Memory: 27kB
-> Nested Loop Left Join (cost=11.42..3731.90 rows=332 width=12) (actual time=0.123..4.446 rows=334 loops=1)
Output: main.id, main.main_column, secondary.secondary_column
Inner Unique: true
-> Bitmap Heap Scan on public.main (cost=11.00..1036.99 rows=332 width=8) (actual time=0.106..1.021 rows=334 loops=1)
Output: main.id, main.main_column
Recheck Cond: (main.main_column = 5)
Heap Blocks: exact=334
-> Bitmap Index Scan on main_column (cost=0.00..10.92 rows=332 width=0) (actual time=0.056..0.057 rows=334 loops=1)
Index Cond: (main.main_column = 5)
-> Index Scan using secondary_main_id on public.secondary (cost=0.42..8.12 rows=1 width=8) (actual time=0.006..0.006 rows=1 loops=334)
Output: secondary.id, secondary.main_id, secondary.secondary_column
Index Cond: (secondary.main_id = main.id)
Planning Time: 0.761 ms
Execution Time: 5.423 ms
explain analyze verbose select m.id, main_column, secondary_column
from (
select main.id, main_column
from main
where main_column = 5
order by main.id
limit 50
) m
left join secondary on m.id = secondary.main_id
where main_column = 5
order by m.id
limit 50
这会在 2 毫秒内返回相同的结果。 总 EXPLAIN 成本也高出三倍,与我们看到的性能提升一致。
Limit (cost=1048.44..1057.21 rows=1 width=12) (actual time=1.219..2.027 rows=50 loops=1)
Output: m.id, m.main_column, secondary.secondary_column
-> Nested Loop Left Join (cost=1048.44..1057.21 rows=1 width=12) (actual time=1.216..1.900 rows=50 loops=1)
Output: m.id, m.main_column, secondary.secondary_column
Inner Unique: true
-> Subquery Scan on m (cost=1048.02..1048.77 rows=1 width=8) (actual time=1.201..1.515 rows=50 loops=1)
Output: m.id, m.main_column
Filter: (m.main_column = 5)
-> Limit (cost=1048.02..1048.14 rows=50 width=8) (actual time=1.196..1.384 rows=50 loops=1)
Output: main.id, main.main_column
-> Sort (cost=1048.02..1048.85 rows=332 width=8) (actual time=1.194..1.260 rows=50 loops=1)
Output: main.id, main.main_column
Sort Key: main.id
Sort Method: top-N heapsort Memory: 27kB
-> Bitmap Heap Scan on public.main (cost=11.00..1036.99 rows=332 width=8) (actual time=0.054..0.753 rows=334 loops=1)
Output: main.id, main.main_column
Recheck Cond: (main.main_column = 5)
Heap Blocks: exact=334
-> Bitmap Index Scan on main_column (cost=0.00..10.92 rows=332 width=0) (actual time=0.029..0.030 rows=334 loops=1)
Index Cond: (main.main_column = 5)
-> Index Scan using secondary_main_id on public.secondary (cost=0.42..8.44 rows=1 width=8) (actual time=0.004..0.004 rows=1 loops=50)
Output: secondary.id, secondary.main_id, secondary.secondary_column
Index Cond: (secondary.main_id = m.id)
Planning Time: 0.161 ms
Execution Time: 2.115 ms
这里是一个玩具数据集,但是在真实的 DB 上,IO 差异很大(30 行就足够了,不需要取 1000 行),而且时序差异也很快加起来(慢了一个数量级) )。
所以我的问题是:有什么方法可以让规划人员了解 JOIN 可以在流程的后期应用吗? 它似乎可以自动应用来获得相当大的性能提升。
【问题讨论】:
-
您确实已经自己回答了标题中的问题:是的,使用子查询。
-
旁白:两个查询不等价。您必须在外部查询中重复
order by main.id以保证排序顺序。添加的LEFT JOIN可能会改变行的顺序。 -
@ErwinBrandstetter 我的问题更多:为什么 PG 不自动执行此操作,而模式中的所有信息都可以有效地自动执行此子查询?
-
子查询中的
LIMIT 50与外部查询中的LIMIT 50不同。 -
@wildplasser 它是,因为它是 LEFT JOIN(所以 1+ 行)并且是唯一的(所以最多 1 行);所以具有唯一左连接的 50 行将始终为 50 行(可能带有 NULL)
标签: postgresql left-join query-optimization