Оптимизация нескольких соединений - PullRequest
11 голосов
/ 18 января 2010

Я пытаюсь найти способ ускорить особенно громоздкий запрос, который объединяет некоторые данные по дате в нескольких таблицах.Ниже приведен полный (безобразный) запрос вместе с EXPLAIN ANALYZE, чтобы показать, насколько это ужасно.

Если кто-то может взглянуть и посмотреть, могут ли они обнаружить какие-либо серьезные проблемы (что, вероятно,я не парень из Postgres) это было бы превосходно.

Так что вот так.Запрос:

SELECT 
 to_char(p.period, 'DD/MM/YY') as period,
 coalesce(o.value, 0) AS outbound,
 coalesce(i.value, 0) AS inbound
FROM (
 SELECT
  date '2009-10-01' + s.day 
  AS period 
  FROM generate_series(0, date '2009-10-31' - date '2009-10-01') AS s(day)
) AS p 
LEFT OUTER JOIN(
 SELECT
  SUM(b.body_size) AS value, 
  b.body_time::date AS period 
 FROM body AS b 
 LEFT JOIN 
  envelope e ON e.message_id = b.message_id 
 WHERE 
  e.envelope_command = 1 
  AND b.body_time BETWEEN '2009-10-01' 
  AND (date '2009-10-31' + INTERVAL '1 DAY') 
 GROUP BY period 
 ORDER BY period
) AS o ON p.period = o.period
LEFT OUTER JOIN( 
 SELECT 
  SUM(b.body_size) AS value, 
  b.body_time::date AS period 
 FROM body AS b 
 LEFT JOIN 
  envelope e ON e.message_id = b.message_id 
 WHERE 
  e.envelope_command = 2 
  AND b.body_time BETWEEN '2009-10-01' 
  AND (date '2009-10-31' + INTERVAL '1 DAY') 
 GROUP BY period 
 ORDER BY period
) AS i ON p.period = i.period 

EXPLAIN ANALYZE можно найти здесь: на объяснить.depesz.com

Любые комментарии или вопросы приветствуются.

Приветствия

Ответы [ 3 ]

16 голосов
/ 18 января 2010

При оптимизации запросов всегда нужно учитывать две вещи:

  • Какие индексы можно использовать (вам может потребоваться создать индексы)
  • Как пишется запрос (вам может потребоваться изменить запрос, чтобы оптимизатор запросов мог найти соответствующие индексы и не перечитывать данные избыточно)

Несколько замечаний:

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

  • Ваши подзапросы фильтруются в том же диапазоне дат, что и generate_series. Это дублирование, и оно ограничивает способность оптимизатора выбирать наиболее эффективную оптимизацию. Я подозреваю, что это могло быть сделано для повышения производительности, поскольку оптимизатору не удалось использовать индекс для столбца даты (body_time)?

  • ПРИМЕЧАНИЕ : На самом деле мы очень хотели бы использовать индекс для Body.body_time

  • ORDER BY в подзапросах в лучшем случае является избыточным. В худшем случае это может заставить оптимизатор запросов отсортировать набор результатов перед объединением; и это не обязательно хорошо для плана запроса. Скорее, применять только порядок в конце для окончательного отображения.

  • Использование LEFT JOIN в ваших подзапросах неуместно. Предполагая, что вы используете соглашения ANSI для поведения NULL (и вы должны это делать), любые внешние объединения к envelope вернут envelope_command=NULL, и, следовательно, они будут исключены из условия envelope_command=? .

  • Подзапросы o и i практически идентичны, за исключением значения envelope_command. Это заставляет оптимизатор сканировать одни и те же базовые таблицы дважды. Вы можете использовать метод сводной таблицы , чтобы присоединиться к данным один раз и разбить значения на 2 столбца.

Попробуйте следующее, использующее технику разворота:

SELECT  p.period,
        /*The pivot technique in action...*/
        SUM(
        CASE WHEN envelope_command = 1 THEN body_size
        ELSE 0
        END) AS Outbound,
        SUM(
        CASE WHEN envelope_command = 2 THEN body_size
        ELSE 0
        END) AS Inbound
FROM    (
        SELECT  date '2009-10-01' + s.day AS period
        FROM    generate_series(0, date '2009-10-31' - date '2009-10-01') AS s(day)
        ) AS p 
        /*The left JOIN is justified to ensure ALL generated dates are returned
          Also: it joins to a subquery, else the JOIN to envelope _could_ exclude some generated dates*/
        LEFT OUTER JOIN (
        SELECT  b.body_size,
                b.body_time,
                e.envelope_command
        FROM    body AS b 
                INNER JOIN envelope e 
                  ON e.message_id = b.message_id 
        WHERE   envelope_command IN (1, 2)
        ) d
          /*The expressions below allow the optimser to use an index on body_time if 
            the statistics indicate it would be beneficial*/
          ON d.body_time >= p.period
         AND d.body_time < p.period + INTERVAL '1 DAY'
GROUP BY p.Period
ORDER BY p.Period

РЕДАКТИРОВАТЬ : добавлен фильтр, предложенный Томом Х.

3 голосов
/ 20 января 2010

Опираясь на предложения Крейга Янга , вот исправленный запрос, который выполняется за ~ 1,8 секунды для набора данных, над которым я работаю. Это небольшое улучшение по сравнению с оригинальными ~ 2,0 и огромное улучшение по сравнению с Крейгом, которое заняло ~ 22 с.

SELECT
    p.period,
    /* The pivot technique... */
    SUM(CASE envelope_command WHEN 1 THEN body_size ELSE 0 END) AS Outbound,
    SUM(CASE envelope_command WHEN 2 THEN body_size ELSE 0 END) AS Inbound
FROM
(
    /* Get days range */
    SELECT date '2009-10-01' + day AS period
    FROM generate_series(0, date '2009-10-31' - date '2009-10-01') AS day
) p
    /* Join message information */
    LEFT OUTER JOIN
    (
        SELECT b.body_size, b.body_time::date, e.envelope_command
        FROM body AS b 
            INNER JOIN envelope e ON e.message_id = b.message_id 
        WHERE
            e.envelope_command IN (2, 1)
            AND b.body_time::date BETWEEN (date '2009-10-01') AND (date '2009-10-31')
    ) d ON d.body_time = p.period
GROUP BY p.period
ORDER BY p.period
0 голосов
/ 18 января 2010

Я удалил свой сервер PostgreSQL пару дней назад, поэтому вам, вероятно, придется поиграть с этим, но, надеюсь, это хорошее начало для вас.

Ключи:

  1. Вам не нужны подзапросы - просто выполняйте прямые объединения и агрегируйте
  2. Вы должны иметь возможность использовать ВНУТРЕННИЕ СОЕДИНЕНИЯ, которые обычно более производительны, чем ВНЕШНИЕ СОЕДИНЕНИЯ

Если ничего другого, я думаю, что приведенный ниже запрос немного яснее.

Я использовал таблицу календаря в своем запросе, но вы можете заменить ее на generate_series, как вы ее использовали.

Кроме того, в зависимости от индексации, может быть лучше сравнить body_date с> = и <, чем извлекать часть даты и сравнивать.Я не знаю достаточно о PostgreSQL, чтобы знать, как он работает за кулисами, поэтому я бы попробовал оба подхода, чтобы увидеть, какой сервер лучше оптимизировать.В псевдокоде вы должны делать: body_date> = date (time = midnight) И body_date

SELECT
    CAL.calendar_date AS period,
    SUM(O.body_size) AS outbound,
    SUM(I.body_size) AS inbound
FROM
    Calendar CAL
INNER JOIN Body OB ON
    OB.body_time::date = CAL.calendar_date
INNER JOIN Envelope OE ON
    OE.message_id = OB.message_id AND
    OE.envelope_command = 1
INNER JOIN Body IB ON
    IB.body_time::date = CAL.calendar_date
INNER JOIN Envelope IE ON
    IE.message_id = IB.message_id AND
    IE.envelope_command = 2
GROUP BY
    CAL.calendar_date
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...