Как мне «мыслить лучше» при чтении плана запросов PostgreSQL? - PullRequest
19 голосов
/ 25 февраля 2010

Я потратил более часа сегодня, ломая голову над планом запроса, который я не мог понять. Запрос был UPDATE, и он просто не работал вообще. Полностью зашла в тупик: pg_locks показал, что тоже ничего не ждет. Теперь я не считаю себя лучшим или худшим читателем плана запросов, но я считаю, что это исключительно сложно. Мне интересно как их читать? Есть ли методика, которой следуют Pg-асы, чтобы точно определить ошибку?

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

                                         QUERY PLAN                                         
--------------------------------------------------------------------------------------------
 Nested Loop Anti Join  (cost=47680.88..169413.12 rows=1 width=77)
   Join Filter: ((co.fkey_style = v.chrome_styleid) AND (co.name = o.name))
   ->  Nested Loop  (cost=5301.58..31738.10 rows=1 width=81)
         ->  Hash Join  (cost=5301.58..29722.32 rows=229 width=40)
               Hash Cond: ((io.lot_id = iv.lot_id) AND ((io.vin)::text = (iv.vin)::text))
               ->  Seq Scan on options io  (cost=0.00..20223.32 rows=23004 width=36)
                     Filter: (name IS NULL)
               ->  Hash  (cost=4547.33..4547.33 rows=36150 width=24)
                     ->  Seq Scan on vehicles iv  (cost=0.00..4547.33 rows=36150 width=24)
                           Filter: (date_sold IS NULL)
         ->  Index Scan using options_pkey on options co  (cost=0.00..8.79 rows=1 width=49)
               Index Cond: ((co.fkey_style = iv.chrome_styleid) AND (co.code = io.code))
   ->  Hash Join  (cost=42379.30..137424.09 rows=16729 width=26)
         Hash Cond: ((v.lot_id = o.lot_id) AND ((v.vin)::text = (o.vin)::text))
         ->  Seq Scan on vehicles v  (cost=0.00..4547.33 rows=65233 width=24)
         ->  Hash  (cost=20223.32..20223.32 rows=931332 width=44)
               ->  Seq Scan on options o  (cost=0.00..20223.32 rows=931332 width=44)
(17 rows)

Проблема с этим планом запроса - я полагаю, что я понимаю - вероятно, лучше всего сказать RhodiumToad (он определенно лучше в этом, поэтому я сделаю ставку на его объяснение лучше) irc://irc.freenode.net/#postgresql:

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

Он продолжает:

это может повлиять на любое объединение, но обычно объединения с подзапросами являются наиболее вероятными

Теперь, когда я прочитал этот план, первое, что я заметил, было Nested Loop Anti Join, это стоило 169,413 (я буду придерживаться верхних границ). Это Anti-Join ломается до результата Nested Loop по стоимости 31,738 и результата Hash Join по стоимости 137,424. Теперь 137,424, намного больше, чем 31,738, поэтому я знал, что проблема заключалась в хэш-соединении.

Затем я перехожу к EXPLAIN ANALYZE сегменту Hash Join вне запроса. Выполнено за 7 секунд. Я удостоверился, что были индексы (lot_id, vin) и (co.code и v.code) - были. Я отключил seq_scan и hashjoin по отдельности и заметил увеличение скорости менее чем на 2 секунды. Недостаточно близко, чтобы объяснить, почему он не прогрессировал через час.

Но после всего этого я совершенно неправ! Да, это была более медленная часть запроса, но из-за бита rows="1" (я предполагаю, что он был на Nested Loop Anti Join). Вот это ошибка (отсутствие способностей) в планировщике, неверно оценивающая количество строк? Как я должен читать это, чтобы прийти к такому же выводу RhodiumToad сделал?

Это просто rows="1", что должно заставить меня понять это?

Я выполнил VACUUM FULL ANALYZE на всех задействованных таблицах, и это Postgresql 8.4.

Ответы [ 2 ]

23 голосов
/ 26 февраля 2010

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

В вашем примере вы начнете с:

           ->  Seq Scan on options io  (cost=0.00..20223.32 rows=23004 width=36)
                 Filter: (name IS NULL)

Запустите EXPLAIN ANALYZE SELECT * FROM options WHERE name IS NULL; и посмотрите, совпадают ли возвращенные строки с оценкой. Отклонение в 2 раза обычно не проблема, вы пытаетесь определить порядок различий.

Затем см. EXPLAIN ANALYZE SELECT * FROM vehicles WHERE date_sold IS NULL; возвращает ожидаемое количество строк.

Затем перейдите на один уровень вверх к хеш-соединению:

     ->  Hash Join  (cost=5301.58..29722.32 rows=229 width=40)
           Hash Cond: ((io.lot_id = iv.lot_id) AND ((io.vin)::text = (iv.vin)::text))

Проверьте, приведет ли EXPLAIN ANALYZE SELECT * FROM vehicles AS iv INNER JOIN options io ON (io.lot_id = iv.lot_id) AND ((io.vin)::text = (iv.vin)::text) WHERE iv.date_sold IS NULL AND io.name IS NULL; к 229 строкам.

Повышение еще на один уровень добавляет INNER JOIN options co ON (co.fkey_style = iv.chrome_styleid) AND (co.code = io.code) и, как ожидается, вернет только одну строку. Вероятно, именно в этом и заключается проблема, потому что, если фактическое количество строк изменяется от 1 до 100, общая оценка стоимости обхода внутреннего цикла вмещающего вложенного цикла отключается с коэффициентом 100.

Основная ошибка, которую совершает планировщик, вероятно, состоит в том, что он ожидает, что два предиката для объединения в co не зависят друг от друга и умножают их селективности. Хотя в действительности они могут быть сильно коррелированными, а селективность ближе к MIN (s1, s2), а не к s1 * s2.

2 голосов
/ 26 февраля 2010

Вы анализировали таблицы? А что pg_stats говорит об этих таблицах? План запроса основан на статистике, она должна быть в порядке. А какую версию вы используете? 8,4

Затраты можно рассчитать, используя статистику, количество повторных страниц, количество строк и параметры в postgresql.conf для констант стоимости планировщика.

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

...