SQL公共表表达式(CTE)实战:用高中考试数据学会高效查询

在 SQL 查询中,面对多层嵌套子查询(比如 “先查班级平均分,再查高于平均分的学生,最后统计各科达标率”),代码往往会变得杂乱难懂 —— 缩进嵌套像 “千层饼”,修改时需要逐层定位,可读性和维护性极差。而公共表表达式(CTE,Common Table Expressions) 正是解决这一问题的 “利器”,它能将复杂逻辑拆分为多个独立的 “临时结果集”,让 SQL 代码像 “搭积木” 一样清晰。本文将以高中考试数据为场景,从基础到进阶,带你掌握 CTE 的核心用法。

一、什么是 CTE?一句话看懂核心价值

CTE 是一种临时的、可复用的结果集,它在SELECTINSERTUPDATE等语句中定义,仅在当前查询中生效(查询结束后自动消失,不会像表一样持久化)。其核心价值在于:

  • 拆分复杂逻辑:将多步查询拆分为多个独立 CTE,每步只做一件事;
  • 提升可读性:给临时结果集命名(如cte_class_avg表示 “班级平均分”),代码意图一目了然;
  • 支持递归查询:这是 CTE 独有的能力,可用于处理层级数据(如高中年级 - 班级 - 学生的层级关系)。

CTE 的基础语法非常简单,用WITH关键字定义,格式如下:

WITH 临时结果集名称1 AS (

    -- 第一步查询逻辑:生成临时结果集1

    SELECT 列1, 列2... FROM 表名 WHERE 条件

),

临时结果集名称2 AS (

    -- 第二步查询逻辑:可引用临时结果集1

    SELECT 列A, 列B... FROM 临时结果集名称1 WHERE 条件

)

-- 最终查询:引用前面定义的临时结果集

SELECT * FROM 临时结果集名称2;

二、实战场景:用高中考试数据学 CTE

为了让例子更贴近生活,我们先定义 3 张高中考试相关的表,后续所有 CTE 示例都基于这 3 张表展开:

基础数据表结构

  1. students(学生表):存储学生基本信息

student_id

student_name

class

grade

101

张三

1 班

高一

102

李四

1 班

高一

103

王五

2 班

高一

104

赵六

2 班

高二

  1. courses(课程表):存储考试科目信息

course_id

course_name

1

数学

2

语文

3

英语

  1. exam_scores(考试成绩表):存储学生各科成绩

score_id

student_id

course_id

score

exam_date

1

101

1

85

2024-06-10

2

101

2

78

2024-06-10

3

102

1

92

2024-06-10

4

102

2

88

2024-06-10

5

103

1

76

2024-06-10

6

103

3

95

2024-06-10

7

104

2

80

2024-06-10

三、CTE 的 3 类核心用法

3.1 单 CTE:解决 “一步查询” 的简化需求

当查询需要先做一次数据筛选 / 计算,再基于结果做最终查询时,单 CTE 能避免子查询嵌套,让逻辑更清晰。

场景 1:查询 2024 年 6 月考试中,数学成绩高于班级平均分的学生姓名和成绩

如果不用 CTE,需要嵌套子查询(先查数学班级平均分,再查高于平均分的学生):

-- 无CTE:子查询嵌套,可读性差

SELECT

  s.student_name,

  e.score

FROM students s

JOIN exam_scores e ON s.student_id = e.student_id

JOIN courses c ON e.course_id = c.course_id

WHERE

  c.course_name = '数学'

  AND e.exam_date = '2024-06-10'

  AND e.score > (

    -- 子查询:计算数学班级平均分

    SELECT AVG(score)

    FROM exam_scores

    JOIN courses ON exam_scores.course_id = courses.course_id

    WHERE course_name = '数学' AND exam_date = '2024-06-10'

  );

用 CTE 改写后,逻辑拆分为两步,一目了然:

-- 单CTE:先算数学平均分,再查高于平均分的学生

WITH cte_math_avg AS (

    -- 第一步:计算2024年6月数学考试的班级平均分

    SELECT AVG(score) AS math_class_avg

    FROM exam_scores e

    JOIN courses c ON e.course_id = c.course_id

    WHERE c.course_name = '数学' AND e.exam_date = '2024-06-10'

)

-- 第二步:查询高于平均分的学生信息

SELECT

  s.student_name,

  e.score,

  m.math_class_avg  -- 可直接引用CTE的平均分

FROM students s

JOIN exam_scores e ON s.student_id = e.student_id

JOIN courses c ON e.course_id = c.course_id

JOIN cte_math_avg m ON 1=1  -- 关联CTE(因CTE只有1行数据,用1=1匹配)

WHERE

  c.course_name = '数学'

  AND e.exam_date = '2024-06-10'

  AND e.score > m.math_class_avg;

结果

student_name

score

math_class_avg

李四

92

84.33

优势:CTE 将 “计算平均分” 和 “筛选学生” 拆分为两个独立步骤,后续修改时(如改考试日期),只需定位到对应的 CTE,无需在嵌套中找子查询。

3.2 多 CTE:处理 “多步依赖” 的复杂需求

当查询需要多步计算,且后续步骤依赖前序步骤的结果时(比如 “先算各科平均分→再算学生总分→最后查总分前 10 且各科高于平均分的学生”),多 CTE 能像 “流水线” 一样逐步处理。

场景 2:2024 年 6 月考试中,查询高一年级学生中 “总分≥240 分” 且 “各科成绩不低于该科年级平均分” 的学生名单及各科成绩

这个需求需要 3 步计算,用多 CTE 拆解:

WITH

-- 第一步:计算高一年级各科的年级平均分

cte_grade_course_avg AS (

    SELECT

      c.course_name,

      AVG(e.score) AS grade_course_avg  -- 高一某科的平均分

    FROM exam_scores e

    JOIN students s ON e.student_id = s.student_id

    JOIN courses c ON e.course_id = c.course_id

    WHERE s.grade = '高一' AND e.exam_date = '2024-06-10'

    GROUP BY c.course_name

),

-- 第二步:计算高一年级每个学生的总分

cte_student_total AS (

    SELECT

      s.student_id,

      s.student_name,

      s.class,

      SUM(e.score) AS total_score  -- 学生总分

    FROM students s

    JOIN exam_scores e ON s.student_id = e.student_id

    WHERE s.grade = '高一' AND e.exam_date = '2024-06-10'

    GROUP BY s.student_id, s.student_name, s.class

    HAVING SUM(e.score) ≥ 240  -- 先筛选总分≥240的学生

),

-- 第三步:获取高一年级学生的各科成绩(关联第一步的平均分)

cte_student_scores AS (

    SELECT

      s.student_id,

      s.student_name,

      c.course_name,

      e.score,

      g.grade_course_avg

    FROM students s

    JOIN exam_scores e ON s.student_id = e.student_id

    JOIN courses c ON e.course_id = c.course_id

    JOIN cte_grade_course_avg g ON c.course_name = g.course_name

    WHERE s.grade = '高一' AND e.exam_date = '2024-06-10'

    AND e.score ≥ g.grade_course_avg  -- 筛选各科高于平均分的记录

)

-- 最终查询:关联第二步(总分)和第三步(各科达标),获取符合条件的学生

SELECT

  t.student_name,

  t.class,

  t.total_score,

  -- 用CASE WHEN将各科成绩拼接成一列(方便展示)

  GROUP_CONCAT(

    CONCAT(sc.course_name, ':', sc.score, '( avg:', ROUND(sc.grade_course_avg,1), ')')

    SEPARATOR ' | '

  ) AS course_scores

FROM cte_student_total t

JOIN cte_student_scores sc ON t.student_id = sc.student_id

-- 确保学生所有参考科目都达标(通过COUNT匹配科目数量)

GROUP BY t.student_id, t.student_name, t.class, t.total_score

HAVING COUNT(sc.course_name) = (

  SELECT COUNT(DISTINCT course_name)

  FROM cte_grade_course_avg

);

结果

student_name

class

total_score

course_scores

李四

1 班

180

数学:92 (avg:84.3)

(注:示例中李四总分 180 是因只考了两科,实际场景需根据科目数量调整总分阈值)

优势:多 CTE 将 “算平均分→算总分→筛达标学生” 拆分为 3 个独立模块,每个模块可单独测试(比如先运行cte_grade_course_avg验证平均分是否正确),大幅降低调试难度。

3.3 递归 CTE:处理 “层级数据” 的特殊需求

递归 CTE 是 CTE 的 “进阶技能”,它由 “基础查询(锚点成员)” 和 “递归查询(递归成员)” 两部分组成,能循环处理层级关系数据(如高中的 “年级→班级→学生” 层级、科目分类层级等)。

场景 3:学校有 “年级 - 班级 - 学生” 的三级层级关系(如 “高一→1 班→张三”),需生成所有层级的树形结构列表

首先,我们构造一张层级表grade_class_student(实际场景可通过学生表关联生成):

id

name

parent_id

type

1

高一

NULL

年级

2

高二

NULL

年级

3

高一 1 班

1

班级

4

高一 2 班

1

班级

5

张三

3

学生

6

李四

3

学生

7

王五

4

学生

用递归 CTE 生成树形结构:

WITH RECURSIVE cte_tree AS (

    -- 1. 基础查询(锚点成员):先获取最顶层的“年级”(parent_id为NULL)

    SELECT

      id,

      name,

      parent_id,

      type,

      CAST(name AS CHAR(100)) AS full_path  -- 记录完整层级路径(如“高一→1班→张三”)

    FROM grade_class_student

    WHERE parent_id IS NULL  -- 顶层节点:年级

    

    UNION ALL  -- 2. 递归查询(递归成员):关联子节点(班级→学生)

    SELECT

      g.id,

      g.name,

      g.parent_id,

      g.type,

      -- 拼接路径:父节点路径 + 当前节点名称

      CONCAT(ct.full_path, '→', g.name) AS full_path

    FROM grade_class_student g

    JOIN cte_tree ct ON g.parent_id = ct.id  -- 子节点的parent_id = 父节点的id

)

-- 最终查询:展示所有层级的树形结构

SELECT

  id,

  name,

  type,

  full_path AS 层级路径

FROM cte_tree

ORDER BY id;

结果

id

name

type

层级路径

1

高一

年级

高一

2

高二

年级

高二

3

高一 1 班

班级

高一→高一 1 班

4

高一 2 班

班级

高一→高一 2 班

5

张三

学生

高一→高一 1 班→张三

6

李四

学生

高一→高一 1 班→李四

7

王五

学生

高一→高一 2 班→王五

原理:递归 CTE 会循环执行 “关联子节点” 的逻辑,直到没有新的子节点(即所有层级都遍历完成),非常适合生成报表中的树形结构(如年级班级学生统计报表)。

四、CTE 的性能优化与注意事项

4.1 性能优化:避免重复计算

CTE 虽然是临时结果集,但部分数据库(如 MySQL)不会对 CTE 结果进行缓存 —— 如果在同一查询中多次引用同一个 CTE,数据库会重复执行 CTE 的逻辑,导致性能浪费。

优化方案:若需多次引用 CTE,可将 CTE 结果存入临时表:

-- 1. 将CTE结果存入临时表

CREATE TEMPORARY TABLE tmp_math_avg AS

WITH cte_math_avg AS (

    SELECT AVG(score) AS math_avg FROM exam_scores WHERE course_id = 1

)

SELECT * FROM cte_math_avg;

-- 2. 多次引用临时表(避免重复计算)

SELECT * FROM students s

JOIN exam_scores e ON s.student_id = e.student_id

JOIN tmp_math_avg m ON 1=1

WHERE e.score > m.math_avg;

SELECT COUNT(*) FROM exam_scores e

JOIN tmp_math_avg m ON 1=1

WHERE e.score < m.math_avg;

4.2 注意事项:CTE 的 “临时特性”

  • 不可重复定义:同一查询中,多个 CTE 不能重名;
  • 生命周期短:CTE 仅在当前查询中生效,查询结束后立即消失,无法跨查询引用;
  • 递归终止条件:递归 CTE 必须有明确的终止条件(否则会无限循环),比如子节点不存在时自动终止。

五、总结:CTE 不是 “花架子”,而是效率工具

很多开发者觉得 CTE 只是 “美化代码” 的工具,实则不然 —— 在复杂查询场景中,CTE 的价值远超 “美化”:

  1. 降低维护成本:拆分逻辑后,修改某一步只需调整对应的 CTE,无需动整体代码;
  2. 支持递归查询:这是子查询无法实现的功能,能轻松处理层级数据;
  3. 便于调试:可单独运行某一个 CTE,验证中间结果是否正确,定位问题更高效。

以高中考试数据为例,我们用 CTE 解决了 “成绩筛选”“多步统计”“树形结构生成” 等真实需求,这些场景若用子查询实现,代码复杂度会翻倍。建议在日常开发中,遇到嵌套超过 2 层的子查询时,就尝试用 CTE 重构 —— 你会发现 SQL 代码突然变得 “清爽” 了。

如果觉得本文有帮助,欢迎点赞收藏!若有 CTE 相关的实战问题(比如复杂考试统计场景),可在评论区留言

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值