SQL里怎么存变量?怎么优雅处理递归?不妨了解下CTE!

什么是CTE?

CTE(Common Table Expressions)是一种SQL标准,对于我们来说就是一套语法,提供的是 "命名的临时结果集(named temporary result set)" ,用于提高 SQL 的可读性、可维护性,以及支持递归查询。

CTE解决了什么问题?

我觉得有两点,一是个让你写SQL的时候能存临时变量 ,二是处理递归。第一个帮你省事,第二个做到SQL以前做不到的事。

CTE的临时变量

  • 写SQL的一个痛点就是没有地方存存临时变量 。比如你要写一个很复杂的查询,其中有一个平均值,而这个平均值是一个子查询SQL(假如还超复杂有几十行SQL),而你要还要用很多次,那么每次用的时候你都要复制这个子查询SQL,是不是很麻烦。那么现在有了CTE,你可以直接把这个平均值SQL (以表的形式)当做一个临时变量,像是 common_avg_value_table,后面只需调用common_avg_value_table.value就能拿到值,是不是很方便。
  • 如此一来你写SQL的时候可以转变观念,没有CTE的时候,你的SQL开上去是层层嵌套的紧密结构,有了CTE则可以使用模块化的思维,像变形金刚合体一样,每次处理一部分,最后汇总结果。

CTE的变量不是真本量

虽然我说CTE能够存临时变量 ,但更像是一种不太严谨的类比,更严谨的说法是"命名的临时结果集",我举个例子你就明白了。

假如一个报表中有10个地方都用到了一个 CTE 变量avg_table.value,这个avg_table里面就是一个类似于select avg(xxx) as value from xxx的查询,这个查询一次花1秒,你报表中调用10次,这个查询就会执行10次,也就用了10秒在这上面。

这种调用特性其实不是变量,更像是引用或者说函数。这在大数据量 / 性能敏感 SQL 情况下需要注意。

当然如果你想做到一次查询,多次复用的功能,MySQL 8.0.31+ 提供了一种"物化(MATERIALIZED)"的语法,能实现这一点,感兴趣的可以了解,但要注意物化 ≠ 一定更快,否则官方默认就支持了,所以还是了解清楚再考虑使用吧。

CTE使用示例

假设你要算一个复杂的报表: "找出今年每个月销售额超过平均值的部门"

sql 复制代码
-- 传统做法(子查询嵌套):
SELECT department_id, monthly_sales
FROM 
(SELECT department_id, SUM(amount) as monthly_sales FROM sales GROUP BY department_id)
as s
WHERE monthly_sales > 
(
  SELECT AVG(total_sales) FROM 
   (
   SELECT SUM(amount) as total_sales FROM sales GROUP BY department_id
   ) as t
);


-- CTE 做法(模块化)
WITH DeptSales AS (
    -- 模块1:计算每个部门的销售额
    SELECT department_id, SUM(amount) AS monthly_sales
    FROM sales
    GROUP BY department_id
),
AvgSales AS (
    -- 模块2:基于模块1计算全局平均值
    SELECT AVG(monthly_sales) AS global_avg 
    FROM DeptSales
)
-- 模块3:最终过滤输出
SELECT * FROM DeptSales 
WHERE monthly_sales > (SELECT global_avg FROM AvgSales);

Recursive CTE (递归 CTE)

数据库与递归

标准的 SQL 语言最初是基于关系代数设计的,其核心是"集合论"。集合论擅长处理横向的过滤和合并,但天生不擅长处理纵向的、具有依赖关系的迭代逻辑(即递归)。

所以以前数据库处理递归还挺有意思的:

  1. 用存储过程,直接编程干
  2. 加字段,没有什么是加一个字段解决不了的问题,如果有那就加两个,比如path,存储像是1/5/12/30这样的字符,查询就where path like '1/5/%'一把抓出所有下属。
  3. 数据库的私有语法:在CTE成为SQL标准前,Oracle就使用connect by prior处理递归。

递归 CTE 介绍

  • 前面说了数据库处理递归不方便,所以就出了一套专门处理递归的 SQL 规范,Recursive CTE,它的出现本质上是把"有限递归"纳入声明式查询模型中
  • 需注意MySQL 递归 CTE 有限制,默认最大递归深度:cte_max_recursion_depth(默认 1000),这个你可以改

CTE递归示例

sql 复制代码
-- 模板
WITH RECURSIVE cte_name AS (
    -- 1. 初始成员 (Anchor Member): 递归的起点/基准情形
    SELECT ... 
    
    UNION [ALL]
    
    -- 2. 递归成员 (Recursive Member): 定义"如何从上一步推导到下一步"
    SELECT ... FROM cte_name INNER JOIN table ON ...
)
-- 3. 外部查询: 最终结果
SELECT * FROM cte_name;

-- 示例
WITH RECURSIVE calendar AS (
    SELECT '2024-01-01' AS date_day -- 起点
    UNION ALL
    SELECT date_add(date_day, INTERVAL 1 DAY) -- 迭代规则:加一天
    FROM calendar
    WHERE date_day < '2024-01-10' -- 终止条件
)
SELECT * FROM calendar;

CTE 总结一下

  • CTE提供了命名结果集功能,为模块化SQL建立了基础。
  • RECURSIVE CTE 把"有限递归"纳入声明式查询模型中,从此递归SQL也能上桌吃饭了。
  • CTE 也不是万能灵药,可能的性能问题要注意。
相关推荐
不像程序员的程序媛2 小时前
系统cpu内存负载资源分析
运维·服务器·数据库
Jane Chiu3 小时前
Oracle DBMS_REDEFINITION(在线重定义)
数据库·oracle·分区表
全栈前端老曹3 小时前
【MongoDB】安全与权限管理 —— 用户认证、角色权限、SSL 加密
前端·javascript·数据库·安全·mongodb·nosql·ssl
怕孤单的草丛4 小时前
缓存管理面临的主要问题
java·数据库·缓存
我会尽全力 乐观而坚强4 小时前
MySQL零基础入门(二)
数据库·mysql·adb
kali-Myon5 小时前
某校园门禁系统高危 SQL 注入漏洞挖掘复盘
数据库·sql·安全·web安全
程序员在囧途6 小时前
likeadmin-api API 中转站怎么做统一报价?从 /pricing、/user/balance 到 task_id 回执的落地方法
运维·服务器·数据库·likeadmin-api·api中转站·token计费
龙石数据7 小时前
MySQL 全量同步到 Hive 怎么做?三步配置教程
数据库·hive·mysql·数据治理·数据中台
程序员在囧途8 小时前
likeadmin-api API 算力超市怎么做供应商切换?统一鉴权、task_id 和 callback_url 才能稳交付
java·服务器·数据库·开放api·likeadmin-api·api算力超市