各位观众老爷,大家好!我是今天的主讲人,江湖人称“代码老司机”,今天咱们聊点MySQL里比较高阶的东西:CTE,也就是Common Table Expressions(通用表表达式),尤其是在复杂递归查询中的应用。别害怕,听名字唬人,其实掌握了套路,你会发现它简直是SQL里的瑞士军刀,锋利又实用!
Part 1: CTE是个啥?为什么要用它?
简单来说,CTE就是一个命名的临时结果集,你可以在一个SELECT, INSERT, UPDATE, 或者 DELETE 语句里引用它。你可以把它想象成一个SQL里的变量,这个变量存的是一张表。
那为啥要用CTE呢?直接写子查询不行吗? 当然行,但是…
- 可读性爆炸提升! CTE让你的SQL语句逻辑更清晰,更容易理解。复杂的子查询嵌套几层,估计连你自己过几天都看不懂了。
- 代码复用! 你可以在同一个查询里多次引用同一个CTE,避免重复计算,提高效率。
- 递归查询! 这是CTE最酷炫的地方,可以实现树形结构、族谱关系等等的查询,用传统的SQL写起来简直是噩梦。
Part 2: CTE基本语法和简单示例
CTE的基本语法长这样:
WITH CTE_name AS (
-- CTE的定义,就是一个SELECT语句
SELECT column1, column2, ...
FROM table_name
WHERE condition
)
-- 主查询,引用CTE
SELECT column1, column2, ...
FROM CTE_name
WHERE condition;
咱们来个简单的例子,假设我们有一张 employees
表,包含员工ID (employee_id
)、姓名 (employee_name
) 和部门ID (department_id
)。
CREATE TABLE employees (
employee_id INT PRIMARY KEY,
employee_name VARCHAR(255),
department_id INT
);
INSERT INTO employees (employee_id, employee_name, department_id) VALUES
(1, '张三', 101),
(2, '李四', 101),
(3, '王五', 102),
(4, '赵六', 102),
(5, '孙七', 103);
现在,我们要查出所有部门ID等于101的员工姓名。用CTE可以这样写:
WITH Department101Employees AS (
SELECT employee_name
FROM employees
WHERE department_id = 101
)
SELECT employee_name
FROM Department101Employees;
这个例子很简单,但是它展示了CTE的基本用法:先定义一个名为 Department101Employees
的CTE,然后在主查询里引用它。
Part 3: CTE进阶:多个CTE和相互引用
CTE的强大之处在于,你可以在同一个查询里定义多个CTE,而且这些CTE还可以相互引用!
例如,我们想查出所有部门ID等于101的员工姓名,并且还要查出这些员工的部门ID。我们可以这样写:
WITH Department101 AS (
SELECT department_id
FROM employees
WHERE department_id = 101
),
Department101Employees AS (
SELECT employee_name, department_id
FROM employees
WHERE department_id IN (SELECT department_id FROM Department101)
)
SELECT employee_name, department_id
FROM Department101Employees;
在这个例子中,我们定义了两个CTE:Department101
和 Department101Employees
。 Department101Employees
CTE引用了 Department101
CTE的结果。
Part 4: 重头戏:CTE递归查询!
现在,咱们进入今天的主菜:CTE递归查询。这是CTE最强大的功能,可以用来处理层级结构的数据。
假设我们有一张 employee_hierarchy
表,用来表示员工的上下级关系。包含员工ID (employee_id
) 和上级员工ID (manager_id
)。
CREATE TABLE employee_hierarchy (
employee_id INT PRIMARY KEY,
employee_name VARCHAR(255),
manager_id INT
);
INSERT INTO employee_hierarchy (employee_id, employee_name, manager_id) VALUES
(1, '张三', NULL), -- 张三是CEO,没有上级
(2, '李四', 1), -- 李四是张三的下属
(3, '王五', 1), -- 王五也是张三的下属
(4, '赵六', 2), -- 赵六是李四的下属
(5, '孙七', 3); -- 孙七是王五的下属
现在,我们要查出所有张三(CEO)的下属,包括直接下属和间接下属。这就是一个典型的递归查询的场景。用CTE可以这样写:
WITH RECURSIVE EmployeeHierarchyCTE AS (
-- 基础情况:找到CEO
SELECT employee_id, employee_name, manager_id, 0 AS level
FROM employee_hierarchy
WHERE manager_id IS NULL
UNION ALL
-- 递归情况:找到CEO的下属
SELECT e.employee_id, e.employee_name, e.manager_id, cte.level + 1 AS level
FROM employee_hierarchy e
JOIN EmployeeHierarchyCTE cte ON e.manager_id = cte.employee_id
)
SELECT employee_id, employee_name, level
FROM EmployeeHierarchyCTE
WHERE employee_id != 1; -- 排除CEO
这个例子是CTE递归查询的核心模板,我们来详细解释一下:
WITH RECURSIVE EmployeeHierarchyCTE AS (...)
:WITH RECURSIVE
关键字告诉MySQL这是一个递归CTE。EmployeeHierarchyCTE
是CTE的名字。- 基础情况(Base Case):
SELECT employee_id, employee_name, manager_id, 0 AS level FROM employee_hierarchy WHERE manager_id IS NULL
这部分查询定义了递归的起始点,也就是找到CEO(manager_id
为NULL
的员工)。level
列表示员工的层级,CEO的层级为0。 UNION ALL
:UNION ALL
将基础情况和递归情况的结果合并起来。注意这里必须用UNION ALL
,而不是UNION
。UNION
会去重,可能会导致递归查询提前终止。- 递归情况(Recursive Case):
SELECT e.employee_id, e.employee_name, e.manager_id, cte.level + 1 AS level FROM employee_hierarchy e JOIN EmployeeHierarchyCTE cte ON e.manager_id = cte.employee_id
这部分查询定义了递归的规则,也就是找到所有上级是当前CTE中员工的员工。e.manager_id = cte.employee_id
是递归的关键,它将employee_hierarchy
表和EmployeeHierarchyCTE
连接起来,找到所有是当前CTE中员工下属的员工。cte.level + 1
表示下属的层级比上级高一级。 - 主查询:
SELECT employee_id, employee_name, level FROM EmployeeHierarchyCTE WHERE employee_id != 1;
这部分查询从EmployeeHierarchyCTE
中选择需要的列,并且排除CEO。
运行这个查询,你会得到以下结果:
employee_id | employee_name | level |
---|---|---|
2 | 李四 | 1 |
3 | 王五 | 1 |
4 | 赵六 | 2 |
5 | 孙七 | 2 |
这正是张三的所有下属,以及他们的层级。
Part 5: CTE递归查询应用场景举例
除了员工上下级关系,CTE递归查询还可以应用于很多其他场景:
- 族谱关系: 查询某个人的所有祖先或者后代。
- 组织架构: 查询某个部门的所有子部门。
- 商品分类: 查询某个商品分类的所有子分类。
- 菜单层级: 查询某个菜单的所有子菜单。
- 图结构: 在图中查找路径。
- 银行账户转账: 查询某个账户的所有转账记录,包括直接转账和间接转账。
咱们再来一个稍微复杂点的例子:商品分类。假设我们有一张 categories
表,包含分类ID (category_id
) 和父分类ID (parent_category_id
)。
CREATE TABLE categories (
category_id INT PRIMARY KEY,
category_name VARCHAR(255),
parent_category_id INT
);
INSERT INTO categories (category_id, category_name, parent_category_id) VALUES
(1, '电子产品', NULL),
(2, '电脑', 1),
(3, '笔记本电脑', 2),
(4, '台式机', 2),
(5, '手机', 1),
(6, '智能手机', 5),
(7, '功能手机', 5);
现在,我们要查出所有“电脑”分类的子分类,包括直接子分类和间接子分类。我们可以这样写:
WITH RECURSIVE CategoryHierarchyCTE AS (
-- 基础情况:找到“电脑”分类
SELECT category_id, category_name, parent_category_id, 0 AS level
FROM categories
WHERE category_name = '电脑'
UNION ALL
-- 递归情况:找到“电脑”分类的子分类
SELECT c.category_id, c.category_name, c.parent_category_id, cte.level + 1 AS level
FROM categories c
JOIN CategoryHierarchyCTE cte ON c.parent_category_id = cte.category_id
)
SELECT category_id, category_name, level
FROM CategoryHierarchyCTE;
运行这个查询,你会得到以下结果:
category_id | category_name | level |
---|---|---|
2 | 电脑 | 0 |
3 | 笔记本电脑 | 1 |
4 | 台式机 | 1 |
Part 6: CTE递归查询注意事项和优化
CTE递归查询虽然强大,但是也需要注意一些问题:
- 递归深度: 递归查询可能会无限循环,导致数据库崩溃。MySQL默认限制了递归深度为1000,你可以通过
SET @@max_sp_recursion_depth = <depth>;
来修改这个限制。但是,设置过高的递归深度可能会导致性能问题。 - 性能问题: 递归查询通常比非递归查询慢,特别是当数据量很大时。尽量避免在生产环境中使用复杂的递归查询。
- 死循环: 如果你的数据中存在循环引用,递归查询会陷入死循环。例如,A是B的上级,B又是A的上级。在设计数据结构时,要尽量避免循环引用。
- 终止条件: 确保你的递归查询有明确的终止条件,否则它会一直运行下去。
优化建议:
- 索引: 在
manager_id
和parent_category_id
等用于连接的列上创建索引,可以提高递归查询的性能。 - 限制递归深度: 设置合适的递归深度,避免无限循环。
- 简化递归逻辑: 尽量简化递归查询的逻辑,避免不必要的计算。
- 考虑其他方案: 如果递归查询性能太差,可以考虑使用其他方案,例如预先计算好层级关系,或者使用专门的图数据库。
Part 7: 总结
CTE是MySQL中一个非常强大的功能,尤其是在处理复杂递归查询时。掌握CTE的语法和应用,可以让你写出更清晰、更高效的SQL语句。但是,CTE递归查询也需要谨慎使用,注意递归深度、性能问题和死循环等问题。
好了,今天的讲座就到这里,希望大家有所收获!下次有机会再和大家分享更多MySQL的技巧。 记住,代码的路上,永无止境! 咱们下期再见!