6.5 流程控制结构

/*
期末考试成绩出来了,怎么给每个学生的成绩自动打等级?
        比如:>=90 → 优秀、>=75 → 良好、>=60 → 及格、否则 → 不及格
        在Python里你会用 if-else,MySQL也一样!

  这节课我们学习三大"流程控制结构"在 MySQL 中的使用方式。
  ─────────────────────────────────────────────────────────

  【板书结构】
  流程控制结构
  ├── 顺序结构(默认,从上到下执行)
  ├── 分支结构
  │   ├── IF函数          (双分支,可用于 SELECT 查询中)
  │   ├── CASE结构        (多分支,类似 switch,可用于 SELECT 查询中)
  │   └── IF结构          (多分支,只能用在 BEGIN...END 中)
  └── 循环结构
      ├── WHILE           (先判断,后执行)
      ├── REPEAT          (先执行,后判断)
      └── LOOP            (死循环,手动 LEAVE 退出)
*/


-- ============================================================
--  第一部分:分支结构
-- ============================================================


-- ------------------------------------------------------------
-- 知识点1:IF函数
-- ------------------------------------------------------------
/*
  【讲解】
  语法:IF(条件, 值1, 值2)
  功能:条件为TRUE返回值1,否则返回值2 → 实现"双分支"
  特点:可以直接在 SELECT 语句中使用,非常灵活。

  类比:就像三元运算符  条件 ? 值1 : 值2

  【适用场景】
  - SELECT 查询时对字段做简单的二选一判断
  - BEGIN...END 内外均可使用
*/

-- 【演示1-1】IF函数在SELECT中使用
-- 需求:查询 sc 表,显示每位学生各科成绩是否及格(>=60 及格,否则 不及格)
-- 涉及表:sc(选课成绩表)、student(学生表,需要关联学号查姓名)
sql
SELECT
    sc.Sno                  AS 学号,
    student.Sname           AS 姓名,
    sc.Cno                   AS 课程号,
    sc.score                AS 成绩,
    IF(sc.score >= 60, '及格', '不及格') AS 是否及格
FROM sc
JOIN student ON sc.Sno = student.Sno
ORDER BY sc.Sno, sc.Cno;
-- 【演示1-2】IF函数嵌套(了解即可)
-- 嵌套IF实现三级判断(可读性较差,推荐使用CASE)
sql
SELECT
    student.Sname           AS 姓名,
    sc.score                AS 成绩,
    IF(sc.score >= 90, '优秀',
        IF(sc.score >= 60, '及格', '不及格')
    ) AS 等级
FROM sc
JOIN student ON sc.Sno = student.Sno;
-- ------------------------------------------------------------
-- 知识点2:CASE结构
-- ------------------------------------------------------------
/*
  【讲解】
  CASE 有两种语法形式:

  ★ 形式一(等值判断,类似 switch):
  ─────────────────────────────────────
  CASE 变量或表达式
    WHEN 值1 THEN 结果1
    WHEN 值2 THEN 结果2
    ...
    ELSE 默认结果
  END

  ★ 形式二(范围/条件判断,类似 if-elseif-else):
  ─────────────────────────────────────────────────
  CASE
    WHEN 条件1 THEN 结果1
    WHEN 条件2 THEN 结果2
    ...
    ELSE 默认结果
  END

  【适用场景】
  - SELECT 查询中(最常用)以end 结尾。
  - BEGIN...END 内使用时,结果改为 语句; 并以 END CASE 结尾
*/

-- 【演示2-1】CASE形式一:等值判断
-- 需求:根据课程号显示课程名称
sql
SELECT
    student.Sname           AS 姓名,
    sc.Cno                   AS 课程号,
    CASE sc.Cno
        WHEN '01' THEN '语文'
        WHEN '02' THEN '数学'
        WHEN '03' THEN '英语'
        WHEN '04' THEN '计算机'
        ELSE '未知课程'
    END AS 课程名称,
    sc.score                AS 成绩
FROM sc
JOIN student ON sc.Sno = student.Sno;
-- 【演示2-2】CASE形式二:范围条件判断(最常用!)
-- 需求:根据成绩显示等级(结合 grades 等级表 的分数区间来写条件)
-- grades 表中的等级区间:优秀>=90、良好>=75、及格>=60、不及格<60
sql
SELECT
    student.Sname           AS 姓名,
    sc.Cno                   AS 课程号,
    sc.score                AS 成绩,
    CASE
        WHEN sc.score >= 90 THEN '优秀'
        WHEN sc.score >= 75 THEN '良好'
        WHEN sc.score >= 60 THEN '及格'
        ELSE '不及格'
    END AS 等级
FROM sc
JOIN student ON sc.Sno = student.Sno
ORDER BY sc.score DESC;
/*
  ★ 教学提示:
  CASE 与 IF函数对比:
  ┌───────────┬───────────────────┬───────────────────┐
  │           │    IF函数         │   CASE结构        │
  ├───────────┼───────────────────┼───────────────────┤
  │ 适用分支  │   仅双分支        │   多分支(推荐)  │
  │ 可读性    │   较低(嵌套时)  │   高              │
  │ 适用位置  │ SELECT/BEGIN END  │ SELECT/BEGIN END  │
  └───────────┴───────────────────┴───────────────────┘
  结论:多分支判断推荐使用 CASE,更清晰!
*/


-- ------------------------------------------------------------
-- 知识点3:IF结构(BEGIN...END 专用)
-- ------------------------------------------------------------
/*
  【讲解】
  语法:
  ─────────────────────────────────────
  IF 条件1 THEN
      语句1;
  ELSEIF 条件2 THEN
      语句2;
  ...
  ELSE
      语句n;
  END IF;
  ─────────────────────────────────────

  ⚠️ 重要区别:
  - IF函数  → SELECT语句里用,返回一个"值"
  - IF结构  → BEGIN...END 里用,执行一段"语句",只能在存储过程/函数中使用

  【类比】
  就像编程语言中的 if-elseif-else 代码块
*/

-- 【演示3-1】IF结构在存储函数中的使用
-- 需求:创建函数,传入成绩,返回等级(优秀/良好/及格/不及格)
sql
DROP FUNCTION IF EXISTS fn_get_grade;
CREATE FUNCTION fn_get_grade(score DECIMAL(5,1))
RETURNS CHAR(10)
DETERMINISTIC
BEGIN
    DECLARE grade CHAR(10);   -- 声明局部变量存储等级
    IF score >= 90 THEN
        SET grade = '优秀';
    ELSEIF score >= 75 THEN
        SET grade = '良好';
    ELSEIF score >= 60 THEN
        SET grade = '及格';
    ELSE
        SET grade = '不及格';
    END IF;

    RETURN grade;
END;

-- 测试调用
sql
SELECT
    fn_get_grade(97.0)  AS 成绩97的等级,
    fn_get_grade(85.0)  AS 成绩85的等级,
    fn_get_grade(65.0)  AS 成绩65的等级,
    fn_get_grade(55.0)  AS 成绩55的等级;
-- 结合 sc 表调用:查询所有学生成绩及等级
sql
SELECT
    sc.Sno                  AS 学号,
    student.Sname           AS 姓名,
    sc.Cno                   AS 课程号,
    sc.score                AS 成绩,
    fn_get_grade(sc.score)  AS 等级
FROM sc
JOIN student ON sc.Sno = student.Sno
ORDER BY sc.score DESC;
-- ============================================================
--  第二部分:循环结构
-- ============================================================
/*
  【导语】
  完成分支结构的学习后,我们来看另一类重要结构——循环。
  MySQL提供了三种循环语句:WHILE、REPEAT、LOOP
  它们都必须在 BEGIN...END 中使用(即存储过程/函数内)。

  循环控制关键字:
  ┌──────────┬──────────────────────────────┐
  │ ITERATE  │ 跳过本次,直接进入下一次循环 │
  │          │ (相当于其他语言的 continue)│
  ├──────────┼──────────────────────────────┤
  │ LEAVE    │ 退出整个循环                 │
  │          │ (相当于其他语言的 break)   │
  └──────────┴──────────────────────────────┘
*/


-- ------------------------------------------------------------
-- 知识点4:WHILE 循环(先判断,后执行)
-- ------------------------------------------------------------
/*
  【讲解】
  语法:
  ─────────────────────────────────────
  [标签:] WHILE 循环条件 DO
      循环体;
  END WHILE [标签];
  ─────────────────────────────────────

  执行流程:
  1. 先检查 循环条件
  2. 条件为TRUE → 执行循环体 → 回到步骤1
  3. 条件为FALSE → 退出循环

  类比:while(条件) { 循环体 }

  ⚠️ 注意:若初始条件就为FALSE,循环体一次都不会执行。
*/

-- 【演示4-1】WHILE 无控制语句:计算 1+2+3+...+100
sql
DROP PROCEDURE IF EXISTS proc_while1;
CREATE PROCEDURE proc_while1()
BEGIN
    DECLARE i INT DEFAULT 1;   -- 循环变量
    DECLARE s INT DEFAULT 0;   -- 累加和
    WHILE i <= 100 DO
        SET s = s + i;
        SET i = i + 1;
    END WHILE;

    SELECT s AS '1到100的累加和';
END;
sql
CALL proc_while1();   -- 期望结果:5050
-- 【演示4-2】WHILE 使用传入/传出参数:计算 1+2+...+n
sql
DROP PROCEDURE IF EXISTS proc_while2;
CREATE PROCEDURE proc_while2(IN n INT, OUT s INT)
BEGIN
    DECLARE i INT DEFAULT 1;
    SET s = 0;
    WHILE i <= n DO
        SET s = s + i;
        SET i = i + 1;
    END WHILE;
END;

-- 调用:计算1到100的和
sql
CALL proc_while2(100, @result);
SELECT @result AS '1到n的累加和';
-- 【演示4-3】WHILE + ITERATE(跳过偶数,只累加奇数)
sql
DROP PROCEDURE IF EXISTS proc_while3;
CREATE PROCEDURE proc_while3()
BEGIN
    DECLARE i INT DEFAULT 0;
    DECLARE s INT DEFAULT 0;
    loop100:WHILE i < 100 DO
        SET i = i + 1;
        IF i % 2 = 0 THEN
            ITERATE loop100; -- 必须加循环标签
        END IF;
        SET s = s + i;
    END WHILE;

    SELECT s AS '1到100奇数之和';
END;
sql
CALL proc_while3();
-- ------------------------------------------------------------
-- 知识点5:REPEAT 循环(先执行,后判断)
-- ------------------------------------------------------------
/*
  【讲解】
  语法:
  ─────────────────────────────────────
  [标签:] REPEAT
      循环体;
  UNTIL 结束循环的条件      -- 注意:条件成立时【退出】!
  END REPEAT [标签];
  ─────────────────────────────────────

  执行流程:
  1. 先执行循环体
  2. 检查 UNTIL 后的条件
  3. 条件为TRUE → 退出循环(与WHILE相反!)
  4. 条件为FALSE → 回到步骤1继续

  类比:do { 循环体 } while(!条件)  (注意取反!)

  ⚠️ 关键区别:
  - WHILE:条件为TRUE时【继续】循环
  - REPEAT:UNTIL条件为TRUE时【退出】循环

  ⚠️ 特点:循环体至少执行一次(先做再判断)
*/

-- 【演示5-1】REPEAT:计算 1+2+3+...+100
sql
DROP PROCEDURE IF EXISTS proc_repeat1;
CREATE PROCEDURE proc_repeat1()
BEGIN
    DECLARE i INT DEFAULT 1;
    DECLARE s INT DEFAULT 0;
    add100: REPEAT
        SET s = s + i;
        SET i = i + 1;
    UNTIL i > 100           -- 当 i > 100 时退出
    END REPEAT add100;

    SELECT s AS '1到100的累加和(REPEAT)';
END;
sql
CALL proc_repeat1();   -- 期望结果:5050
-- ------------------------------------------------------------
-- 知识点6:LOOP 循环(无限循环 + LEAVE 退出)
-- ------------------------------------------------------------
/*
  【讲解】
  语法:
  ─────────────────────────────────────
  [标签:] LOOP
      IF 退出条件 THEN
          LEAVE [标签];   -- 退出循环
      END IF;
      循环体;
  END LOOP [标签];
  ─────────────────────────────────────

  特点:
  - LOOP 本身不带条件,是个"死循环框架"
  - 必须配合 LEAVE 才能退出,否则无限循环!
  - 退出条件判断 通常放在循环体最开始

  类比:while(true) { if(条件) break; 循环体 }

  ⚠️ 使用规范:
  - 必须给 LOOP 加标签(add100、main_loop 等)
  - LEAVE 后面跟标签名,不能省略
*/

-- 【演示6-1】LOOP:计算 1+2+3+...+100
sql
DROP PROCEDURE IF EXISTS proc_loop1;
CREATE PROCEDURE proc_loop1()
BEGIN
    DECLARE i INT DEFAULT 1;
    DECLARE s INT DEFAULT 0;
    add100: LOOP
        IF i > 100 THEN
            LEAVE add100;   -- 退出循环
        END IF;
        SET s = s + i;
        SET i = i + 1;
    END LOOP add100;

    SELECT s AS '1到100的累加和(LOOP)';
END;
sql
CALL proc_loop1();   -- 结果:5050
-- ------------------------------------------------------------
-- 三种循环对比总结(重点!)
-- ------------------------------------------------------------
/*
  ┌────────────┬─────────────────┬────────────────┬────────────────────────────┐
  │  循环类型  │  执行顺序       │  退出条件时机  │  特点一句话                │
  ├────────────┼─────────────────┼────────────────┼────────────────────────────┤
  │  WHILE     │ 先判断,后执行  │ 条件为FALSE时  │ 最常用,类似其他语言while  │
  │  REPEAT    │ 先执行,后判断  │ UNTIL条件TRUE  │ 至少执行一次,类似do-while │
  │  LOOP      │ 先执行(无判断)│ LEAVE手动退出  │ 死循环框架,必须LEAVE退出  │
  └────────────┴─────────────────┴────────────────┴────────────────────────────┘

  LOOP:  死循环框架,必须手动 LEAVE 退出
  WHILE: 先判断,后执行,条件成立才循环
  REPEAT:先执行,后判断,UNTIL条件成立就退出
*/


-- ============================================================
--  第三部分:综合实战 — 批量生成学生成绩测试数据
-- ============================================================
/*
  【任务背景】(口述)
  ─────────────────────────────────────────────────────────
  在真实项目中,往往需要大量测试数据来验证功能。
  手动一条一条插入太慢,用循环自动生成100条——这是循环最常见的实际应用。

  下面我们创建一张模拟成绩表,用循环向其中批量插入测试数据。
  ─────────────────────────────────────────────────────────

  需求:创建 t_stu_test 模拟成绩表,向其中批量插入测试数据
  规则:
    - 学号:T001 ~ Tn(LPAD格式化补零)
    - 姓名:测试学生001 ~ 测试学生n
    - 课程:固定为 'MySQL数据库'
    - 成绩:50~100分随机
    - 考试日期:2026-01-01 ~ 2026-06-30 之间随机
*/

-- 创建模拟成绩测试表(独立表,无外键,方便批量插入演示)
sql
DROP TABLE IF EXISTS t_stu_test;
CREATE TABLE t_stu_test (
    id        INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
    stu_no    VARCHAR(10)  NOT NULL COMMENT '学号',
    stu_name  VARCHAR(20)  NOT NULL COMMENT '姓名',
    subject   VARCHAR(20)  NOT NULL COMMENT '科目',
    score     DECIMAL(5,1) NOT NULL COMMENT '成绩',
    exam_date DATE         NOT NULL COMMENT '考试日期'
) COMMENT='学生成绩测试表(用于循环批量插入演示)';
-- 【实战演示】使用 WHILE 批量插入100条学生成绩数据
sql
DROP PROCEDURE IF EXISTS proc_gen_test_scores;
CREATE PROCEDURE proc_gen_test_scores(IN total INT)
BEGIN
    DECLARE i INT DEFAULT 1;
    -- 清空旧数据(仅用于测试环境)
    TRUNCATE TABLE t_stu_test;

    -- 循环插入数据
    WHILE i <= total DO
        INSERT INTO t_stu_test (stu_no, stu_name, subject, score, exam_date)
        VALUES (
            CONCAT('T', LPAD(i, 3, '0')),              -- 学号:T001, T002...
            CONCAT('测试学生', LPAD(i, 3, '0')),        -- 姓名:测试学生001...
            'MySQL数据库',                                -- 固定科目
            ROUND(50 + RAND() * 50, 1),                  -- 成绩:50~100随机,保留1位小数
            DATE_ADD('2026-01-01',
                INTERVAL FLOOR(RAND() * 180) DAY)        -- 日期:2026-01-01起180天内随机
        );
        SET i = i + 1;
    END WHILE;

    -- 输出插入结果统计
    SELECT
        COUNT(*)              AS 总记录数,
        MIN(score)            AS 最低分,
        MAX(score)            AS 最高分,
        ROUND(AVG(score), 2)  AS 平均分
    FROM t_stu_test;
END;

-- 调用:生成100条测试数据
sql
CALL proc_gen_test_scores(100);
-- 查看插入的数据(前20条)
sql
SELECT * FROM t_stu_test LIMIT 20;
-- 结合 fn_get_grade:查询所有学生成绩与等级
sql
SELECT
    stu_no                  AS 学号,
    stu_name                AS 姓名,
    subject                 AS 科目,
    score                   AS 成绩,
    fn_get_grade(score)     AS 等级,
    exam_date               AS 考试日期
FROM t_stu_test
ORDER BY score DESC
LIMIT 20;
-- 统计各等级人数分布
sql
SELECT
    fn_get_grade(score)    AS 等级,
    COUNT(*)               AS 人数,
    ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM t_stu_test), 1) AS '占比(%)'
FROM t_stu_test
GROUP BY fn_get_grade(score)
ORDER BY MIN(score) DESC;
-- ============================================================
--  课堂练习(请同学独立完成)
-- ============================================================
/*
  【练习1】★ 基础练习 — IF结构
  使用 IF结构 创建存储函数 fn_bmi_level(bmi FLOAT):
  - bmi < 18.5   → 偏瘦
  - 18.5 ≤ bmi < 24  → 正常
  - 24 ≤ bmi < 28    → 偏胖
  - bmi ≥ 28     → 肥胖
  测试:SELECT fn_bmi_level(17.5), fn_bmi_level(22), fn_bmi_level(26), fn_bmi_level(30);

  ─────────────────────────────────────────────────────────────

  【练习2】★ CASE结构练习
  基于 sc 表,用 CASE结构 查询每位学生各门课程的成绩等级,
  显示格式:学号、姓名、课程号、成绩、等级
  (提示:结合 student 表查姓名,结合 fn_get_grade 函数)

  ─────────────────────────────────────────────────────────────

  【练习3】★ 循环练习
  创建存储过程 proc_sum_even(IN n INT, OUT s INT):
  - 使用 WHILE + ITERATE 计算 1~n 中所有偶数之和
  - 调用:CALL proc_sum_even(100, @r); SELECT @r;(期望:2550)

  ─────────────────────────────────────────────────────────────

  【练习4】★★ 综合练习
  修改 proc_gen_test_scores,使其支持指定科目名称和分数范围:
  CREATE PROCEDURE proc_gen_scores2(
      IN total     INT,
      IN sub_name  VARCHAR(20),
      IN min_score INT,
      IN max_score INT
  )
  向 t_stu_test 表批量插入指定数量的数据,
  科目固定为 sub_name,成绩在 [min_score, max_score] 范围内随机生成。

  调用示例:生成50条 Python编程 科目成绩,分数在60~100之间
  CALL proc_gen_scores2(50, 'Python编程', 60, 100);
*/


-- ============================================================
--  练习参考答案
-- ============================================================

-- 练习1 答案
sql
DROP FUNCTION IF EXISTS fn_bmi_level;
CREATE FUNCTION fn_bmi_level(bmi FLOAT)
RETURNS VARCHAR(10)
DETERMINISTIC
BEGIN
    DECLARE level VARCHAR(10);
    IF bmi < 18.5 THEN
        SET level = '偏瘦';
    ELSEIF bmi < 24 THEN
        SET level = '正常';
    ELSEIF bmi < 28 THEN
        SET level = '偏胖';
    ELSE
        SET level = '肥胖';
    END IF;
    RETURN level;
END;
SELECT fn_bmi_level(17.5) AS BMI17_5,
       fn_bmi_level(22)   AS BMI22,
       fn_bmi_level(26)   AS BMI26,
       fn_bmi_level(30)   AS BMI30;
-- 练习2 答案(CASE结构)
sql
SELECT
    sc.Sno                  AS 学号,
    student.Sname           AS 姓名,
    sc.Cno                   AS 课程号,
    sc.score                AS 成绩,
    CASE
        WHEN sc.score >= 90 THEN '优秀'
        WHEN sc.score >= 75 THEN '良好'
        WHEN sc.score >= 60 THEN '及格'
        ELSE '不及格'
    END AS 等级
FROM sc
JOIN student ON sc.Sno = student.Sno
ORDER BY sc.Sno, sc.Cno;
-- 练习3 答案
sql
DROP PROCEDURE IF EXISTS proc_sum_even;
CREATE PROCEDURE proc_sum_even(IN n INT, OUT s INT)
BEGIN
    DECLARE i INT DEFAULT 0;
    SET s = 0;
    even_loop: WHILE i < n DO
        SET i = i + 1;
        IF i % 2 <> 0 THEN
            ITERATE even_loop;   -- 奇数跳过
        END IF;
        SET s = s + i;           -- 偶数累加
    END WHILE even_loop;
END;
sql
CALL proc_sum_even(100, @r);
SELECT @r AS '1到100偶数之和';   -- 期望:2550
-- 练习4 答案
sql
DROP PROCEDURE IF EXISTS proc_gen_scores2;
CREATE PROCEDURE proc_gen_scores2(
    IN total     INT,
    IN sub_name   VARCHAR(20),
    IN min_score  INT,
    IN max_score  INT
)
BEGIN
    DECLARE i INT DEFAULT 1;
    WHILE i <= total DO
        INSERT INTO t_stu_test (stu_no, stu_name, subject, score, exam_date)
        VALUES (
            CONCAT('T', LPAD(i, 3, '0')),
            CONCAT('测试学生', LPAD(i, 3, '0')),
            sub_name,
            ROUND(min_score + RAND() * (max_score - min_score), 1),
            CURDATE()
        );
        SET i = i + 1;
    END WHILE;

    SELECT COUNT(*) AS 已插入记录数 FROM t_stu_test;
END;

-- 调用示例:生成50条 Python编程 科目成绩,分数在60~100之间
sql
CALL proc_gen_scores2(50, 'Python编程', 60, 100);
-- ============================================================
--  课堂小结(教师口述要点)
-- ============================================================
/*
  今天学习了流程控制结构,重点掌握:

  【分支】
  ✅ IF(条件, 值1, 值2)   → 双分支,SELECT中用
  ✅ CASE...WHEN...END    → 多分支,SELECT中或BEGIN END中用(推荐)
  ✅ IF...ELSEIF...END IF → 多分支,只能在 BEGIN END 中用

  【循环】
  ✅ WHILE 先判断,后执行   → 最常用
  ✅ REPEAT 先执行,后判断  → UNTIL条件成立时退出(注意方向)
  ✅ LOOP 死循环框架         → 必须用 LEAVE 手动退出

  【循环控制】
  ✅ LEAVE   → break  退出循环
  ✅ ITERATE → continue  跳本次,继续下一次

  【实战应用】
  ✅ 用循环批量插入测试数据(真实开发中常见操作)

  【课后作业】
  1. 完成四道课堂练习
  2. 尝试使用 REPEAT 循环改写批量插入存储过程
  3. 使用 LOOP 循环计算 1~50 的所有奇数之和
  4. 预习下一节:游标的使用

================================================================================
  END OF LESSON DESIGN
================================================================================
*/