在不使用CTE的情况下,是否存在逻辑等效且有效的查询版本?

时间:2013-07-10 23:20:25

标签: postgresql common-table-expression

我对postgresql 9.2系统有一个查询,它的正常形式大约需要20秒,但使用CTE时只需要120ms。

为简洁起见,我简化了两个查询。

这是正常形式(大约需要20秒):

SELECT *
FROM tableA
WHERE (columna = 1 OR columnb = 2) AND
    atype = 35 AND
    aid IN (1, 2, 3)
ORDER BY modified_at DESC
LIMIT 25;

以下是此查询的说明:http://explain.depesz.com/s/2v8

CTE表格(约120ms):

WITH raw AS (
    SELECT *
    FROM tableA
    WHERE (columna = 1 OR columnb = 2) AND
        atype = 35 AND
        aid IN (1, 2, 3)
)
SELECT *
FROM raw
ORDER BY modified_at DESC
LIMIT 25;

以下是CTE的解释:http://explain.depesz.com/s/uxy

只需将ORDER BY移动到查询的外部,就可以将成本降低99%。

我有两个问题:1)是否有一种方法可以构建第一个查询而不使用CTE,从而在逻辑上等效更高性能2)这种性能差异说明了规划者如何确定如何获取数据?

关于上述问题,是否有其他统计信息或其他计划程序提示有助于提高第一个查询的效果?


编辑:取消限制还会导致查询使用堆扫描而不是向后索引扫描。没有LIMIT查询在40ms内完成。

看到LIMIT我尝试使用LIMIT 1LIMIT 2等的效果后,使用LIMIT 1和10s + {{1}时,查询执行时间不到100毫秒}> 1。

在考虑了这个之后,问题2归结为为什么规划器在一种情况下使用索引扫描而在另一种逻辑上等效的情况下使用位图堆扫描+排序?在两种情况下,我如何“帮助”规划者使用有效的计划?


更新: 我接受了克雷格的答案,因为它是最全面和最有帮助的。我最终解决问题的方法是使用一个实际上相当的查询,虽然在逻辑上不等同。问题的根源是在modified_at上向后索引扫描索引。为了告知规划人员这不是一个好主意,我添加了LIMIT形式的谓词。这包括应用程序的足够数据,但阻止了规划人员沿着向后索引扫描路径向下移动。

这是一个影响较小的解决方案,无需使用子查询或CTE重写查询。因人而异。

2 个答案:

答案 0 :(得分:10)

以下是这种情况发生的原因,以下说明至少为9.3(如果您正在阅读此版本并在较新版本上,请检查以确保它没有更改):

PostgreSQL不会跨CTE边界进行优化。每个CTE子句都是独立运行的,其结果由查询的其他部分使用。所以像这样的查询:

WITH blah AS (
    SELECT * FROM some_table
)
SELECT *
FROM blah
WHERE id = 4;

将导致完整的内部查询被执行。 PostgreSQL不会将id = 4限定条件“压低”到内部查询中。在这方面,CTE是“优化围栏”,可以是好的也可以是坏的;它允许您在需要时覆盖计划程序,但如果您确实需要下推,则会阻止您将CTE用作深层嵌套FROM子查询链的简单语法清理。

如果您将上述内容改为:

SELECT *
FROM (SELECT * FROM some_table) AS blah
WHERE id = 4;

使用FROM中的子查询而不是CTE,Pg会将质量下降到子查询中,并且它们都会很好地运行。

正如您所发现的,当查询计划程序做出糟糕的决定时,这对您有益。在您的情况下,对于表的向后索引扫描似乎非常更昂贵的位图或索引扫描两个较小的索引后跟一个过滤器和排序,但规划人员认为它不会所以它计划查询扫描索引。

当您使用CTE时,它无法将ORDER BY推送到内部查询中,因此您将覆盖其计划并强制它使用它认为的劣质执行计划 - 但是转向好多了。

有一个讨厌的解决方法,可以用于这些称为OFFSET 0黑客的情况,但是如果你想办法让计划者做正确的事情,你应该只使用它 - 如果你必须使用它,请将其归结为一个独立的测试用例,并将其作为可能的查询计划程序错误报告给PostgreSQL邮件列表。

相反,我建议首先查看为什么规划师做出了错误的决定。

第一个候选人是统计数据/估计问题,当我们查看有问题的查询计划时,确实存在3500个错误估计预期结果行的因素。这很重要,但并不是不可能的大,尽管更有意思的是你实际上只有一行计划器期望一个非平凡的行集。但这对我们没有多大帮助;如果行数低于预期,则意味着选择使用索引是更好的选择。

主要问题似乎是使用更小,更具选择性的索引sierra_kilopapa_lima,因为它看到ORDER BY并认为它会节省更多时间进行反向索引扫描并避免排序,而不是实际操作。鉴于只有一个匹配行可以排序,这是有道理的!如果它获得了预期的3500行,那么避免排序可能更有意义,尽管这仍然只是在内存中排序的相当小的行集。

您是否设置了enable_seqscan等参数?如果你这样做,请取消它们;它们仅用于测试,完全不适合生产使用。如果你没有使用enable_参数,我认为值得在PostgreSQL邮件列表pgsql-perform上提出这个问题。然而,匿名计划使这有点困难,特别是因为没有保证一个计划中的标识符引用另一个计划中的相同对象,并且它们与您在查询中写入的问题不匹配。在邮件列表上询问之前,你需要制作一个正确的手工完成版本,其中所有内容都匹配。

您很有可能需要为任何人提供真实值。如果您不想在公共邮件列表上there's another option available执行此操作。 (我应该注意,根据我的个人资料,我为其中一人工作)。

答案 1 :(得分:2)

只是在黑暗中拍摄,但如果你运行

会发生什么
SELECT *
FROM (
    SELECT *
    FROM tableA
    WHERE (columna = 1 OR columnb = 2) AND
        atype = 35 AND
        aid IN (1, 2, 3)
) AS x
ORDER BY modified_at DESC
LIMIT 25;