跳转到主要内容
ClickHouse 支持公共表表达式 (CTE) 、公共标量表达式和递归查询。

公共表表达式

公共表表达式表示命名子查询。 在 SELECT 查询中,凡是允许使用表表达式的地方,都可以通过名称引用它们。 命名子查询可以在当前查询的作用域内,或在子查询的作用域内通过名称引用。 SELECT 查询中,每次引用公共表表达式时,如果该 CTE 未被显式定义为物化 (参见物化公共表表达式) ,都会被替换为其定义中的子查询。 通过在标识符解析过程中隐藏当前 CTE,可以防止递归。 请注意,CTE 并不保证在所有被调用的位置都产生相同的结果,因为每次使用时都会重新执行该查询。

语法

示例

以下是子查询被重新执行的一个示例:
如果 CTE 传递的是确切的结果,而不只是代码片段,你将始终看到 1000000 然而,由于我们引用了两次 cte_numbers,每次都会生成随机数,因此会看到不同的随机结果,例如 280501, 392454, 261636, 196227 等等…

物化公共表表达式

默认情况下,ClickHouse 会在每处引用时内联 CTE 的子查询,因此每次引用都会重新执行该子查询。 添加 MATERIALIZED 关键字后,ClickHouse 会将 CTE 子查询只执行一次,并将结果存储在临时表中,后续所有引用都从该表读取。 这在同一个 CTE 在一次查询中被多次引用时尤其有用 (例如自连接或多个 IN 子查询) ,因为底层计算只需执行一次。
物化 CTE 是一项 Experimental 功能。 它要求启用 analyzer 以及设置 enable_materialized_cte

语法

何时使用

在以下情况下,物化 CTE 最有用:
  • 同一个 CTE 在一次查询中被引用不止一次。 如果没有 MATERIALIZED,每次引用都会独立重新执行该子查询。
  • 该 CTE 包含像 generateRandom 这样的非确定性函数。 物化后可确保所有引用看到的是同一份数据。
  • 该 CTE 涉及开销较大的计算 (聚合、连接、大规模扫描) ,不应重复执行。
如果物化 CTE 只被引用一次,ClickHouse 会自动将其内联回普通子查询,以避免不必要的开销。

示例

示例 1: 对物化 CTE 执行自连接 如果不使用 MATERIALIZED,JOIN 的两侧都会各自执行该子查询。 使用 MATERIALIZED 后,该表只会被扫描一次,JOIN 的两侧都会从同一个临时表中读取。
示例 2: 使用非确定性函数获得确定性结果 使用 generateRandom 的常规 CTE 在每次引用时都会产生不同的结果。 将 CTE 物化后可确保结果一致:
因为两个引用都从同一份物化后的数据中读取,所以结果始终是 1000000 示例 3: 串联物化 CTE 物化 CTE 可以引用其他物化 CTE。 ClickHouse 会解析这些依赖关系,并按正确顺序将其物化:
CTE 定义的先后顺序无关紧要——支持向前引用:

限制

  • 需要启用 Experimental 设置:必须启用设置 enable_materialized_cte
  • 需要启用 analyzer:物化 CTE 仅在启用 analyzer (enable_analyzer = 1) 后才能使用。
  • 不支持与 RECURSIVE 一起使用:不允许同时使用 MATERIALIZEDRECURSIVE 关键字,否则会引发 UNSUPPORTED_METHOD 异常。
  • 禁止使用关联 CTE:物化 CTE 不能引用外层查询作用域中的列。

通用标量表达式

ClickHouse 允许你在 WITH 子句中将任意标量 expression 声明为别名。 通用标量表达式可以在查询中的任何位置引用。
如果通用标量表达式引用的不是常量字面量,而是其他内容,则该 expression 可能会引入自由变量。 ClickHouse 会在尽可能最近的作用域中解析任意标识符,这意味着在名称冲突时,自由变量可能会引用到意料之外的实体,或者导致关联子查询。 建议将 CSE 定义为 lambda function (仅在启用 analyzer 时可用) ,并绑定所有用到的标识符,以获得更可预测的表达式标识符解析行为。

语法

示例

示例 1: 将常量表达式用作”变量”
示例 2: 使用高阶函数约束标识符
示例 3: 将高阶函数与自由变量结合使用 以下示例查询表明,未绑定的标识符会解析为最近作用域中的某个实体。 这里,extensiongen_name lambda 函数的函数体中未绑定。 尽管 extensiongenerated_names 的定义和使用所在的作用域中被定义为公共标量表达式 '.txt',但它会被解析为表 extension_list 中的一列,因为它在 generated_names 子查询中可用。
示例 4: 将 sum(bytes) 表达式结果从 SELECT 子句的列列表中排除
示例 5: 使用标量子查询结果
示例 6: 在子查询中复用表达式

递归查询

可选的 RECURSIVE 修饰符允许 WITH 查询引用其自身的输出。示例: **示例:**对 1 到 100 的整数求和
递归 CTE 依赖于在版本 24.3 中引入的查询分析器。如果你使用的是 24.3+ 版本并遇到 (UNKNOWN_TABLE)(UNSUPPORTED_METHOD) 异常,这说明查询分析器在你的 instance、Role 或 profile 上被禁用了。要启用查询分析器,请开启 allow_experimental_analyzer 设置,或将 compatibility 设置更新到更新的版本。 从 24.8 版本开始,analyzer 已正式升级为生产可用,allow_experimental_analyzer 设置也已重命名为 enable_analyzer
递归 WITH 查询的一般形式始终为:先是一个非递归项,然后是 UNION ALL,最后是一个递归项,其中只有递归项可以包含对查询自身输出的引用。递归 CTE 查询的执行方式如下:
  1. 计算非递归项。将非递归项查询的结果放入临时工作表中。
  2. 只要工作表不为空,就重复以下步骤:
    1. 计算递归项,将工作表的当前内容替换递归自引用。将递归项查询的结果放入临时中间表中。
    2. 用中间表的内容替换工作表中的内容,然后清空中间表。
递归查询通常用于处理层级型数据或树状结构数据。例如,我们可以编写一个执行树遍历的查询: 示例: 树遍历 首先,创建树表:
我们可以用如下查询遍历这些树: **示例:**树遍历

搜索顺序

为了生成深度优先顺序,我们为结果中的每一行计算一个由已访问行构成的数组: 示例: 树遍历的深度优先顺序
要创建广度优先顺序,标准做法是添加一列来跟踪搜索深度: **示例:**按广度优先顺序遍历树

循环检测

首先创建 graph 表:
我们可以使用如下查询遍历该图: 示例: 不检测环的图遍历
但是,如果我们在该图中加入环路,前面的查询就会因 Maximum recursive CTE evaluation depth 错误而失败:
处理环的标准做法是构造一个记录已访问节点的数组: 示例: 带环检测的图遍历

无限查询

只要在外层查询中使用 LIMIT,也可以使用无限递归 CTE 查询: 示例: 无限递归 CTE 查询

行尾逗号

WITH 子句中的最后一个元素后可以加逗号:
最后修改于 2026年6月10日