Menu

SQLite 公用表表达式(CTE):WITH 子句详解

SQLite 中公用表表达式的工作原理:用 WITH 给子查询命名、串联多个 CTE,写出能从上往下读的查询。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

CTE 就是具名子查询

公用表表达式(CTE)是一个被你提取出来并命名的子查询。你不再把一个 SELECT 嵌套在另一个 SELECT 里,而是在最前面用 WITH 定义它、给它命名,然后在主查询中像使用表一样使用这个名字。

它的结构总是这样:

从上往下读:先构建一个名为 customer_totals 的具名结果,再查询这个结果。CTE 的表现就像一个临时的视图,只在这一条语句执行期间存在。

不用 CTE 的同一个查询

下面用子查询实现同样的逻辑,你可以看看 CTE 替代的是什么:

结果一样。但请注意阅读顺序:你的目光得先钻进括号里,弄清楚算的是什么,再出来。CTE 版本的阅读顺序和执行顺序一致:先定义中间结果,再使用它。在小查询上两者差别不大。但在有三四个步骤的查询上,这就是一眼能扫懂的代码和需要逐句破译的代码之间的区别。

一个查询中的多个 CTE

你可以把多个 CTE 串在一起,用逗号分隔。每个 CTE 都可以引用在它之前定义的 CTE,这样就搭建出一条由具名步骤组成的流水线:

一个 WITH,后面跟着用逗号分隔的 CTE 定义。第二个 CTE(big_spenders)读取第一个 CTE(customer_totals),就像读取一张表一样。主 SELECT 跟在最后一个 CTE 定义之后。

一个常见的失误:在第二个 CTE 前面又写一遍 WITH。别这样做,这是语法错误。一个 WITH 就能涵盖所有 CTE。

多次引用同一个 CTE

这正是 CTE 真正胜过子查询的地方。如果你在两个地方需要同一个中间结果,CTE 让你只计算一次、引用两次:

这个 CTE 被引用了两次:一次用来计算平均值,一次作为主数据源。如果不用 CTE,你就得把 GROUP BY 查询写两遍,以后每次修改都要改两个地方。

在 INSERT、UPDATE 和 DELETE 中使用 CTE

CTE 不只用于 SELECT。你可以在 INSERT、UPDATE 或 DELETE 前面加上 WITH 子句,在写操作中使用具名子查询:

CTE 描述了要标记哪些行,INSERT ... SELECT 把它当作数据源。同样的技巧也适用于 DELETE FROM ... WHERE id IN (SELECT id FROM cte),在筛选逻辑比较复杂时分步完成删除。

什么时候该用 CTE

几条经验法则:

  • 查询包含不止一个逻辑步骤。 先聚合,再按聚合结果过滤,然后连接结果:这是一条流水线,每一步用一个 CTE 会让它更易读。
  • 否则你就得重复写同一个子查询。 定义一次,引用两次。
  • 这个子查询值得有个名字。 如果你本来会在子查询上方写一行注释说明它代表什么,那么 CTE 的名字就是那行注释,而且由语法来保证。
  • 你要写一个递归查询。 这只能用 WITH RECURSIVE 实现,下一页会讲到。

什么时候不必用:

  • 只在一个地方用到的简单子查询。WHERE id IN (SELECT id FROM ...) 保持原样就好。
  • 性能关键的查询,并且你已经验证过把逻辑内联写会更快。和其他一些数据库相比,SQLite 通常不会那么激进地把 CTE 当作查询优化的屏障,但对于热点路径,还是值得用 EXPLAIN QUERY PLAN 检查一下。

完整示例

把这些串起来:一个小报表,找出每位客户最大的一笔订单,并与他们的平均订单金额作比较:

两个 CTE,各做一件事。主 SELECT 负责格式化结果。你可以从上往下读这个查询,并单独理解每一步:这正是 CTE 的意义所在。

下一步:递归 CTE

到目前为止讲的都是普通 CTE:一个只求值一次的具名子查询。SQLite 还支持 WITH RECURSIVE,其中的 CTE 会引用自身,用来遍历层级结构、生成序列或遍历图。这就是下一页的内容。

常见问题

SQLite 中的 CTE 是什么?

公用表表达式(CTE)是写在 SELECT、INSERT、UPDATE 或 DELETE 最前面的一个具名子查询。你用 WITH 关键字引入它,给它起个名字,然后在主查询中像使用表一样引用这个名字。CTE 让你可以一步一步地构建结果,从而让复杂查询更容易读懂。

SQLite 中 CTE 和子查询有什么区别?

两者可以得到完全相同的结果:CTE 本质上就是一个被提取出来并命名的子查询。区别在于可读性和复用:同一个查询中可以多次引用一个 CTE,而且它的名字本身就说明了这个中间结果代表什么。一次性的过滤用子查询就够了;涉及多个步骤的逻辑,CTE 更胜一筹。

一个 SQLite 查询里可以有多个 CTE 吗?

可以。在第一个 WITH 之后,用逗号分隔其他 CTE,不要重复写 WITH。每个 CTE 都可以引用在它之前定义的 CTE,这样你就能搭建一条由具名步骤组成的流水线。主 SELECT 跟在最后一个 CTE 之后。

Coddy 编程语言插图

和 Coddy 一起学编程

开始