Nea*_*mar 5 postgresql query-optimization
当在表上加入然后过滤(例如 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 5 的 Seq Scan 时可以看到。
示例架构,带有一个main表和secondary一个:将只返回辅助列,从不过滤。
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;
Run Code Online (Sandbox Code Playgroud)
示例查询:
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;
Run Code Online (Sandbox Code Playgroud)
这是编写查询的最“明显”的方式,在我的计算机上平均需要大约 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
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
这将在 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
Run Code Online (Sandbox Code Playgroud)
这是一个玩具数据集,但在真正的DB上,IO差异很大(30行就够了,不需要取1000行),而且时序差异也很快加起来(慢了一个数量级)。
所以我的问题是:有什么方法可以让计划者了解 JOIN 可以在流程的后期应用?似乎可以自动应用以获得可观的性能提升。
延迟加入很好。对仅产生id值的子查询运行限制操作通常很有帮助。该order by....limit操作必须对较少的数据进行排序才能丢弃它。
select main.id, main.main_column, secondary.secondary_column
from main
join (
select id
from main
where main_column = 5
order by id
limit 50
) selection on main.id = selection.id
left join secondary on main.id = secondary.main_id
order by main.id
limit 50
Run Code Online (Sandbox Code Playgroud)
id添加到 main_column 索引也可能会有所帮助。使用 BTREE 索引,查询规划器知道它可以id从索引中按升序获取值,因此它可以完全跳过排序步骤,只扫描前 50 个值。
create index main_column on main(main_column, id);
Run Code Online (Sandbox Code Playgroud)
编辑在大型表中,查询的繁重工作将是选择main.id要处理的 50 个值。id为了尽可能便宜地获取这 50 个值,您可以使用我建议的子查询来扫描我建议的覆盖索引。获得 50 个值后,通过和id从各个表中查找 50 行的详细信息就很简单了;您拥有正确的索引,并且行数有限。因为行数有限,所以不会花费太多时间。main.idsecondary.main_id
不过,您的表大小似乎太小,无法使各种优化产生太大效果。当表更大时,查询计划会发生很大变化。
| 归档时间: |
|
| 查看次数: |
79 次 |
| 最近记录: |