刚接触MySQL那阵子,我最容易踩的坑就是把大量数据拉到应用层,再用Java、Python等语言做一轮又一轮的字符串截取、日期计算、条件判断。后来带团队做项目久了才发现,SQL里的函数才是处理数据最顺手的那层工具。想快速上手MySQL数据库基础,函数这关迟早要过,它能直接解决“数据格式不统一、统计维度太乱、报表逻辑冗长”这些日常问题,也适合所有刚学会建表、写select的新手,以及准备面试的开发者参考。
这篇文章我会按我自己的盘点习惯讲清楚:函数到底解决什么问题、常用分类怎么记、真实业务里怎么结合存储过程或行转列场景用,再把我实际遇到的环境报错和排查经验一起放出来。全程用命令行和SQL示例说话,尽量让没怎么写过函数的人也能直接抄作业。
1. 先别急着往表里堆函数,先明确它要解决的核心问题
1.1 为什么要在MySQL层直接用函数把数据算完
很多初学者在第一周会纠结:数据库里的数据明明原样取出,在代码里写个if、写个substring也一样能拿到结果,为什么非要学一堆MySQL函数?我的理解是:数据规模和程序结构决定了处理位置。
假设线上有十万条订单记录,你要把每条记录的订单号后四位取出来做分表归档。如果在应用层处理,你得先把十万条数据全量拉出来,再遍历、截取、拼接,不仅占内存,网络IO也会明显变慢。而MySQL自带的字符串函数,直接在SQL执行过程中把每一行转换好,返回的结果已经是你想要的格式。数据库本身是擅长批量运算的引擎,让数据在离源头最近的地方完成精简和计算,链路最短,也最容易保证统计口径一致。
再说一个容易被忽略的点:同一个统计逻辑如果写在应用层,不同项目组可能会写出不同的日期格式化方式,比如一个用yyyy-MM-dd,另一个用%Y-%m-%d,最后连报表口径都对不上。不如把这类公共转换直接沉淀成SQL函数或存储过程,让所有业务都走同一套“加工车间”。
1.2 函数的大类划分与执行时的基本规则
MySQL内置函数总体可以这么记:一类是按行处理的“单行函数”,比如处理某个字段的字符串、日期、数值;另一类是“聚合函数”,比如求和、计数、平均值,它会把多行结果收敛成一个汇总值。还有一类是流程控制,平时比较常用的包括IF、IFNULL、CASE WHEN。
理解这层分类后,再看SQL执行逻辑会顺很多。函数不是随便丢在任何地方都行。你可以把它放在SELECT后面对结果进行加工,也可以在WHERE里做条件过滤,还能用在GROUP BY、ORDER BY、HAVING中。但要注意:单行函数对WHERE条件中的列做加工时,往往会让索引失效,这一点后文会专门展开。
基本使用语法其实很简单,就是函数名(参数1, 参数2...)。比如:
sql复制SELECT NOW(), DATABASE(), USER();
这条SQL能同时看当前时间、当前数据库和当前登录用户。函数有返回类型,比如NOW()返回日期时间类型,LENGTH()返回整数,使用前最好能自己心里有个数,避免在套娃写法里算出来一个不预期的null值。
1.3 动手之前,先把可复现的测试表和初始环境拉起来
谈函数不能光靠背概念,我强烈建议你先建一个能反复折腾的测试表。以下是我平时创建示例表的做法:
sql复制CREATE DATABASE IF NOT EXISTS db_func_demo DEFAULT CHARACTER SET utf8mb4;
USE db_func_demo;
DROP TABLE IF EXISTS user_order;
CREATE TABLE user_order (
id INT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL,
user_name VARCHAR(50),
category VARCHAR(30),
amount DECIMAL(10,2),
pay_time DATETIME,
remark VARCHAR(255)
) ENGINE=InnoDB;
再塞点测试数据。不同MySQL版本安装完成后,有的机器默认就能连上,有的则需要先重置密码或设置环境变量,我先默认你已经能用命令行进入这个库。
sql复制INSERT INTO user_order(order_no, user_name, category, amount, pay_time, remark)
VALUES
('PO20240101001', '张三', '数码', 5999.00, '2024-01-03 12:20:15', ' 需要发票 '),
('PO20240102002', '李四', '图书', 129.00, '2024-01-10 09:05:11', '会员日订单'),
('PO20240215003', '王五', '数码', 299.00, '2024-02-15 22:33:40', NULL);
有了这些数据,下面每一个示例你都能直接运行对比结果。后面章节里出现的函数,几乎都是围绕这张订单表做的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 最常用的5类MySQL函数,逻辑记忆和坑点一次讲透
2.1 字符串函数:乱格式数据的“整形师”
字符串函数是我平时使用频率最高的一类,因为任何业务系统都免不了处理订单号、备注、手机号、地址这类文本。最基础的有CONCAT、SUBSTRING和SUBSTRING_INDEX。
实际项目中我经常看到有人把SUBSTR、SUBSTRING和SUBSTRING_INDEX搞混,这很可以理解。SUBSTR(str, pos)和SUBSTRING(str, pos)效果基本一样,都是从指定位置开始取字符;区别不大。SUBSTRING_INDEX(str, delimiter, count)则是按照分隔符截取,它是处理“a-b-c”格式碎片的好帮手。例如从订单号中取第一个短横线前的部分:
sql复制SELECT order_no,
SUBSTRING(order_no, 1, 10) AS fixed_len_part,
SUBSTRING_INDEX(order_no, '-', 1) AS before_first_dash
FROM user_order
WHERE id = 1;
字符串去空格最常用的是TRIM、LTRIM、RTRIM,这类函数在Excel导入数据时特别管用。导入的数据经常带了不可见空字符,直接等值查询查不到,用TRIM包一层往往能解决。但要注意:它默认只去普通空格,如果字段里还有制表符或换行符,最好配合REPLACE一起处理。
大小写转换函数是UPPER和LOWER。还有一种坑是统计字符长度,中文环境下很多人用LENGTH()突然发现一个“中”字显示为3,原因是没有区分字符和字节。此时应该用CHAR_LENGTH()。
高频的字符串函数用法见这个表:
| 函数 | 功能 | 示例 |
|---|---|---|
| CONCAT(a, b) | 字符串拼接 | CONCAT(user_name, '-', order_no) |
| CONCAT_WS(sep, a, b) | 用指定分隔符拼接 | CONCAT_WS('-', user_name, order_no) |
| SUBSTRING(str, pos, len) | 从第pos位开始取len个字符 | SUBSTRING(order_no, 3, 4) |
| SUBSTRING_INDEX(str, delim, count) | 按分隔符截取 | SUBSTRING_INDEX(remark, '/', 1) |
| TRIM(str) | 去掉两端空格 | TRIM(remark) |
| REPLACE(str, a, b) | 替换字符串 | REPLACE(remark, ' ', '') |
| UPPER / LOWER | 大小写转换 | UPPER(category) |
| CHAR_LENGTH(str) | 返回字符数 | CHAR_LENGTH(user_name) |
| LEFT(str, len) / RIGHT(str, len) | 从左边/右边取字符 | LEFT(order_no, 2) |
| LPAD / RPAD | 填充到指定长度 | LPAD(id, 6, '0') |
| LOCATE(substr, str) | 返回子串位置 | LOCATE('数码', category) |
字符串函数用得多了以后,你会发现自己写的SQL越来越像清洗流水线。我一般会先用TRIM清理外层,再用REPLACE清理内部换行,最后用SUBSTRING_INDEX拆出想要的片段。只要顺序稳定,结果就不会飘。
2.2 数值函数:小技巧容易引发数据语义问题
数值函数的掌握难度不大,但语义上翻车的情况不少。四舍五入用ROUND(x, d),向上取整用CEIL(x),向下取整是FLOOR(x),取绝对值是ABS(x),取模是MOD(x, y)。一个容易出错的点:多数人觉得ROUND只影响显示,其实它已经改变了数据真实值,在做金额汇总时要格外小心。
amount列一旦是DECIMAL(10,2),本身已经保留了两位小数。可如果你之前建表时用了FLOAT或DOUBLE,那么用ROUND也未必救得回来,因为二进制浮点数本身存在精度误差。金融项目尽量别用浮点类型存储金额,这是MySQL里十分痛的一条经验。
还有一类值得提的是“类型转换”函数。热搜词里经常有人搜“mysql中int+5”,其实本质上就是整数类型和常量相加,但如果你和字符串拼在一起,MySQL可能要做隐式转换。示例:
sql复制SELECT id + 5 AS new_id,
CAST(amount AS SIGNED) AS amount_to_int
FROM user_order
WHERE id = 1;
CAST(amount AS SIGNED)会把带小数的金额转换成整数并截断小数部分。这里面有业务风险,比如金额299.99转成299。所以我建议只有在明确知道取整逻辑时才做这种转换,否则数据出来后会被人投诉。
数值函数也常用于生成统计排名。ROUND(AVG(amount), 2)结合聚合一起用,往往能同时解决精度和可读性问题。
2.3 日期时间函数:定时报表和统计场景里最顺手
日期函数是报表开发的核心。MySQL里最常用的就是NOW()、CURDATE()、DATE_FORMAT()、DATEDIFF()、TIMESTAMPDIFF()、DATE_ADD()。比如查看每笔订单是周几下的单:
sql复制SELECT order_no,
pay_time,
DATE_FORMAT(pay_time, '%Y-%m-%d') AS pay_date,
DATE_FORMAT(pay_time, '%W') AS week_name
FROM user_order;
DATE_FORMAT的格式符非常多变。%Y是四位年,%y是两位年,%m是两位月,%d是两位日,%H是24小时制小时,%i是分钟,%s是秒。这里很容易把%i当作分钟,因为很多人习惯写%m表示分钟,实际上它是月份。
计算两个日期相差天数最常用DATEDIFF(end_date, start_date),结果是后减前。如果要计算跨月的间隔,比如相差几个月、几年,我推荐TIMESTAMPDIFF(unit, start_date, end_date),它能处理月份天数差异问题,示例:
sql复制SELECT order_no,
TIMESTAMPDIFF(DAY, pay_time, NOW()) AS days_since_order,
TIMESTAMPDIFF(MONTH, pay_time, NOW()) AS months_since_order
FROM user_order;
日期加减则用DATE_ADD(pay_time, INTERVAL 1 DAY)。例如查询当前时间前30天的订单,可以写成pay_time >= DATE_SUB(NOW(), INTERVAL 30 DAY),这样写比DATE_FORMAT(pay_time, '%Y-%m-%d') = DATE_FORMAT(DATE_SUB(NOW(), INTERVAL 30 DAY), '%Y-%m-%d')更优,原因后面说索引时再讲。
日期函数使用中最大的痛点,是时区。服务器时区如果设置成UTC,而业务在北京时间运行,NOW()会差8小时。我建议在数据库连接串中显式设置serverTimezone=Asia/Shanghai,同时MySQL配置里也统一default-time-zone,避免上线后报表时间对不上。
2.4 聚合函数:同组数据进行汇总时的常见陷阱
谈到聚合函数,大家都认识SUM、COUNT、AVG、MAX、MIN。但真正常年被面试官追问的是两个细节点:COUNT(*)与COUNT(column)的区别,以及HAVING和WHERE的筛选时机差异。
COUNT(*)会统计所有行数,哪怕某些字段是NULL也会被算进去。COUNT(remark)只会统计remark列不为NULL的行。如果业务上要统计实际填写了备注的订单量,用COUNT(remark)反而是正确的。相似的,SUM(amount)在遇到整列NULL时会返回NULL,所以如果需要对外展示0,通常用IFNULL(SUM(amount), 0)包一层。
聚合函数通常配合GROUP BY使用。比如按分类统计订单总金额,并筛掉订单量少于1的分类:
sql复制SELECT category,
COUNT(*) AS order_cnt,
ROUND(SUM(amount), 2) AS total_amount
FROM user_order
GROUP BY category
HAVING COUNT(*) > 0
ORDER BY total_amount DESC;
WHERE是分组之前筛选原始行。HAVING是分组之后筛选聚合结果。这个顺序很容易理解但特别容易在长SQL里写反。我在代码评审中经常发现有人把聚合条件写在WHERE里,比如WHERE COUNT(*) > 1,MySQL直接报错,因为WHERE执行到的时候还没计算聚合值。
聚合函数里还有一个很趁手的扩展,就是配合窗口函数使用,例如SUM(amount) OVER(PARTITION BY category ORDER BY pay_time)能算累计值。MySQL 8.0以上版本已经支持得很好,面试或实际写复杂报表时建议重点看。
2.5 条件控制类函数:SQL读取中写业务规则
条件函数相当于在SQL里写轻量业务逻辑。最基础的是IFNULL(expr1, expr2),它只处理NULL值。比如备注为NULL时展示为“无备注”:
sql复制SELECT order_no,
IFNULL(remark, '无备注') AS show_remark
FROM user_order;
如果多个字段都可能为NULL,用COALESCE更合适。它从左到右返回第一个非NULL值。比如地址填写不完整时,想优先取省、再取市、最后取区:
sql复制SELECT COALESCE(province, city, district, '未知地区') AS full_region
FROM some_address_table;
更复杂的判断用CASE WHEN。CASE分为简单函数和搜索函数,搜索函数最通用:
sql复制SELECT order_no,
CASE
WHEN amount >= 5000 THEN '大额订单'
WHEN amount >= 500 THEN '中等订单'
ELSE '小额订单'
END AS order_level
FROM user_order;
在SELECT中使用CASE做分组统计也很常见。比如一次查出大额订单和普通订单各自的订单量:
sql复制SELECT
SUM(CASE WHEN amount >= 5000 THEN 1 ELSE 0 END) AS large_cnt,
SUM(CASE WHEN amount < 5000 THEN 1 ELSE 0 END) AS normal_cnt
FROM user_order;
这种写法最大的价值是可以把多行条件统计合并到一行结果,避免反复扫表。
3. 实战拆解:把函数真正组合进业务查询中
3.1 需求一:从编码字段中抽取分类,并统一成大写
很多老系统会设计一种有业务含义的编码字段。假设物品编码格式是DL-A1-202401-001,业务上要按第三段和第四段识别类别。字符串函数组合起来会很直接:
sql复制SELECT order_no,
UPPER(SUBSTRING_INDEX(SUBSTRING_INDEX(order_no, '-', 2), '-', -1)) AS second_segment
FROM user_order;
两层SUBSTRING_INDEX的套路其实不难:最外层先用第三个参数2取前两段,第二次再用参数-1取最后一段。这种写法在从“2024-01”中提取年或月时特别顺手。如果编码长度固定,也可以直接用SUBSTRING(order_no, 8, 2)取字符。
我分享这个示例不是让你背这段代码,而是想说:当数据源头不可控时,处理函数链要在脑子里按“先劈开,再取段,最后清格式”的流程走。否则一个字段的格式问题能让你加班大半宿。
3.2 需求二:MySQL用函数做行转列,同时保留排序
行转列是我被问得最多的MySQL场景之一,也正好和热搜词里的“mysql 行转列”对上。经典的案例是把每个月的销售记录从一列日期转成多列月份。没有动态函数的情况下,最常用的是CASE WHEN + GROUP BY + 聚合函数,它的本质就是条件函数展开后聚合。
例如销售明细表如下:
sql复制CREATE TABLE month_sales (
store_name VARCHAR(30),
sale_month VARCHAR(7),
amount DECIMAL(10,2)
);
INSERT INTO month_sales VALUES
('上海店', '2024-01', 100.00),
('上海店', '2024-02', 150.00),
('北京店', '2024-01', 80.00),
('北京店', '2024-03', 220.00);
想转成一行一个门店、列就是1月到3月的结果,可以使用:
sql复制SELECT store_name,
SUM(CASE WHEN sale_month = '2024-01' THEN amount ELSE 0 END) AS jan_amt,
SUM(CASE WHEN sale_month = '2024-02' THEN amount ELSE 0 END) AS feb_amt,
SUM(CASE WHEN sale_month = '2024-03' THEN amount ELSE 0 END) AS mar_amt
FROM month_sales
GROUP BY store_name
ORDER BY jan_amt DESC;
这套思路还会延伸成“科目转列”、“属性转列”。需要注意一点:如果月份数量不固定,不要把几万个CASE硬写在SQL里。要么在前端拼好固定列,要么用存储过程拼动态SQL。那样虽然复杂一点,却不会因为月份增加就改一次底层查询。函数在这个场景中的定位,是把数据形态从细粒度“扭”成宽表,减少处理层的二次透视工作。
反过来“列转行”通常用UNION ALL把多个列合并成多行,比如把上面表反向拆进明细,这也是很实用的功力。
3.3 需求三:在存储过程中组合多个函数,完成批量清洗
复杂清洗如果只是一条SQL能完成,直接写就行。可要处理几十万行历史数据,按批次循环更新,存储过程就很有用。它的优势是能把函数组合、判断、commit节奏都写进同一个数据库对象,减少网络往返。
一份简化示例,把备注中的空格和Excel换行清理掉,同时对空备注做默认值处理:
sql复制DELIMITER //
DROP PROCEDURE IF EXISTS clean_user_order;//
CREATE PROCEDURE clean_user_order()
BEGIN
DECLARE affected_rows INT DEFAULT 0;
UPDATE user_order
SET remark = REPLACE(REPLACE(remark, '\r\n', ' '), '\n', ' ');
UPDATE user_order
SET remark = TRIM(remark);
UPDATE user_order
SET remark = IFNULL(remark, '无备注')
WHERE remark IS NULL OR remark = '';
SET affected_rows = ROW_COUNT();
SELECT affected_rows AS cleaned_rows;
END//
DELIMITER ;
调用方式为CALL clean_user_order();。这个示例中出现的ROW_COUNT()也是一个函数,它返回上一条DML语句影响的行数,适合在存储过程里做简单日志。函数和存储过程配合的时候,很多人会把“函数可以返回一个值”和“存储过程不一定返回值”混淆,我建议记成:函数是“原料加工机”,存储过程是“作业流水线”。
3.4 快速查函数、找命令:与“MySQL数据库命令大全”匹配的入口
开发到一半想不起某个函数的拼写,是常有的事,不建议遇到一个新函数就百度整篇教程,先掌握几个数据库自带的信息命令:
sql复制SHOW FUNCTION STATUS;
SHOW CREATE FUNCTION function_name;
MySQL没有类似HELP一样覆盖全部内置命令的完美帮助手册,但SHOW FUNCTION STATUS可以列出库内自定义函数。对于内置函数,很多客户端在输入函数左括号时会给提示,配合官方文档搜索足够。
另一个常用入口是在命令行查询当前所有字符集设置、版本和数据目录,这些在排查“为什么函数结果和本地库不一样”时会派上用场。比如:
sql复制SHOW VARIABLES LIKE 'version%';
SHOW VARIABLES LIKE 'character_set%';
SHOW VARIABLES LIKE 'time_zone';
真正理解“MySQL数据库命令大全”不是背下来几十个命令,而是能把函数、聚合、存储过程、权限、性能诊断组合起来用。
4. 我实际遇过的“函数执行不了”与运行环境问题
4.1 注意一件事:你看到的“无法识别”错误不是函数问题
说实话,在排查MySQL数据库问题时,我见过太多人慌慌张张发来错误代码截图,比如在命令行中运行某个命令,却提示“无法将‘xxx’项识别为 cmdlet、函数、脚本文件或可运行程序的名称”。很多新手以为这是“函数没定义好”,其实这个错误和MySQL函数一点关系都没有,是操作系统的命令解析器根本找不到对应程序。
Windows下的PowerShell或CMD,报这类错一般是因为命令所在目录没有加入PATH环境变量。比如刚装完MySQL,直接打开新的命令行窗口执行mysql -uroot -p,如果提示mysql不是内部或外部命令,那就要去“环境变量”设置里,把C:\Program Files\MySQL\MySQL Server 8.0\bin加到PATH中,再重新打开一个命令行窗口。同样,很多人在终端运行git、npm、pnpm等命令时出现类似“无法将...项识别为 cmdlet、函数、脚本文件”的提示,也是在提醒你这些可执行文件路径还没被系统找到,而不是脚本逻辑本身不对。
为什么我要在这里特意展开?因为在项目沟通群里,最常见的“MySQL函数跑不出来”,有一小半其实都是连命令行都没进对。先去mysql --version确认版本能正常输出来,再谈SQL函数和权限问题。
4.2 SQL函数操作时容易犯的权限、连接和字段语义问题
当你确实能进到MySQL环境后,执行函数还有几类高频报错。第一类是把函数写在错误位置。比如:
sql复制SELECT order_no FROM user_order WHERE COUNT(*) > 1;
COUNT(*)是聚合函数,不能在WHERE子句直接使用,会报“Invalid use of group function”。应该改用GROUP BY order_no HAVING COUNT(*)>1。
第二类问题是字段类型和函数返回类型不匹配。比如DATE_FORMAT(pay_time, '%Y-%m-%d')已经返回字符串,你再去和日期类型直接比较,容易产生隐式转换。在写WHERE条件时,最稳的是同样先用STR_TO_DATE转成日期,或者直接和字符串格式一致的值比较。如果格式乱了,可以用STR_TO_DATE这个反向函数来修正:
sql复制SELECT STR_TO_DATE('2024/01/03 12:20:15', '%Y/%m/%d %H:%i:%s');
第三类问题是NULL值扩散。字符串函数传入NULL,返回结果往往是NULL;数值函数传入NULL,结果也容易是NULL。多数情况下这种设计很合理,但用CONCAT拼接字段时,一个字段为NULL会拖累整列,建议用CONCAT_WS或IFNULL规避。
4.3 通过图形工具连不上和数据库工具版本混乱的排查思路
网上很多人会搜“dbx数据库工具官网”或者“数据库工具连接报错”。这类工具确实能提高开发效率,但它们本身也会引入版本和驱动差异。如果一台机器装了多个不同发行版或版本的数据库,客户端工具连接时常常报“访问数据库发生错误”或“驱动类不存在”。
我处理这类问题一般从三个位置排查:第一看端口是不是默认的3306,有没有被其他实例占用;第二看连接字符串里指定的数据库协议或驱动版本;第三看用户名有哪些主机权限,比如root@'localhost'只允许本机连接,远端工具自然进不来。
排查时可以分两步验证。先用命令行连接:
sql复制mysql -h127.0.0.1 -P3306 -uroot -p
如果命令行能通而图形工具不通,问题一定绕不开工具本身的驱动配置或SSL设置。如果图形工具能通而命令行不通,大概率是PATH或默认端口问题。
4.4 MySQL安装配置过程中容易被函数连带影响的事项
前面提到“mysql安装教程”和“mysql安装配置教程”这类热门搜索,背后往往对应一类痛点:费半天劲装好MySQL,却无法正常启动,或者服务起来了但忘了初始密码,后面连函数示例都没法运行。作为踩过坑的人,我提醒几件必要动作:
- 安装时选好配置项后,先确认服务是否在“服务”面板里正常启动。
- 8.0以上版本首次初始化会生成临时密码,通常在错误日志里,需要尽快重置。
- 设置
default_authentication_plugin或账号时,尽量避免用旧版mysql_native_password,新版本默认的caching_sha2_password在部分老客户端里连不上,继而引发连接层面的“函数执行报错”。 - 远程访问时要单独授权,不要直接给
root开放所有主机。
这些看起来和函数无关,但我遇到的函数相关项目,很多都是从“环境不对导致SQL执行不到那一步”开始的。先把安装和登录关卡过了,你后面的每条函数示例都会顺畅很多。
5. 面试与工程中问到的“函数细节”和我的个人使用习惯
5.1 把常用函数整理成自己的MySQL速查命令,别等用时才翻
学习MySQL函数,最好的方式不是枯燥地看文档,而是建一张小表,把项目里最常用的输入输出粘贴进去。我电脑里就有一个文本文件,里面不只是函数名,还有当时踩坑的记录。
例如我自己常用的速查片段:
sql复制-- 字符串拼接,跳过NULL
SELECT CONCAT_WS('-', user_name, remark);
-- 日期格式转换,报表常用
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s');
-- 取某个字段值后直接在SQL里做清洗
SELECT TRIM(REPLACE(REPLACE(remark, CHAR(13), ''), CHAR(10), ''));
从MySQL 8.0开始,还支持了窗口函数、通用表表达式,这让许多过去要靠存储过程或临时表实现的复杂统计可以单条SQL完成。面试官如果在现场问“MySQL有哪些窗口函数”,通常想听到ROW_NUMBER()、RANK()、DENSE_RANK()、LAG()、LEAD()等函数名称和适用场景。遇到分组内排名业务时,用窗口函数往往会比手写临时自连接更清晰:
sql复制SELECT order_no,
category,
amount,
ROW_NUMBER() OVER(PARTITION BY category ORDER BY amount DESC) AS rank_in_cat
FROM user_order;
5.2 索引列套函数,性能下降,应尽可能转成表达式写法
这一节我以前强调过很多次,但还是值得再写一遍,因为它对线上影响太大。看这条SQL:
sql复制SELECT * FROM user_order WHERE DATE_FORMAT(pay_time, '%Y-%m-%d') = '2024-01-03';
从语法上没错,但它对pay_time列先做了一层DATE_FORMAT,数据库没法直接用普通索引加速,会退化成全表扫描,数据量一大就明显慢。更好的做法是:
sql复制SELECT * FROM user_order
WHERE pay_time >= '2024-01-03 00:00:00'
AND pay_time < '2024-01-04 00:00:00';
这种区间写法既保留了等值语义,又能让pay_time上的索引生效。同理,WHERE LEFT(category, 1) = '数'大概率会全表扫,而WHERE category LIKE '数%'还好一些,但左侧通配符仍有隐患。
我在代码评审时,会格外注意函数是否出现在WHERE条件对索引列的包装上。如果一定得用函数,就需要考虑新建冗余字段或生成列。MySQL支持在创建表时定义生成列并允许加索引,例如把年月单独抽出来:
sql复制ALTER TABLE user_order
ADD COLUMN pay_date CHAR(10) AS (DATE_FORMAT(pay_time, '%Y-%m-%d')) STORED;
这种方式在读写分离或报表场景中,既能保住函数逻辑,又能兼顾查询性能,代价是额外列会占一点存储空间。
5.3 关于我这些年用MySQL函数的心得:最小化数据搬运
函数确实是熟练工常用的工具,但我更要提醒:函数越用越顺手不假,可不要一个select里塞十几个函数嵌套,最后连自己都看不懂。我自己的原则是先考虑索引、再考虑可读性,最后才谈“用一行写完的成就感”。
每次写报表,我习惯先明确输入字段和期望输出格式,再逆向找合适的MySQL函数。比如需要把时间转成年月,我会先想到DATE_FORMAT;需要去重合并字符串,MySQL老版本没有内置太优雅的函数,需要结合GROUP_CONCAT来处理,这也是一个非常高频且面试喜欢考的点。用它可以轻松把某个分组内的多个值拼成一个字符串:
sql复制SELECT category,
GROUP_CONCAT(user_name ORDER BY amount DESC SEPARATOR ',') AS user_list,
ROUND(SUM(amount), 2) AS total_amount
FROM user_order
GROUP BY category;
这些年,我对MySQL函数最大的感受是:它不是孤立的一个语法点,而是把数据加工逻辑前置到数据库的重要抓手。能在SQL层减少一次网络拉取,就尽量在SQL层处理清;但也不要过度炫技,复杂的函数套娃让后续开发者维护成本陡增。最终追求的不是“用了多高级的函数”,而是业务数据能否稳定、高效、正确地流转到想要的目标形态。
如果你正在学MySQL函数,我个人最建议的路径是:先把这篇文章里的单行函数示例自己在表上跑一遍,再把行转列案例和存储过程案例改造到自己数据上,最后用EXPLAIN看一遍查询计划。踩过几个真实的坑之后,你对函数的理解和记忆会明显比单纯刷题牢固得多。
