MySQL编程进阶之:CTE(Common Table Expressions)在复杂递归查询中的应用。

各位观众老爷,大家好!我是今天的主讲人,江湖人称“代码老司机”,今天咱们聊点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:Department101Department101EmployeesDepartment101Employees 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_idNULL 的员工)。 level 列表示员工的层级,CEO的层级为0。
  • UNION ALL: UNION ALL 将基础情况和递归情况的结果合并起来。注意这里必须用 UNION ALL,而不是 UNIONUNION 会去重,可能会导致递归查询提前终止。
  • 递归情况(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_idparent_category_id 等用于连接的列上创建索引,可以提高递归查询的性能。
  • 限制递归深度: 设置合适的递归深度,避免无限循环。
  • 简化递归逻辑: 尽量简化递归查询的逻辑,避免不必要的计算。
  • 考虑其他方案: 如果递归查询性能太差,可以考虑使用其他方案,例如预先计算好层级关系,或者使用专门的图数据库。

Part 7: 总结

CTE是MySQL中一个非常强大的功能,尤其是在处理复杂递归查询时。掌握CTE的语法和应用,可以让你写出更清晰、更高效的SQL语句。但是,CTE递归查询也需要谨慎使用,注意递归深度、性能问题和死循环等问题。

好了,今天的讲座就到这里,希望大家有所收获!下次有机会再和大家分享更多MySQL的技巧。 记住,代码的路上,永无止境! 咱们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注