Запрос PostgreSQL очень медленный при добавлении подзапроса

10

У меня есть относительно простой запрос к таблице с 1,5M строк:

SELECT mtid FROM publication
WHERE mtid IN (9762715) OR last_modifier=21321
LIMIT 5000;

EXPLAIN ANALYZE вывод:

Limit  (cost=8.84..12.86 rows=1 width=8) (actual time=0.985..0.986 rows=1 loops=1)
  ->  Bitmap Heap Scan on publication  (cost=8.84..12.86 rows=1 width=8) (actual time=0.984..0.985 rows=1 loops=1)
        Recheck Cond: ((mtid = 9762715) OR (last_modifier = 21321))
        ->  BitmapOr  (cost=8.84..8.84 rows=1 width=0) (actual time=0.971..0.971 rows=0 loops=1)
              ->  Bitmap Index Scan on publication_pkey  (cost=0.00..4.42 rows=1 width=0) (actual time=0.295..0.295 rows=1 loops=1)
                    Index Cond: (mtid = 9762715)
              ->  Bitmap Index Scan on publication_last_modifier_btree  (cost=0.00..4.42 rows=1 width=0) (actual time=0.674..0.674 rows=0 loops=1)
                    Index Cond: (last_modifier = 21321)
Total runtime: 1.027 ms

Пока все хорошо, быстро и использует доступные индексы.
Теперь, если я немного изменю запрос, результат будет:

SELECT mtid FROM publication
WHERE mtid IN (SELECT 9762715) OR last_modifier=21321
LIMIT 5000;

EXPLAIN ANALYZEВыход:

Limit  (cost=0.01..2347.74 rows=5000 width=8) (actual time=2735.891..2841.398 rows=1 loops=1)
  ->  Seq Scan on publication  (cost=0.01..349652.84 rows=744661 width=8) (actual time=2735.888..2841.393 rows=1 loops=1)
        Filter: ((hashed SubPlan 1) OR (last_modifier = 21321))
        SubPlan 1
          ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=1)
Total runtime: 2841.442 ms

Не так быстро, и с помощью seq scan ...

Конечно, исходный запрос, выполняемый приложением, немного сложнее и даже медленнее, и, конечно, генерируемый в спящем режиме нет (SELECT 9762715), но медлительность есть даже для этого (SELECT 9762715)! Запрос генерируется hibernate, поэтому изменить их довольно сложно, а некоторые функции недоступны (например, UNIONнедоступны, что было бы быстро).

Вопросы

  1. Почему индекс не может быть использован во втором случае? Как они могли быть использованы?
  2. Могу ли я улучшить производительность запросов другим способом?

Дополнительные мысли

Кажется, что мы могли бы использовать первый случай, вручную выполнив SELECT, а затем поместив полученный список в запрос. Даже с 5000 числами в списке IN () это в четыре раза быстрее, чем второе решение. Однако, это только кажется НЕПРАВИЛЬНЫМ (также, это могло бы быть в 100 раз быстрее :)). Совершенно непонятно, почему планировщик запросов использует совершенно разные методы для этих двух запросов, поэтому я хотел бы найти более удачное решение этой проблемы.

P.Péter
источник
Можете ли вы как-то переписать свой код, чтобы hibernate генерировал JOINвместо IN ()? Кроме того, publicationбыл недавно проанализирован?
Дезсо
Да, я сделал VACUUM ANALYZE и VACUUM FULL. Там не было никаких изменений в производительности. Что касается второго, AFAIR, мы попробовали это, и это не оказало существенного влияния на производительность запросов.
П.Петр
1
Если Hibernate не генерирует правильный запрос, почему бы вам просто не использовать сырой SQL? Это все равно что настаивать на Google переводчике, когда вы уже знаете, как выразить это на английском. Что касается вашего вопроса: это действительно зависит от фактического запроса, скрытого позади (SELECT 9762715).
Эрвин Брандштеттер,
Как я уже говорил ниже, это очень медленный процесс, даже если внутренний запрос является (SELECT 9762715) . На вопрос гибернации: это можно сделать, но требует серьезного переписывания кода, поскольку у нас есть пользовательские критерии запроса гибернации, которые транслируются на лету. Поэтому, по сути, мы будем модифицировать Hibernate, что является огромным предприятием с множеством возможных побочных эффектов.
П.Петр

Ответы:

6

Суть проблемы становится очевидной:

Последующее сканирование при публикации (стоимость = 0,01. 349652,84 строки = 744661 ширина = 8) (фактическое время = 2735,888. 288,393 строки = 1 цикл = 1)

Оценки Postgres вернут 744661 рядов, в то время как, на самом деле, это один ряд. Если Postgres не знает лучше, чего ожидать от запроса, он не может планировать лучше. Нам нужно увидеть фактический запрос, спрятанный за ним, (SELECT 9762715)и, возможно, также знать определение таблицы, ограничения, количество элементов и распределение данных. Очевидно, Postgres не в состоянии предсказать , как несколько строк будут возвращены им. Там могут быть способы переписать запрос, в зависимости от того, что он является .

Если вы знаете, что подзапрос не может вернуть больше nстрок, вы можете просто сообщить Postgres, используя:

SELECT mtid
FROM   publication
WHERE  mtid IN (SELECT ... LIMIT n) --  OR last_modifier=21321
LIMIT  5000;

Если n достаточно мало, Postgres переключится на (растровое) сканирование индекса. Однако это работает только для простого случая. Перестает работать при добавлении ORусловия: планировщик запросов в настоящее время не может справиться с этим.

Я редко использую IN (SELECT ...)для начала. Обычно есть лучший способ реализовать то же самое, часто с EXISTSполусоединением. Иногда с ( LEFT) JOIN( LATERAL) ...

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

Эрвин Брандштеттер
источник
2
Там нет запроса скрыты за (SELECT 9762715) ! Если я выполню именно тот запрос, который вы видите выше. Конечно, исходный запрос гибернации немного сложнее, но мне (кажется, мне) удалось точно определить, где планировщик запросов сбивается, поэтому я представил эту часть запроса. Тем не менее, вышеизложенное объясняет и запросы дословно ctrl-cv.
П.Петр
Что касается второй части, внутренний предел не работает: EXPLAIN ANALYZE SELECT mtid FROM publication WHERE mtid IN (SELECT 9762715 LIMIT 1) OR last_modifier=21321 LIMIT 5000;также выполняется последовательное сканирование, а также выполняется около 3 секунд ...
P.Péter
@ P.Péter: Это работает для меня в моем локальном тесте с реальным подзапросом в Postgres 9.4. Если то, что вы показываете, является вашим реальным запросом, то у вас уже есть решение: используйте первый запрос в своем вопросе с константой вместо подзапроса.
Эрвин Брандштеттер,
Ну, я тоже пытался подзапрос на новом тестовом столе: CREATE TABLE test (mtid bigint NOT NULL, last_modifier bigint, CONSTRAINT test_property_pkey PRIMARY KEY (mtid)); CREATE INDEX test_last_modifier_btree ON test USING btree (last_modifier); INSERT INTO test (mtid, last_modifier) SELECT mtid, last_modifier FROM publication;. И эффект все еще был там для тех же самых запросов test: любой подзапрос приводил к сканированию seq ... Я пробовал и 9.1 и 9.4. Эффект тот же.
П.Петр
1
@ P.Péter: я снова провел тест и понял, что тестировал без ORусловия. Трюк с LIMITработает только для более простого случая.
Эрвин Брандштеттер
6

Мой коллега нашел способ изменить запрос так, чтобы он нуждался в простом переписывании и делал то, что ему нужно, то есть выполнял отбор за один шаг, а затем выполнял дальнейшие операции с результатом:

SELECT mtid FROM publication 
WHERE 
  mtid = ANY( (SELECT ARRAY(SELECT 9762715))::bigint[] )
  OR last_modifier=21321
LIMIT 5000;

Объяснить анализ сейчас:

 Limit  (cost=92.58..9442.38 rows=2478 width=8) (actual time=0.071..0.074 rows=1 loops=1)
   InitPlan 2 (returns $1)
     ->  Result  (cost=0.01..0.02 rows=1 width=0) (actual time=0.010..0.011 rows=1 loops=1)
           InitPlan 1 (returns $0)
             ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.001..0.002 rows=1 loops=1)
   ->  Bitmap Heap Scan on publication  (cost=92.56..9442.36 rows=2478 width=8) (actual time=0.069..0.070 rows=1 loops=1)
         Recheck Cond: ((mtid = ANY (($1)::bigint[])) OR (last_modifier = 21321))
         Heap Blocks: exact=1
         ->  BitmapOr  (cost=92.56..92.56 rows=2478 width=0) (actual time=0.060..0.060 rows=0 loops=1)
               ->  Bitmap Index Scan on publication_pkey  (cost=0.00..44.38 rows=10 width=0) (actual time=0.046..0.046 rows=1 loops=1)
                     Index Cond: (mtid = ANY (($1)::bigint[]))
               ->  Bitmap Index Scan on publication_last_modifier_btree  (cost=0.00..46.94 rows=2468 width=0) (actual time=0.011..0.011 rows=0 loops=1)
                     Index Cond: (last_modifier = 21321)
 Planning time: 0.704 ms
 Execution time: 0.153 ms

Кажется, мы можем создать простой синтаксический анализатор, который находит и переписывает все подвыборы таким образом, и добавить его в спящий режим, чтобы манипулировать собственным запросом.

P.Péter
источник
Это звучит весело. Не проще ли просто удалить все SELECTs, как у вас в первом запросе в вопросе?
Дезсо
Конечно, я мог бы сделать двухэтапный подход: сделать SELECTотдельно, а затем сделать внешний выбор со статическим списком после IN. Однако это значительно медленнее (в 5-10 раз, если подзапрос имеет более нескольких результатов), так как у вас есть дополнительные обходы по сети, плюс вы форматируете postgres много результатов, а затем анализируете эти результаты Java (а затем делаете то же самое снова в обратном направлении). Решение выше делает то же самое семантически, оставляя процесс внутри postgres. В общем, в настоящее время это кажется самым быстрым способом с минимальной модификацией в нашем случае.
П.Петр
Ах я вижу. Чего я не знал, так это того, что вы можете получить много удостоверений личности одновременно.
Дезсо
1

Ответьте на второй вопрос: Да, вы можете добавить ORDER BY в свой подзапрос, что окажет положительное влияние. Но это похоже на решение «EXISTS (подзапрос)» по производительности. Существует значительная разница даже с подзапросом, приводящим к двум строкам.

SELECT mtid FROM publication
WHERE mtid IN (SELECT #column# ORDER BY #column#) OR last_modifier=21321
LIMIT 5000;
ики
источник