优化存在多个 JOIN LATERAL 时的慢查询

Mar*_*tin 0 sql postgresql postgresql-performance lateral-join

这是将 SUM group by 添加到 COUNT DISTINCT 查询中的后续优化(尽管进行了一些优化和联接简化)。

我想知道是否可以优化以下需要完成的PostgreSQL 13.1查询130322.2ms。通常,如果只有一个JOIN LATERAL存在,它会在几毫秒内完成。

我最迷失的是,每个子查询JOIN LATERAL都有一个ON基于其自己的子查询分数的条件,我如何优化查询,可能减少子查询的数量JOIN LATERAL,但仍然得到相同的结果。

从我看来,当条件添加到代替OR中的某些 WHERE时,它似乎会变慢。看:JOIN LATERALAND

SELECT count(*)
FROM subscriptions q
JOIN LATERAL (
      SELECT
        SUM(ts.score) AS sum_score
      FROM
        quiz_answers qa
        JOIN answers a ON a.id = qa.answer_id
        JOIN tag_scores ts ON ts.answer_id = a.id
      WHERE
        qa.quiz_id = q.quiz_id
        AND ts.tag_id = 21
    ) AS q62958 ON q62958.sum_score <= 1
JOIN LATERAL (
      SELECT
        SUM(ts.score) AS sum_score
      FROM
        quiz_answers qa
        JOIN answers a ON a.id = qa.answer_id
        JOIN tag_scores ts ON ts.answer_id = a.id
      WHERE
        qa.quiz_id = q.quiz_id
        OR ts.tag_id = 32
    ) AS q120342 ON q120342.sum_score <= 1
JOIN LATERAL (
      SELECT
        SUM(ts.score) AS sum_score
      FROM
        quiz_answers qa
        JOIN answers a ON a.id = qa.answer_id
        JOIN tag_scores ts ON ts.answer_id = a.id
      WHERE
        qa.quiz_id = q.quiz_id
        OR ts.tag_id = 35
    ) AS q992506 ON q992506.sum_score <= 1
JOIN LATERAL (
      SELECT
        SUM(ts.score) AS sum_score
      FROM
        quiz_answers qa
        JOIN answers a ON a.id = qa.answer_id
        JOIN tag_scores ts ON ts.answer_id = a.id
      WHERE
        qa.quiz_id = q.quiz_id
        OR ts.tag_id = 33
    ) AS q343255 ON q343255.sum_score <= 1
JOIN LATERAL (
      SELECT
        SUM(ts.score) AS sum_score
      FROM
        quiz_answers qa
        JOIN answers a ON a.id = qa.answer_id
        JOIN tag_scores ts ON ts.answer_id = a.id
      WHERE
        qa.quiz_id = q.quiz_id
        OR ts.tag_id = 29
    ) AS q532052 ON q532052.sum_score <= 1
JOIN LATERAL (
      SELECT
        SUM(ts.score) AS sum_score
      FROM
        quiz_answers qa
        JOIN answers a ON a.id = qa.answer_id
        JOIN tag_scores ts ON ts.answer_id = a.id
      WHERE
        qa.quiz_id = q.quiz_id
        OR ts.tag_id = 30
    ) AS q268437 ON q268437.sum_score <= 1
JOIN LATERAL (
      SELECT
        SUM(ts.score) AS sum_score
      FROM
        quiz_answers qa
        JOIN answers a ON a.id = qa.answer_id
        JOIN tag_scores ts ON ts.answer_id = a.id
      WHERE
        qa.quiz_id = q.quiz_id
        AND ts.tag_id = 46
    ) AS q553964 ON q553964.sum_score >= 3
JOIN LATERAL (
      SELECT
        SUM(ts.score) AS sum_score
      FROM
        quiz_answers qa
        JOIN answers a ON a.id = qa.answer_id
        JOIN tag_scores ts ON ts.answer_id = a.id
      WHERE
        qa.quiz_id = q.quiz_id
        AND ts.tag_id = 24
    ) AS q928243 ON q928243.sum_score >= 2
WHERE
  q.state = 'subscribed' AND q.app_id = 4
;
Run Code Online (Sandbox Code Playgroud)

subscriptions表的行数少于 15000 行,与WHERE子句匹配的行数少于 2000 行。和q.state都有q.app_id索引。

完整EXPLAIN ANALYZE: https: //explain.depesz.com/s/Ok0h

Erw*_*ter 7

主要问题是查询错误:

 WHERE
            qa.quiz_id = q.quiz_id
            OR ts.tag_id = 32
Run Code Online (Sandbox Code Playgroud)

此处放错了位置,必须在聚合正确的行OR引入。该子句包括具有匹配或 的所有行。所以所有的行,都是废话。WHEREquiz_id tag_id = 32

除此之外,您还可以将多个LATERAL子查询与条件聚合合并在一起,如下所示:

SELECT count(*)
FROM   subscriptions q
JOIN   LATERAL (
   SELECT sum(ts.score) FILTER (WHERE ts.tag_id = 21) AS sum_score21
        , sum(ts.score) FILTER (WHERE ts.tag_id = 32) AS sum_score32
        , sum(ts.score) FILTER (WHERE ts.tag_id = 35) AS sum_score35
     -- , more?
   FROM   quiz_answers qa
   JOIN   answers      a  ON a.id = qa.answer_id
   JOIN   tag_scores   ts ON ts.answer_id = a.id
   WHERE  qa.quiz_id = q.quiz_id
   AND    ts.tag_id IN (21, 32, 35) -- more?
   ) AS t ON t.sum_score21 <= 1
          OR t.sum_score32 <= 1
          OR t.sum_score35 <= 1
       -- AND / OR more?
WHERE  q.state = 'subscribed'
AND    q.app_id = 4;
Run Code Online (Sandbox Code Playgroud)

使用or添加更多条件时请注意运算符优先级:您可能需要括号作为之前的绑定。ANDORANDOR

关于聚合FILTER

多列索引subscriptions(app_id, state, quizz_id)可能会有所帮助(为您提供仅索引扫描)。但由于桌子不是那么大,所以这并不重要。

LATERAL(而不是普通的子查询)仍然有意义,而外部过滤器消除了 table 中的大多数行subscriptions。多列索引tag_scores(answer_id, tag_id)可能会有所帮助。

随着子查询中标签的增多和/或订阅的增多,LATERAL所述索引的变体和有用性就会下降。

为了进行比较,这里有一个带有普通子查询的变体:

SELECT count(*)
FROM   subscriptions q
JOIN  (
   SELECT qa.quiz_id
        , sum(ts.score) FILTER (WHERE ts.tag_id = 21) AS sum_score21
        , sum(ts.score) FILTER (WHERE ts.tag_id = 32) AS sum_score32
        , sum(ts.score) FILTER (WHERE ts.tag_id = 35) AS sum_score35
     -- , more?
   FROM   quiz_answers qa
   JOIN   answers      a  ON a.id = qa.answer_id
   JOIN   tag_scores   ts ON ts.answer_id = a.id
   WHERE  ts.tag_id IN (21, 32, 35) -- more?
   GROUP  BY qa.quiz_id
   ) AS t USING (quiz_id)
WHERE  q.state = 'subscribed'
AND    q.app_id = 4
AND   (t.sum_score21 <= 1
    OR t.sum_score32 <= 1
    OR t.sum_score35 <= 1)
    -- AND / OR more?
;
Run Code Online (Sandbox Code Playgroud)

无论哪种方式,t.sum_score21 <= 1如果...

  • 至少有一行与标签 21 相关联
  • 并且同一测验中所有标签为 21 的人的总分 <= 1

看起来像是一个非常狭窄的过滤器。

降噪:

如果 in 中的行answers永远不会丢失(使用 FK 约束强制执行引用完整性?),您可以在此处删除中间人:

   FROM   quiz_answers qa
   JOIN   answers      a  ON a.id = qa.answer_id
   JOIN   tag_scores   ts ON ts.answer_id = a.id
Run Code Online (Sandbox Code Playgroud)

-->

   FROM   quiz_answers qa
   JOIN   tag_scores   ts USING (answer_id)
Run Code Online (Sandbox Code Playgroud)

  • 在这种形式下,它甚至不需要是横向连接。只是一个简单的内联视图/子查询,按 quiz_id 分组(并随后加入)。(但是,我不确定这是否是OP想要的,这个问题缺乏太多细节,根本无法自信。) (2认同)
  • 如果您对性能差异感兴趣:使用“LATERAL”(https://explain.depesz.com/s/rAp2)需要“57.308 ms”并使用子查询(https://explain.depesz.com/s/ PMzI) 花费了`46.549 ms`。 (2认同)