SELECT DISTINCT ON子查询使用效率低下的计划


8

我有一张桌子progresses(当前包含数十万条记录):

    Column     |            Type             |                        Modifiers                        
---------------+-----------------------------+---------------------------------------------------------
 id            | integer                     | not null default nextval('progresses_id_seq'::regclass)
 lesson_id     | integer                     | 
 user_id       | integer                     | 
 created_at    | timestamp without time zone | 
 deleted_at    | timestamp without time zone | 
Indexes:
    "progresses_pkey" PRIMARY KEY, btree (id)
    "index_progresses_on_deleted_at" btree (deleted_at)
    "index_progresses_on_lesson_id" btree (lesson_id)
    "index_progresses_on_user_id" btree (user_id)

和视图v_latest_progresses,其将查询最近progressuser_idlesson_id

SELECT DISTINCT ON (progresses.user_id, progresses.lesson_id)
  progresses.id AS progress_id,
  progresses.lesson_id,
  progresses.user_id,
  progresses.created_at,
  progresses.deleted_at
 FROM progresses
WHERE progresses.deleted_at IS NULL
ORDER BY progresses.user_id, progresses.lesson_id, progresses.created_at DESC;

用户可以在任何给定课程上获得许多进度,但是我们经常想查询一组给定用户或课程(或两者的组合)中最近创建的进度。

v_latest_progresses当我指定一组user_ids 时,该视图可以很好地做到这一点,甚至表现出色:

# EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" WHERE "v_latest_progresses"."user_id" IN ([the same list of ids given by the subquery in the second example below]);
                                                                               QUERY PLAN                                                                                                                                         
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Unique  (cost=526.68..528.66 rows=36 width=57)
   ->  Sort  (cost=526.68..527.34 rows=265 width=57)
         Sort Key: progresses.user_id, progresses.lesson_id, progresses.created_at
         ->  Index Scan using index_progresses_on_user_id on progresses  (cost=0.47..516.01 rows=265 width=57)
               Index Cond: (user_id = ANY ('{ [the above list of user ids] }'::integer[]))
               Filter: (deleted_at IS NULL)
(6 rows)

但是,如果我尝试执行相同的查询以user_id子查询替换s 集,它将变得非常无效率:

# EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" WHERE "v_latest_progresses"."user_id" IN (SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44);
                                             QUERY PLAN                                              
-----------------------------------------------------------------------------------------------------
 Merge Semi Join  (cost=69879.08..72636.12 rows=19984 width=57)
   Merge Cond: (progresses.user_id = users.id)
   ->  Unique  (cost=69843.45..72100.80 rows=39969 width=57)
         ->  Sort  (cost=69843.45..70595.90 rows=300980 width=57)
               Sort Key: progresses.user_id, progresses.lesson_id, progresses.created_at
               ->  Seq Scan on progresses  (cost=0.00..31136.31 rows=300980 width=57)
                     Filter: (deleted_at IS NULL)
   ->  Sort  (cost=35.63..35.66 rows=10 width=4)
         Sort Key: users.id
         ->  Index Scan using index_users_on_company_id on users  (cost=0.42..35.46 rows=10 width=4)
               Index Cond: (company_id = 44)
(11 rows)

我要弄清楚的是PostgreSQL为什么要执行 DISTINCTprogresses在第二个示例中的子查询过滤之前在整个表上查询。

有人会对如何改进此查询有任何建议吗?

Answers:


11

亚伦

在最近的工作中,我一直在研究PostgreSQL的一些类似问题。PostgreSQL几乎总是很擅长生成正确的查询计划,但是并不总是完美的。

一些简单的建议是确保ANALYZE在您的progresses表上运行,以确保您具有更新的统计信息,但这不能保证解决您的问题!

由于可能对于这篇文章来说太冗长了,我发现在统计信息收集ANALYZE和查询计划程序中有些奇怪的行为可能需要长期解决。在短期内,技巧是重写查询以尝试破解所需的查询计划。

在无法访问您的数据进行测试的情况下,我将提出以下两个建议。

1)使用 ARRAY()

PostgreSQL在查询计划器中对数组和记录集的处理方式不同。有时您最终会得到相同的查询计划。在这种情况下,就像我的许多情况一样,您却没有。

在原始查询中,您有:

EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" 
IN (SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44);

作为尝试修复它的第一步,请尝试

EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" =
ANY(ARRAY(SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44));

注意子查询从IN到的更改=ANY(ARRAY())

2)使用CTE

如果我的第一个建议不起作用,另一个技巧是强制进行单独的优化。我知道许多人会使用此技巧,因为CTE中的查询是与主查询分开进行优化和具体化的。

EXPLAIN 
WITH user_selection AS(
  SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44
)
SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" =
ANY(ARRAY(SELECT "id" FROM user_selection));

本质上,通过user_selection使用WITH子句创建CTE ,是在要求PostgreSQL对子查询执行单独的优化。

SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44

然后实现这些结果。然后,我再次使用该=ANY(ARRAY())表达式尝试手动操作计划。

在这些情况下,您可能无法仅相信的结果EXPLAIN,因为它已经认为它找到了成本最低的解决方案。确保运行,EXPLAIN (ANALYZE,BUFFERS)...以了解其在时间和页面读取方面的实际成本。


事实证明,您的第一个建议会产生奇迹。该查询的费用为,比144.07..144.6我得到的70,000还要低!非常感谢你。
亚伦

1
哈!很高兴我能帮上忙。我在这些“查询计划黑客”问题中苦苦挣扎。这是科学之上的一点艺术。
克里斯(Chris)

多年来,我一直在左右学习技巧,以使数据库能够执行我想要的事情,我不得不说,这是我处理过的一种奇怪的情况。这确实是一门艺术。非常感谢您的深思熟虑的解释!
亚伦
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.