MySQL不看视频 · SSH 实操

第六单元 · 四类内置函数 · 第 14 / 21 节

日期与流程函数

建 lesson14_func(5 行,含 NULL 日期与离职日期),用 YEAR / MONTH / DAY / DATE_ADD / DATEDIFF / DATE_FORMAT 做日期计算,全部以固定基准 2024-07-01 为准;再用 IF / IFNULL / NULLIF / CASE 在结果里做判断,讲清 NULL 条件、CASE 顺序与 ELSE 的作用,最后合成一张离职月份 + 绩效档的报表。

2 个步骤 · 进度只存本机

约 22 分钟。上一节的函数处理文字和数字,这一节处理两件更实际的事:日期怎么加减、怎么算相差几天;以及“如果满足条件就显示 A,否则显示 B”这种判断怎么写在查询里。所有日期计算都用一个固定基准日 2024-07-01,这样你每次跑出来的数字都一样。

SSH / Linux 终端MySQL 终端输出示例(只读)复制按钮只复制命令,不含 $ / mysql> 提示符和输出。

已完成 0 / 2 步

你现在在哪个提示符

应该在 mysql>。断了就先回到学习终端(PowerShell):

SSH / Linux 终端bash
ssh root@8.148.204.152
SSH / Linux 终端bash
mysql-learning

进去以后确认当前库:

MySQL 终端sql
SELECT DATABASE();
输出示例(只读,不要粘贴执行)text不可复制
+------------+
| DATABASE() |
+------------+
| itcast     |
+------------+
1 row in set (0.00 sec)

显示 itcast 就对了。如果是 NULL,执行 USE itcast;——它只切换当前连接用哪个库,不动数据。

第一步:日期函数:加减、相差、格式化

先建这一节的表。5 个人,有的人有入职日期、有的人没有;有的人已经离职,离职日期才有值:

MySQL 终端sql
SHOW TABLES LIKE 'lesson14_func';
输出示例(只读,不要粘贴执行)text不可复制
Empty set (0.00 sec)

Empty set 说明可以建;如果已经有一行,跳到下面的 SELECT 核对 5 行,不要删表重建。

MySQL 终端sql
CREATE TABLE lesson14_func (
  id INT PRIMARY KEY COMMENT '编号',
  name VARCHAR(20) NOT NULL COMMENT '姓名',
  joined DATE COMMENT '入职日期',
  left_at DATE COMMENT '离职日期',
  score DECIMAL(4,1) COMMENT '绩效分',
  dept VARCHAR(20) COMMENT '部门'
) COMMENT='本教程第六单元日期/流程函数练习表';
输出示例(只读,不要粘贴执行)text不可复制
Query OK, 0 rows affected (0.03 sec)
MySQL 终端sql
INSERT INTO lesson14_func (id, name, joined, left_at, score, dept) VALUES
  (1, '张伟', '2024-03-02', NULL, 88.5, '研发'),
  (2, '李娜', '2023-11-20', '2024-06-30', 92.0, '销售'),
  (3, '王强', '2024-02-29', NULL, NULL, '研发'),
  (4, '赵敏', NULL, NULL, 79.5, '行政'),
  (5, '陈晨', '2024-01-15', '2024-05-10', 85.0, NULL);
输出示例(只读,不要粘贴执行)text不可复制
Query OK, 5 rows affected (0.01 sec)
Records: 5  Duplicates: 0  Warnings: 0
MySQL 终端sql
SELECT id, name, joined, left_at, score, dept FROM lesson14_func ORDER BY id;
输出示例(只读,不要粘贴执行)text不可复制
+----+--------+------------+------------+-------+--------+
| id | name   | joined     | left_at    | score | dept   |
+----+--------+------------+------------+-------+--------+
|  1 | 张伟   | 2024-03-02 | NULL       |  88.5 | 研发   |
|  2 | 李娜   | 2023-11-20 | 2024-06-30 |  92.0 | 销售   |
|  3 | 王强   | 2024-02-29 | NULL       |  NULL | 研发   |
|  4 | 赵敏   | NULL       | NULL       |  79.5 | 行政   |
|  5 | 陈晨   | 2024-01-15 | 2024-05-10 |  85.0 | NULL   |
+----+--------+------------+------------+-------+--------+
5 rows in set (0.00 sec)

王强的入职日期是 2024-02-29——2024 年是闰年,2 月有 29 号;赵敏没有入职日期;张伟、王强还在职,所以 left_at 是 NULL。

取出年、月、日:YEAR / MONTH / DAY

MySQL 终端sql
SELECT id, joined, YEAR(joined) AS 年, MONTH(joined) AS 月, DAY(joined) AS 日
FROM lesson14_func ORDER BY id;
输出示例(只读,不要粘贴执行)text不可复制
+----+------------+------+------+------+
| id | joined     | 年   | 月   | 日   |
+----+------------+------+------+------+
|  1 | 2024-03-02 | 2024 |    3 |    2 |
|  2 | 2023-11-20 | 2023 |   11 |   20 |
|  3 | 2024-02-29 | 2024 |    2 |   29 |
|  4 | NULL       | NULL | NULL | NULL |
|  5 | 2024-01-15 | 2024 |    1 |   15 |
+----+------------+------+------+------+
5 rows in set (0.00 sec)

三个函数各取一段,返回的是数字(不是字符串),可以直接比较大小或做运算。赵敏没有日期,三列都是 NULL。

加减日期:DATE_ADD / DATE_SUB 和 INTERVAL

MySQL 终端sql
SELECT id, joined,
       DATE_ADD(joined, INTERVAL 3 MONTH) AS 转正日,
       DATEDIFF('2024-07-01', joined) AS 到结算日的天数
FROM lesson14_func ORDER BY id;
输出示例(只读,不要粘贴执行)text不可复制
+----+------------+------------+-----------------------+
| id | joined     | 转正日     | 到结算日的天数        |
+----+------------+------------+-----------------------+
|  1 | 2024-03-02 | 2024-06-02 |                   121 |
|  2 | 2023-11-20 | 2024-02-20 |                   224 |
|  3 | 2024-02-29 | 2024-05-29 |                   123 |
|  4 | NULL       | NULL       |                  NULL |
|  5 | 2024-01-15 | 2024-04-15 |                   168 |
+----+------------+------------+-----------------------+
5 rows in set (0.00 sec)

INTERVAL 3 MONTH 是“加 3 个月”,常用来算 3 个月试用期后的转正日。DATEDIFF(后面的日期, 前面的日期) 返回两个日期相差的天数——注意参数顺序:DATEDIFF('2024-07-01', joined) 是“结算日减入职日”,所以是正数;写反了就会得到负数。这里没有用 CURDATE(),而是写死 2024-07-01,就是为了让今天、明天、明年跑出来的结果都一样。

格式化:DATE_FORMAT 把日期变成你要的样子

MySQL 终端sql
SELECT id, joined, DATE_FORMAT(joined, '%Y年%m月%d日') AS 中文日期,
       DATE_FORMAT(joined, '%Y-%m') AS 年月
FROM lesson14_func ORDER BY id;
输出示例(只读,不要粘贴执行)text不可复制
+----+------------+-------------------+---------+
| id | joined     | 中文日期          | 年月    |
+----+------------+-------------------+---------+
|  1 | 2024-03-02 | 2024年03月02日    | 2024-03 |
|  2 | 2023-11-20 | 2023年11月20日    | 2023-11 |
|  3 | 2024-02-29 | 2024年02月29日    | 2024-02 |
|  4 | NULL       | NULL              | NULL    |
|  5 | 2024-01-15 | 2024年01月15日    | 2024-01 |
+----+------------+-------------------+---------+
5 rows in set (0.00 sec)

%Y 是四位年份、%m 是两位月份、%d 是两位日期;%Y-%m 只保留年月,常用于按月汇总。格式化结果是字符串,不再是日期类型——再拿它做日期加减就不行了。

两个日期相减:算在职天数

MySQL 终端sql
SELECT id, joined, left_at, DATEDIFF(left_at, joined) AS 在职天数
FROM lesson14_func ORDER BY id;
输出示例(只读,不要粘贴执行)text不可复制
+----+------------+------------+--------------+
| id | joined     | left_at    | 在职天数     |
+----+------------+------------+--------------+
|  1 | 2024-03-02 | NULL       |         NULL |
|  2 | 2023-11-20 | 2024-06-30 |          223 |
|  3 | 2024-02-29 | NULL       |         NULL |
|  4 | NULL       | NULL       |         NULL |
|  5 | 2024-01-15 | 2024-05-10 |          116 |
+----+------------+------------+--------------+
5 rows in set (0.00 sec)

只有李娜和陈晨两行有结果(223 天、116 天),其余是 NULL——因为 left_at 是 NULL,DATEDIFF 拿到 NULL 就返回 NULL。这和第 13 节的规则是同一条:NULL 进,NULL 出。陈晨的部门也是 NULL,本节第二步会用 IFNULL 给它一个显示值。

月末与跨月:DATE_ADD 会“贴”到月末

MySQL 终端sql
SELECT DATE_ADD('2024-01-31', INTERVAL 1 MONTH) AS 一月末加一月,
       LAST_DAY('2024-02-10') AS 二月最后一天,
       DATE_SUB('2024-03-01', INTERVAL 1 DAY) AS 前一天;
输出示例(只读,不要粘贴执行)text不可复制
+--------------------+--------------------+------------+
| 一月末加一月       | 二月最后一天       | 前一天     |
+--------------------+--------------------+------------+
| 2024-02-29         | 2024-02-29         | 2024-02-29 |
+--------------------+--------------------+------------+
1 row in set (0.00 sec)

2024-01-31 加一个月不是 2024-02-31(不存在),MySQL 把它贴到 2 月最后一天 2024-02-29;LAST_DAY 直接给某月的最后一天。这是日期函数和数字函数最不一样的地方:月份长度不固定,结果会按日历调整。

今天和此刻:CURDATE() / NOW() 每次都变

MySQL 终端sql
SELECT CURDATE() AS 今天, NOW() AS 此刻;
输出示例(只读,不要粘贴执行)text不可复制
+------------+---------------------+
| 今天       | 此刻                |
+------------+---------------------+
| 2026-10-27 | 2026-10-27 04:27:29 |
+------------+---------------------+
1 row in set (0.00 sec)

上面是抓取当天的真实输出。你执行时会看到你自己的日期和时间——这两个函数的输出不是定值,所以本节的练习和验收都不依赖它们,全部改用固定日期 2024-07-01。

第二步:流程函数:在查询里写“如果……就……”

IF:三参数,条件为真取第二个,否则取第三个

MySQL 终端sql
SELECT id, score, IF(score >= 85, '达标', '未达标') AS 是否达标
FROM lesson14_func ORDER BY id;
输出示例(只读,不要粘贴执行)text不可复制
+----+-------+--------------+
| id | score | 是否达标     |
+----+-------+--------------+
|  1 |  88.5 | 达标         |
|  2 |  92.0 | 达标         |
|  3 |  NULL | 未达标       |
|  4 |  79.5 | 未达标       |
|  5 |  85.0 | 达标         |
+----+-------+--------------+
5 rows in set (0.00 sec)

88.5、92.0、85.0 都是“达标”,79.5 是“未达标”。王强的 score 是 NULL,结果是“未达标”——**条件 NULL >= 85 的结果是 NULL,不是真,IF 走 else 分支**。这一点和很多人以为的“NULL 会报错”不同,它只是不满足条件。

IFNULL:是 NULL 就换成一个默认值

MySQL 终端sql
SELECT id, left_at, IFNULL(left_at, '在职') AS 状态
FROM lesson14_func ORDER BY id;
输出示例(只读,不要粘贴执行)text不可复制
+----+------------+------------+
| id | left_at    | 状态       |
+----+------------+------------+
|  1 | NULL       | 在职       |
|  2 | 2024-06-30 | 2024-06-30 |
|  3 | NULL       | 在职       |
|  4 | NULL       | 在职       |
|  5 | 2024-05-10 | 2024-05-10 |
+----+------------+------------+
5 rows in set (0.00 sec)

IFNULL(值, 默认值):值不是 NULL 就原样返回,是 NULL 就返回第二个参数。它只判断 NULL,不判断空字符串——空字符串 '' 不是 NULL,会被原样返回。

NULLIF:相等就变成 NULL

MySQL 终端sql
SELECT NULLIF(5, 5) AS 相等变NULL, NULLIF(5, 4) AS 不等保留, NULLIF('研发', '研发') AS 文本相等变NULL;
输出示例(只读,不要粘贴执行)text不可复制
+---------------+--------------+---------------------+
| 相等变NULL    | 不等保留     | 文本相等变NULL      |
+---------------+--------------+---------------------+
|          NULL |            5 | NULL                |
+---------------+--------------+---------------------+
1 row in set (0.00 sec)

NULLIF(a, b) 在 a = b 时返回 NULL,否则返回 a。它常用来“把某个特殊值清成 NULL”,也可以放在除法分母上防止除以 0。

MySQL 终端sql
SELECT id, dept, NULLIF(dept, '研发') AS 非研发部门
FROM lesson14_func ORDER BY id;
输出示例(只读,不要粘贴执行)text不可复制
+----+--------+-----------------+
| id | dept   | 非研发部门      |
+----+--------+-----------------+
|  1 | 研发   | NULL            |
|  2 | 销售   | 销售            |
|  3 | 研发   | NULL            |
|  4 | 行政   | 行政            |
|  5 | NULL   | NULL            |
+----+--------+-----------------+
5 rows in set (0.00 sec)

研发部两行变成 NULL;销售、行政原样保留;陈晨的部门本来就是 NULL,结果还是 NULL。

CASE:多分支,比嵌套 IF 好读

MySQL 终端sql
SELECT id, score,
       CASE WHEN score >= 90 THEN 'A'
            WHEN score >= 80 THEN 'B'
            ELSE 'C' END AS 等级
FROM lesson14_func ORDER BY id;
输出示例(只读,不要粘贴执行)text不可复制
+----+-------+--------+
| id | score | 等级   |
+----+-------+--------+
|  1 |  88.5 | B      |
|  2 |  92.0 | A      |
|  3 |  NULL | C      |
|  4 |  79.5 | C      |
|  5 |  85.0 | B      |
+----+-------+--------+
5 rows in set (0.00 sec)

这是“搜索式 CASE”:从上往下判断,第一个为真的分支生效。92.0 是 A;88.5 和 85.0 是 B;79.5 是 C;王强的 NULL 一个条件都不满足,落到 ELSE 'C'。**如果连 ELSE 都不写,不满足任何分支时结果是 NULL**,不会报错。

MySQL 终端sql
SELECT id, dept,
       CASE dept WHEN '研发' THEN '技术线'
                 WHEN '销售' THEN '业务线'
                 ELSE '其他' END AS 序列
FROM lesson14_func ORDER BY id;
输出示例(只读,不要粘贴执行)text不可复制
+----+--------+-----------+
| id | dept   | 序列      |
+----+--------+-----------+
|  1 | 研发   | 技术线    |
|  2 | 销售   | 业务线    |
|  3 | 研发   | 技术线    |
|  4 | 行政   | 其他      |
|  5 | NULL   | 其他      |
+----+--------+-----------+
5 rows in set (0.00 sec)

这是“简单 CASE”:拿 dept 的值逐个和 WHEN 后面的值比相等。陈晨的 dept 是 NULL,NULL 和谁都不相等,所以落到 ELSE '其他'——如果想让 NULL 单独显示“未分配”,要像上一节那样先写 WHEN dept IS NULL THEN ...。

合起来:一条查询给出离职月份和绩效档

MySQL 终端sql
SELECT name,
       IFNULL(DATE_FORMAT(left_at, '%Y-%m'), '在职') AS 离职月份,
       CASE WHEN score IS NULL THEN '待评'
            WHEN score >= 85 THEN '高'
            ELSE '普通' END AS 绩效档
FROM lesson14_func ORDER BY id;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------------+-----------+
| name   | 离职月份     | 绩效档    |
+--------+--------------+-----------+
| 张伟   | 在职         | 高        |
| 李娜   | 2024-06      | 高        |
| 王强   | 在职         | 待评      |
| 赵敏   | 在职         | 普通      |
| 陈晨   | 2024-05      | 高        |
+--------+--------------+-----------+
5 rows in set (0.00 sec)

三层嵌套:DATE_FORMAT(NULL, ...) 先返回 NULL,IFNULL 把它换成“在职”;CASE 的第一支专门处理 score IS NULL,所以王强显示“待评”而不是被当成低分。**判断 NULL 要用 IS NULL,不能写 score = NULL**——后者永远不成立。

小结:P31 函数小结

函数按“处理什么”分组记:处理文字找 CONCAT/LENGTH/SUBSTRING/REPLACE,处理数字找 ROUND/CEIL/FLOOR/MOD,处理日期找 DATE_ADD/DATEDIFF/DATE_FORMAT,需要在结果里做判断就用 IF/IFNULL/NULLIF/CASE。它们都只影响这次查询显示的值,不会改表里的数据。真正要落盘得用 UPDATE,那是另一节的内容。选函数时先看列的类型,再看要不要处理 NULL,最后才考虑格式。

常见报错怎么办

看到意思怎么办
1292 Incorrect date value日期不存在(如 2 月 30 日)或格式不对换成真实存在的日期;DATE 列只接受 YYYY-MM-DD 这类可识别写法
1582 Incorrect parameter count函数参数个数不对DATEDIFF 要两个日期,DATE_ADD 要“日期 + INTERVAL 单位”
说明 刻意写错的例子:插入一个不存在的日期
MySQL 终端sql
INSERT INTO lesson14_func (id, name, joined) VALUES (6, '测试', '2024-02-30');
输出示例(只读,不要粘贴执行)text不可复制
ERROR 1292 (22007): Incorrect date value: '2024-02-30' for column 'joined' at row 1

2024 年 2 月没有 30 号,严格模式直接拒绝,整行都不会插入。NULL 可以插(表示“不知道”),但不存在的日期不行——这是两回事。

说明 刻意写错的例子:DATEDIFF 只给了一个日期
MySQL 终端sql
SELECT DATEDIFF('2024-07-01') AS 只给一个日期;
输出示例(只读,不要粘贴执行)text不可复制
ERROR 1582 (42000): Incorrect parameter count in the call to native function 'DATEDIFF'

DATEDIFF 要两个日期参数,少一个就是 1582(参数个数不对),不是“日期格式错”。格式错通常报 1292。

练习

先自己做,再展开提示、答案和常见错。

基础 1. 列出每个人的入职年份和月份。

提示 提示

两个函数分别取年和月,年份用 YEAR,月份用 MONTH。

参考答案 答案
MySQL 终端sql
SELECT name, YEAR(joined) AS 年, MONTH(joined) AS 月 FROM lesson14_func ORDER BY id;
输出示例(只读,不要粘贴执行)text不可复制
+--------+------+------+
| name   | 年   | 月   |
+--------+------+------+
| 张伟   | 2024 |    3 |
| 李娜   | 2023 |   11 |
| 王强   | 2024 |    2 |
| 赵敏   | NULL | NULL |
| 陈晨   | 2024 |    1 |
+--------+------+------+
5 rows in set (0.00 sec)
说明 常见错

用 DATE_FORMAT(joined, '%Y-%m') 也能显示年月,但结果是字符串;题目要的是两个数字列,后面要比较大小就得用 YEAR/MONTH。

**基础 2. 以 2024-07-01 为结算日,每个人已经入职多少天?没有入职日期的怎么显示?**

提示 提示

DATEDIFF(结算日, 入职日);没有入职日期的那行自然是 NULL。

参考答案 答案
MySQL 终端sql
SELECT name, DATEDIFF('2024-07-01', joined) AS 到结算日天数
FROM lesson14_func ORDER BY id;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------------------+
| name   | 到结算日天数       |
+--------+--------------------+
| 张伟   |                121 |
| 李娜   |                224 |
| 王强   |                123 |
| 赵敏   |               NULL |
| 陈晨   |                168 |
+--------+--------------------+
5 rows in set (0.00 sec)
说明 常见错

把参数顺序写反成 DATEDIFF(joined, '2024-07-01'),会得到负数;或者用 CURDATE() 代替固定日期,明天再跑数字就变了。

进阶 3. 没有部门的显示“未分配”,其余显示原部门。

提示 提示

只判断 NULL 就够了,用 IFNULL。

参考答案 答案
MySQL 终端sql
SELECT name, IFNULL(dept, '未分配') AS 部门 FROM lesson14_func ORDER BY id;
输出示例(只读,不要粘贴执行)text不可复制
+--------+-----------+
| name   | 部门      |
+--------+-----------+
| 张伟   | 研发      |
| 李娜   | 销售      |
| 王强   | 研发      |
| 赵敏   | 行政      |
| 陈晨   | 未分配    |
+--------+-----------+
5 rows in set (0.00 sec)
说明 常见错

用 CASE dept WHEN NULL THEN ... 处理 NULL——= NULL 永远不成立,这个分支永远不会进。判断 NULL 只能用 IS NULL 或 IFNULL。

进阶 4. 按绩效分分档:90 及以上 A,80–89 B,其余 C;没有分数的显示“待评”。

提示 提示

CASE 的第一支先处理 score IS NULL,再写分数区间;顺序不能反。

参考答案 答案
MySQL 终端sql
SELECT name,
       CASE WHEN score IS NULL THEN '待评'
            WHEN score >= 90 THEN 'A'
            WHEN score >= 80 THEN 'B'
            ELSE 'C' END AS 等级
FROM lesson14_func ORDER BY id;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+
| name   | 等级   |
+--------+--------+
| 张伟   | B      |
| 李娜   | A      |
| 王强   | 待评   |
| 赵敏   | C      |
| 陈晨   | B      |
+--------+--------+
5 rows in set (0.00 sec)
说明 常见错

把 WHEN score >= 80 写在 WHEN score >= 90 前面,92 分也会先命中 80 那一支变成 B。CASE 是从上往下第一个命中就停。

**概念 5. 为什么 IF(score >= 85, '达标', '未达标') 对王强显示“未达标”,而不是报错或显示 NULL?**

提示 提示

先想 NULL >= 85 的结果是什么。

参考答案 答案

NULL >= 85 的结果是 NULL(未知),IF 只把“真”当作满足条件,NULL 不算真,所以走 else 分支,得到“未达标”。这不是错误,也不是 NULL 传播——IF 的两个返回值本身都不是 NULL。

说明 常见错

以为“NULL 参与比较会报错”。不会报错;它只是让条件不成立。要区分“没分数”和“分数低”,得像上一题那样先用 IS NULL 单独判断。

本单元验收

  1. YEAR('2024-02-29') 返回 2024,能说出为什么 2024 年有 2 月 29 日。
  2. DATEDIFF('2024-07-01', '2024-03-02') 返回 121,能说出参数顺序的含义。
  3. DATE_ADD('2024-01-31', INTERVAL 1 MONTH) 返回 2024-02-29,知道这是按日历贴到月末。
  4. IFNULL(left_at, '在职') 对 3 个 NULL 行显示“在职”,能说出它和 NULLIF 的区别。
  5. 能写出一个带 CASE 的两档判断,并解释为什么不写 ELSE 时可能得到 NULL。

做到这里

做到这里今天可以停。你现在能对日期取年月日、做加减、算相差天数、格式化成中文日期,也能用 IF/IFNULL/NULLIF/CASE 在结果里做判断,并且知道 NULL 在条件和函数里的行为。

函数这一单元到这里结束。下一节开始讲约束:怎么让数据库自己拦住不合法的数据(非空、唯一、主键、默认值)。

来源与边界

本节对应 BV1Kr4y1i7ru 平台 part 29(原标题「29. 基础-函数-日期函数」)、part 30(「30. 基础-函数-流程函数」)与 part 31(「31. 基础-函数-小结」,本页以小节承接),主题与同版讲义《MySQL-基础篇》3.3 日期函数(p57–59)、3.4 流程函数(p59)、3.1–3.4 小结(p53–61)对应;讲义为公开镜像,仅作事实核对。无逐字稿,未声称听过讲解。表名 lesson14_func、5 行数据、全部查询与练习均为本教程原创,未复制讲义正文;页面每段输出都在作者隔离的 MySQL 8.0.46 实例上逐条实跑,0.00 sec 是测试机耗时,你的屏幕可能不同。日期计算统一用固定基准 2024-07-01;CURDATE()/NOW() 的输出只展示抓取当次的样子,不作为定值。