优化多个连接

时间:2010-01-18 15:06:58

标签: sql postgresql optimization join

我正在试图找出一种方法来加速一个特别麻烦的查询,该查询在几个表中按日期汇总了一些数据。下面是完整(丑陋)的查询以及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 ANALYZEon explain.depesz.com

任何意见或问题都表示赞赏。

干杯

3 个答案:

答案 0 :(得分:16)

优化查询时总会考虑两件事:

  • 可以使用哪些索引(您可能需要创建索引)
  • 如何编写查询(您可能需要更改查询以允许查询优化器能够找到适当的索引,并且不会冗余地重新读取数据)

一些观察结果:

  • 您在加入日期之前正在执行日期操作。作为一般规则,这将阻止查询优化器使用索引,即使它存在。您应该尝试编写表达式,使得索引列在表达式的一侧保持不变。

  • 您的子查询过滤到与generate_series相同的日期范围。这是一个重复,它限制了优化器选择最有效优化的能力。我怀疑可能已经写入以提高性能,因为优化器无法在日期列(body_time)上使用索引?

  • 注意:我们实际上非常希望在Body.body_time上使用索引

  • 子查询中的
  • ORDER BY最多是多余的。在最坏的情况下,它可能会强制查询优化器在加入之前对结果集进行排序;这不一定对查询计划有利。而是仅在最后应用订购以进行最终显示。

  • 在子查询中使用LEFT JOIN是不合适的。假设您对NULL行为使用ANSI约定(并且您应该这样做),任何外部加入envelope将返回envelope_command=NULL,因此这些将是条件envelope_command=?排除。

  • 除了o值之外,子查询ienvelope_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

编辑:添加了Tom H建议的过滤器。

答案 1 :(得分:3)

在Craig Young的suggestions的基础上,这里是修改后的查询,在我正在处理的数据集中运行约1.8秒。这是对原版~2.0秒的略微改进,以及对Craig的大幅提升,耗时约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

答案 2 :(得分:0)

我几天前卸载了我的PostgreSQL服务器,所以你可能不得不玩这个,但希望这对你来说是一个好的开始。

关键是:

  1. 您不应该需要子查询 - 只需执行直接连接和聚合
  2. 您应该能够使用INNER JOIN,这通常比OUTER JOIN更高效
  3. 如果没有别的,我认为下面的查询更清楚一点。

    我在查询中使用了一个日历表,但您可以在使用它时将其替换为generate_series。

    此外,根据索引,将body_date与&gt; =和&lt;进行比较可能更好。而不是拉出日期部分并进行比较。我不太了解PostgreSQL在幕后知道它是如何工作的,所以我会尝试两种方法来查看服务器可以更好地优化。在伪代码中你会做:body_date&gt; = date(time = midnight)AND body_date&lt;日期+ 1(时间=午夜)。

    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