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который запрашивает для самого последнего progressпо user_idи lesson_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_ids подзапросом, он станет очень неэффективным:

# 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 хочет выполнить DISTINCTзапрос ко всей progressesтаблице, прежде чем выполнить фильтрацию по подзапросу во втором примере.

Кто-нибудь посоветует, как улучшить этот запрос?

Аарон
источник

Ответы:

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));

По сути, создавая CTE user_selectionс помощью WITHпредложения, вы просите PostgreSQL выполнить отдельную оптимизацию подзапроса.

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

а затем материализовать эти результаты. Затем я снова использую =ANY(ARRAY())выражение, чтобы попытаться вручную манипулировать планом.

В этих случаях вы, вероятно, не можете доверять только результатам EXPLAIN, потому что он уже думал, что он нашел наименее дорогостоящее решение. Обязательно запустите, EXPLAIN (ANALYZE,BUFFERS)...чтобы узнать, что это действительно стоит с точки зрения времени и чтения страницы.

Крис
источник
Оказывается, твое первое предложение творит чудеса. Стоимость этого запроса 144.07..144.6НАМНОГО ниже 70 000, которые я получаю! Большое спасибо.
Аарон
1
Ха! Рад, что смог помочь. Я много борюсь с этими вопросами «взлома плана запроса»; это немного искусства на вершине науки.
Крис
В течение многих лет я изучал приемы, позволяющие базам данных делать то, что я хочу, и должен сказать, что это была одна из странных ситуаций, с которыми я сталкивался. Это действительно искусство. Я действительно ценю ваше хорошо продуманное объяснение!
Аарон