Разница в плане запроса: внутреннее соединение / правое соединение "наибольшие n на группу", самостоятельное объединение, агрегированный запрос - PullRequest
0 голосов
/ 23 февраля 2020

Для небольшого хранилища данных Postgres 10 я проверял улучшения в наших аналитических запросах и обнаружил довольно медленный запрос, в котором возможное улучшение в основном сводилось к этому подзапросу (класс c проблема наибольших чисел на группу) ):

SELECT s_postings.*
FROM dwh.s_postings
JOIN (SELECT s_postings.id,
          max(s_postings.load_dts) AS load_dts
      FROM dwh.s_postings
      GROUP BY s_postings.id) AS current_postings
ON s_postings.id = current_postings.id AND s_postings.load_dts = current_postings.load_dts

При следующем плане выполнения:

"Gather  (cost=23808.51..38602.59 rows=66 width=376) (actual time=1385.927..1810.844 rows=170847 loops=1)"
"  Workers Planned: 2"
"  Workers Launched: 2"
"  ->  Hash Join  (cost=22808.51..37595.99 rows=28 width=376) (actual time=1199.647..1490.652 rows=56949 loops=3)"
"        Hash Cond: (((s_postings.id)::text = (s_postings_1.id)::text) AND (s_postings.load_dts = (max(s_postings_1.load_dts))))"
"        ->  Parallel Seq Scan on s_postings  (cost=0.00..14113.25 rows=128425 width=376) (actual time=0.016..73.604 rows=102723 loops=3)"
"        ->  Hash  (cost=20513.00..20513.00 rows=153034 width=75) (actual time=1195.616..1195.616 rows=170847 loops=3)"
"              Buckets: 262144  Batches: 1  Memory Usage: 20735kB"
"              ->  HashAggregate  (cost=17452.32..18982.66 rows=153034 width=75) (actual time=836.694..1015.499 rows=170847 loops=3)"
"                    Group Key: s_postings_1.id"
"                    ->  Seq Scan on s_postings s_postings_1  (cost=0.00..15911.21 rows=308221 width=75) (actual time=0.032..251.122 rows=308168 loops=3)"
"Planning time: 1.184 ms"
"Execution time: 1912.865 ms"

Оценка строки абсолютно неверна! Для меня странная вещь, если я сейчас изменю соединение на правое объединение:

SELECT s_postings.*
FROM dwh.s_postings
RIGHT JOIN (SELECT s_postings.id,
      max(s_postings.load_dts) AS load_dts
   FROM dwh.s_postings
   GROUP BY s_postings.id) AS current_postings
ON s_postings.id = current_postings.id AND s_postings.load_dts = current_postings.load_dts

С планом выполнения:

"Hash Right Join  (cost=22829.85..40375.62 rows=153177 width=376) (actual time=814.097..1399.673 rows=170848 loops=1)"
"  Hash Cond: (((s_postings.id)::text = (s_postings_1.id)::text) AND (s_postings.load_dts = (max(s_postings_1.load_dts))))"
"  ->  Seq Scan on s_postings  (cost=0.00..15926.10 rows=308510 width=376) (actual time=0.011..144.584 rows=308419 loops=1)"
"  ->  Hash  (cost=20532.19..20532.19 rows=153177 width=75) (actual time=812.587..812.587 rows=170848 loops=1)"
"        Buckets: 262144  Batches: 1  Memory Usage: 20735kB"
"        ->  HashAggregate  (cost=17468.65..19000.42 rows=153177 width=75) (actual time=553.633..683.850 rows=170848 loops=1)"
"              Group Key: s_postings_1.id"
"              ->  Seq Scan on s_postings s_postings_1  (cost=0.00..15926.10 rows=308510 width=75) (actual time=0.011..157.000 rows=308419 loops=1)"
"Planning time: 0.402 ms"
"Execution time: 1469.808 ms"

Оценка строк намного лучше!

Мне известно, что, например, параллельное последовательное сканирование может в некоторых условиях снизить производительность, но не должно изменять оценку строки !? Если я правильно помню, агрегатные функции также в любом случае блокируют правильное использование индексов и также не видят каких-либо потенциальных выгод от дополнительной многомерной статистики, например, для кортежа id, load_dts. База данных VACUUM ANALYZE d.

Для меня запросы логически одинаковы.

Есть ли способ поддержать планировщик запросов, чтобы сделать более точные предположения об оценках или улучшить запрос? Может быть, кто-то знает причину, по которой существует эта разница?

Редактировать: Ранее условие соединения было ON s_postings.id::text = current_postings.id::text Я изменил это на ON s_postings.id = current_postings.id, чтобы никого не смущать. Удаление этого преобразования не меняет план запроса.

Edit2: Как показано ниже, существует другое решение проблемы greatest-n-per-group:

SELECT p.*
FROM (SELECT p.*,
             RANK() OVER (PARTITION BY p.id ORDER BY p.load_dts DESC) as seqnum
      FROM dwh.s_postings p
     ) p
WHERE seqnum = 1;

Действительно хорошее решение, но, к сожалению, запрос Планировщик также недооценивает количество строк:

"Subquery Scan on p  (cost=44151.67..54199.31 rows=1546 width=384) (actual time=1742.902..2594.359 rows=171269 loops=1)"
"  Filter: (p.seqnum = 1)"
"  Rows Removed by Filter: 137803"
"  ->  WindowAgg  (cost=44151.67..50334.83 rows=309158 width=384) (actual time=1742.899..2408.240 rows=309072 loops=1)"
"        ->  Sort  (cost=44151.67..44924.57 rows=309158 width=376) (actual time=1742.887..1927.325 rows=309072 loops=1)"
"              Sort Key: p_1.id, p_1.load_dts DESC"
"              Sort Method: quicksort  Memory: 172275kB"
"              ->  Seq Scan on s_postings p_1  (cost=0.00..15959.58 rows=309158 width=376) (actual time=0.007..221.240 rows=309072 loops=1)"
"Planning time: 0.149 ms"
"Execution time: 2666.645 ms"

Ответы [ 2 ]

1 голос
/ 24 февраля 2020

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

Оценка строки абсолютно неверна!

Хотя это, очевидно, верно, я не Не думаю, что неправильная оценка приводит к чему-то особенно плохому.

Я знаю, что, например, параллельное последовательное сканирование может в некоторых условиях снизить производительность, но не должно изменять оценку строки!?

Правильно. Это изменение в типе JOIN вызывает изменение оценки и, в свою очередь, вызывает изменение в распараллеливании. Думая, что это должно принести sh больше кортежей до лидера (а не дисквалифицировать их в рабочих), препятствует параллельным планам из-за parallel_tuple_cost.

Если я правильно помню, агрегатные функции также блокируют правильное использование индексов

Нет, индекс для (id, load_dts) или даже просто (id) должен использоваться для агрегирования, но поскольку вам нужно прочитать всю таблицу, он, вероятно, будет медленнее читать весь индекс и всю таблицу, чем просто читать всю таблицу в HashAgg. Вы можете проверить, считает ли PostgreSQL, что он способен использовать такой индекс, установив enable_seqscan = off. В любом случае, если он выполняет сканирование seq, он не считает индекс пригодным для использования. В противном случае он просто считает использование индекса контрпродуктивным.

Есть ли способ поддержать планировщик запросов, чтобы сделать более точные предположения об оценках или улучшить запрос? Может быть, кто-то знает причину, по которой существует эта разница?

Планировщику не хватает понимания, чтобы знать, что каждый id,max(load_dts) из производной таблицы должен происходить как минимум из одной строки в исходной таблице. Вместо этого он применяет два условия в ON как независимые переменные и даже не знает, какими будут наиболее распространенные значения / гистограммы для вашей производной таблицы, поэтому не может предсказать степень перекрытия. Но с RIGHT JOIN он знает, что возвращается каждая строка в производной таблице, независимо от того, найдено совпадение в «другой» таблице или нет. Если вы создаете временную таблицу из своего производного подзапроса и анализируете ее, а затем используете эту таблицу в соединении, вы должны получить более точные оценки, потому что она хотя бы знает, насколько перекрываются распределения в каждом столбце. Но эти более точные оценки вряд ли будут загружаться в гораздо лучшие планы, поэтому я бы не стал беспокоиться об этой сложности.

Вы, вероятно, можете получить некоторую предельную скорость, переписав ее в запрос DISTINCT ON, но это не будет волшебно лучше. Также обратите внимание, что они не эквивалентны. Объединение вернет все строки, которые связаны для первого места в данном идентификаторе, в то время как DISTINCT ON вернет произвольную одну из них (если вы не добавите столбцы в ORDER BY, чтобы разорвать связи)

0 голосов
/ 23 февраля 2020

Используйте функции окна:

SELECT p.*
FROM (SELECT p.*,
             RANK() OVER (PARTITION BY p.id ORDER BY p.load_dts DESC) as seqnum
      FROM dwh.s_postings p
     ) p
WHERE seqnum = 1;

Или, что еще лучше, если вы хотите, чтобы по одной строке на id использовалось DISTINCT ON:

SELECT DISTINCT ON (p.id) p.*
FROM dwh.s_postings p
ORDER BY p.id, p.load_dts DESC;

Если бы мне пришлось размышлять, преобразование id - которое совершенно не нужно - сбрасывает оптимизатор. С right join ясно, что все строки хранятся в одной из таблиц, и это может помочь при вычислении статистики.

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