RSS
菜单
全部文章快讯开发科技深度热点

第 9 章 常用函数大全

内容摘要

函数是 MySQL 把原始数据加工成业务结果的"工具箱",几乎每条稍复杂的 SELECT 都会用到它。本篇在前面章节学完增删改查、连接、子查询的基础上,系统梳理字符串、日期时间、数值、流程控制与加密哈希五大类常用函数。学完后你能独立完成数据清洗、格式化输出、时间差计算、空值兜底等日常开发任务。本示例基于 MySQL 8.0 语法。

📖 本篇简介:函数是 MySQL 把原始数据加工成业务结果的"工具箱",几乎每条稍复杂的 SELECT 都会用到它。本篇在前面章节学完增删改查、连接、子查询的基础上,系统梳理字符串、日期时间、数值、流程控制与加密哈希五大类常用函数。学完后你能独立完成数据清洗、格式化输出、时间差计算、空值兜底等日常开发任务。本示例基于 MySQL 8.0 语法。

目录

  • 1 为什么要学MySQL函数
  • 2 字符串函数
  • 3 日期与时间函数
  • 4 数值函数
  • 5 流程控制函数
  • 6 加密与哈希函数
  • 7 常用函数速查表
  • 8 常见坑与注意事项
  • 9 练习题

1 为什么要学MySQL函数

数据库里存的往往是"半成品":手机号是 13812345678,但页面要显示成 138****5678;下单时间是 2026-10-07 14:23:05,但报表只关心"哪一天";金额存的是 19.9,结算要按四舍五入取整。这些加工工作,既可以在应用层用 Python/Java 做,也可以直接用 SQL 函数在数据库层完成。

把计算下推到数据库层的好处很直接:减少网络传输量(只传加工后的结果)、利用数据库已有的索引与并行能力、让口径统一(多个报表用同一条 SQL,不会各算各的)。当然也不是什么计算都该塞进 SQL——涉及复杂业务规则、需要频繁迭代的逻辑,放在应用层更好维护。函数用得好,是效率;用得滥,是灾难。

MySQL 的函数大致分五类:

| 类别 | 作用 | 典型场景 |

|---|---|---|

| 字符串函数 | 拼接、截取、替换、大小写转换 | 脱敏、格式化、清洗 |

| 日期时间函数 | 取当前时间、加减时间、格式化 | 报表按天/月统计、超时判断 |

| 数值函数 | 四舍五入、取整、求模 | 金额结算、分页 |

| 流程控制函数 | 根据条件返回不同值 | 空值兜底、分类打标 |

| 加密哈希函数 | 单向散列、对称加密 | 密码存储、敏感数据 |

下面逐类展开。所有示例基于 MySQL 8.0,可在自己的测试库中直接执行。

2 字符串函数

字符串是业务里最常被加工的数据。先看一组最常用的:

-- 字符串常用函数演示(MySQL 8.0)
SELECT
  CONCAT('Hello', ' ', 'MySQL')   AS s1,   -- 拼接:Hello MySQL
  LENGTH('abc中文')                AS s2,   -- 字节数:utf8mb4 下为 9(3 个字母 3 字节 + 2 个中文 6 字节)
  CHAR_LENGTH('abc中文')           AS s3,   -- 字符数:6
  UPPER('abc')                     AS s4,   -- 转大写:ABC
  LOWER('ABC')                     AS s5,   -- 转小写:abc
  TRIM('  hi  ')                   AS s6,   -- 去首尾空格:hi
  SUBSTRING('hello world', 1, 5)   AS s7,   -- 从第 1 个字符取 5 个:hello
  REPLACE('a-b-c', '-', '_')       AS s8;   -- 替换:a_b_c

这里有个新手最容易踩的坑:LENGTH 和 CHAR_LENGTH 的区别。LENGTH 返回字节数,CHAR_LENGTH 返回字符数。一个中文在 utf8mb4 下占 3~4 个字节,所以统计"字符串有几个字"一定要用 CHAR_LENGTH。

下面用 Python + sqlite3 实跑验证字符级截取与替换的行为(sqlite3 的 LENGTH 按字符计,正好用来理解"字符数"这个概念):

# 用 sqlite3 实跑验证字符串函数行为(Python 3.14 + sqlite3 3.53)
import sqlite3
con = sqlite3.connect(':memory:')
cur = con.cursor()
rows = cur.execute('''
SELECT UPPER('mysql'), LOWER('MySQL'), LENGTH('abc中文'),
       SUBSTR('hello world',1,5), TRIM('  hi  '), REPLACE('a-b-c','-','_')
''').fetchall()
for r in rows:
    print(r)
con.close()

实跑输出:

('MYSQL', 'mysql', 5, 'hello', 'hi', 'a_b_c')

注意 sqlite3 里 LENGTH('abc中文') 得到 5(按字符),而在 MySQL 里对应概念要用 CHAR_LENGTH('abc中文') 才是 5;若误用 LENGTH,MySQL 返回的是 9(字节数)。这个差异在跨数据库迁移时尤其要小心。

2.1 实用:手机号与身份证脱敏

业务里经常要对敏感信息脱敏,标准做法是"保留前几位、星号代替中间、保留后几位":

-- 手机号脱敏:138****5678(MySQL 8.0)
SELECT
  CONCAT(
    SUBSTRING(phone, 1, 3),   -- 取前 3 位
    '****',                   -- 固定 4 个星号
    SUBSTRING(phone, 8)       -- 从第 8 位取到末尾,即后 4 位
  ) AS masked_phone
FROM user;

-- 身份证脱敏:保留前 6 位与后 4 位,中间用 8 个星号
SELECT CONCAT(SUBSTRING(id_card,1,6), '********', SUBSTRING(id_card,15))
FROM user;

2.2 其他常用字符串函数

| 函数 | 作用 | 示例 | 结果 |

|---|---|---|---|

| CONCAT_WS(分隔符, s1, s2...) | 带分隔符拼接 | CONCAT_WS('-','2026','10','07') | 2026-10-07 |

| LEFT(s,n) / RIGHT(s,n) | 取左/右 n 个字符 | RIGHT('abcdef',3) | def |

| LPAD(s,n,填充) / RPAD | 左/右补齐到 n 个字符 | LPAD('7',3,'0') | 007 |

| INSTR(s,子串) | 子串首次出现位置 | INSTR('hello','l') | 3 |

| LOCATE(子串,s) | 同 INSTR | LOCATE('l','hello') | 3 |

| REVERSE(s) | 反转字符串 | REVERSE('abc') | cba |

| SPACE(n) | 生成 n 个空格 | CONCAT('[',SPACE(3),']') | [ ] |

CONCAT_WS 的好处是自动跳过 NULL:CONCAT('a',NULL,'b') 结果是 NULL(任何字符串拼 NULL 都是 NULL),而 CONCAT_WS('-','a',NULL,'b') 结果是 a-b。这在拼地址、拼标签时非常实用。

2.3 实战:用字符串函数清洗脏数据

假设导入了一批用户数据,字段里混着多余空格、全半角混用、手机号带横杠。清洗思路就是"先用函数把脏数据标准化,再做查询":

-- 脏数据清洗示例(MySQL 8.0)
-- 原始表 user_raw(name VARCHAR(50), phone VARCHAR(20), address VARCHAR(200))
SELECT
  TRIM(BOTH ' ' FROM name)                       AS clean_name,  -- 去掉首尾空格
  REPLACE(REPLACE(phone, '-', ''), ' ', '')      AS clean_phone,  -- 去掉横杠和空格
  CONCAT_WS(' ',                                 -- 用空格拼接非空地址段
            address, '')                         AS addr
FROM user_raw;

常见的脏数据形态与对应函数:

| 脏数据形态 | 问题 | 清洗函数 |

|---|---|---|

| ' 张三 ' | 首尾空格 | TRIM |

| '138-1234-5678' | 含横杠 | REPLACE(phone,'-','') |

| 'ABC' | 全角字符 | 应用层转半角后入库 |

| 'abc@EXAMPLE.com' | 大小写不一 | LOWER(email) 统一 |

| '' 空串 | 当作 NULL 处理 | NULLIF(col,'') |

这里 NULLIF(col,'') 很常用:业务里空字符串和 NULL 往往是两回事,但脏数据里经常混在一起,用 NULLIF('','') 把空串转成 NULL,后续 IFNULL、聚合统计才不会把空串算成"有值"。

3 日期与时间函数

时间是报表类业务的核心维度。先记住最常用的"取当前时间":

-- 时间取值(MySQL 8.0)
SELECT
  NOW()                AS now1,      -- 日期+时间:2026-10-07 14:23:05
  CURDATE()            AS today,    -- 仅日期:2026-10-07
  CURTIME()            AS time1,     -- 仅时间:14:23:05
  CURRENT_TIMESTAMP    AS now2;      -- 与 NOW() 等价

3.1 日期加减与截取

DATE_ADD / DATE_SUB 做时间偏移,EXTRACT / YEAR / MONTH / DAY 做截取:

-- 日期加减与截取(MySQL 8.0)
SELECT
  DATE_ADD('2026-10-07', INTERVAL 7 DAY)        AS next_week,  -- 一周后
  DATE_SUB('2026-10-07', INTERVAL 1 MONTH)     AS last_month, -- 一个月前
  DATEDIFF('2026-10-07', '2026-01-01')          AS diff_days, -- 相差天数:279
  YEAR('2026-10-07')                            AS y,          -- 2026
  MONTH('2026-10-07')                           AS m,          -- 10
  DAY('2026-10-07')                             AS d,          -- 7
  HOUR(NOW())                                   AS h;          -- 当前小时

DATEDIFF 只算日期部分的天数差,忽略时间;如果要精确到秒的差,用 TIMESTAMPDIFF(单位, 开始, 结束):

-- 精确时间差(MySQL 8.0)
SELECT
  TIMESTAMPDIFF(SECOND, '2026-10-07 14:00:00', '2026-10-07 14:23:05') AS sec_diff,  -- 1385
  TIMESTAMPDIFF(MONTH, '2026-01-15', '2026-10-07')                     AS mon_diff; -- 8

3.2 日期格式化

DATE_FORMAT 把时间格式化成任意字符串,占位符对照表:

| 占位符 | 含义 | 示例 |

|---|---|---|

| %Y | 4 位年 | 2026 |

| %m | 2 位月 | 10 |

| %d | 2 位日 | 07 |

| %H | 24 小时制小时 | 14 |

| %i | 分钟 | 23 |

| %s | 秒 | 05 |

| %W | 星期名 | Wednesday |

| %Y-%m | 年月 | 2026-10 |

-- 格式化输出(MySQL 8.0)
SELECT
  DATE_FORMAT(NOW(), '%Y-%m-%d')        AS day1,   -- 2026-10-07
  DATE_FORMAT(NOW(), '%Y年%m月%d日')     AS cn1,   -- 2026年10月07日
  DATE_FORMAT(NOW(), '%Y-%m')           AS month1;-- 2026-10

报表里按"年月"分组统计时,DATE_FORMAT(create_time, '%Y-%m') 就是最常用的分组键。

3.3 实战:按时间维度做报表

日报、周报、月报是 BI 最常见的需求。核心都是"把时间字段截断成某个粒度,再分组聚合":

-- 按年月统计订单量与金额(MySQL 8.0)
SELECT
  DATE_FORMAT(create_time, '%Y-%m')      AS ym,          -- 年月
  COUNT(*)                              AS order_cnt,    -- 订单数
  SUM(amount)                          AS total_amt      -- 总金额
FROM orders
WHERE create_time >= '2026-01-01'
GROUP BY DATE_FORMAT(create_time, '%Y-%m')
ORDER BY ym;

按"周"统计可以用 YEARWEEK(create_time),它返回 202640 这样的"年+周"编号;判断"是否本周"则用 YEARWEEK(create_time) = YEARWEEK(CURDATE())。

| 报表粒度 | 分组键写法 | 结果示例 |

|---|---|---|

| 日 | DATE(create_time) | 2026-10-07 |

| 周 | YEARWEEK(create_time) | 202640 |

| 月 | DATE_FORMAT(create_time,'%Y-%m') | 2026-10 |

| 季 | QUARTER(create_time) | 4 |

| 年 | YEAR(create_time) | 2026 |

注意:YEARWEEK 默认周日为一周开始,国内习惯周一开始,可加第二参数 YEARWEEK(create_time, 3) 表示周一开头且按 ISO 周计算,跨周统计口径才不会乱。

4 数值函数

数值函数处理金额、数量、比例。最常用的是四舍五入与取整:

-- 数值函数(MySQL 8.0)
SELECT
  ROUND(19.567, 2)    AS r1,   -- 四舍五入保留 2 位:19.57
  ROUND(19.567, 0)    AS r2,   -- 取整:20
  CEILING(19.1)       AS c1,   -- 向上取整:20
  FLOOR(19.9)         AS f1,   -- 向下取整:19
  TRUNCATE(19.567, 2) AS t1,   -- 截断(不四舍五入):19.56
  MOD(10, 3)          AS m1,   -- 取模:1
  ABS(-5)             AS a1,  -- 绝对值:5
  POW(2, 10)          AS p1;  -- 幂:1024

注意 ROUND 与 TRUNCATE 的区别:ROUND(19.567, 2) 是 19.57(四舍五入),TRUNCATE(19.567, 2) 是 19.56(直接砍后面的小数)。金额结算该用哪个要和业务确认,别想当然。

分页场景常常用 CEILING 算总页数:

-- 计算总页数(每页 20 条)(MySQL 8.0)
SELECT CEILING(COUNT(*) / 20) AS total_pages
FROM order_table;

4.1 实战:百分比与排名计算

做转化率、占比这类指标时,数值函数配合 ROUND 控制小数位:

-- 计算各地区销售额占比(MySQL 8.0)
SELECT
  region,
  SUM(amount)                                   AS total,
  ROUND(SUM(amount) / (SELECT SUM(amount) FROM orders) * 100, 2) AS pct
FROM orders
GROUP BY region;

用 Python + sqlite3 实跑一遍占比计算逻辑,确认聚合后结果正确:

# sqlite3 实跑验证分组聚合与占比计算(Python 3.14 + sqlite3 3.53)
import sqlite3
con = sqlite3.connect(':memory:')
cur = con.cursor()
cur.executescript('''
CREATE TABLE orders(region TEXT, amount INT);
INSERT INTO orders VALUES ('East',100),('East',200),('West',150),('West',50);
''')
total = cur.execute('SELECT SUM(amount) FROM orders').fetchone()[0]
for region, amt in cur.execute('SELECT region, SUM(amount) FROM orders GROUP BY region'):
    print(region, amt, round(amt/total*100, 2))
con.close()

实跑输出:

East 300 60.0
West 200 40.0

对应到 MySQL,把 round(amt/total*100,2) 换成 ROUND(SUM(amount)/总金额*100,2) 即可,逻辑完全一致。

5 流程控制函数

流程控制函数让 SQL 能"根据条件返回不同值"。最常用三个:IF、IFNULL、NULLIF(CASE WHEN 更完整的写法放在下一篇专题讲)。

-- 流程控制函数(MySQL 8.0)
SELECT
  IF(10 > 5, '大', '小')                 AS c1,  -- 条件为真返回第一个:大
  IFNULL(NULL, '默认值')                 AS c2,  -- 第一个为 NULL 则返回第二个:默认值
  IFNULL('实际值', '默认值')             AS c3,  -- 第一个非 NULL 直接返回:实际值
  NULLIF('a', 'a')                       AS c4,  -- 两值相等返回 NULL:NULL
  NULLIF('a', 'b')                       AS c5;  -- 两值不等返回第一个:a
  • IF(条件, a, b):三元表达式,条件真返回 a,假返回 b。
  • IFNULL(a, b):a 为 NULL 时返回 b,否则返回 a。常用于"字段为空时给个兜底文案"。
  • NULLIF(a, b):a 等于 b 时返回 NULL,否则返回 a。常用来避免除零错误。

除零保护是 NULLIF 的经典用法:

-- 避免除零错误(MySQL 8.0)
SELECT
  100 / NULLIF(0, 0)        AS safe_div;  -- 分母为 0 时 NULLIF 返回 NULL,整体结果为 NULL 而不是报错

5.1 实战:用 IF + IFNULL 给用户打标

把前面学的函数组合起来:按消费金额给用户分等级,空值给默认文案:

-- 用户标签输出(MySQL 8.0)
SELECT
  user_id,
  IFNULL(nickname, '匿名用户')                         AS name,        -- 空昵称兜底
  IF(total_amount >= 10000, 'VIP',
     IF(total_amount >= 1000, '会员', '普通'))          AS level,       -- 多级分类
  IFNULL(phone, '未绑定')                              AS phone_show
FROM user_stat;

这里 IF 嵌套实现了三档分类,IFNULL 负责空值兜底。注意 IF 嵌套不要超过 3 层,超过后可读性会急剧下降——更复杂的多分支应该用下一篇要讲的 CASE WHEN。

6 加密与哈希函数

这类函数用于把敏感信息变成不可逆或可逆的密文。密码绝对不能存明文,业界标准是加盐后用单向哈希存储。

-- 加密哈希函数(MySQL 8.0)
SELECT
  MD5('abc123')                    AS h1,  -- 32 位 MD5(已不安全,仅作历史兼容)
  SHA1('abc123')                   AS h2,  -- 40 位 SHA-1
  SHA2('abc123', 256)              AS h3,  -- 64 位 SHA-256(推荐)
  SHA2(CONCAT('salt_', 'abc123'), 256) AS h4; -- 加盐后再哈希

| 函数 | 输出长度 | 单向/可逆 | 安全性 |

|---|---|---|---|

| MD5(s) | 32 位十六进制 | 单向 | 弱,已被彩虹表破解,不推荐新系统 |

| SHA1(s) | 40 位十六进制 | 单向 | 一般,也有碰撞风险 |

| SHA2(s,256) | 64 位十六进制 | 单向 | 较强,可用 |

| AES_ENCRYPT(s,key) | 二进制 | 可逆 | 对称加密,需保管好密钥 |

要点:

1. 存密码用 SHA2(密码 + 随机盐, 256),盐要每个用户随机,存到用户表里。

2. MD5 和 SHA1 都不要用于新系统的密码存储。

3. AES_ENCRYPT 是可逆的,适合需要解密还原的字段(如身份证),但密钥管理是另一个难题,通常更建议在应用层做加解密,数据库只存密文。

6.1 关于已废弃函数的提醒

MySQL 8.0 里有些老函数已经或将要被移除,看到老代码要心里有数:

| 函数 | 状态 | 替代方案 |

|---|---|---|

| PASSWORD(str) | 8.0 已移除 | 用应用层 bcrypt/Argon2 |

| ENCODE/DECODE | 已不推荐 | AES_ENCRYPT/AES_DECRYPT |

| MD5 | 保留但不推荐新用 | SHA2(s,256) |

面试或读老项目代码时,遇到 PASSWORD() 不要照着写——它在 8.0 直接报错,且设计本就不适合做业务密码存储。

7 常用函数速查表

把本篇最常用的函数浓缩成一张速查表,开发时随手查:

| 类别 | 函数 | 一句话作用 |

|---|---|---|

| 字符串 | CONCAT / CONCAT_WS | 拼接 / 带分隔符拼接 |

| 字符串 | SUBSTRING(s,pos,len) | 截取子串 |

| 字符串 | LEFT / RIGHT | 取左/右 n 字符 |

| 字符串 | UPPER / LOWER | 大小写转换 |

| 字符串 | TRIM / LTRIM / RTRIM | 去空格 |

| 字符串 | REPLACE(s,old,new) | 替换 |

| 字符串 | LENGTH / CHAR_LENGTH | 字节数 / 字符数 |

| 字符串 | LPAD / RPAD | 补齐 |

| 日期 | NOW / CURDATE / CURTIME | 当前时间 |

| 日期 | DATE_ADD / DATE_SUB | 时间加减 |

| 日期 | DATEDIFF / TIMESTAMPDIFF | 时间差 |

| 日期 | YEAR / MONTH / DAY / HOUR | 截取时间分量 |

| 日期 | DATE_FORMAT | 格式化 |

| 数值 | ROUND / TRUNCATE | 四舍五入 / 截断 |

| 数值 | CEILING / FLOOR | 向上/向下取整 |

| 数值 | MOD / ABS | 取模 / 绝对值 |

| 流程 | IF(cond,a,b) | 三元 |

| 流程 | IFNULL(a,b) | 空值兜底 |

| 流程 | NULLIF(a,b) | 相等返回 NULL |

| 加密 | MD5 / SHA1 / SHA2 | 单向哈希 |

8 常见坑与注意事项

1. `LENGTH` 当字符数用:统计"几个人名"时用了 LENGTH,结果一个中文算 3~4,长度全错。统一用 CHAR_LENGTH 数字符。

2. `CONCAT` 遇到 NULL 变 NULL:CONCAT('a', NULL, 'b') 结果是 NULL。拼接字段可能为空时用 CONCAT_WS,或先 IFNULL(字段,'') 兜底。

3. 日期格式串写错:%m 是月份、%i 才是分钟,写反会得到离谱的数字。DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s') 是标准写法。

4. 在索引列上套函数导致索引失效:WHERE DATE(create_time) = '2026-10-07' 这种写法会让 create_time 上的索引用不上(后面索引篇详讲)。范围查询用 WHERE create_time >= '2026-10-07' AND create_time < '2026-10-08'。

5. `ROUND` 的银行家舍入:MySQL 的 ROUND 对 .5 是"四舍六入五成双",金额恰好是 x.5 时结果可能和预期差 1 分,对账前要确认口径。

6. 用 MD5 存密码:MD5 太快、彩虹表泛滥,新系统一律 SHA2(密码+随机盐, 256) 起步,更稳的是应用层用 bcrypt/Argon2。

8.1 本章小结

函数不是背出来的,是用出来的。建议把速查表收藏在编辑器旁边,遇到具体需求时先想"这个加工动作属于哪一类"——是改字符串、算时间、调数值、做判断还是加密——再去查对应函数。真正写多了你会发现,日常反复用的其实就是十多个函数,其他的需要时再查即可。下一篇要讲的 CASE WHEN,是流程控制里最强大、也最能体现 SQL 表达力的写法,务必吃透。

9 练习题

练习 1:写一个手机号脱敏 SQL

已知表 user(phone VARCHAR(11)),查出所有用户手机号,显示成前 3 位 + **** + 后 4 位。

<details>

<summary>参考答案</summary>

-- 手机号脱敏(MySQL 8.0)
SELECT CONCAT(SUBSTRING(phone,1,3), '****', SUBSTRING(phone,8)) AS masked
FROM user;

</details>

练习 2:统计"今天"新增订单数

表 orders(create_time DATETIME),统计今天(自然日)的订单条数。要求能用上索引。

<details>

<summary>参考答案</summary>

不要写 WHERE DATE(create_time) = CURDATE()(索引失效),改用范围:

-- 今天订单数(MySQL 8.0)
SELECT COUNT(*) FROM orders
WHERE create_time >= CURDATE()
  AND create_time <  DATE_ADD(CURDATE(), INTERVAL 1 DAY);

</details>

练习 3:把金额按"万"格式化

已知 amount DECIMAL(10,2),查出金额并显示成"xx.x 万"形式(如 123456 显示 12.3万),保留 1 位小数。

<details>

<summary>参考答案</summary>

-- 金额转万元(MySQL 8.0)
SELECT CONCAT(ROUND(amount/10000, 1), '万') AS amt_wan
FROM orders;

</details>

练习 4:避免除零

写一条 SQL,计算每个部门的"人均成本" total_cost / headcount,当 headcount 为 0 时结果不要报错。

<details>

<summary>参考答案</summary>

-- 人均成本除零保护(MySQL 8.0)
SELECT dept, total_cost / NULLIF(headcount, 0) AS avg_cost
FROM dept_stat;

</details>

练习 5:拼接完整收货地址

表 addr(user_id, province, city, detail),其中 city 可能为 NULL。要求查出完整地址,城市为空时不出现多余空格。

<details>

<summary>参考答案</summary>

用 CONCAT_WS 自动跳过 NULL:

-- 拼接收货地址(MySQL 8.0)
SELECT user_id,
       CONCAT_WS(' ', province, city, detail) AS full_addr
FROM addr;

如果硬写 CONCAT(province, city, detail),只要 city 为 NULL,整条地址都会变 NULL,这正是 CONCAT_WS 的用武之地。

</details>

下一篇预告:第 10 章 条件逻辑与CASE WHEN

— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 9 章 常用函数大全》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。