如何防止 PostgreSQL 重写 OUTER JOIN 查询?

Dor*_* W. 3 postgresql join execution-plan explain

我的查询是:

SELECT  Acol1, Acol2, Bcol1, Bcol2, Ccol1, Ccol2
FROM    tableA LEFT JOIN
            (tableB FULL JOIN tableC ON (Bcol1 = Ccol1))
            ON (Acol1 = Bcol1)
Run Code Online (Sandbox Code Playgroud)

EXPLAIN ANALYZE给我:

                                                      QUERY PLAN                                                      
----------------------------------------------------------------------------------------------------------------------
 Hash Right Join  (cost=99.65..180.45 rows=1770 width=24) (actual time=0.043..0.103 rows=3 loops=1)
   Hash Cond: (tableb.bcol1 = tablea.acol1)
   ->  Hash Left Join  (cost=49.83..104.08 rows=1770 width=16) (actual time=0.011..0.062 rows=3 loops=1)
         Hash Cond: (tableb.bcol1 = tablec.ccol1)
         ->  Seq Scan on tableb  (cost=0.00..27.70 rows=1770 width=8) (actual time=0.001..0.002 rows=3 loops=1)
         ->  Hash  (cost=27.70..27.70 rows=1770 width=8) (actual time=0.004..0.004 rows=3 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 1kB
               ->  Seq Scan on tablec  (cost=0.00..27.70 rows=1770 width=8) (actual time=0.001..0.002 rows=3 loops=1)
   ->  Hash  (cost=27.70..27.70 rows=1770 width=8) (actual time=0.014..0.014 rows=3 loops=1)
         Buckets: 1024  Batches: 1  Memory Usage: 1kB
         ->  Seq Scan on tablea  (cost=0.00..27.70 rows=1770 width=8) (actual time=0.009..0.011 rows=3 loops=1)
 Total runtime: 0.151 ms
Run Code Online (Sandbox Code Playgroud)

Postgres 将和之间的完整外连接更改为外连接,因为稍后左外连接无论如何都会消除空值。它相当于原始查询。tableBtableCtableA

然而,我正在破解 Postgres 来实现我的连接枚举相关算法并进行实验。我不希望 Postgres 将完整外连接更改为左外连接。有办法这样做吗?

Erw*_*ter 5

您可以根据您的目的引入优化障碍

前言

对于这个问题的目的来说,普通EXPLAIN(不带)就足够了。ANALYZE表达式周围的括号ON只是噪音。添加表别名是为了明确。

我们确实看到“Full Join”表示完全连接本身:

SELECT * FROM tableB b FULL JOIN tableC c ON b.Bcol1 = c.Ccol1;
Run Code Online (Sandbox Code Playgroud)

我们可以用子查询重写完整连接:

SELECT a.Acol1, a.Acol2, d.Bcol1, d.Bcol2, d.Ccol1, d.Ccol2
FROM   tableA a
LEFT   JOIN (
   SELECT *  -- sort out conflicting names with aliases
   FROM   tableB b FULL JOIN tableC c ON b.Bcol1 = c.Ccol1
   ) d ON a.Acol1 = d.Bcol1;
Run Code Online (Sandbox Code Playgroud)

您必须在子查询中整理出与别名冲突的名称 - 但话又说回来,您在任何情况下都需要在外部表中对SELECT基础表中同名的多个列执行此操作。

查询仍然作为一个整体进行优化,因为子查询不会施加优化障碍。您仍然会看到“Left Join”“Right Join”。但是,我们可以扩展此形式来得出解决方案:

解决方案1. OFFSET 0hack(未记录)

EXPLAIN
SELECT a.Acol1, a.Acol2, d.Bcol1, d.Bcol2, d.Ccol1, d.Ccol2
FROM   tableA a
LEFT   JOIN (
   SELECT *  -- you'll have to sort out conflicting names with aliases
   FROM   tableB b FULL JOIN tableC c ON b.Bcol1 = c.Ccol1
   OFFSET 0  -- undocumented hack
   ) d ON a.Acol1 = d.Bcol1;
Run Code Online (Sandbox Code Playgroud)

您将看到“完全加入”

为什么?一旦子查询使用OFFSET子句,查询规划器/优化器就会单独规划子查询。OFFSET 0是逻辑噪声,但 Postgres 仍然考虑该子句,该子句使其成为有效实现子查询的查询提示。(尽管 Postgres 不支持查询提示。)这是一个备受争议的问题。有关的:

解决方案 2. 使用 CTE(已记录)

EXPLAIN
WITH cte AS MATERIALIZED ( -- requires "MATERIALIZED" in Postgres 12 or later!
   SELECT *  -- you'll have to sort out conflicting names with aliases
   FROM   tableB b FULL JOIN tableC c ON b.Bcol1 = c.Ccol1
   ) 
SELECT a.Acol1, a.Acol2, d.Bcol1, d.Bcol2, d.Ccol1, d.Ccol2
FROM   tableA a
LEFT   JOIN cte d ON a.Acol1 = d.Bcol1;
Run Code Online (Sandbox Code Playgroud)

您还会看到“完全加入”

Postgres 11 的手册(之前AS MATERIALIZED添加):

查询的一个有用属性WITH是,每次执行父查询时仅对它们求值一次,即使父查询或同级查询多次引用它们WITH。因此,可以将多个地方需要的昂贵计算放在WITH查询中以避免冗余工作。另一种可能的应用是防止对带有副作用的函数进行不必要的多重评估。然而,问题的另一面是,与普通子查询相比,优化器不太能够将父查询的限制推送到WITH查询中。查询WITH通常会按写入的方式进行评估,而不抑制父查询随后可能丢弃的行。(但是,如上所述,如果对查询的引用仅需要有限数量的行,则评估可能会提前停止。)

Postgres 12开始,手册添加了:

但是,如果WITH查询是非递归且无副作用的(即,它不包含SELECT易失性函数),则可以将其折叠到父查询中,从而允许两个查询级别的联合优化。默认情况下,如果父查询WITH仅引用该查询一次,则会发生这种情况,但如果父查询WITH多次引用该查询,则不会发生这种情况。您可以通过指定 MATERIALIZED强制单独计算查询WITH或指定NOT MATERIALIZED强制将其合并到父查询中来覆盖该决定。后一种选择存在重复计算查询的风险 ,但如果查询的每次使用仅需要查询完整输出的一小部分,则WITH它仍然可以节省净成本 。WITHWITH

大胆强调我的。

db<>在这里
摆弄旧的sqlfiddle