CTEReadme

来自 PostgreSQL wiki
跳转至导航跳转至搜索

使用方法

定义

WITH 子句允许定义在一个相同的 SELECT 语句中有效的表表达式。表表达式被称为“通用表表达式”(CTE)。CTE 的用例与 VIEW 类似,但它比 VIEW 更方便。与 VIEW 不同,您无需事先定义 CTE。

示例

CTE 允许使用递归查询。若要使用递归查询,您需要添加 RECURSIVE 关键字。以下是WITH RECURSIVE子句用法的一个示例。表“部门”以邻接列表的形式表示一个组织的结构。

CREATE TABLE department (
    id INTEGER PRIMARY KEY,  -- department ID
    parent_department INTEGER REFERENCES department, -- upper department ID
    name TEXT -- department name
);

INSERT INTO department (id, parent_department, "name")
VALUES
     (0, NULL, 'ROOT'),
     (1, 0, 'A'),
     (2, 1, 'B'),
     (3, 2, 'C'),
     (4, 2, 'D'),
     (5, 0, 'E'),
     (6, 4, 'F'),
     (7, 5, 'G');

-- department structure represented here is as follows:
--
-- ROOT-+->A-+->B-+->C
--      |         |
--      |         +->D-+->F
--      +->E-+->G

若要提取 A 下的所有部门,您可以使用以下递归查询

WITH RECURSIVE subdepartment AS
(
    -- non-recursive term
    SELECT * FROM department WHERE name = 'A'

    UNION ALL

    -- recursive term
    SELECT d.*
    FROM
        department AS d
    JOIN
        subdepartment AS sd
        ON (d.parent_department = sd.id)
)
SELECT *
FROM subdepartment
ORDER BY name;

递归工作原理

上述查询的含义可以解释如下。

给定三个表,中间表(IntermediateTable);工作表(WorkTable);结果表(ResultTable)。

1) 初始化

执行非递归词(SELECT * FROM department WHERE name = 'A')并将结果分配给 ResultTable 和 WorkTable。

ResultTable = WorkTable = ('A')

使 IntermediateTable 为空。

IntermediateTable = ()

2) 执行递归查询

用 WorkTable 替换子部门,并执行递归词

SELECT d.*
FROM
    department AS d
JOIN
    WT AS sd
    ON d.parent_department = sd.id

接下来,将查询的结果分配给 IntermediateTable。将 IntermediateTable 添加到 ResultTable,并将 IntermediateTable 替换为 WorkTable。使 IntermediateTable 为空。

3) 检查递归是否结束

如果 IntermediateTable 在 2) 中为空,则递归执行结束。返回 ResultTable。

否则,转到 2)。

“subdepartment” 是一款 CTE,其中包含递归表达式,因为它在其定义中引用自身。在上面的查询中,首先,评估非递归项。然后评估递归项并将结果不断添加到前一个结果集中,直到没有更多要处理的数据。最后,执行最后一个 SELECT,并且从结果集中提取数据。

实施

分析递归查询

要表示 CTE,将新结构成员“withClause”添加到 SelectStmt 结构,用于存储 SELECT 语句的已分析信息。 withClause 的定义如下:

/*
 * WithClause -
 *     reprensentation of WITH clause
 */
typedef struct WithClause
{
    NodeTag      type;            /* T_WithClause */
    List        *subquery;        /* subquery list (list of RangeSubselect) */
    bool         recursive;       /* true = WITH RECURSIVE */
} WithClause;

如果未给出 RECURSIVE 关键字,则 CTE 将转换为 WithClause.subquery,并将 WithClause.recursive 设置为 false。

如果给出了 RECURSIVE 关键字,则存储在 WithClause.subquery 中的原始分析树表示子查询,该子查询是 UNION ALL 的非递归项和递归项。不允许在非递归项和递归项之间使用 UNION ALL 以外的其他集合操作。 WithClause.subquery.subquery.larg 表示“SELECT * FROM department WHERE name = 'A'”,而 WithClause.subquery.subquery.rarg 在上面的示例中表示“SELECT d.* FROM department AS d JOIN subdepartment AS sd ON d.parent_department = sd.id”。

分析递归查询

要分析 CTE,我们将以下新成员“p_ctenamespace”、“p_recursive_namespace”和“p_in_with_clause”添加到 ParseState 结构中。

p_ctenamespace 是非递归 CTE 的命名空间和 RangeSubselects 的列表。以下是 ParseState 的注释摘录:

* [1] Note that p_ctenamespace is a namespace for "relations" but distinct
*     from p_relnamespace. p_ctenamespace is a list of relations that can be
*     referred to in a FROM or JOIN clause (in addition to normal tables and
*     views). p_relnamespace is the list of relations which already have been
*     listed in such clauses and therefore can be referred to in qualified
*     variable references. Also, note that p_ctenamespace is a list of
*     RangeSubselects, not a list of range table entries.

p_recursive_namespace 是递归 CTE 的命名空间,并且是 RangeRecursive 的列表。 RangeRecursive 是新引入的结构

/*
 * RangeRecursive - recursive call appearing in a FROM clause
 */
typedef struct RangeRecursive
{
    NodeTag        type;
    Node       *subquery;        /* the untransformed sub-select clause */
    Alias       *alias;            /* table alias & optional column aliases */
    List       *coldeflist;        /* list of ColumnDef nodes to describe result */
    Node        *non_recursive_term;    /* analyze result of non recusive term */
    bool        recursive;        /* true if recursive query */
} RangeRecursive;

ParseState.p_rtable 是 RangeTblEntry 的列表。为了处理 CTE,将新成员“self_reference”和“non_recursive_query”添加到 RangeTblEntry 中。

/*
 * Fields valid for a RECURSIVE CTE (else NIL)
 */
bool        self_reference; /* delay analyzing SelectStmt */
Query        *non_recursive_query; /* non-recursive term */

如果使用“RECURSIVE”关键字的 CTE 实际上不是递归的,则将“recursive”设置为 false,并将 CTE 视为子查询并将其添加到 ParseState.p_rtable 中。

如果 CTE 引用自身,分析将被延迟,将 self_reference 设置为 true,并将 non_recursive_term 填入。

可能有多个 CTE 元素(查询名称)。

WITH RECURSIVE x AS (SELECT * from y),
               y AS (SELECT * FROM pg_class)
SELECT * FROM x;

在这种情况下,我们应该先分析 y,因为 x 依赖于 y。要查找此类依赖项信息,请执行拓扑排序。有关更多详细信息,请参阅 makeDependencyGraph()。

接下来,我们检查递归查询是否遵循 SQL 标准。标准不允许以下示例查询。

- 非递归项和递归项必须与 UNION ALL 一起使用

WITH RECURSIVE x(n) AS (SELECT 1 UNION SELECT n+1 FROM x)
    SELECT * FROM x;
WITH RECURSIVE x(n) AS (SELECT 1 INTERSECT SELECT n+1 FROM x)
    SELECT * FROM x;

- 非递归项不存在

WITH RECURSIVE x(n) AS (SELECT n FROM x)
    SELECT * FROM x;

- 递归项必须位于左侧

WITH RECURSIVE x(n) AS (SELECT n FROM x UNION ALL SELECT 1)
    SELECT * FROM x;

- 不允许 LEFT JOIN 位于递归项的右侧

 WITH RECURSIVE x(n) AS (SELECT a FROM y WHERE a = 1
    UNION ALL
    SELECT x.n+1 FROM y LEFT JOIN x ON x.n = y.a WHERE n < 10)
SELECT * FROM x;

- 不允许 RIGHT JOIN 位于递归项的左侧

WITH RECURSIVE x(n) AS (SELECT a FROM y WHERE a = 1    
    UNION ALL
    SELECT x.n+1 FROM x RIGHT JOIN y ON x.n = y.a WHERE n < 10)
SELECT * FROM x;

‐ 在递归术语中不允许 FULL JOIN

WITH RECURSIVE x(n) AS (SELECT a FROM y WHERE a = 1
    UNION ALL
    SELECT x.n+1 FROM x FULL JOIN y ON x.n = y.a WHERE n < 10)
SELECT * FROM x;

‐ WHERE 子句具有在递归术语中使用递归名称的子查询

 term not allowed
WITH RECURSIVE x(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM x
                          WHERE n IN (SELECT * FROM x))
  SELECT * FROM x;

‐ 在递归术语中不允许 GROUP BY、HAVING

WITH RECURSIVE x(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM x GROUP BY n)
  SELECT * FROM x;

‐ 在递归术语中不允许聚合函数

WITH RECURSIVE x(n) AS (SELECT 1 UNION ALL SELECT count(*) FROM x)
  SELECT * FROM x;

除上述限制外,我们还增加了更多限制

‐ 在递归查询中不允许 ORDER BY

WITH RECURSIVE x(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM x ORDER BY 1)
  SELECT * FROM x;

‐ 在递归查询中不允许 LIMIT OFFSET

WITH RECURSIVE x(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM x LIMIT 10 OFFSET 1)
  SELECT * FROM x;

‐ 在递归查询中不允许 FOR UPDATE

WITH RECURSIVE x(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM x FOR UPDATE)
  SELECT * FROM x;

‐ 在递归术语中不允许带有使用递归名称的子查询的目标列表

WITH RECURSIVE x(id) AS (values (1)
    UNION ALL
    SELECT (SELECT * FROM x) FROM x WHERE id < 5
) SELECT * FROM x;

‐ 不支持相互递归调用

WITH RECURSIVE
  x (id) AS (SELECT 1 UNION ALL SELECT id+1 FROM y WHERE id < 5),
  y (id) AS (SELECT 1 UNION ALL SELECT id+1 FROM x WHERE id < 5)
SELECT * FROM x;

请注意,SQL Server 和 Firebird 也不允许相互递归查询。Oracle 不支持 WITH RECURSIVE,但有自己的 CONNECT BY 语法。

在 WITH RECURSIVE 子句中定义的表被识别为解析树节点中 RangeTblEntry 中 RTEKind 之外的 RTE_RECURSIVE。将结构类型为 RangeRecursive 的名称空间 “p_recursive_namespace” 添加到 ParseState 结构中。

typedef struct RangeRecursive
{
    NodeTag        type;
    Node       *subquery; /* whole subquery */
    Alias      *alias;            /* table alias & optional column aliases */
    List       *coldeflist;        /* list of ColumnDef nodes */
    Node       *non_recursive_term; /* analyze result of non recursive term */
    bool       recursive; /* true if recursive */
} RangeRecursive;

non_recursive_term 保留对非递归术语进行分析的结果。使用这个推断出递归查询的类型。

改写器

应用于改写器的更改很小。fireRIRrules() 递归展开子查询。如果是递归查询,我们只需做同样的事情。

生成计划

添加了新计划节点“递归”和“递归扫描”。递归计划是用于执行 WITH RECURSIVE 查询的顶级计划。在下面的示例中,Recursion 计划表示“SELECT * FROM subdepartment”部分的执行计划。递归计划始终具有“追加”子计划,该子计划表示非递归部分和递归部分的“UNION ALL”的执行计划。非递归部分的计划没有什么特别之处,并且是根据非递归查询部分生成的普通计划。递归扫描表示递归部分的计划。

以下是 EXPLAIN 输出的示例。

EXPLAIN WITH RECURSIVE subdepartment AS
(
    -- non recursive term
    SELECT * FROM department WHERE name = 'A'

    UNION ALL

    -- recursive term
    SELECT d.* FROM department AS d, subdepartment AS sd
        WHERE d.parent_department = sd.id
)
SELECT * FROM subdepartment;

                                    QUERY PLAN                                     
-----------------------------------------------------------------------------------
 Recursion on subdepartment  (cost=0.00..50.76 rows=12 width=40)
   ->  Append  (cost=0.00..50.64 rows=12 width=40)
         ->  Seq Scan on department  (cost=0.00..24.50 rows=6 width=40)
               Filter: (name = 'A'::text)
         ->  Hash Join  (cost=0.01..26.02 rows=6 width=40)
               Hash Cond: (d.parent_department = sd.id)
               ->  Seq Scan on department d  (cost=0.00..21.60 rows=1160 width=40)
               ->  Hash  (cost=0.00..0.00 rows=1 width=4)
                     ->  Recursive Scan on sd  (cost=0.00..0.00 rows=1 width=4)

表示递归计划的结构如下

typedef struct Recursion
{
    Scan    scan;
    Plan    *subplan;
    List    *subrtable;        /* temporary workspace for planner */
} Recursion;

表示 RecursiveScan 计划的结构如下

typedef struct RecursiveScan
{
    Scan        scan;
} RecursiveScan;


执行器

为了执行有关 CTE 的计划,我们添加了新成员“es_tuplestorestate”、“es_rscan_tupledesc”和“es_disallow_tuplestore”。es_tuplestorestate 用于记忆工作表 (WT) 的 TupleStoreState。es_rscan_tupledesc 用于记忆 CTE 返回的数据类型的类型信息。实际上未使用 es_disallow_tuplestore。

为了管理递归查询执行状态,添加了以下结构。

typedef struct RecursionState
{
    ScanState    ss;
    PlanState  *subplan;        /* plan for non recursive query */
    bool recursive_empty; /* if recursive term does exist, true */
    Tuplestorestate *intermediate_tuplestorestate; /* intermediate table (IT) */
    Tuplestorestate *working_table;        /* working table (WT) */
    bool init_done; /* initialization flag */
} RecursionState;

typedef struct RecursiveScanState
{
    ScanState    ss;
    TupleDesc    tupdesc;
    Tuplestorestate **working_table;    /* working table */
} RecursiveScanState;

初始化递归计划

这是由 ExecInitRecursion() 完成的。将创建工作表 (WT) 并将其指针存储在 RecursionState.working_table 中。将创建中间表 (IT) 并将其指针存储在 RecursionState.intermediate_tuplestorestate 中。

在初始化中,必须使用上面创建的 WT,并将指向 WT 的指针存储在主执行器状态 (Estate) 中的 es_tuplestorestate 中。

扫描类型信息取自非递归查询(保存于 RecursionState.subquery)并存储在 RecursionState.ss 和 Estate/es_rscan_tupledesc 中。

执行递归计划

递归计划由 ExecRecursion() 执行。主力是 RecursionNext()。首先执行非递归术语的计划,并将结果存储在 WT 中。然后执行递归术语的计划,该术语是递归计划的子计划。如果计划返回一行或多行,则将它们存储在 IT 中。交换 IT 和 WT 然后重新创建 IT。如果没有返回行,则结束递归且 ExecRecursion() 返回 NULL。

初始化 RecursiveScan 计划

这与 RecursiveScan 的初始化非常相似,但扫描类型信息取自 ExecInitRecursion() 初始化的 Estate.es_rscan_tupledesc。

执行 RecursiveScan 计划

Recursive 扫描由 ExecRecursiveScan() 执行。主力是 RecursivescanNext()。它只返回存储在 WT 中的元组。工作表存储在 Estate 结构中。

执行器中的其他更改

  1. 哈希连接计划并不总是在哈希连接结束前重新创建哈希。如果哈希连接的子计划是 RecursiveScan,则需要重新创建哈希。为了解决此问题,PlanState 结构添加了“has_recursivescan”,当上层计划的子计划是 RecursiveScan 时置为 true,并且重新创建哈希。
  2. 处理递归时执行 exec_append_initialize_next()。该函数现为全局函数。

限制

  1. SEARCH 和 CYCLE 子句尚未实现。
  2. 不允许相互递归。
  3. 只有 UNION ALL 的最后一个 SELECT 可以包含递归名称。
  4. 递归和 RecursiveScan 计划的开销始终为 0。

CTE 的更多乐趣

人们越来越多地将 CTE 用于 SQL 表达式。已启动 CTE 奖金页面 来鼓励人们使用它。还有一些依赖它们的 代码片段