☰
MySQL常用函数实战:从字符串处理到聚合统计全解析
2026/10/6 4:06:35 网站建设 项目流程

1. 为什么我建议每一个刚接触MySQL的人先学透函数

说实话,很多零基础的朋友一上来就盯着增删改查,SELECT、INSERT、UPDATE、DELETE用得很溜,但一碰到“要把用户名统一转成大写”“要按月份统计订单量”“要处理空值显示缺省文案”这类需求,就立刻卡壳,然后跑去问同事或者翻搜索引擎,翻半天抄来一段自己都看不懂的SQL。

问题的根源不是笨,而是缺了MySQL常用函数这一环。

函数本质上就是MySQL帮你预封装好的一批“加工工具”:你把原始数据喂进去,它啪地一下返回加工后的结果。比如你想把" hello world "两边空格去掉,不想自己写循环遍历,一个TRIM()就完事。你想知道两个日期相差几天,不想手算日历,一个DATEDIFF()直接给答案。这些东西不涉及复杂算法,纯粹是熟练度的问题,但恰恰是熟练度决定了你写SQL的速度和代码的干净程度。

这篇内容的目标很明确:让零基础的人建立起对MySQL函数的整体认知,把工作中最高频、最能提升效率的那批函数讲透。我不打算按官方文档的顺序罗列几百个函数让你背,那样没有意义。我会按照“字符串处理、数值计算、日期时间、条件逻辑、聚合统计、综合实战”这条主线来拆,每一步都结合真实业务场景给例子,每个例子都保证你能直接复制到自己电脑上跑通。

适合谁看?准备入行数据分析师、后端开发、运维,或者正在自学MySQL准备面试的朋友,都合适。有一个基础前提:你至少已经会把MySQL装好、能连上库、能执行最简单的SELECT语句。如果连安装都还没搞定,建议先花半小时把环境搭好再回来看这篇,不然光看不练,看完就忘。

2. 字符串函数:数据清洗和格式化的大半壁江山

2.1 CONCAT拼接与CONCAT_WS分隔符拼接

真实业务里,字符串拼接是最常见的需求。比如用户表里有first_name和last_name两个字段,你要在前端显示完整姓名;订单表里有年份和订单号两个字段,你要生成一个带业务前缀的单号。这两种场景都用CONCAT。

-- 基础拼接:把两列合成一列 SELECT CONCAT(last_name, first_name) AS full_name FROM users; -- 拼接时带固定前缀 SELECT CONCAT('HN', '-', order_year, '-', order_no) AS biz_no FROM orders;

这里有一个非常容易踩的坑:CONCAT只要有一个参数为NULL,整个结果就是NULL。比如上面第一个例子,如果某个用户的first_name为空,那一行返回的就是NULL,前端拿到空值直接显示空白,排查起来很费劲。

我个人的习惯是:凡是做拼接之前,先用IFNULL或者COALESCE把可能为空的字段处理掉。

SELECT CONCAT(IFNULL(last_name, ''), IFNULL(first_name, '')) AS full_name FROM users;

另一个更省事的写法是CONCAT_WS,它的第一个参数是分隔符,之后的参数里会自动跳过NULL值,这个特性在拼接带分隔符的字段串时非常方便:

SELECT CONCAT_WS('-', 'HN', order_year, order_no) AS biz_no FROM orders;

如果你要把某个列的多行值拼成一行一个字符串,那就要用到后面会讲到的聚合函数GROUP_CONCAT,这里先留个印象。

2.2 SUBSTRING截取与LEFT/RIGHT快速取边

截取函数在解析编号、提取省份、处理日志字符串时高频出现。语法是SUBSTRING(str, pos, len),pos从1开始计数,这是很多新手困惑的地方,他们习惯性认为从0开始,结果截出来的字符串总差一位。

-- 从第4位开始截取3个字符 SELECT SUBSTRING('2024-ORD-001', 5, 3); -- 结果: ORD -- 如果只给两个参数,表示从第pos位截到末尾 SELECT SUBSTRING('2024-ORD-001', 6); -- 结果: RD-001 -- 截取左边3个字符 SELECT LEFT('mysql函数', 5); -- 结果: mysql -- 截取右边4个字符 SELECT RIGHT('2024-ORD-001', 3); -- 结果: 001

这里我特别推荐养成用LEFT和RIGHT的习惯,因为它们在语义上更直观,尤其当你在代码评审时,别人一眼就能看出“你要取左边几位”,而SUBSTRING在很多团队里需要多一点思考成本。

2.3 大小写转换、去空格与替换

数据录入不规范是常态:用户注册时邮箱存了大小写混合的地址、导入的外部数据两边带着空格、文案里某段固定内容需要全局替换。这组函数基本能覆盖:

-- 统一转大写/小写 SELECT UPPER('mysql'), LOWER('MYSQL'); -- 结果: MYSQL, mysql -- 去掉字符串两边的空格(中间的空格不影响) SELECT TRIM(' hello mysql '); -- 结果: hello mysql -- 去掉左边空格 SELECT LTRIM(' left space'); -- 去掉右边空格 SELECT RTRIM('right space '); -- 替换字符串中的指定内容 SELECT REPLACE('前端展示区-未支付', '未支付', '待付款'); -- 结果: 前端展示区-待付款

顺带提一个细节:TRIM默认去除的是空格,但它也可以指定去除字符,比如TRIM(LEADING '0' FROM '000123')可以把数字前面的零去掉,这个在处理以字符串形式存储的数字时很有用。

还有LENGTH和CHAR_LENGTH的区别,很多面试官爱考。LENGTH返回的是字节数,一个中文在UTF-8编码下占3个字节;CHAR_LENGTH返回的是字符数,一个中文算1个字符。判断“字符串是否超长”时要用CHAR_LENGTH,否则统计结果会被中文字符数翻倍放大。

SELECT LENGTH('你好'), CHAR_LENGTH('你好'); -- 结果: 6, 2

3. 数值函数与日期时间函数:统计报表里离不开的两大支柱

3.1 数值处理:四舍五入、向上取整、向下取整、绝对值、取模

数值函数是写统计类SQL的地基。你要算客单价、算折扣率、算库存周转,几乎每步都离不开取整和精度控制。

-- 四舍五入,保留2位小数 SELECT ROUND(3.14159, 2); -- 结果: 3.14 SELECT ROUND(3.145, 2); -- 结果: 3.15,注意这里不是直接截断 -- 向上取整:只要有小数就进1 SELECT CEIL(3.01); -- 结果: 4 SELECT CEIL(3.00); -- 结果: 3 -- 向下取整:直接丢掉小数部分 SELECT FLOOR(3.99); -- 结果: 3 -- 绝对值 SELECT ABS(-5); -- 结果: 5 -- 取模(余数) SELECT MOD(10, 3); -- 结果: 1

这里要专门提醒一下ROUND的精度问题。MySQL的ROUND使用的是“四舍五入”还是“四舍六入五成双”,取决于版本和底层库的浮点实现,比如在某些边界情况下ROUND(2.675, 2)可能返回2.67而不是2.68。如果你在做金额计算且对精度要求极高,千万别只依赖ROUND,建议结合DECIMAL类型存储,金额字段从一开始就用DECIMAL(10,2),不要用FLOAT和DOUBLE。这一点在财务对账场景里是血的教训,我见过不止一个人因为浮点误差导致对不上账,最后排查半天才发现是字段类型的问题。

3.2 日期时间获取:NOW、CURDATE、CURTIME和它们的时区注意事项

日期时间函数是报表查询的绝对高频。每天的统计任务、每月的结算任务、每隔半小时的增量拉取,全部依赖日期函数。

-- 当前日期时间 SELECT NOW(); -- 结果: 2025-01-20 14:33:21 SELECT SYSDATE(); -- 结果: 2025-01-20 14:33:21 -- 当前日期(不含时间) SELECT CURDATE(); -- 结果: 2025-01-20 -- 当前时间(不含日期) SELECT CURTIME(); -- 结果: 14:33:21

NOW()和SYSDATE()在绝大多数情况下表现一致,但有一个细微差别:NOW()在一个SQL语句中返回的是语句开始执行的时间,而SYSDATE()返回的是该函数被调用那一刻的时间点。在长时间运行的复杂SQL里,多次调用SYSDATE()可能出现时间不一致,因此我默认都写NOW(),只有特殊需求才用SYSDATE()。

务必要注意时区问题。如果你的MySQL连接串或者服务器时区设置不对,NOW()返回的时间和业务期望的时间可能差好几个小时。这是配置层面的问题,但会在函数使用中直接暴露。常见的处理方式是在数据库连接参数里显式指定serverTimezone,或者在会话级别执行SET time_zone = '+08:00',确保业务日志和统计口径一致。

3.3 日期格式化与计算:DATE_FORMAT、DATEDIFF、DATE_ADD

这三兄弟是我个人认为整个日期函数体系里最值得花时间练熟的。

DATE_FORMAT负责把日期转成任意你想要的展示格式:

SELECT DATE_FORMAT(NOW(), '%Y-%m-%d'); -- 结果: 2025-01-20 SELECT DATE_FORMAT(NOW(), '%Y年%m月%d日'); -- 结果: 2025年01月20日 SELECT DATE_FORMAT(NOW(), '%H:%i:%s'); -- 结果: 14:33:21 SELECT DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'); -- 结果: 2025-01-20 14:33:21

注意区分%Y和%y:大写%Y返回四位年份,小写%y返回两位年份;%m是两位月份,%c是不带前导零的月份;%H是24小时制,%h是12小时制。这些细节在拼报表字段时特别容易出错,我就是靠反复查文档才彻底记住的。

DATEDIFF计算两个日期相差的天数,注意是“前一个减后一个”:

-- 计算从今天到本月底还有多少天 SELECT DATEDIFF('2025-01-31', CURDATE()); -- 结果: 11(取决于当天日期) -- 计算两个订单日期间隔天数 SELECT DATEDIFF('2025-01-20', '2025-01-01'); -- 结果: 19

DATE_ADD用于给指定日期增加或减少时间间隔,interval的单位可以是DAY、MONTH、YEAR、HOUR、MINUTE、SECOND,也可以组合写:

-- 增加30天 SELECT DATE_ADD('2025-01-20', INTERVAL 30 DAY); -- 结果: 2025-02-19 -- 减少3个月(用负数) SELECT DATE_ADD('2025-01-20', INTERVAL -3 MONTH); -- 结果: 2024-10-20 -- 增加1小时30分钟 SELECT DATE_ADD('2025-01-20 14:33:21', INTERVAL '1:30' HOUR_MINUTE);

DATE_SUB是DATE_ADD的孪生函数,语义上等于“加负数”。我个人习惯统一用DATE_ADD配合负值,因为少记一个函数名字,逻辑上也更统一。

还有个常见的取年月日函数YEAR()、MONTH()、DAY(),做按年按月分组统计时频率极高:

SELECT YEAR('2025-01-20'), MONTH('2025-01-20'), DAY('2025-01-20'); -- 结果: 2025, 1, 20

4. 条件控制函数:让SQL具备“如果...那么...”的逻辑能力

4.1 IF与IFNULL:处理空值和二分支场景

SQL不是只能做“无脑的搬运”,它会通过条件函数做出逻辑判断。最基础的是IF函数,它的语法是IF(条件, 真值, 假值),适合二分支场景。

-- 判断库存状态,低于100显示“库存告急”,否则显示“库存充足” SELECT product_name, stock, IF(stock < 100, '库存告急', '库存充足') AS stock_status FROM products;

另一个高频场景是IFNULL,专门用来处理NULL值的兜底显示:

-- 如果备注为空,显示“无备注” SELECT order_no, IFNULL(remark, '无备注') AS remark_display FROM orders;

SQL里的NULL和空字符串''是两个完全不同的概念。NULL表示“没有值”,空字符串表示“有值,这个值是空”。很多新手在WHERE条件里写WHERE remark = ''去过滤空备注,结果NULL值的行一条都没出来,必须写成WHERE remark IS NULL OR remark = ''才能两网打尽。

4.2 CASE WHEN:多条件分支的利器

IF只能处理两分支,如果要处理多条件分级,CASE WHEN是标准答案。比如按订单金额分等级:

SELECT order_no, amount, CASE WHEN amount >= 10000 THEN '大客户' WHEN amount >= 5000 THEN '中客户' WHEN amount >= 1000 THEN '小客户' ELSE '零星客户' END AS customer_level FROM orders;

CASE WHEN还有一个常见姿势:配合聚合函数做“行转列”统计。比如你有一张订单表,字段包括order_date和order_status,你想统计“每个月的已支付订单数和未支付订单数”各是多少:

SELECT DATE_FORMAT(order_date, '%Y-%m') AS month, SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS paid_cnt, SUM(CASE WHEN status = 'unpaid' THEN 1 ELSE 0 END) AS unpaid_cnt FROM orders GROUP BY DATE_FORMAT(order_date, '%Y-%m');

这种方式比写多个子查询干净得多,也是面试中常考的“条件聚合”写法。核心思路:CASE WHEN配合SUM,把“行”里的分类信息转换成“列”上的计数值。

5. 聚合函数:从单行视野切换到整体统计的钥匙

5.1 COUNT、SUM、AVG、MAX、MIN逐个说清

聚合函数最大的特点是“把多行数据压缩成一行结果”。它们通常配合GROUP BY按分组统计来用。

-- 订单总数 SELECT COUNT(*) FROM orders; -- 已支付订单数:COUNT(column)会自动忽略NULL SELECT COUNT(order_id) FROM orders WHERE status = 'paid'; -- 订单总金额 SELECT SUM(amount) FROM orders; -- 客单价:平均订单金额 SELECT AVG(amount) FROM orders; -- 最大/最小订单金额 SELECT MAX(amount), MIN(amount) FROM orders;

这里我必须强调一个COUNT(*)和COUNT(1)和COUNT(column)的区别,这是面试题里出现率极高的问题。

COUNT(*)和COUNT(1)在MySQL InnoDB引擎下没有本质性能差异,都会数行数。但COUNT(column)只会统计该列不为NULL的行数。所以在统计用户数量时,如果你是查user_id这种有唯一约束的列,三种写法没差别;但如果你不小心count了一个可空的列,结果会少,排查半天才发现是NULL被忽略导致的。

5.2 GROUP BY与聚合函数的配合:统计报表的分组逻辑

聚合函数单独用的频率其实不高,它几乎总是和GROUP BY搭配:先按某个维度分组,再对各组做聚合。

SELECT status, COUNT(*) AS order_cnt, SUM(amount) AS total_amount, AVG(amount) AS avg_amount FROM orders GROUP BY status;

还有一个很多人会搞错的地方:当你用了GROUP BY,SELECT后面出现的普通列必须要么是分组列,要么被聚合函数包裹。比如SELECT order_no, COUNT(*) FROM orders GROUP BY status这句话在MySQL默认配置下不报错,但语义上是错误的——order_no在分组后有多个值,取哪一个完全是MySQL随机选的。这个问题在ONLY_FULL_GROUP_BY模式下会直接报错,而MySQL 8.0默认开启了这个模式。我的建议是:永远不要写这种“侥幸”SQL,分组查询就该严格把SELECT的列限定好。

5.3 GROUP_CONCAT:把多行值拼成一行字符串

这是一个被低估的实用函数。它可以把分组内的多个值拼成一个字符串,在生成汇总描述时特别好用。

-- 把每个客户的所有订单号拼接成一列,逗号分隔 SELECT customer_id, GROUP_CONCAT(order_no ORDER BY order_date SEPARATOR '、') AS all_order_nos FROM orders GROUP BY customer_id;

GROUP_CONCAT默认用逗号分隔,你可以通过SEPARATOR指定其他分隔符;组内排序用ORDER BY控制;如果拼接结果很长,可以通过SET GROUP_CONCAT_MAX_LEN = 10240调整最大长度。这些细节在实际工作中总会用到,建议收藏一下。

6. 一组从零到一能跑通的完整示例:订单数据综合统计

前面每类函数都是单独讲的,这一节我们把这些函数串起来,做一个接近真实业务的任务。假设你有一张电商订单表orders,字段如下:

CREATE TABLE orders ( order_id INT PRIMARY KEY, customer_name VARCHAR(50), order_date DATETIME, amount DECIMAL(10, 2), status VARCHAR(20) -- paid/unpaid/cancelled );

先插入几条测试数据:

INSERT INTO orders VALUES (1, '张三', '2025-01-05 09:12:00', 1500.00, 'paid'), (2, '李四', '2025-01-08 14:30:00', 800.00, 'unpaid'), (3, '王五', '2025-01-12 18:45:00', 12000.00, 'paid'), (4, '张三', '2025-01-15 21:00:00', 600.00, 'unpaid'), (5, '李四', '2025-02-01 10:05:00', 2600.00, 'cancelled'), (6, '王五', '2025-02-03 16:20:00', 4300.00, 'paid');

需求:生成一份“按月、按客户”的订单统计报表,要求包含客户名称、下单月份、总订单数、总金额、平均单笔金额、最大单笔金额;金额大于等于10000的标记为“大单客户”,否则显示“普通客户”;已取消的订单不计入统计。

SELECT customer_name, DATE_FORMAT(order_date, '%Y-%m') AS month, COUNT(*) AS total_orders, SUM(amount) AS total_amount, ROUND(AVG(amount), 2) AS avg_amount, MAX(amount) AS max_amount, CASE WHEN SUM(amount) >= 10000 THEN '大单客户' ELSE '普通客户' END AS customer_flag FROM orders WHERE status != 'cancelled' GROUP BY customer_name, DATE_FORMAT(order_date, '%Y-%m') ORDER BY month, total_amount DESC;

你看,这一条SQL里串起了字符串函数DATE_FORMAT、聚合函数COUNT/SUM/AVG/MAX、数值函数ROUND、条件函数CASE WHEN、过滤条件WHERE、排序ORDER BY,覆盖了本节前面讲的所有重点。能在脑子里直接写出这种SQL,说明你对常用函数的基本功已经过关了。

每步都可以单独测试,比如先跑SELECT * FROM orders WHERE status != 'cancelled'看过滤结果,再一层层加上GROUP BY和聚合,最后加CASE WHEN。

7. 函数使用中的常见误区和性能边界,我说点文档里不写的

7.1 在WHERE条件里用函数会导致索引失效

这是所有MySQL性能问题里我最想强调的一点。你给某个字段建了索引,但如果查询时在WHERE子句里对该字段使用了函数,MySQL大概率会放弃索引扫描,转而做全表扫描。

-- 不建议:对索引字段order_date使用函数 SELECT * FROM orders WHERE DATE(order_date) = '2025-01-20'; -- 建议:直接使用范围查询,走索引 SELECT * FROM orders WHERE order_date >= '2025-01-20 00:00:00' AND order_date < '2025-01-21 00:00:00';

两种写法结果一样,但第二种能命中索引,数据量大时性能差距是数量级的。写SQL时养成一个条件反射:能用范围表达式解决的,就不要把字段包进函数里。

7.2 隐式类型转换会悄无声息地拖慢查询

字符串函数和数值函数混用时,MySQL会自动做隐式类型转换。比如你的订单编号order_no是VARCHAR类型,存的全是数字字符串,查询时写WHERE order_no = 1234(数字),MySQL会把每一行的字符串都转成数字再比较,索引同样可能失效。

正确的做法是让类型匹配:如果你的字段是字符串,查询参数就写字符串,比如WHERE order_no = '1234'。这个细节在初期不容易被察觉,数据量一上来就会变成慢查询。

7.3 NULL值的坑在函数里无处不在

前面讲CONCAT和COUNT时都提到了NULL,这里做一个集中总结,用一张表说清楚:

场景结果说明
CONCAT('a', NULL)NULL拼接时任何参数为NULL,结果返回NULL
IFNULL(NULL, 'x')'x'专门处理NULL的兜底函数
COUNT(NULL列)0该列非NULL的行才会计数
SUM(NULL列)NULL若该列全部为NULL,SUM返回NULL
WHERE 列 = NULL查不到SQL中判断NULL必须用IS NULL或IS NOT NULL
NULL <=> NULL1<=>是NULL安全等于运算符,极少用但要知道

牢记这组规律,可以避免我在实际排错中见过的绝大多数SQL“奇怪结果”问题。

7.4 函数嵌套可读性维护

函数可以无限嵌套,比如TRIM(REPLACE(LOWER(name), ' ', '')),但嵌套超过三层,后面维护的人(包括三个月后的自己)读起来就会崩溃。我的习惯是:超过两层嵌套就考虑用子查询把中间结果拆开,或者用SQL注释标注每一步在干什么。

-- 可读性更好的写法 SELECT TRIM(LOWER(customer_name)) AS clean_name, CHAR_LENGTH(TRIM(customer_name)) AS name_len FROM users;

8. 最后分享一点我的实际使用心得

MySQL常用函数这门功夫,最大的特点就是“练一次记终生”。我见过很多人买了厚厚的SQL教程,从头翻到尾,合上书一条也写不出来。我自己学的时候就是拿公司真实的订单表、用户表反复折腾:把昨天的取数需求全部用函数重写一遍,能拆函数就拆函数,能合并查询就合并查询。这样练两三天,几乎所有常用函数都能在写SQL时不假思索地用出来。

再补一个小技巧:在你常用的客户端工具(Navicat、DBeaver乃至命令行)里,可以用SELECT 函数名(参数)这种方式快速测试函数结果,不用建表、不用插数据,立刻就能看到返回值。比如SELECT DATE_FORMAT(NOW(), '%Y-%m-%d');敲一下回车,结果就出来了,比翻文档快得多。

另外一个容易被忽略的细节是,MySQL的版本之间函数行为会有细微差异,尤其是日期处理和字符集相关函数。如果你们的线上环境是5.7,你本地用的是8.0,那像是字符串的默认排序规则、GROUP BY的严格模式、某些日期函数的边界行为都可能不一样。写好的SQL上线前,建议在目标版本环境里跑一遍,避免“本地好好的,线上就报错”的尴尬。

如果你是把这里面的例子一个个亲手敲完、跑通的,那MySQL常用函数这块的地基已经打得差不多了。之后不管是在数据分析岗位上写报表,还是在后端开发里拼查询,都会发现,大部分让人头疼的取数需求,翻来覆去用的其实就是这些函数的不同排列组合。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询