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 很容易组合出"对同一行数据多次修改"的情况,一旦出现这种情况,结果是不可预测的。