干我们这行的写SQL就像写字一样MySQL内置函数就是最常用的那套笔画。别小看这几十个函数用得好原来要写十几行业务逻辑的查询一行就能解决用得不好线上慢查询一抓一大把报表算错数还得老板亲自来问。这篇文章我把平时实战中高频使用、坑也踩得最多的内置函数整套拆开讲一遍配合具体场景和写法你能直接用上。1. 先搞清楚函数分类与选型思路MySQL内置函数看着多实际分类并不复杂核心就六类字符串函数、数值函数、日期时间函数、聚合函数、流程控制函数以及近几个版本越来越常用的JSON函数。窗口函数虽然是分析场景的利器但语法上相对独立我放到后面单独讲。这里先说一个很关键的思路能用内置函数解决的事尽量不要用应用层Java/Python代码去算。原因是数据库函数直接跑在存储引擎之上避免了数据传输和应用服务器内存占用尤其在WHERE、ORDER BY、GROUP BY这些关键子句里内置函数处理得当还能走索引需要注意写法后面细说。反之如果你把数据捞出来在代码里逐行算数据量一上来性能和资源消耗都会很难看。怎么快速找到对应函数我一般遵循三个原则先明确你要操作的对象类型是字符串、数字、日期还是字段间关系判断直接去对应分类里找。再确认想要的输出是要截取、拼接、转换、聚合还是生成布尔值判断这一步能帮你在同类型函数里快速缩小范围。最后考虑可读性和兼容性同样的效果能用标准SQL写的就不要用方言特性比如字符串拼接用CONCAT就比用||更稳妥虽然MySQL也支持||但还有SQL_MODE的坑。2. 字符串函数最常用的拼图工具字符串函数是日常写SQL用到频率最高的一类没有之一。尤其是做数据清洗和报表字段加工的时候几乎天天打交道。2.1 拼接与截取CONCAT、SUBSTRING、LEFT/RIGHT-- 拼接用户姓名手机号脱敏 SELECT CONCAT(last_name, first_name, , LEFT(phone, 3), ****, RIGHT(phone, 4)) FROM user_info; -- 截取取订单号从第5位开始的6个字符 SELECT SUBSTRING(order_no, 5, 6) AS short_no FROM order_table;CONCAT有个比较容易踩的坑只要任何一个参数为NULL整个结果就是NULL。这在做报表拼接时非常坑。比如地址字段省份为空市区正常CONCAT之后整条地址都没了。实际开发中我推荐用CONCAT_WS第一个参数是分隔符后面参数即使有NULL也会跳过SELECT CONCAT_WS( , province, city, district, detail_address) FROM user_address;SUBSTRING有三个常被记混的写法。SUBSTRING(str, pos)是从第pos位截到末尾SUBSTRING(str, pos, len)是从第pos位开始截len个长度还有SUBSTRING(str FROM pos FOR len)这种SQL标准写法。要注意MySQL的索引位置是从1开始数的不是从0开始别和Java里的substring混了。2.2 查找与替换LOCATE、REPLACE、TRIMLOCATE(substr, str)返回子串第一次出现的位置常用于判断是否包含某个关键字配合IF或CASE来用。注意别和INSTR搞混INSTR(str, substr)参数顺序正好相反。-- 找出所有商品名里包含“限量”的订单 SELECT order_id, product_name FROM order_detail WHERE LOCATE(限量, product_name) 0;REPLACE做字符串替换是最直观的。但是有一个细节REPLACE是全局替换不是只替换第一个匹配项。如果你只想去掉字符串中间的一个特定空格手一快就全部替换了。遇到这种需求建议先用LOCATE定位再用SUBSTRING手工拼一下。TRIM函数不只是去首尾空格还能指定去除字符-- 去掉字符串两端的竖线分隔符 SELECT TRIM(BOTH | FROM |sku_001|sku_002|); -- 常见组合清洗数据时把换行符和制表符也一起去掉 SELECT TRIM(REPLACE(REPLACE(column_name, \r, ), \n, ));2.3 长度与格式转换CHAR_LENGTH、LENGTH、LPAD/RPADCHAR_LENGTH和LENGTH的差别值得重视。CHAR_LENGTH按字符数计算一个中文算1LENGTH按字节计算在UTF-8下中文一个字符算3字节。比如你要开发一个姓名长度校验必须用CHAR_LENGTH否则老外的名字和中文名字统计口径完全乱了。LPAD/RPAD在生成定长编码、流水号时特别好用。比如订单号需要统一8位-- 把自增ID补成8位左边补0 SELECT LPAD(order_id, 8, 0) AS order_no_padded FROM orders;还有一个比较少人关注但很实用的函数FIELD(value, val1, val2, ...)它返回value在参数列表中的位置。灵活用可以解决自定义排序的需求比如你想让状态按“待付款→已付款→已发货→已完成”的顺序展示而不是默认的字母序SELECT order_id, status FROM orders WHERE status IN (pending, paid, shipped, completed) ORDER BY FIELD(status, pending, paid, shipped, completed);2.4 字符串函数的常用注意点汇总场景推荐函数避坑要点拼接多字段CONCAT_WS避免某字段NULL导致整体为NULL截取中间内容SUBSTRING起始位置从1开始判断包含关系LOCATE参数顺序是子串在前定长流水号LPAD长度超过定义时左补失效清洗首尾字符TRIM支持BOTH/LEADING/TRAILING三种方位统计字符个数CHAR_LENGTH区分字符与字节3. 数值函数与精度陷阱数值函数看似简单但金融、统计类业务对精度要求极高一个ROUND用错可能对不上账。3.1 四舍五入ROUND、TRUNCATEROUND是按指定位数四舍五入TRUNCATE则直接截断不四舍五入。这两个区别一定要刻在脑子里SELECT ROUND(123.456, 2); -- 123.46 SELECT TRUNCATE(123.456, 2); -- 123.45 SELECT ROUND(123.456, 0); -- 123 SELECT ROUND(123.456, -1); -- 120负数表示小数点左边ROUND还有一个隐藏行为第二参数不写的时候默认四舍五入到整数。但如果你写ROUND(2.5)和ROUND(3.5)结果可能和你学数学时的预期不一样——MySQL的ROUND采用“四舍六入五成双”的银行家舍入法的一半机会2.5会变成23.5会变成4。具体看浮点数的二进制表示所以金融计算里不要依赖ROUND而是用DECIMAL类型配合应用层算法或者在SQL里先扩大100倍用整数运算。3.2 取整FLOOR、CEIL、CEILINGFLOOR向下取整CEIL向上取整。分页场景算总页数就是经典用法SELECT CEIL(COUNT(*) / 20) AS total_pages FROM orders;负数场景容易混淆FLOOR(-1.2)结果是-2CEIL(-1.2)结果是-1。如果你需要的是向零取整即直接把小数部分去掉那是TRUNCATE(x, 0)别搞混了。3.3 绝对值、符号与幂运算ABS、SIGN、POW/POWER、SQRT、MOD这些基础函数不复杂但MOD有两个地方要留心MOD对负数结果也是负数比如MOD(-7, 3)返回值是-1不是2。做取模分表时建议先对绝对值取模再乘符号避免出现负数分表键。取模还可以用来判断奇偶、周期性任务比如每天跑批时只处理偶数天的数据WHERE MOD(day_of_month, 2) 0。随机数RAND()是个有个性的函数不传参数每次调用返回0到1之间的浮点数传入固定种子后序列就固定了。测试环境需要稳定抽样时用RAND(100)这种固定种子的写法能让每次结果一致方便复现问题。-- 随机抽样固定种子测试环境可复现 SELECT * FROM user_info ORDER BY RAND(20240501) LIMIT 10;3.4 数值与字符串互相转换CAST和CONVERT都支持类型转换。常规写法SELECT CAST(123.45 AS DECIMAL(10,2)); SELECT CONVERT(2024-01-15, DATE);需要注意字符串转数值时MySQL的隐式转换可能会造成索引失效。比如字段是varchar类型你WHERE num_field 123MySQL会自动把字符串和数字比较时转成数值一旦字段上有索引就很可能没法正常走。实践中应该养成左边字段不动、右边参数主动CAST成对应类型的习惯。4. 日期时间函数业务报表的基石日期时间函数是数据分析类需求的基础。很多看起来不复杂的需求比如统计昨日、本月、上季度拆开用对函数后实现差异很大。4.1 获取当前时间NOW、CURDATE、CURTIME、SYSDATENOW()和CURDATE()最常用。NOW()返回当前日期时间CURDATE()只返回日期CURTIME()只返回时间。这里要特别注意NOW()和SYSDATE()的差异NOW()在一条SQL语句执行时取一次值整条语句内恒定SYSDATE()是函数实际执行时获取当前时间。这在长查询、存储过程循环里会导致时间不一致。-- 潜藏的BUG同一SQL里两次调用SYSDATE()结果可能差几秒 -- 建议业务统一用NOW() SELECT SYSDATE(), SLEEP(2), SYSDATE();4.2 日期格式化与解析DATE_FORMAT、STR_TO_DATEDATE_FORMAT是最常用的展示层函数。格式串里**%Y是四位年份%y是两位年份%m是两位月份%c是不带前导零的月份%d是两位日%e是不带前导零的日%H是24小时制%h是12小时制**。这些细节拼错一个就全表数据错乱。SELECT DATE_FORMAT(create_time, %Y-%m-%d %H:%i:%s) AS formatted_time FROM orders; -- 统计某小时段的订单数 SELECT DATE_FORMAT(create_time, %Y-%m-%d %H) AS hour_bucket, COUNT(*) FROM orders GROUP BY hour_bucket;STR_TO_DATE是DATE_FORMAT的逆操作把字符串按指定格式解析成日期。ETL场景经常需要处理各种来源的日志时间戳SELECT STR_TO_DATE(2024/06/15 22:30:00, %Y/%m/%d %H:%i:%s);4.3 日期加减DATE_ADD、DATE_SUB、INTERVALDATE_ADD和DATE_SUB配合INTERVAL关键字可以做日、周、月、季度、年的加减-- 三天前的订单 SELECT * FROM orders WHERE create_time DATE_SUB(NOW(), INTERVAL 3 DAY); -- 下个季度的起始日 SELECT DATE_ADD(2024-04-01, INTERVAL 1 QUARTER);性能建议WHERE子句里不要让函数套在字段上。比如WHERE DATE(create_time) CURDATE()会让索引失效。正确写法是-- 推荐写法字段裸露函数作用在右值 SELECT * FROM orders WHERE create_time CURDATE() AND create_time DATE_ADD(CURDATE(), INTERVAL 1 DAY);4.4 提取日期组成部分YEAR、MONTH、DAY、WEEK、QUARTER这些函数在报表分组里很常用SELECT YEAR(create_time) AS year_no, MONTH(create_time) AS month_no, DAY(create_time) AS day_no, QUARTER(create_time) AS quarter_no, WEEK(create_time) AS week_no FROM orders;WEEK函数有个模式参数需要注意WEEK(date, 0)表示以周日为一周的第一天WEEK(date, 1)表示以周一为一周的第一天。国内业务习惯按周一作为一周开始所以直接写WEEK(create_time)得到的周数可能和业务口径对不上建议明确写WEEK(create_time, 1)。4.5 日期差值DATEDIFF、TIMESTAMPDIFFDATEDIFF只按天算差值精确到天SELECT DATEDIFF(2024-06-20, 2024-06-15); -- 5TIMESTAMPDIFF更灵活可以指定单位SECOND、MINUTE、HOUR、DAY、WEEK、MONTH、QUARTER、YEAR-- 用户注册时长按月 SELECT user_id, TIMESTAMPDIFF(MONTH, register_time, NOW()) AS registered_months FROM user_info;TIMESTAMPDIFF在算年龄时有个小技巧用YEAR单位计算年龄结果自动忽略未满整年的部分比手动DATEDIFF再除以365要准确因为它不会受闰年影响。5. 聚合函数与分组统计的进阶玩法聚合函数是报表类SQL的灵魂。除了常用的COUNT、SUM、AVG、MAX、MIN这里我想重点聊几个实战中容易忽略的细节。5.1 COUNT的三种写法和语义差异COUNT()统计所有行数COUNT(1)和COUNT()性能基本相同COUNT(column_name)只统计该列非NULL的行数。这个差异在可空字段上体现得最明显SELECT COUNT(*) AS total_orders, COUNT(pay_time) AS paid_orders, -- 未支付订单pay_time是NULL不会计入 COUNT(DISTINCT user_id) AS unique_users FROM orders;COUNT(DISTINCT column)在数据量大时性能损耗明显。如果需求只是判断某个值是否存在优先用EXISTS不要COUNT然后判断0。5.2 SUM的空值陷阱SUM函数对NULL值不会报错但它会把NULL直接忽略而不是当作0。如果整组都是NULLSUM返回NULL不是0。报表展示时你期望的是0结果页面上显示一个空字符串前端还得特判。建议组合COALESCESELECT COALESCE(SUM(amount), 0) AS total_amount FROM orders WHERE status pending;5.3 GROUP_CONCAT的妙用GROUP_CONCAT可以把分组内的某个字段拼接成一个字符串适合做一对多关系的汇总展平。比如一个订单对应多个商品标签需要把标签变成一行SELECT order_id, GROUP_CONCAT(tag_name ORDER BY sort_no SEPARATOR ,) AS tag_list FROM order_tags GROUP BY order_id;GROUP_CONCAT有两个限制很坑默认最大长度是1024字节拼接超长会被静默截断第二个是排序和去重语法比较好记在字段里写ORDER BY或者DISTINCT。如果业务场景需要更长结果可以在会话级别或者配置里调大group_concat_max_len。5.4 HAVING与WHERE的分工WHERE在分组前过滤原始行HAVING在分组后过滤聚合结果。这个属于基础中的基础但实际开发中常见的问题是有人为了省事把原本可以在WHERE里过滤的条件写进了HAVING。假如数据量几百万这个写法会让分组和聚合白白处理一批不应该进入的数据慢查询往往就是这么来的。-- 正确写法先WHERE过滤掉“已取消”订单再做聚合 SELECT user_id, SUM(amount) AS total_spent FROM orders WHERE status ! cancelled GROUP BY user_id HAVING SUM(amount) 1000;6. 流程控制函数把判断逻辑写进SQL很多在代码里写的if-else逻辑其实可以直接用MySQL的流程控制函数在SQL层完成。合理使用能大量减少应用层数据搬运让统计逻辑更集中。6.1 IF函数与IFNULL的适用边界IF(expr, val1, val2)是最基础的三元判断。适合简单二选一SELECT product_name, IF(stock_count 0, 有货, 缺货) AS stock_status FROM products;IFNULL(a, b)是IF的简化版专门处理NULL值逻辑等价于IF(a IS NOT NULL, a, b)。注意IFNULL只能判断一个参数如果是空字符串它不是NULLIFNULL不会回退。6.2 CASE WHEN真正的多分支判断CASE WHEN比IF灵活得多支持多条件、范围判断SELECT order_id, CASE WHEN amount 5000 THEN 大额订单 WHEN amount 1000 THEN 中等订单 WHEN amount 0 THEN 小额订单 ELSE 异常订单 END AS order_level FROM orders;用法上建议留意的是CASE WHEN按顺序短路匹配。所以条件的先后顺序要仔细安排把最具体的条件放在前面否则会出现某个订单落入第一个匹配分支后就结束了。另一种写法CASE column WHEN value THEN ...是等值匹配写法不支持范围判断看情况选即可。6.3 NULLIF与COALESCE的妙用NULLIF(a, b)当a等于b时返回NULL否则返回a。常见场景是避免除以零SELECT total_amount / NULLIF(total_count, 0) AS avg_amount FROM stats;当total_count为0时NULLIF把它变成NULL整个除法的结果就是NULL避免了报错比应用层加if判断更简洁。COALESCE是查找列表中第一个非NULL值SELECT COALESCE(real_name, nick_name, 匿名用户) AS display_name FROM user_info;这里有一个嵌套使用的进阶技巧先NULLIF把不希望当成合法值的值转成NULL再COALESCE取备用值。-- 电话号码为空字符串时展示手机号手机号也空展示座机 SELECT COALESCE(NULLIF(phone, ), mobile, landline) AS contact FROM customer;7. JSON函数MySQL也能当文档数据库用MySQL 5.7之后JSON支持越来越完善8.0版本里JSON函数已经足够应对大部分轻量文档存储场景。如果你们业务有“变长属性”的存储需求JSON字段可以省掉一堆扩展表。7.1 查询JSONJSON_EXTRACT与-、-运算符JSON_EXTRACT(json_doc, path)提取JSON中指定路径的值-- 假设extra_info存的是{address:{city:广州,district:天河},level:3} SELECT JSON_EXTRACT(extra_info, $.address.city) AS city, JSON_EXTRACT(extra_info, $.level) AS level FROM users;-和-是简写-返回带引号的JSON值-返回纯字符串。实际使用中对比等值条件时你要用-因为-取出来的值还带着双引号和字符串比较会失败。这个坑我遇到不止一次。-- 错误示例大概率匹配不上 SELECT * FROM users WHERE extra_info-$.level 3; -- 正确示例 SELECT * FROM users WHERE extra_info-$.level 3;7.2 生成JSONJSON_OBJECT与JSON_ARRAY做接口返回需要组装JSON时不用在应用层拼字符串SQL里直接生成SELECT user_id, JSON_OBJECT( name, nick_name, tags, JSON_ARRAY(vip, 老用户) ) AS user_json FROM users;7.3 JSON聚合JSON_ARRAYAGG与JSON_OBJECTAGG这两个是聚合函数在报表里把分组内的行聚合成JSON数组或对象SELECT category_id, JSON_ARRAYAGG(product_name) AS product_list FROM products GROUP BY category_id;JSON_OBJECTAGG(key, value)可以把分组内的键值对聚合成一个JSON对象比如成绩单SELECT student_id, JSON_OBJECTAGG(course_name, score) AS score_map FROM score_table GROUP BY student_id;7.4 JSON修改与删除JSON_SET、JSON_INSERT、JSON_REMOVE这几个函数在日常维护场景里会用到。JSON_SET是更新或插入指定路径JSON_INSERT是只插入更新不存在路径-- 更新或新增key UPDATE users SET extra_info JSON_SET(extra_info, $.level, 5) WHERE user_id 1001; -- 删除某个key UPDATE users SET extra_info JSON_REMOVE(extra_info, $.old_key) WHERE user_id 1001;用JSON字段时我强烈建议JSON里的key名不要用中文虽然MySQL支持但后续所有JSON_EXTRACT路径都会变长可读性直线下降排查问题很痛苦。8. 窗口函数与分析查询窗口函数在MySQL 8.0里终于补齐了。以前要“分组内排名”、“分组内TopN”这类需求得用临时变量或者自连接写法又绕又慢。现在有ROW_NUMBER、RANK、DENSE_RANK、SUM OVER等函数清晰得多。8.1 分组排序ROW_NUMBER与RANK/DENSE_RANK的区别ROW_NUMBER()为分组内每行分配唯一的连续序号RANK()会跳过排名如并列第一之后直接到第3名DENSE_RANK()不跳排名并列第一之后还是第2名。看这个对比SELECT student_name, subject_name, score, ROW_NUMBER() OVER (PARTITION BY subject_name ORDER BY score DESC) AS row_no, RANK() OVER (PARTITION BY subject_name ORDER BY score DESC) AS rank_no, DENSE_RANK() OVER (PARTITION BY subject_name ORDER BY score DESC) AS dense_no FROM scores;如果成绩相同RANK和DENSE_RANK并列ROW_NUMBER必须分出先后顺序由ORDER BY决定是不确定的。如果你要求并列成绩必须同排名用RANK如果要求每个学生都有一个唯一排名号码用ROW_NUMBER。8.2 分组TopN窗口函数子查询找出每个分类下销量最高的商品SELECT category_id, product_name, sales_cnt FROM ( SELECT category_id, product_name, sales_cnt, ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY sales_cnt DESC) AS rn FROM product_sales ) t WHERE rn 1;这条SQL是面试高频题也是实际业务里“排行榜”“筛选每条分组最新记录”的通用写法。8.3 移动计算SUM/AVG OVER移动总和、移动平均值在趋势分析里常用SELECT create_date, daily_amount, SUM(daily_amount) OVER (ORDER BY create_date) AS cumulative_amount, AVG(daily_amount) OVER (ORDER BY create_date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS avg_7d FROM daily_stats;第一行SUM OVER是累计总和。第二行AVG OVER用ROWS BETWEEN指定窗口范围算的是“最近7天平均”。窗口计算的结果可以让后续SQL语句直接使用减少应用层循环效率提升非常明显。8.4 窗口函数使用禁忌窗口函数不能直接用在WHERE子句里必须包一层子查询或CTE。CTEWITH子句在涉及多个窗口计算的复杂查询里可读性提升明显WITH ranked AS ( SELECT department_id, employee_name, salary, RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rk FROM employees ) SELECT * FROM ranked WHERE rk 3;另外窗口函数虽然强大但数据集在内存/临时盘空间有限时会落地到磁盘超大结果集下性能不一定好。报表场景可以放心用OLTP高并发查询里尽量别把窗口函数甩到业务主路上。9. 常见问题排查与性能实战最后这部分我系统梳理一下内置函数实际使用中容易踩的坑和排查方向都是平时支持一线开发时反复遇到的问题。9.1 函数导致索引失效这是最普遍的性能杀手。核心原则一句话左边字段保持原样右边参数随便套函数。-- 反例DATE函数包住字段索引失效 SELECT * FROM orders WHERE DATE(create_time) 2024-06-15; -- 正例字段裸露右侧用区间 SELECT * FROM orders WHERE create_time 2024-06-15 00:00:00 AND create_time 2024-06-16 00:00:00;字符串字段和数值比较varchar字段存的全是数字时MySQL会自找麻烦做隐式转换让索引失效。排查慢查询时EXPLAIN看到typeALL且rows巨大先看一眼WHERE条件两边的类型是否一致。9.2 隐式字符集转换两张表联表查询关联字段一个utf8mb4和一个utf8或排序规则不一致utf8mb4_general_ci和utf8mb4_unicode_ciMySQL可能对字段做隐式转换索引失效还是小事严重时结果乱套。排查方法SHOW CREATE TABLE看字符集或EXPLAIN看Extra里有没有Using whereUsing index之外的异常提示。9.3 聚合函数加WHERE和HAVING混用结果不直观比如统计“近30天下单用户中累计消费超过1万元”的名单有人先GROUP BY再加HAVING SUM(amount) 10000结果把最近30天之外的消费也统计进去了。正确做法是先用WHERE限定窗口再聚合再HAVING。9.4 数值精度丢失报表金额对不上账先看有没有直接拿FLOAT/DOUBLE字段做累加或ROUND。MySQL的FLOAT在累加高精度金额时可能有二进制浮点误差。金额字段建议使用DECIMAL(10,2)或DECIMAL(12,2)。已经用了FLOAT的存量表至少要在聚合函数外面套ROUND控制精度但根本解法还是改字段类型。9.5 处理NULL不统一NULL在整个函数体系里的行为非常不统一一定要主动用IFNULL、COALESCE、NULLIF去规范。业务上明确“空0”的数值字段建议在表设计时就用NOT NULL DEFAULT 0省一堆后面的麻烦。9.6 常用函数排查速查表现象可能原因解决方向CONCAT结果空白某个字段为NULL改用CONCAT_WS或包IFNULL日期分组对不上时区未统一检查jdbc连接serverTimezone与session_time_zone周统计数字不对WEEK默认周日开始显式指定WEEK(date, 1)COUNT比明细少字段有NULL值按业务需求选择COUNT(*)或COUNT(column)SUM结果为空该组全部为NULLCOALESCE(SUM(...), 0)字符串数字比较不走索引隐式类型转换统一字段类型或CAST右侧条件GROUP_CONCAT被截断默认长度1024调大group_concat_max_lenJSON比较条件匹配不上用了-而非-等值判断用-取纯文本10. 几个实战经验总结最后分享一条我个人体会很深的经验内置函数虽然概念不难但每次上线之前建议把SQL里涉及的每个函数都在测试库里跑一遍边界值。比如字符串为、字段为NULL、数值取到-1、日期正好是闰年2月29这些边界情况往往才是生产事故的高发区。还有一个习惯值得养成函数套函数嵌套超过三层的时候就该考虑拆开写用子查询先加工一层再有外部查询继续加工。可读性好排查问题也方便。我以前接过一个历史报表字段里套了6层函数调了一晚上才理清逻辑。后来宁可多写一层CTE也不做这种无人能维护的“函数套娃”。再补一个小技巧像DATE_FORMAT这种格式化函数在GROUP BY里可以直接按格式化后的字符串分组但如果只按年月分组其实用YEAR(create_time), MONTH(create_time)两层分组效率更高对索引更友好因为格式化会强制转字符串。具体取舍还要看表的索引结构和数据量。MySQL内置函数的边界远不止我上述列举的比如加密函数MD5/SHA2、空间函数、全文检索函数MATCH AGAINST。真正常用的就集中在上面这几类。把常用的一百多个函数吃透边用边总结自己的场景效果远比背函数手册好。希望这篇能帮你少踩几个坑遇到报表问题能更快定位方向。