Почему для этого запроса используется неправильный индекс? - PullRequest
0 голосов
/ 21 апреля 2020

У меня проблема с использованием индекса Postgresql на двух разных хостах (локальном и удаленном) для одного и того же запроса. Вопрос, о котором идет речь, следующий:

SELECT COUNT(*) 
FROM (
  SELECT  1 AS one 
  FROM "big_table" 
  WHERE "big_table"."user_id" = 13 
    AND "big_table"."action" = 1 
    AND (big_table.created_at >= '2018-12-09 23:00:00'::timestamp without time zone) 
  ORDER BY big_table.created_at desc LIMIT 15 OFFSET 10
) subquery_for_count;

Я не могу изменить этот запрос, так как он генерируется библиотекой, которую мы используем, поэтому я бы хотел найти решение без необходимости Измени это. Если я запускаю команду EXPLAIN локально с указанным запросом, мой экземпляр Postgres выводит следующее:

local_host=# EXPLAIN SELECT COUNT(*) FROM (SELECT  1 AS one FROM "big_table" WHERE "big_table"."user_id" = 13 AND "big_table"."action" = 1 AND (big_table.created_at >= '2018-12-09 23:00:00'::timestamp without time zone) ORDER BY big_table.created_at desc LIMIT 15 OFFSET 10) subquery_for_count;
                                                              QUERY PLAN                                                              
--------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=8.59..8.60 rows=1 width=8)
   ->  Limit  (cost=8.57..8.58 rows=1 width=12)
         ->  Sort  (cost=8.57..8.57 rows=1 width=12)
               Sort Key: big_table.created_at DESC
               ->  Index Scan using big_table_idx_user_action_transfers on big_table  (cost=0.56..8.56 rows=1 width=12)
                     Index Cond: ((user_id = 13) AND (action = 1))
                     Filter: (created_at >= '2018-12-09 23:00:00'::timestamp without time zone)
(7 rows)

Это нормально, он (частично) использует составной индекс для user_id и action как и ожидалось. Однако, если я выполню запрос в удаленной системе, я получу следующий вывод EXPLAIN:

remote_host=# EXPLAIN SELECT COUNT(*) FROM (SELECT  1 AS one FROM "big_table" WHERE "big_table"."user_id" = 13 AND "big_table"."action" = 1 AND (big_table.created_at >= '2018-12-09 23:00:00'::timestamp without time zone) ORDER BY big_table.created_at desc LIMIT 15 OFFSET 10) subquery_for_count;
                                                                 QUERY PLAN                                                                  
---------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=8472.67..8472.68 rows=1 width=8)
   ->  Limit  (cost=3389.25..8472.48 rows=15 width=12)
         ->  Index Scan Backward using index_big_table_on_created_at on big_table  (cost=0.44..4492554.51 rows=13257 width=12)
               Index Cond: (created_at >= '2018-12-09 23:00:00'::timestamp without time zone)
               Filter: ((user_id = 13) AND (action = 1))
(5 rows)

Как видно, на удаленном хосте база данных использует индекс на created_at вместо user_id и action как моя локальная установка. Это приводит к тому, что этот запрос становится слишком медленным на удаленном хосте (> 1 минуты до завершения), потому что есть много записей, которые удовлетворяют условию индекса, и фильтрация всех этих операций занимает много времени. Но на моей локальной установке это довольно быстро (~ 1 с до завершения). В моей локальной и удаленной таблицах одинаковое количество записей (~ 25 млн.) И примерно одинаковое распределение данных. Мы запускаем демон Vacuum на удаленном хосте, поэтому VACUUM ANALYZE делается довольно часто. Кроме того, индексы настроены одинаково в обеих системах.

Я уже пытался найти решения этой проблемы, но до сих пор не нашел ничего полезного, кроме запуска VACUUM ANALYZE и проверки существуют индексы для связанных атрибутов.

Может быть, у кого-то из вас есть подсказки? Конечно, я мог бы добавить составной индекс для всех используемых атрибутов (user_id, action и created_at), но я все еще сильно озадачен тем, почему «правильный» индекс не используется в этом случае на удаленном компьютере. host.

Оба хоста используют версию 9.6 Postgres (9.6.9 на локальном хосте и 9.6.17 на удаленном хосте, если быть точным).

1 Ответ

1 голос
/ 21 апреля 2020

Он может использовать один индекс для предоставления фильтра, а затем выполнять сортировку. Или он может использовать другой для предоставления ORDER BY, а затем рано останавливаться на основе LIMIT. Он должен выбирать, так как он не может сделать оба. PostgreSQL не может знать, что все с "big_table"."user_id" = 13 AND "big_table"."action" = 1 также было создано долгое время go, поэтому он не знает, что ранняя остановка на основе LIMIT на самом деле не остановится очень рано.

Трудно понять, каков твой вопрос. Вы, кажется, знаете, что ответ, построить индекс на (user_id, action, created_at). Сделайте это, если хотите, чтобы проблема с производительностью была решена.

Вы говорите, что запутались, а также что не можете изменить запрос. Возможность изменить запрос, поступающий из вашего приложения, не имеет отношения к решению вашей путаницы. Даже если неудачные ограничения по инструменту мешают вам реализовать решение, это не мешает вам понять ни решение, ни проблему.

Вы ищете понимание или решение?

Мои локальные и удаленные таблицы имеют одинаковое количество записей (~ 25 млн.) И примерно одинаковое распределение данных

Существует много аспектов распределения данных. Может быть, они похожи в некоторых, но не в других. Просмотр выходных данных EXPLAIN (ANALYZE, BUFFERS) для обоих серверов может действительно помочь, но, вероятно, будет недостаточно. Также было бы неплохо видеть EXPLAIN (ANALYZE, BUFFERS) с медленного сервера, когда он использует быстрый план. Это можно сделать, отбросив неверный индекс или изменив запрос, чтобы он использовал ORDER BY (big_table.created_at + interval '0') desc. Вам не нужно заставлять ваше приложение выполнять этот запрос, вы можете запустить его вручную.

Если подумать, просмотр EXPLAIN (ANALYZE, BUFFERS) для быстродействующего сервера с медленным планом может быть даже больше полезно. Вы, вероятно, можете сделать это, изменив запрос на использование ...WHERE ("big_table"."user_id" + 0 = 13) AND...

...