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

Aar*_*ron 8 postgresql postgresql-9.4

我有一张表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)
Run Code Online (Sandbox Code Playgroud)

和视图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;
Run Code Online (Sandbox Code Playgroud)

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

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)
Run Code Online (Sandbox Code Playgroud)

但是,如果我尝试执行相同的查询,用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)
Run Code Online (Sandbox Code Playgroud)

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

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

Chr*_*ris 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);
Run Code Online (Sandbox Code Playgroud)

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

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));
Run Code Online (Sandbox Code Playgroud)

注意子查询从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));
Run Code Online (Sandbox Code Playgroud)

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

SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44
Run Code Online (Sandbox Code Playgroud)

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

在这些情况下,您可能不能只相信 的结果EXPLAIN,因为它已经认为它找到了成本最低的解决方案。确保运行一个EXPLAIN (ANALYZE,BUFFERS)...以找出它在时间和页面读取方面的真正成本。