【问题标题】:Why is the query planner unable to transform a correlated subquery?为什么查询计划器无法转换相关子查询?
【发布时间】:2018-05-21 01:53:39
【问题描述】:

How does PostgreSQL approach a 1 + n query? 中,我了解到关联子查询可以重写为左连接:

select   film_id, title,
         (
           select     array_agg(first_name)
           from       actor
           inner join film_actor using(actor_id)
           where      film_actor.film_id = film.film_id
         ) as actors
from     film
order by title;

select   f.film_id, f.title, array_agg(a.first_name)
from     film f
   left join film_actor fa using(film_id)
   left join actor      a  using(actor_id)
group by f.film_id
order by f.title;

机器人查询返回相同的结果,但第二个查询执行得更好。

这让我想知道:为什么查询规划器不能自己做这样的转换?

我知道为什么不是所有相关的子查询都可以转换为连接,但我看不出这个特定查询有任何问题。

更新性能

我尝试比较性能如下。我执行了 100 次第一个查询的 2 个连续循环,然后是第二个查询 100 次的 2 个连续循环。在这两种情况下我都忽略了第一个循环,因为我认为这是一个热身循环。

第一个查询 100 倍我得到 16 秒,第二个查询 100 倍我得到 11 秒。

解释如下:

相关子查询:

 Index Scan using idx_title on film  (cost=0.28..24949.50 rows=1000 width=51) (actual time=0.690..74.828 rows=1000 loops=1)
   SubPlan 1
     ->  Aggregate  (cost=24.84..24.85 rows=1 width=32) (actual time=0.068..0.068 rows=1 loops=1000)
       ->  Hash Join  (cost=10.82..24.82 rows=5 width=6) (actual time=0.034..0.055 rows=5 loops=1000)
         Hash Cond: (film_actor.actor_id = actor.actor_id)
         ->  Bitmap Heap Scan on film_actor  (cost=4.32..18.26 rows=5 width=2) (actual time=0.025..0.040 rows=5 loops=1000)
               Recheck Cond: (film_id = film.film_id)
               Heap Blocks: exact=5075
               ->  Bitmap Index Scan on idx_fk_film_id  (cost=0.00..4.32 rows=5 width=0) (actual time=0.015..0.015 rows=5 loops=1000)
                 Index Cond: (film_id = film.film_id)
         ->  Hash  (cost=4.00..4.00 rows=200 width=10) (actual time=0.338..0.338 rows=200 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 17kB
               ->  Seq Scan on actor  (cost=0.00..4.00 rows=200 width=10) (actual time=0.021..0.133 rows=200 loops=1)
 Planning time: 1.277 ms
 Execution time: 75.525 ms

加入:

 Sort  (cost=748.60..751.10 rows=1000 width=51) (actual time=35.865..36.060 rows=1000 loops=1)
   Sort Key: f.title
   Sort Method: quicksort  Memory: 199kB
   ->  GroupAggregate  (cost=645.31..698.78 rows=1000 width=51) (actual time=23.953..34.204 rows=1000 loops=1)
     Group Key: f.film_id
     ->  Sort  (cost=645.31..658.97 rows=5462 width=25) (actual time=23.910..25.210 rows=5465 loops=1)
           Sort Key: f.film_id
           Sort Method: quicksort  Memory: 619kB
           ->  Hash Left Join  (cost=84.00..306.25 rows=5462 width=25) (actual time=2.098..16.237 rows=5465 loops=1)
             Hash Cond: (fa.actor_id = a.actor_id)
             ->  Hash Right Join  (cost=77.50..231.03 rows=5462 width=21) (actual time=1.786..10.636 rows=5465 loops=1)
               Hash Cond: (fa.film_id = f.film_id)
               ->  Seq Scan on film_actor fa  (cost=0.00..84.62 rows=5462 width=4) (actual time=0.018..2.221 rows=5462 loops=1)
               ->  Hash  (cost=65.00..65.00 rows=1000 width=19) (actual time=1.753..1.753 rows=1000 loops=1)
                 Buckets: 1024  Batches: 1  Memory Usage: 59kB
                 ->  Seq Scan on film f  (cost=0.00..65.00 rows=1000 width=19) (actual time=0.029..0.819 rows=1000 loops=1)
             ->  Hash  (cost=4.00..4.00 rows=200 width=10) (actual time=0.286..0.286 rows=200 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 17kB
               ->  Seq Scan on actor a  (cost=0.00..4.00 rows=200 width=10) (actual time=0.016..0.114 rows=200 loops=1)
 Planning time: 1.648 ms
 Execution time: 36.599 ms

【问题讨论】:

  • stackoverflow.com/a/50426511/905902in 您的情况,标题可能在功能上依赖于film_id,但优化器不理解这一点。
  • 当有一部电影没有演员时,您对第一个查询的期望是什么?第二个查询呢?
  • WW: null 作为演员在这两种情况下都很好。如果需要,我可以添加 coalesce(...)
  • explain ANALYZE,请(并且:比较预期与观察,预期=1000 是可疑的......)
  • 我添加了explain analyze

标签: sql postgresql postgresql-10


【解决方案1】:

评论太多了。

你的相关子查询的重写应该是这样的:

select film_id, title, a.actors
from   film
left join
  (         
           select     film_actor.film_id, array_agg(first_name) as actors
           from       actor
           inner join film_actor using(actor_id)
           group by   film_actor.film_id
  ) as a
on a.film_id = film.film_id
order by title;

关于性能,标量相关子查询对于优化器来说似乎很难,我不希望它们的性能与手动重写相同或更好。

【讨论】:

  • 谢谢!您能否解释一下为什么左连接应该是这样的,与“我的”版本相比有什么问题?
  • @JellyOrns:当然再次:-) 标量子查询可以替换为左连接,在您的情况下,子查询基于两个表的内连接。两个左连接可能会从 film_actor 返回一行,而 actor 中没有匹配的行。
  • 知道了!虽然我相信在这种情况下这是不可能的,因为从film_actor.actor_idactor.actor_id 有一个外键(我没有提到,所以你假设没有外键是正确的)。也就是说,如果我们在外部/根film 查询中添加where 子句和/或limit 子句,我假设您的查询会执行得很差?也就是说,我相信只有当我们获取所有部电影而不是一些电影时才需要这种方法?
  • 只有当您知道 film.film_id 是唯一的时,这才是准确的。这是一个合理的假设——对于人类来说。我不知道是否有任何数据库使用唯一约束来重写查询计划。
  • @GordonLinoff 你能解释一下你的意思吗?我不确定我现在是否在关注你...
【解决方案2】:

我有点惊讶第二个表现更好。例如,第二个应该出现语法错误,因为order bygroup by 之前——但我明白你的意思。

但您的问题的答案是,虽然 SQL 是一种描述性语言而不是一种过程性语言,但查询的结构(对于某些数据库)会影响执行计划。如果您查看了说明,那么这两个查询显然就是这种情况。

更重要的答案是,尽管查询看起来相同,但它们在语义上并不相等。特别是,如果 film.film_id 不是唯一的,它们会返回不同的答案。

【讨论】:

  • 抱歉,我修正了查询。我在没有真正测试的情况下即时创建查询。
  • 第一个查询在子查询中甚至没有group by
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-11-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-03-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多