在不确定性下管理 CTE 执行顺序

Jim*_*Bob 5 postgresql cte unique-constraint

出于性能原因,我正在编写一个大型的多步 CTE 。

在一个查询中,数据必须从一个表移动到另一个表,但移动的行数是不确定的,可能为零。

在后续表中,删除来自前一个查询的源,但必须在完成前一个查询之后。

最后,在上面的第二个查询完成后,必须写入行来代替已删除的行。

在前两个查询中,我使用 RETURNING 来强制执行顺序。

在第二个查询中,我确定第一个查询是由这个子查询完成的

(SELECT COUNT(*) FROM first_query) >= 0
Run Code Online (Sandbox Code Playgroud)

在第三个查询中,我确定第二个查询是由这个子查询完成的

SELECT EXISTS (SELECT 1 FROM second_query)
Run Code Online (Sandbox Code Playgroud)

确定第一个查询已完成的子查询是否正确?

用于确定必须返回行的第二个查询已完成最佳准确性、精度和性能的子查询?

使用上述子查询来强制执行顺序会导致重复的键值违规。

查询小节

WITH copy_to_other_table AS (
    INSERT INTO other_table (column_a, column_b) 
        SELECT column_a, column_b 
            FROM main_table
        WHERE column_a = $1::bigint
        RETURNING *
),
main_table_deleted AS (
    DELETE FROM main_table WHERE column_a = $1::bigint 
        AND (SELECT COUNT(*) FROM copy_to_other_table) >= 0         
        RETURNING *
)
INSERT INTO main_table (column_a, column_b) 
        SELECT column_a, column_b 
            FROM another_table WHERE column_a = $1::bigint 
             AND EXISTS (SELECT 1 FROM main_table_deleted) 
Run Code Online (Sandbox Code Playgroud)

这是违反唯一约束的最终查询。

ype*_*eᵀᴹ 7

这应该可行,但我不确定这是否是效率方面的最佳选择:

WITH copy_to_other_table AS (
    INSERT INTO other_table (column_a, column_b) 
        SELECT column_a, column_b 
            FROM main_table
        WHERE column_a = 1
),
main_table_deleted AS (
    DELETE 
    FROM main_table 
    WHERE column_a = 1 
      AND NOT EXISTS (SELECT 1 FROM another_table 
                      WHERE column_a = 1
                        AND column_b = main_table.column_b)               

)
INSERT INTO main_table (column_a, column_b) 
        SELECT column_a, column_b 
            FROM another_table WHERE column_a = 1
        EXCEPT 
        SELECT column_a, column_b 
            FROM main_table WHERE column_a = 1 ;
Run Code Online (Sandbox Code Playgroud)

但是原始查询有什么问题?

  • 首先,(SELECT COUNT(*) ...) >= 0完全是多余的。一个count总总是返回0以上的值,这样的条件是总是如此。

  • 其次,根本不需要有任何条件,因为main您想要复制到other表中的所有行,您还希望它们从main. 在删除它们之前没有理由“检查”它们是否已复制。所有 3 个子查询(2 个 CTE 和主查询)将“看到”具有相同行数和数据的相同表。

  • 第三部分比较棘手。乍一看,第二(删除)和第三(插入)部分之间的“交互”似乎不需要检查。两者都在同一个表中,但如果在主查询之前执行第二个 cte,那么一切都应该很好。
    唉,执行顺序不连续。来自Postgres 文档

    WITH 中的数据修改语句只执行一次,并且总是完成,与主查询是否读取所有(或实际上任何)其输出无关。请注意,这与 WITH 中的 SELECT 规则不同:如上一节所述,仅在主查询需要其输出时才会执行 SELECT。

    WITH 中的子语句彼此并发执行,并与主查询同时执行。因此,在 WITH 中使用数据修改语句时,指定更新实际发生的顺序是不可预测的。所有语句都使用相同的快照执行(参见第 13 章),因此它们无法“看到”彼此对目标表的影响。这减轻了行更新实际顺序的不可预测性的影响,并且意味着RETURNING数据是在不同WITH子语句和主查询之间传达更改的唯一方式。

  • 作为测试,您可以更改 3 个子语句的顺序。结果将是相同的

    WITH main_table_deleted AS (
        DELETE 
        FROM main_table 
        WHERE column_a = 1 
          AND NOT EXISTS (SELECT 1 FROM another_table 
                          WHERE column_a = 1
                            AND column_b = main_table.column_b)               
    
    ),
     copy_to_other_table AS (
        INSERT INTO other_table (column_a, column_b) 
            SELECT column_a, column_b 
                FROM main_table
            WHERE column_a = 1
    )
    INSERT INTO main_table (column_a, column_b) 
            SELECT column_a, column_b 
                FROM another_table WHERE column_a = 1
            EXCEPT 
            SELECT column_a, column_b 
                FROM main_table WHERE column_a = 1 ;
    
    Run Code Online (Sandbox Code Playgroud)
  • 相关问题是何时检查唯一约束。我不是 100% 确定这些检查与 CTE 相结合的细节,但应在语句末尾检查唯一约束。似乎每个修改 cte 也同时检查它们。
    注意:老实说,这种行为似乎是一个错误 - 除非我错过了文档中的某些内容。)

  • 关于您的最后一个问题,将隔离级别设置为SERIALIZABLE不会解决问题,因为整个操作是一个语句,包含 3 个子语句。但是,您可以将操作拆分为 2 或 3 个语句,然后将它们一个接一个地执行。所以,第二个会看到第一个和第三个的结果,前两个的结果。(如果这样做,请将 2 或 3 个语句放在事务中,以将操作与其他正在执行的语句隔离开来。)


另一种方法 - 更接近您的原始查询 - 将使用该RETURNING子句以特定顺序强制执行子语句,即第二个之后的第三个(第一个可以保持RETURNING不变并同时执行)。在SQLFIdle-3 中测试:

WITH copy_to_other_table AS (
    INSERT INTO other_table (column_a, column_b) 
        SELECT column_a, column_b 
            FROM main_table
        WHERE column_a = 1
),
main_table_deleted AS (
    DELETE FROM main_table WHERE column_a = 1        
        RETURNING *
)
INSERT INTO main_table (column_a, column_b) 
        SELECT column_a, column_b 
            FROM another_table WHERE column_a = 1
        EXCEPT 
        (TABLE main_table_deleted EXCEPT TABLE another_table) ;
Run Code Online (Sandbox Code Playgroud)

或者通过首先执行删除(第二个)cte,然后RETURNIING在其他两个中使用其输出来稍微改进:

WITH main_table_deleted AS (
    DELETE FROM main_table WHERE column_a = 1        
        RETURNING *
),
copy_to_other_table AS (
    INSERT INTO other_table (column_a, column_b) 
        TABLE  main_table_deleted
)
INSERT INTO main_table (column_a, column_b) 
        SELECT column_a, column_b 
            FROM another_table WHERE column_a = 1
        EXCEPT 
        (TABLE main_table_deleted EXCEPT TABLE another_table) ;
Run Code Online (Sandbox Code Playgroud)


Jim*_*Bob 6

这似乎是问题的原因,来自 ypercube 引用的同一页面:

不支持尝试在单个语句中更新同一行两次。只发生了一个修改,但要可靠地预测哪一个并不容易(有时是不可能的)。这也适用于删除已在同一语句中更新的行:仅执行更新。因此,您通常应该避免尝试在单个语句中两次修改单个行。特别要避免编写可能影响由主语句或同级子语句更改的相同行的 WITH 子语句。这种声明的影响是不可预测的。

约束

在测试更复杂的查询之后,似乎在子语句级别应用了唯一约束,而在语句级别应用了外键。