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

PostgreSQL: WITH Queries (Common Table Expressions)的更多相关文章

  1. Using Recursive Common table expressions to represent Tree structures

    http://www.postgresonline.com/journal/archives/131-Using-Recursive-Common-table-expressions-to-repre ...

  2. The ORDER BY clause is invalid in views, inline functions, derived tables, subqueries, and common table expressions, unless TOP or FOR XML is also specified.

    https://stackoverflow.com/questions/30045871/sorting-the-view-based-on-frequency-in-sql-server Just ...

  3. Common Table Expressions (CTE)

    子查询有时使用起来很麻烦,因为所有的过滤和匹配逻辑都必须集成到子查询表达式中.如果只需要执行一个任务,且只需要使用一次杳询表达式,子查询是很好的选择.但子查询不能被重用,也不能很好地支持多个需求.这个 ...

  4. CTE(Common Table Expression) 公用表表达式

    在编写T-SQL代码时,往往需要临时存储某些结果集.前面我们已经广泛使用和介绍了两种临时存储结果集的方法:临时表和表变量.除此之外,还可以 使用公用表表达式的方法.公用表表达式(Common Tabl ...

  5. SQLServer中的CTE(Common Table Expression)通用表表达式使用详解

    概述 我们经常会编写由基本的 SELECT/FROM/WHERE 类型的语句派生而来的复杂 SQL 语句.其中一种方案是需要编写在 FROM 子句内使用派生表(也称为内联视图)的 Transact-S ...

  6. 通用表表达式(Common Table Expression)

    问题:编写由基本的 SELECT/FROM/WHERE 类型的语句派生而来的复杂 SQL 语句. 方案1:编写在From子句内使用派生表(内联视图)的T-SQL查询语句. 方案2:使用视图 方案3:使 ...

  7. with as (cte common table expression) 公共表表达式

    SQL中 with as 的用法——使用公用表表达式(CTE)  公用表表达式 (CTE) 可以认为是在单个 SELECT.INSERT.UPDATE.DELETE 或 CREATE VIEW 语句的 ...

  8. [SoapUI] Common XPath expressions

    选取节点 表达式 描述 nodename 选取此节点的所有子节点. / 从根节点选取. // 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置. . 选取当前节点. .. 选取当前节点的父节点 ...

  9. 学习笔记之Nearest-Neighbour Searching with PostGIS

    PostgreSQL: Documentation: 10: 7.8. WITH Queries (Common Table Expressions) https://www.postgresql.o ...

随机推荐

  1. Java Annotations, Java Reflection, and Java Test Runner

    www.vogella.com/tutorials/JavaAnnotations/article.html

  2. android studio - 导入工程报错[Plugin with id 'com.android.application' not found]

    出错现象: 大概意思是找不到:com.android.application 插件,以上现象对于初学者来说会经常碰到,下面分析下产生的原因. 原因分析 首先来看看导入后的工程结构: 对于此工程结构,是 ...

  3. BodyTagSupport小案例1

    做了个简单的实验:写一个tag,将tag body中的内容打印成一个三角形 代码很简单就不赘述了,直接贴在下面,值得注意的是这个图(摘自李兴华JAVA开发实战经典) 在做的过程中遇到了如下问题: 1. ...

  4. Freemarker自定义方法

    在项目中有一个需求,每个物品有一个guid,存在数据库中,而在页面上需要显示一个对应的业务数据值,暂且叫做serverId,serverId是通过guid移位计算得来.serverId只需要显示,后台 ...

  5. 跟着百度学PHP[16]-验证码的学习

    一个验证码需要有以下步骤: 验证底图 验证码内容 生成验证码 对比校验 验证码需要依靠PHP的GD扩展库.一些集成环境是默认安装了GD拓展库. <?php //创建一个100*30px图片,默认 ...

  6. Jenkins ssh 部署war到tomcat 虚拟目录

    1>完成Jenkins服务环境的搭建,此处不再详述,不会的童鞋可参考下面的博客 http://www.cnblogs.com/zz0412/tag/jenkins/ 2>安装Publish ...

  7. IPC之共享内存

    man 7 shm_overview shm_overview - Overview of POSIX shared memory. 同样,SystemV实现的共享内存是旧的机制,但应用广泛:Posi ...

  8. 如何使用 PsExec 执行远程命令

    前言 这紧紧只是一篇纯技术分享.我们的程序在运行时需要连接多台目标机器,并拷贝目标机器上特定文件夹中的文件.为了方便访问,要在每台目标机器上建立一个特定用户,所以 PsExec 成了实现此功能的一个方 ...

  9. C0304 备份最后一天所有修改的文件

    #! /bin/bash backupfile=backup-$(date +%m-%d-%Y) archive=${1:-$backupfile} # 上边内容, 参数替换 ${} echo $ar ...

  10. 【转】不用程序mysql也可以查询今天、昨天、7天、近30天、本月、上一月的数据

    今天   select * from 表名 where to_days(时间字段名) = to_days(now());   昨天 SELECT * FROM 表名 WHERE TO_DAYS( NO ...