存在子选择还是内部连接?

nee*_*zer 6 postgresql performance timestamp greatest-n-per-group postgresql-performance

我正在进入我的神秘查询的下一个级别。看起来在一个存在的内部有一个子选择,但在同一个表上。我认为这可能可以通过INNER JOIN更高的方式简化。

使用 PostgreSQL 9.4.2。
表定义 ( /d+):https : //gist.github.com/neezer/879f5d3649ca1903c6f3
基数:

billing_pricequote: 1,462,625 行
billing_pricequotestatus: 3,331,657 行
billing_lineitem: 43,687,855 行

这是原始查询,不建议对里面的子EXISTS查询进行修改

SELECT i.quote_id, i.acct_id AS account_id, SUM(i.delta_amount) AS amt
FROM billing_lineitem i
INNER JOIN billing_pricequote pq ON i.quote_id = pq.id
WHERE pq.date_applied AT TIME ZONE 'PST' BETWEEN '2016-02-02T00:00:00'::timestamp
                                AND '2016-03-03T22:27:41.734102-08:00'::timestamptz
AND EXISTS(
  SELECT s1.quote_id
  FROM billing_pricequotestatus s1
  INNER JOIN (
    SELECT DISTINCT ON (quote_id) quote_id, MAX(created_at) AS max_created_at
    FROM billing_pricequotestatus
    WHERE quote_id=i.quote_id
    GROUP BY quote_id, created_at
    ORDER BY quote_id, created_at DESC
  ) AS s2
  ON s1.quote_id = s2.quote_id
  AND s1.created_at = s2.max_created_at
  WHERE s1.name IN ('adjustment','payment','billable')
)
GROUP BY i.quote_id, i.acct_id
;
Run Code Online (Sandbox Code Playgroud)

我注意到看着怪异的部分是SELECTbilling_pricequotestatus,然后在里面的同桌另一子查询INNER JOIN

我尝试通过我的其他 SO 帖子中修改来改变它:

SELECT i.quote_id, i.acct_id AS account_id, SUM(i.delta_amount) AS amt
FROM billing_lineitem i
INNER JOIN billing_pricequote pq ON i.quote_id = pq.id
WHERE pq.date_applied AT TIME ZONE 'PST' BETWEEN '2016-02-02T00:00:00'::timestamp
                                AND '2016-03-03T22:27:41.734102-08:00'::timestamptz
AND EXISTS(
  SELECT quote_id, MAX(created_at) AS max_created_at
  FROM billing_pricequotestatus
  WHERE quote_id=i.quote_id
  AND name IN ('adjustment','payment','billable')
  GROUP BY quote_id
)
GROUP BY i.quote_id, i.acct_id
;
Run Code Online (Sandbox Code Playgroud)

这将我的执行时间减少了一半(~40 秒到~20 秒),但产生的结果略有不同(原始查询返回 28,895 行,但我的新查询返回 28,917 行)。我不清楚为什么我的修改没有产生等效的输出(它需要)。

EXPLAIN ANALYZE对于两个查询
解释原始查询 depesz.com 的分析。

非常感谢您对此的任何帮助/指导!


我尝试用 a 更新@ypercube?? 的答案LATERAL JOIN,并且性能似乎大致相同(每个人的获胜次数都相同,不到一秒):

SELECT i.quote_id, i.acct_id AS account_id, SUM(i.delta_amount) AS amt
FROM billing_lineitem i
INNER JOIN billing_pricequote pq ON i.quote_id = pq.id
LEFT JOIN LATERAL
( SELECT name
  FROM billing_pricequotestatus
  WHERE quote_id = i.quote_id
  ORDER BY created_at DESC
  LIMIT 1
) pqs ON true
WHERE pq.date_applied AT TIME ZONE 'PST' BETWEEN '2016-02-02T00:00:00'::timestamp
                                AND '2016-03-03T22:27:41.734102-08:00'::timestamptz
AND pqs.name IN ('adjustment', 'payment', 'billable')
GROUP BY i.quote_id, i.acct_id
;
Run Code Online (Sandbox Code Playgroud)

解释分析。

有没有其他建议可以让这个低于 10 秒?

Erw*_*ter 6

据我了解,您的子查询的目的是:
选择最新相关条目billing_pricequotestatus具有合格name.

第二个查询不正确

我不清楚为什么我的修改没有产生等效的输出

第一个查询从中选择最新的billing_pricequotestatus并检查是否name符合条件 ( name IN ('adjustment','payment','billable'))。

第二个查询是倒退的:它检查任何符合条件的行name(不仅仅是最后一个)。此外,在EXISTS半连接中计算聚合也没有意义。你不想那样。它不是等价的。

因此,您会从第二个查询中获得更多行。

时间范围不正确

这个谓词一团糟。低效且可能不正确 - 或者至少是一个滴答作响的炸弹:

WHERE pq.date_applied AT TIME ZONE 'PST'
       BETWEEN '2016-02-02T00:00:00'::timestamp
           AND '2016-03-03T22:27:41.734102-08:00'::timestamptz 
Run Code Online (Sandbox Code Playgroud)

该列date_applied的类型为timestamptz。该构造AT TIME ZONE 'PST'将其转换为类型,timestamp并按硬编码为时区缩写“PST”的时间偏移进行移位 - 这是一个糟糕的举动。它使表达式不可sargable。这更昂贵,更重要的是,排除了在date_applied.

更糟糕的是,时区缩写'PST'不知道 DST 或任何历史性的时间变化。如果您的时区有(或过去有)夏令时,并且您的时区跨越不同的 DST 时段,则您当前的表达很可能不正确

您需要使用适用的时区名称而不是缩写来获得一致的本地时间 - 这甚至更昂贵。

还有另一个问题:虽然列值被硬编码的时间偏移量 ('PST') 移动,你的上限'2016-03-03T22:27:41.734102-08:00'::timestamptz是作为timestamptz和静默强制匹配数据类型提供的timestamp。由于未提供明确的时间偏移,因此强制转换默认为当前 session时区。因此,您可以根据会话的当前时区设置获得不同的结果。我想不出一个有意义的用例。

不要做任何这些。不要翻译timestamptzdate_applied于当地时间可言,像你这样不混合数据类型和不不同的方式来铸造混合。相反,按原样使用该列并提供timestamptz参数。

询问

SELECT i.quote_id, i.acct_id AS account_id, sum(i.delta_amount) AS amt
FROM   billing_pricequote pq
JOIN   LATERAL (
   SELECT name
   FROM   billing_pricequotestatus
   WHERE  quote_id = pq.id
   ORDER  BY created_at DESC
   LIMIT  1
   ) pqs ON pqs.name IN ('adjustment', 'payment', 'billable')
JOIN   billing_lineitem i ON i.quote_id = pq.id
WHERE  pq.date_applied BETWEEN (timestamp '2016-02-02T00:00:00' AT TIME ZONE 'PST')  -- !
                           AND timestamptz '2016-03-03T22:27:41.734102-08:00'
GROUP  BY 1,2;
Run Code Online (Sandbox Code Playgroud)

注意LATERAL连接,而不是LEFT JOIN,让它INNER JOIN立即实现你的谓词。

或者使用@ypercube 概述的等效相关子查询。不确定哪个更快。

另外请注意,我的基础LATERAL JOINbilling_pricequote-加入到billing_lineitem。这样我们可以尽早消除行,这应该更便宜。

指数

目前,您将获得:

对 billing_pricequote pq 进行 Seq 扫描

仅选择了 150 万行中的 7 万行,大约为 5%。索引date_applied可能会有所帮助,但作用不大。然而,这种多列索引应该有助于大幅如果你能得到仅索引扫描的吧:

CREATE INDEX foo ON billing_pricequotestatus (quote_id, created_at DESC, name);
Run Code Online (Sandbox Code Playgroud)

使用name_id代替比name下面建议的更有效。

统计数据

Postgres 高估了您的时间范围的选择性:

(cost=0.00..88,546.50 rows=7,313 width=4) (实际时间=2.353..767.408 rows=70,623 loops=1)

增加列的统计目标可能会有所帮助date_applied。详情在这里:

表定义

示例billing_pricequotestatus

name似乎是几种可能的类型之一。将更多规范化并仅使用 4 字节integer引用查找表而不是varchar(20)在 3.3M 行中重复,这将有助于提高性能。此外,像我演示的那样重新排序列(如果可能)会有所帮助:

   Column   |           Type           |              Modifiers
------------+--------------------------+------------------------------------------
 id         | integer                  | not null default nextval('...
 quote_id   | integer                  | not null
 created_at | timestamp with time zone | not null
 updated_at | timestamp with time zone | not null
 name_id    | integer                  | not null REFERENCES name_table(name_id)
 notes      | text                     | not null
Run Code Online (Sandbox Code Playgroud)

请参阅上面关于对齐和填充的链接。要测量行大小:

而“名称”不是一个好的标识符。我会用一些描述性的东西来代替。


ype*_*eᵀᴹ 5

我认为EXISTS子查询:

AND EXISTS(
  SELECT s1.quote_id
  FROM billing_pricequotestatus s1
    INNER JOIN 
      ( SELECT DISTINCT ON (quote_id) quote_id, MAX(created_at) AS max_created_at
        FROM billing_pricequotestatus
        WHERE quote_id=i.quote_id
        GROUP BY quote_id, created_at
        ORDER BY quote_id, created_at DESC
      ) AS s2
    ON s1.quote_id = s2.quote_id
    AND s1.created_at = s2.max_created_at
  WHERE s1.name IN ('adjustment','payment','billable')
)
Run Code Online (Sandbox Code Playgroud)

可以简化为相关子查询:

AND   ( SELECT name
        FROM billing_pricequotestatus
        WHERE quote_id = i.quote_id
        ORDER BY created_at DESC
        LIMIT 1
      ) IN ('adjustment', 'payment', 'billable')
Run Code Online (Sandbox Code Playgroud)

索引(quote_id, created_at DESC, name)将有很大帮助。

如果你的Postgres版本是9.3以上的,也可以用LATERALjoin来写,可能会提高效率。


您在 SO 上发布的第一个问题不包括 ,WHERE quote_id = i.quote_id因此其他人不可能知道子查询是相关的。您在那里得到的答案对于这种情况是正确的。


归档时间:

查看次数:

1351 次

最近记录:

7 年,9 月 前