PostgreSQL: WITH Queries (Common Table Expressions)

WITH 允许在 SELECT 语句中定义"表"的表达式,这个"表"的表达式称之为"公共表表达式(Common Table Expression)",简称 CTE,仅在该 SELECT 语句范围内有效。CTE 的作用和临时表类似,CTE 的使用和 VIEW(视图) 类似,区别在于 CTE 不需要提前定义,VIEW 需要提前定义。

SELETCT IN WITH

先来看一个例子:

WITH regional_sales AS (
        SELECT region, SUM(amount) AS total_sales
        FROM orders
        GROUP BY region
     ), top_regions AS (
        SELECT region
        FROM regional_sales
        WHERE total_sales > (SELECT SUM(total_sales)/10 FROM regional_sales)
     )
SELECT region,
       product,
       SUM(quantity) AS product_units,
       SUM(amount) AS product_sales
FROM orders
WHERE region IN (SELECT region FROM top_regions)
GROUP BY region, product;

该例中,使用 WITH 定义了两个 CTE, 对应两张表:regional_sales 和 top_regions ,其结果集就是紧随其后的SELECT:

SELECT region, SUM(amount) AS total_sales FROM orders GROUP BY region;  -- regional_sales 结果集
SELECT region FROM regional_sales WHERE total_sales > (SELECT SUM(total_sales)/10 FROM regional_sales); -- top_regions 结果集

SQL含义就不多解释了,只是想展示 WITH 语法基本格式与用法。需要明确一点 "这是一个SQL语句" 。

再来看一个例子:

WITH RECURSIVE t(n) AS (
    VALUES (1)
    UNION ALL
    SELECT n+1 FROM t WHERE n < 100
)
SELECT sum(n) FROM t;

这个例子是用 CTE 配合 RECURSIVE(递归),对 1 ~ 100 自然数求和。

举这个例子目的不在于递归,而是想说明定义CTE的时候,可以采用 "t(n) AS (...)" 形式,t 是表名,n 是自定义列名,可定义多列,如: t(n,m) , t(a,b,c) 。但需注意的是,定义的列数必须和 AS 后面的结果集保持一致,数据类型无须定义,由结果集匹配。

WITH RECURSIVE

WITH 配合 RECURSIVE 关键字,可以递归查询生成 CTE 结果集,请看下面的例子:

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

创建 department 表,表中数据是一对多的父子关系,呈现树结构。假如要获取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;

重点就是解释一下该语句的递归(RECURSIVE)流程,仅仅是逻辑理解该递归语句,具体底层执行原理不清楚:

首先假设两张临时表:工作表(WorkTable),结果表(ResultTable)。

第一步:执行非递归部分(non-recursive term),即:

-- non-recursive term
SELECT * FROM department WHERE name = 'A'

  返回结果集同时放在 WITH 定义的 subdepartment 和 ResultTable 中。

第二步执行递归部分(recursive term),即

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

  执行完后得到临时结果集,放入 WorkTable 中;

  WorkTable 若不为空,用 WorkTable 结果集替换 subdepartment 结果集,同时把 WorkTable 结果集加入到 ResultTable 结果集中。然后清空 WorkTable,回到第二步继续执行。

  WorkTable 若为空,跳出循环。

第三步:递归结束,跳出循环后,ResultTable 就保存着最终的结果集,用 ResultTable 结果集替换 subdepartment 结果集。

WITH RECURSIVE 死循环

WITH RECURSIVE t(n) AS (
    SELECT 1
    UNION ALL
    SELECT n+1 FROM t
)
SELECT n FROM t LIMIT 100;

这个 WITH 中,递归是个死循环,在与 WITH 平级的 SELECT 上面加 "LIMIT 100" 的限制,可以避免死循环。PostgreSQL原理是:一旦递归出来的结果集数据,能够满足该 SELECT 的查询要求,就会终止递归,不再做无谓的查询。

注意:使用 LIMIT 避免递归死循环,隐藏着一个重要的前提,就是递归出来的结果集要能够满足使用要求!什么意思呢?就拿这个例子来说,假设在外层 SELECT 语句中再加一个 ORDER BY 条件,想想这个死循环能被 LIMIT 破解吗?不能!因为在最终的结果集出来之前,是不能 ORDER BY 的,最终结果集什么时候能出来呢?永远也出不来,因为它是个死循环。

该特性最好只在测试环境中使用,官方不建议在生产环境中使用,因为在其他系统中,都有可能因为实现原理和工作机制的不同,导致结果集的偏差。

Data-Modifying Statements in WITH

WITH 中不仅可以使用 SELECT 查询数据,还可以使用 INSERT,UPDATE,DELETE 等修改数据,用法大同小异。

看例子:

WITH moved_rows AS (
    DELETE FROM products
    WHERE
        "date" >= '2010-10-01' AND
        "date" < '2010-11-01'
    RETURNING *
)
INSERT INTO products_log
SELECT * FROM moved_rows;

该 SQL 的含义是从 products 表中删除部分数据,然后把删除信息记录在 product_log 表中。

该 WITH 中使用 RETURNING 子句返回被 DELETE 的数据(不是products表的原始数据)给 moved_rows ,RETURNING 后面是 products 表列名,可以指定返回部分列(逗号分割),也可以使用 * 表示返回所有列。如果 WITH 中没有 RETURNING 子句,而与 WITH 平级的 SQL 又引用了 moved_rows ,整个 SQL 就会报错。

没有 RETURNING 的例子:

WITH t AS (
    DELETE FROM foo
)
DELETE FROM bar;

这个 WITH 中可以没有 RETURNING 子句,因为外面没有引用 t 的主SQL 。这样写SQL没有什么意义,它会删除 foo 表和 bar 表中的所有数据,但是返回给客户端的影响行数只有 "DELETE FROM bar" 的结果,没有 "DELETE FROM foo" 的结果。

小结:在 WITH 中修改数据,是一个完全独立于主SQL的部分,也就是说,无论主SQL是否依赖于 WITH 的输出,WITH 中的修改操作都会执行。这不同于在 WITH 中使用 SELETCT:如果主SQL依赖 WITH 输出,SELECT 就执行,如果不需要 SELECT 就不执行。

RETURNING

WITH中可以有多处修改数据的操作,这些操作和主SQL是并发执行的,因此他们的执行顺序是不可预测的。又因为它们属于同一个SQL,在一个快照里面执行,所以它们使用的都是目标表的原始数据,彼此之间也看不到各自对目标表的影响。为了避免这种不可预测的结果,PostgreSQL 就利用了 RETURNING 子句来交流彼此对目标表的 "改变" ,换句话说就是,RETURNING 数据是目标表发生改变的那部分数据,是改变后的新数据。

来个例子:

WITH t AS (
    UPDATE products SET price = price * 1.05
    RETURNING *
)
SELECT * FROM products;

WITH 子句对 products 进行了 UPDATE 操作,但是主SQL查询出来的依然是未修改前的 products 数据。

再来一个:

WITH t AS (
    UPDATE products SET price = price * 1.05
    RETURNING *
)
SELECT * FROM t;

这个主SQL查询出来的就是修改后发生变化的数据。

多次修改同一行数据

在同一个SQL中,不支持多次 UPDATE 同一行数据。多次 UPDATE 时,只有一个 UPDATE 可以被执行,但具体是哪个无法预测。这个规则同样适用于 DELETE ,不能 DELETE 一个已经 UPDATE 的数据。

因此,应该避免在同一个SQL中,针对同一行进行多次修改操作的行为。尤其是在 WITH 子句中,主SQL和 WITH 的多个 CTE 很容易组合出"对同一行数据多次修改"的情况,一旦出现这种情况,结果是不可预测的。

参考资料:

  PostgreSQL 9.1.19 Documentation

  CTEReadme

原文地址:https://www.cnblogs.com/litmmp/p/5133765.html