Запрос PostgreSQL занимает слишком много времени

У меня есть база данных с несколькими сотнями миллионов строк. Я запускаю следующий запрос:

select * from "Payments" as p
inner join "PaymentOrders" as po
on po."Id" = p."PaymentOrderId"
inner join "Users" as u
On u."Id" = po."UserId"
INNER JOIN "Roles" as r
on u."RoleId" = r."Id"
Where r."Name" = 'Moses'
LIMIT 1000

Когда предложение where находит совпадение в базе данных, я получаю результат через несколько миллисекунд, но если я изменю запрос и укажу несуществующее r."Name" в предложении where, это займет слишком много времени. Я предполагаю, что PostgreSQL выполняет последовательное сканирование таблицы Payments (которая содержит наибольшее количество строк), сравнивая каждую строку одну за другой.

Разве postgresql не достаточно умен, чтобы сначала проверить, содержит ли таблица Roles строку с Name 'Moses'?

В таблице Roles всего 15 строк, а в таблице Payments ~350 миллионов.

Я использую PostgreSQL 9.2.1.

Кстати, этот же запрос к той же схеме/данным занимает 0,024 мс на MS SQL Server.

Я обновлю вопрос и опубликую данные EXPLAIN ANALYZE через несколько часов.


Вот объяснение результатов анализа: http://explain.depesz.com/s/7e7


А вот конфигурация сервера:

version PostgreSQL 9.2.1, compiled by Visual C++ build 1600, 64-bit
client_encoding UNICODE
effective_cache_size    4500MB
fsync   on
lc_collate  English_United States.1252
lc_ctype    English_United States.1252
listen_addresses    *
log_destination stderr
log_line_prefix %t 
logging_collector   on
max_connections 100
max_stack_depth 2MB
port    5432
search_path dbo, "$user", public
server_encoding UTF8
shared_buffers  1500MB
TimeZone    Asia/Tbilisi
wal_buffers 16MB
work_mem    10MB

Я запускаю postgresql на процессоре i5 (4 ядра, 3,3 ГГц), 8 ГБ ОЗУ и Crucial m4 SSD 128 ГБ.


ОБНОВЛЕНИЕ Это похоже на ошибку в планировщике запросов. По рекомендации Эрвина Брандштеттера я сообщил об этом в список рассылки ошибок Postgresql.


person Davita    schedule 15.11.2012    source источник
comment
А пока отправьте простой EXPLAIN на explain.depesz.com и дайте ссылку на него в своем сообщении. Он запустится мгновенно. Спасибо, что включили версию PostgreSQL и полезные подробности. Еще одна вещь, которую стоит попробовать: ANALYZE таблицы и посмотрите, улучшится ли производительность. Может быть, ваша статистика полностью отсутствует? Вы также можете увидеть это в explain analyze, это самая распространенная причина того, что оценки количества строк совершенно неверны. Пожалуйста, добавьте комментарий при обновлении, так как SO не отправляет уведомления об изменении вопроса.   -  person Craig Ringer    schedule 16.11.2012
comment
@CraigRinger большое спасибо за вашу помощь. Я проанализировал все таблицы в базе данных и выполнил только запрос на объяснение. вот результат: explain.depesz.com/s/hol. тем не менее, анализ объяснения все еще выполняется, и я опубликую результаты, как только он завершится. Еще раз спасибо.   -  person Davita    schedule 16.11.2012
comment
@CraigRinger, извини, Крейг, я не понимаю, что ты имеешь в виду. Я не носитель английского языка :). в любом случае, вот объяснение результатов анализа: explain.depesz.com/s/7e7 любая идея, как исправить этот медленный запрос? Еще раз большое спасибо :)   -  person Davita    schedule 16.11.2012
comment
Это нормально. То, что я прошу, это EXPLAIN ANALYZE (или, по крайней мере, EXPLAIN) для запроса, когда он быстрый. Вы показали, что такое план запроса проблемы. Было бы полезно увидеть, что выбирает PostgreSQL, когда имя существует, план запроса, который быстро дает результат.   -  person Craig Ringer    schedule 16.11.2012
comment
@CraigRinger, верно. вот оно: explain.depesz.com/s/YY5Y. Я думаю, что это тот же план, но благодаря LIMIT строки обрабатываются быстрее, и ему не нужно проходить через всю таблицу..? Я очень новичок в PostgreSQL, извините за плохое понимание :)   -  person Davita    schedule 16.11.2012
comment
Я думаю, что здесь происходит, так это то, что PostgreSQL рассчитывает выполнить только небольшую часть вложенного цикла, прежде чем найдет достаточно строк, чтобы удовлетворить ваш LIMIT. Это случай, когда роль найдена, так как достаточно строк, чтобы удовлетворить ограничение, быстро найдено. Однако если роль не найдена, она продолжает поиск, пытаясь удовлетворить условию соединения (u."RoleId" = r."Id") для достаточного количества строк. Pg выполняет этот запрос в странном порядке. Кажется очень странным не искать таблицу ролей сначала. Это не похоже на проблему со статистикой, поскольку все оценки количества строк соответствуют медленному плану.   -  person Craig Ringer    schedule 16.11.2012
comment
Мне было бы интересно посмотреть, повлияло ли добавление ограничения UNIQUE (и, следовательно, индекса UNIQUE) на Roles.RoleId, если это совместимо с вашей моделью данных. Также покажите свою конфигурацию из wiki.postgresql.org/wiki/Server_Configuration и прочтите wiki.postgresql.org/wiki/Slow_Query_Questions . Меня особенно интересует ваш join_collapse_limit, хотя по умолчанию он должен быть достаточно высоким, чтобы не иметь значения для этого запроса.   -  person Craig Ringer    schedule 16.11.2012
comment
@CraigRinger Я обновил свой пост. Если я правильно понимаю, столбец Roles.Id является первичным ключом, и я думаю, что он уже уникален. Спасибо за ссылку, я посмотрю, может найду решение. Это действительно странно :/   -  person Davita    schedule 16.11.2012
comment
Я также рекомендую опубликовать сообщение в списке рассылки pgsql-performance и дать ссылку на этот вопрос. Включите показанный вами запрос, оба объяснения анализа с пояснениями, что есть что, и вывод psql \d+ для ваших таблиц, обрезанный от ненужных столбцов, если хотите.   -  person Craig Ringer    schedule 16.11.2012
comment
@CraigRinger благодарит Крейга за ваше время. Я попробую это :)   -  person Davita    schedule 16.11.2012
comment
Что произойдет, если вы удалите предложение LIMIT для случая, когда роль не найдена? У меня есть подозрение, что это приведет к быстрому плану - сделать LIMIT виновником.   -  person Erwin Brandstetter    schedule 16.11.2012
comment
Кроме того: пожалуйста, добавьте определения таблиц (включая индексы). Я согласен, что LIMIT воняет (но LIMIT без ORDER BY должен быть в порядке, кстати), но я думаю, что здесь тоже есть запах катезии. IOW: Упростите, используйте count(*) или что-то в этом роде.   -  person wildplasser    schedule 17.11.2012


Ответы (2)


Наконец удачная попытка

Моя другая идея - согласно комментарию:
Что произойдет, если вы удалите предложение LIMIT для случая, когда роль не найдена? У меня есть подозрение, что это приведет к быстрому плану - сделать LIMIT виновником.

Возможно, вы сможете решить свою проблему, поместив свой запрос в подзапрос и применив LIMIT только к внешнему запросу (не проверено):

SELECT *
FROM  (
   SELECT *
   FROM   "Roles"         AS r  
   JOIN   "Users"         AS u  ON u."RoleId" = r."Id"
   JOIN   "PaymentOrders" AS po ON po."UserId" = u."Id"
   JOIN   "Payments"      AS p  ON p."PaymentOrderId" = po."Id"
   WHERE  r."Name" = 'Moses'
  ) x
LIMIT  1000;

Согласно комментарию: @Davita проверил и исключил этот обходной путь. ответ @Kevin позже разъяснил, почему обходной путь не удался: используйте CTE вместо подзапроса.< br> Или проверьте наличие роли, прежде чем использовать большой запрос, чтобы исключить плохой случай.

Это оставляет вопросы к PostgreSQL относительно оптимизации запросов с LIMIT.

Появилось несколько недавних отчетов об ошибках, касающихся планов запросов с LIMIT. Я цитирую Саймона Риггса, комментирующего один из этих отчетов здесь:

Часто встречаются очень плохие планы с LIMIT. Это плохо для нас, потому что добавление LIMIT обычно/должно делать запросы быстрее, а не медленнее.

Нам нужно что-то сделать.

Первая попытка без успеха

Я пропустил, что @Craig уже упоминал join_collapse_limit в комментариях. Так что это было ограниченное использование:

Повлияет ли изменение порядка предложений JOIN?

SELECT *
FROM   "Roles"         AS r  
JOIN   "Users"         AS u  ON u."RoleId" = r."Id"
JOIN   "PaymentOrders" AS po ON po."UserId" = u."Id"
JOIN   "Payments"      AS p  ON p."PaymentOrderId" = po."Id"
WHERE  r."Name" = 'Moses'
LIMIT  1000

Связано: вы случайно не испортили настройку join_collapse_limit или geqo_threshold? Очень низкое значение может помешать планировщику изменить порядок ваших предложений JOIN, что может объяснить вашу проблему.

Если это не решит проблему, я бы попытался создать индекс для "Roles"(Name). Не то, чтобы это имело какой-либо смысл только с 15 строками, но я попытался бы устранить подозрение, что неверная статистика или параметры стоимости (или даже ошибка) заставляют планировщика полагать, что последовательное сканирование «Ролей» будет дороже, чем оно есть на самом деле.

person Erwin Brandstetter    schedule 16.11.2012
comment
Если бы изменение порядка соединения что-то изменило, я бы посчитал это ошибкой в ​​PostgreSQL. - person a_horse_with_no_name; 16.11.2012
comment
Спасибо, Эрвин, я попробовал ваши предложения, я изменил join_collapse_limit на 16, а затем на 64, но результат тот же. Я попробовал ваш запрос, и ничего не изменилось. Я добавил индекс B-TREE в Roles.Name, но без изменений: | Я действительно смущен здесь. - person Davita; 16.11.2012
comment
@a_horse_with_no_name: при небольшом количестве таблиц планировщик переупорядочивает JOIN в соответствии со своей наилучшей оценкой. Но оптимальный порядок JOIN — это проблема O(n!) Таким образом, существует предел, после которого используются более общие попытки оптимизации. Конечно, это не должно быть проблемой для всего 4 столов. Выполнение запроса явно начинается здесь не с того конца, поэтому мой выстрел в темноте в join_collapse_limit - и теперь добавленный geqo_threshold. - person Erwin Brandstetter; 16.11.2012
comment
@Davita: Извините, по крайней мере, это исключает некоторые возможные (теоретические) причины. Вы тоже смотрели на geqo_threshold? (Добавлено позже.) - person Erwin Brandstetter; 16.11.2012
comment
@ErwinBrandstetter еще раз спасибо, Эрвин, я только что попытался установить geqo_threshold (хотя я понятия не имею, что это такое: D) на 40, но все тот же план запроса. - person Davita; 16.11.2012
comment
Следующая идея: VACUUM FULL ANALYZE "Role" Если это ничего не даст, я бы попробовал VACUUM FULL ANALYZE в базе данных — это будет дорого с миллионами строк! Так что, возможно, в нерабочее время, если у вас есть одновременная нагрузка. Но это все лишь выстрелы в темноту.. - person Erwin Brandstetter; 16.11.2012
comment
@ErwinBrandstetter спасибо, Эрвин, я уже пробовал VACUUM FULL ANALYZE, и это не помогло. Я также повторно проанализировал таблицу ролей после добавления index. - person Davita; 16.11.2012
comment
Согласно документации, явный синтаксис соединения заставляет PostgreSQL объединять таблицы в указанный порядок. Так что, если исходный запрос и тот, который предложил Эрвин, дают разные планы, то здесь что-то не так. - person vyegorov; 16.11.2012
comment
@vyegorov: явный синтаксис соединения только задает порядок, когда join_collapse_limit меньше количества таблиц. Вы имели в виду это, верно? - person Erwin Brandstetter; 16.11.2012
comment
@ErwinBrandstetter, вы были правы, LIMIT был причиной неправильного плана запроса. Я удалил LIMIT, и все работает отлично :) Большое спасибо. Не могли бы вы обновить свой пост, чтобы я мог его принять, а другие сочли его полезным? Еще раз спасибо :) - person Davita; 16.11.2012
comment
Замечательно, большое спасибо за исчерпывающий ответ. Только одно примечание: отправка запроса в подзапрос не решает проблему, похоже, это большая проблема в PostgreSQL, но, тем не менее, проверка существования в первую очередь определенно является решением. Может быть, кто-нибудь может подсказать, куда я могу сообщить об этой проблеме (если она вообще считается проблемой?). Большое спасибо, Эрвин. - person Davita; 16.11.2012
comment
@Davita: Вы очень сотрудничали, пожалуйста. Вы можете сообщить об этом [email protected] или использовать форму, предоставленную здесь. Дополнительные параметры: postgresql.org/community/lists Я предлагаю вам указать ссылку на эту страницу для контекста. . Я помню, что у меня были подобные проблемы в прошлом, может быть, это давняя проблема .. - person Erwin Brandstetter; 16.11.2012
comment
@ErwinBrandstetter Еще раз спасибо, я сообщил о проблеме :) - person Davita; 16.11.2012
comment
@Davita: может быть, добавить ссылку на пост к вопросу - как только он у вас появится. - person Erwin Brandstetter; 16.11.2012
comment
@ErwinBrandstetter да, я отправил им ссылку на эту тему и дал им много объяснений :) - person Davita; 16.11.2012
comment
@Davita: я имею в виду наоборот: добавьте эту ссылку archives.postgresql .org/pgsql-bugs/2012-11/msg00089.php на ваш вопрос. :) - person Erwin Brandstetter; 16.11.2012
comment
@ErwinBrandstetter извините, мой плохой :)) - person Davita; 16.11.2012
comment
@Davita: я добавил ссылку на похожие отчеты об ошибках в свой ответ. - person Erwin Brandstetter; 16.11.2012
comment
@ErwinBrandstetter Ой, уже было много отчетов, надеюсь, еще один не повредит :) - person Davita; 16.11.2012
comment
@Davita: Как я уже писал: may be a long-standing issue... Напоминание не помешает. Ваш случай особенно ясен. - person Erwin Brandstetter; 16.11.2012

Как было предложено пару раз в потоке в списке производительности сообщества PostgreSQL, вы можете обойти эту проблему, установив барьер оптимизации с помощью CTE, например:

WITH x AS
(
SELECT *
  FROM "Payments" AS p
  JOIN "PaymentOrders" AS po ON po."Id" = p."PaymentOrderId"
  JOIN "Users" as u ON u."Id" = po."UserId"
  JOIN "Roles" as r ON u."RoleId" = r."Id"
  WHERE r."Name" = 'Moses'
)
SELECT * FROM x
  LIMIT 1000;

Вы также можете получить хороший план для своего исходного запроса, если установите более высокую цель статистики для «Роли». «Имя», а затем АНАЛИЗ. Например:

ALTER TABLE "Roles"
  ALTER COLUMN "Name" SET STATISTICS 1000;
ANALYZE "Roles";

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

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

  • Уменьшить random_page_cost. Это во многом зависит от того, насколько сильно кэшированы ваши данные. Учитывая таблицу с сотнями миллионов строк, вы, вероятно, не захотите опускаться ниже 2; хотя, если активный набор данных в вашей базе данных сильно закэширован, вы можете уменьшить его до значения seq_page_cost, и вы можете уменьшить их оба на порядок.

  • Убедитесь, что для параметра Effective_cache_size установлено значение суммы shared_buffers и того, что кэширует ваша ОС. Это не выделяет никакой памяти; он просто сообщает оптимизатору, насколько велика вероятность того, что индексные страницы останутся в кеше при активном доступе. При более высоком значении индексы выглядят лучше по сравнению с последовательным сканированием.

  • Увеличьте cpu_tuple_cost где-то в диапазоне от 0,03 до 0,05. Я обнаружил, что значение по умолчанию 0,01 слишком низкое. Я часто получаю лучшие планы, увеличивая его, и никогда не видел, чтобы значение в предложенном мной диапазоне вызывало выбор худших планов.

  • Убедитесь, что ваша настройка work_mem разумна. В большинстве сред, в которых я запускал PostgreSQL, это было в диапазоне от 16 МБ до 64 МБ. Это позволит лучше использовать хеш-таблицы, сканирование растровых индексов, сортировку и т. д. и может полностью изменить ваши планы; почти всегда в лучшую сторону. Остерегайтесь устанавливать этот уровень на уровне, который дает хорошие планы, если у вас большое количество соединений - вы должны учитывать тот факт, что каждое соединение может выделять столько памяти для каждого узла выполняемого запроса. «Практическое правило» состоит в том, чтобы рассчитать, что вы достигнете пика примерно в это время настройки max_connections. Это одна из причин, по которой разумно ограничить фактическое количество подключений к базе данных с помощью пула подключений.

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

Я согласен с тем, что нам нужно что-то сделать, чтобы оттолкнуть оптимизатора от «рискованных» планов, даже если они выглядят так, как будто в среднем они будут выполняться быстрее; но я буду немного удивлен, если настройка вашей конфигурации таким образом, чтобы оптимизатор лучше моделировал фактические затраты каждой альтернативы, не заставит его использовать эффективный план.

person kgrittn    schedule 20.11.2012
comment
Вау, это был отличный ответ. Спасибо друг и +1 :) - person Davita; 20.11.2012