ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MySQL函数全解析:单行函数与聚合函数的原理、应用及性能优化

MySQL函数全解析:单行函数与聚合函数的原理、应用及性能优化 1. 先把函数的家底摸清楚单行函数和聚合函数的分野1.1 为什么我们的SQL里离不开函数上个月帮业务部门整理一份用户留存报表卡在一个很不起眼的环节上注册时间字段是 datetime 类型长这样2024-03-15 10:24:11但运营想按2024-03这种月粒度看人数还要在展示层附带注册满60天的标记。如果没有函数这个需求要么让Java/C程序逐行处理要么在SQL里先查出全量数据再手工拼格式听起来就很痛苦。这就是MySQL函数存在的意义它把数据加工这件事从应用层下沉到了数据库层。你写WHERE register_time ...拿到的可能只是原始数据而函数可以在查询过程中直接完成截取、拼接、转换、统计、判断让结果集本身就是想要的最终形态。我见过很多新手绕过函数硬扛在Excel里加班到凌晨处理导出数据其实一条DATE_FORMAT就能解决。函数用得好不好直接影响两条线一是SQL写的长短和可读性二是查询性能。更重要的是函数是MySQL面试高频考点——这在技术面试里几乎必问从简单的CHAR_LENGTH和LENGTH有什么区别到复杂的DATE用函数后为什么索引失效背后全是函数相关的深入理解。1.2 两大类函数的工作方式完全不同在MySQL里函数大体分成两类这个分野非常重要因为它决定了语法上的硬性规则。单行函数对查询结果集中的每一行独立处理输入一行输出一行。比如UPPER(name)把某一行名字转大写不影响行数。常见的有字符串函数、数值函数、日期时间函数、流程控制函数。聚合函数对一组行做汇总输入多行输出一行。比如COUNT(*)、SUM(salary)、AVG(score)、MAX(age)。它经常和GROUP BY配合使用没有GROUP BY时则把整张表当成一组。这两类函数最典型的语法区别是聚合函数不能直接出现在WHERE子句里。-- 错误写法WHERE中不能用聚合函数 SELECT department, AVG(salary) FROM employees WHERE AVG(salary) 8000 GROUP BY department; -- 正确写法用HAVING过滤分组结果 SELECT department, AVG(salary) FROM employees GROUP BY department HAVING AVG(salary) 8000;很多新手的困惑都出在这里其实只要记住一句话WHERE是对每一行做筛选发生在分组之前HAVING是对分组结果做筛选发生在分组之后。聚合函数只能对已经形成的分组汇总所以只能待在HAVING或SELECT里。单行函数和聚合函数的关系不是对立的它们经常叠加使用。比如SELECT department, ROUND(AVG(salary), 2) FROM employees GROUP BY department;这串代码里AVG()是聚合函数ROUND()是单行函数先把一组员工的工资求平均再对平均值做四舍五入。这种组合在实际报表里极其常见。2. 字符串函数日常取报表时最常用的一组2.1 拼接、截取、替换全家桶字符串函数是日常开发中用到频率最高的一类几乎每个查询都会碰到。先说拼接MySQL提供了CONCAT()和它的进阶版CONCAT_WS()。SELECT CONCAT(last_name, , first_name) AS full_name FROM employees;CONCAT只要有一个参数是NULL整个结果就变成NULL这一点极容易踩坑。比如用户昵称字段为空拼出来的地址就是空的。解决办法有两个用IFNULL()把可能为NULL的字段先变成空字符串或者换成CONCAT_WS()。-- CONCAT_WS第一个参数是分隔符剩下的参数即使有NULL也会自动跳过 SELECT CONCAT_WS( - , province, city, district) AS full_address FROM users;CONCAT_WS在处理地址拼接、标签拼接这类有固定分隔符的场景时比CONCAT干净很多少写不少IFNULL嵌套。截取字符串有三个常用函数SUBSTRING、LEFT、RIGHT。格式分别是SUBSTRING(str, pos, len) -- 从pos开始截取len个字符 LEFT(str, len) -- 从左边截取len个字符 RIGHT(str, len) -- 从右边截取len个字符需要注意SUBSTRING的起始位置从 1 开始不是从 0 开始这和很多编程语言的数组下标不完全一样。如果你写SUBSTRING(abcdef, 0, 3)MySQL 会把它自动当成从位置 1 开始处理返回abc。这个细节在写分页截取或者解析编码时容易造成混淆建议统一从 1 开始思考。替换函数REPLACE()很简单但有个使用上的思维误区它替换的是字符串里所有匹配的子串不是只替换第一个。比如REPLACE(2024-03-15, -, /)会把两个横杠都替换得到2024/03/15。如果想只替换某一次出现的位置那得先用LOCATE()找到位置再构造日常业务里这种需求反而少直接REPLACE全量替换通常就够了。TRIM()系列也值得多说一句。TRIM()默认去掉字符串首尾空格还有LTRIM()和RTRIM()分别处理左侧、右侧。但要注意TRIM()是去掉头尾的空格字符串中间的空格它管不了。如果要把多个连续空格压缩成单空格需要用REPLACE(name, , )或借助正则REGEXP_REPLACE()。MySQL 8.0 中REGEXP_REPLACE的写法是SELECT REGEXP_REPLACE(这是一段 包含 多余空格 的文本, , );2.2 字符长度和字节长度一个经典的坑长度函数有两个长得非常像含义却完全不同CHAR_LENGTH(str)返回字符个数中文、英文、数字都算一个字符。LENGTH(str)返回字节数和字符集强相关。utf8mb4 编码下一个汉字占 3 个字节一个英文字母占 1 个字节。SELECT CHAR_LENGTH(数据库) AS char_cnt, LENGTH(数据库) AS byte_cnt; -- 结果char_cnt 3byte_cnt 9utf8mb4这个差别实战中太常见了。比如规定用户昵称最长 20 个字符你在应用层用strlen()按字节数校验到数据库发现明明没写几个字却报Data too long——多半就是字节和字符没分开算。做字段长度校验一定要用CHAR_LENGTH对应的语义如果非要按字节截断可以用LEFT()配合LENGTH()判断但更推荐直接在设计表时把VARCHAR(50)和字符集的关系想清楚。另外大小写转换UPPER()和LOWER()也要和字符集联系起来理解MySQL 的默认排序规则collation一般是utf8mb4_general_ci或utf8mb4_unicode_ci_ci结尾就是 case-insensitive即查询时WHERE name abc也能匹配ABC。如果你一定要区分大小写除了改排序规则也可以在函数层面用BINARY关键词或者LIKE BINARY。这些知识点不用背但遇到功能表现和预期不符时要知道往字符集和排序规则的方向排查。3. 数值函数格式化、取整和类型转换3.1 取整家族的四兄弟别记混了数值函数里最容易让人犯迷糊的是ROUND、TRUNCATE、CEIL、FLOOR这四兄弟。直接看表函数作用例子结果ROUND(x, d)四舍五入保留d位小数ROUND(3.456, 2)3.46TRUNCATE(x, d)直接截断保留d位小数TRUNCATE(3.456, 2)3.45CEIL(x)/CEILING(x)向上取整为整数CEIL(3.2)4FLOOR(x)向下取整为整数FLOOR(3.8)3ROUND和TRUNCATE的差别就是要不要进位一个是四舍五入一个是硬截断。有的业务场景对精度非常敏感比如算佣金如果该截断却用了四舍五入每一单多一分钱批量跑下来就是一笔不小的误差。CEIL和FLOOR也是向下、向上的问题。还有一个很常见的应用场景分页计算总页数时CEIL(total_count / page_size)比任何手动判断都简洁——总记录数除以每页条数后向上取整正好是总页数。MOD(x, y)求余数使用频率也不低比如判断奇偶、按用户ID做分库分表的取模路由。写法上和Java取模的语义一致SELECT MOD(10, 3); -- 1另外一个容易忽略的小函数是ABS()绝对值。看起来很简单但它在计算两个时间点之间相隔的秒数时就很有用因为日期相减可能出现负数包一层ABS()就能保证结果为正。3.2 格式化数字和类型转换线上报表经常遇到数字太长展示不方便的场景比如金额1234567.891希望显示成1,234,567.89。MySQL 提供了FORMAT()函数SELECT FORMAT(1234567.891, 2); -- 结果1,234,567.89注意FORMAT()返回的是字符串类型不是数值类型。这一点在后续做数值比较或者排序时如果没注意可能出现类型隐式转换的问题——虽说 MySQL 在处理字符串和数字比较时一般会转换但我建议所有对展示格式有要求的字段在SQL层就明确知道它是字符串避免订单金额字段意外变成按字典序排序导致顺序不对。类型转换函数有两个CAST()和CONVERT()。它们的区别不大写法不同而已SELECT CAST(2024-03-15 AS DATE); SELECT CONVERT(2024-03-15, DATE);CAST(123 AS UNSIGNED)可以把字符串转成整数CAST(123.45 AS DECIMAL(10,2))可以转成定点数并保留两位小数。我在实际开发中的经验是优先使用CAST因为它是SQL标准语法可读性也更好CONVERT在MySQL里还有一个特殊用途是转换字符集比如CONVERT(name USING gbk)这个场景一般用在处理历史遗留的乱码数据时。RAND()随机函数也值得记一下RAND()返回 0 到 1 之间的随机小数。要生成一个 [a, b] 区间内的随机整数可以这样写SELECT FLOOR(a RAND() * (b - a 1));比如随机抽 10 条用户记录做活动测试直接ORDER BY RAND() LIMIT 10就能实现。但这里要提醒一句ORDER BY RAND()在大表上性能很差它会全表扫描并为每行生成随机数再排序。如果表有几十万行甚至更多建议先查出一个较小的ID范围再用RAND()取偏移量或者干脆在应用层做随机不然高峰期一条查询就能把数据库拖垮。4. 日期时间函数报表统计的硬通货4.1 获取当前时间别把NOW和SYSDATE搞混日期时间函数是我认为MySQL所有函数里性价比最高的一组因为几乎所有业务都脱离不了时间。首先是获取当前时间的三个常用函数SELECT NOW(); -- 2024-05-10 14:30:25 SELECT CURDATE(); -- 2024-05-10 SELECT CURTIME(); -- 14:30:25NOW()返回年月日时分秒CURDATE()只返回日期CURTIME()只返回时间。还有一个容易被混淆的是SYSDATE()它和NOW()的区别在于执行时机NOW()是语句开始执行的时间点SYSDATE()是函数真正被调用到的那一刻。在一条慢SQL里SYSDATE()可能比NOW()晚几十毫秒甚至更久对于基于时间的一致性判断建议用NOW()避免因为执行时间长导致判断基准漂移。4.2 DATE_FORMAT日期格式化的一把刀DATE_FORMAT(date, format)是把日期按指定格式输出的核心函数。格式符非常多常用的记这几个就够了格式符含义示例%Y四位年份2024%y两位年份24%m两位月份03%c月份1-12不带前导03%d两位日05%e日不带前导05%H24小时制小时14%i分钟30%s秒25%W星期名Friday%w星期几数字5最常见的用法是按月份分组统计SELECT DATE_FORMAT(created_at, %Y-%m) AS month, COUNT(*) AS order_cnt, SUM(amount) AS total_amount FROM orders GROUP BY DATE_FORMAT(created_at, %Y-%m) ORDER BY month;这个查询是月度销售报表的模板代码几乎所有团队都写过。把%Y-%m换成分隔符也能很轻松地变成季度粒度%Y-%m结合QUARTER或者按天统计。和DATE_FORMAT配对的是STR_TO_DATE(str, format)作用刚好相反把字符串解析成日期。比如外部系统导进来一批数据时间是15/03/2024这种格式直接用字符串比较会出错必须先转成标准日期SELECT STR_TO_DATE(15/03/2024, %d/%m/%Y); -- 结果2024-03-154.3 日期差值和加减计算业务上经常要算距离今天多少天两个日期差几个月常用函数有DATEDIFF()和TIMESTAMPDIFF()。SELECT DATEDIFF(2024-05-10, 2024-03-15); -- 56只算天数差 SELECT TIMESTAMPDIFF(MONTH, 2024-03-15, 2024-05-10); -- 1不足整月则向下取整DATEDIFF只接受日期部分即使传入datetime也只看日期两个参数相减得到天数。TIMESTAMPDIFF更灵活单位可以是SECOND、MINUTE、HOUR、DAY、WEEK、MONTH、QUARTER、YEAR而且会自动对齐单位边界。这里有一个细节值得记牢TIMESTAMPDIFF(MONTH, 2024-01-31, 2024-02-29)返回 1但如果两个日期在参数顺序上写反了会返回负数。日期加减运算主要靠DATE_ADD()和DATE_SUB()SELECT DATE_ADD(2024-05-10, INTERVAL 7 DAY); -- 2024-05-17 SELECT DATE_SUB(2024-05-10, INTERVAL 1 MONTH); -- 2024-04-10INTERVAL后面可以跟DAY、HOUR、MINUTE、MONTH、YEAR等。我之前写一个续费提醒筛选已到期 3 到 7 天内的用户就用DATE_SUB(CURDATE(), INTERVAL 7 DAY)和DATE_SUB(CURDATE(), INTERVAL 3 DAY)把边界算出来再做范围查询代码简洁性能也好。还有一组常用提取函数YEAR(date)、MONTH(date)、DAY(date)、QUARTER(date)、WEEK(date)。比如统计每季度新注册用户数SELECT YEAR(created_at) AS yr, QUARTER(created_at) AS quarter, COUNT(*) AS cnt FROM users GROUP BY YEAR(created_at), QUARTER(created_at);最后必须提醒的是时区问题。MySQL 的NOW()返回的是数据库会话时区的时间。如果应用服务器和数据库服务器在不同的时区或者连接串里设置了serverTimezone可能出现代码里打印时间和数据库时间差8小时的现象。排查这种问题第一件事就是执行SELECT NOW();看数据库当前时间到底是多少然后再看 JDBC 连接参数。函数本身没问题往往是配置层面的锅。5. 流程控制函数SQL 里的 if-else 和 switch-case5.1 IF、IFNULL、NULLIF、COALESCE 四选一很多开发者以为 SQL 只能死板地返回列值其实流程控制函数非常灵活。先说最简单的IF()它接受三个参数IF(expr, value_if_true, value_if_false) -- 类似于Java里的三元运算符 SELECT name, IF(age 18, 成年, 未成年) AS age_group FROM users;IFNULL(expr1, expr2)专门处理 NULL如果expr1不是 NULL 就返回它否则返回expr2。这个函数在处理可空字段的兜底展示时非常好用比如手机号没填默认展示未填写SELECT user_name, IFNULL(phone, 未填写) AS phone_display FROM users;NULLIF(expr1, expr2)则是反过来的逻辑如果两个表达式相等返回 NULL否则返回expr1。它常用于防止除零错误。比如计算人均消费金额时如果消费人数为 0直接除会报错可以先NULLIF(count, 0)让分母变成 NULL除法结果就是 NULL再配合IFNULL显示 0SELECT department, IFNULL(SUM(amount) / NULLIF(COUNT(*), 0), 0) AS avg_amount FROM orders GROUP BY department;COALESCE(expr1, expr2, ...)是IFNULL的多参数版本从左到右返回第一个非 NULL 的值。比如用户有手机号、邮箱、微信号多个联系方式展示时取优先级最高的那个非空值SELECT COALESCE(phone, email, wechat_id, 无联系方式) AS contact FROM users;这几个函数为什么值得一起讲因为它们在面试中经常被混在一起问尤其是考察你对 NULL 语义的理解程度。MySQL 中的 NULL 和空字符串是两码事IFNULL(, 默认)返回空字符串IFNULL(NULL, 默认)才返回默认值。具体业务里到底空字符串代表什么、NULL代表什么得在表设计阶段就约定清楚不然函数用得越多越混乱。5.2 CASE WHEN比 IF 更强大的条件分支如果条件分支比较复杂比如多条件、多区间IF的嵌套写起来非常痛苦这时候应该用CASE WHEN。语法有两种-- 简单表达式写法 CASE sex WHEN M THEN 男 WHEN F THEN 女 ELSE 未知 END -- 搜索表达式写法更常用 CASE WHEN score 90 THEN 优秀 WHEN score 60 THEN 及格 ELSE 不及格 END推荐使用搜索写法因为它的适用范围更广可以对不同字段做不同条件的判断。CASE WHEN不仅能在SELECT列表里用还能在WHERE、ORDER BY、GROUP BY里用这一点很多新手不知道。比如按自定义规则排序-- 优先级VIP用户排最前然后按注册时间倒序 SELECT user_name, is_vip, created_at FROM users ORDER BY CASE WHEN is_vip 1 THEN 0 ELSE 1 END, created_at DESC;又比如在GROUP BY里做自定义分组SELECT CASE WHEN age 18 THEN 少年 WHEN age 30 THEN 青年 WHEN age 50 THEN 中年 ELSE 老年 END AS age_band, COUNT(*) AS cnt FROM users GROUP BY age_band;很多人以为GROUP BY后面只能跟字段名其实它可以跟SELECT列表里的别名也可以跟整个表达式。用CASE WHEN做分桶统计比在应用层写逻辑再循环汇总要高效得多。CASE WHEN还有一个隐藏的价值它可以把行转列。比如一张表记录每个学生的各科成绩想展示成一行一个学生三列分别显示语数英就可以这样写SELECT student_id, MAX(CASE WHEN subject 语文 THEN score END) AS chinese, MAX(CASE WHEN subject 数学 THEN score END) AS math, MAX(CASE WHEN subject 英语 THEN score END) AS english FROM scores GROUP BY student_id;这是CASE WHEN加聚合函数配合的经典用法做透视图表时经常用强烈建议掌握。6. 聚合函数和 GROUP BY 深度绑定的统计核心6.1 COUNT 的三种写法面试常问的细节问题聚合函数是报表需求的半壁江山其中COUNT的写法争议最多面试也常被追问COUNT(*)、COUNT(1)、COUNT(字段)有什么区别COUNT(*)统计所有行数包括某一列为 NULL 的行。COUNT(1)和COUNT(*)在 MySQL 8.0 中性能基本一致也统计所有行。COUNT(字段)统计该字段非 NULL 的行数。如果字段是 NULL这一行不计数。三者的性能差异在 MySQL 最新版本里几乎可以忽略真正的区别是语义差别。需要统计到底有多少行用COUNT(*)需要统计某个字段有值的记录数用COUNT(字段)。SUM也容易踩 NULL 的坑SUM(amount)会自动忽略 NULL如果所有行都是 NULL返回结果也是 NULL 而不是 0。所以展示时经常要包一层IFNULL(SUM(amount), 0)。AVG同样忽略 NULL。要注意它和先求和再除以行数不一样因为被 NULL 过滤后分母变了。举一个例子三个员工的工资分别是 10000、20000、NULLAVG(salary)结果是 15000 而不是 10000。如果要让 NULL 按 0 参与平均需要先处理成 0 再聚合SELECT department, AVG(IFNULL(salary, 0)) AS avg_salary_with_null_as_zero FROM employees GROUP BY department;MAX和MIN比较简单但也有一个细节比较 VARCHAR 时按照字典序不是数字序。比如字符串100和99MAX返回的是99而不是100因为它们先按第一位字符比较。如果字段是字符串类型却要取最大数值需要先用CAST转成数字再取MAX。COUNT(DISTINCT expr)是去重计数的利器比如统计每个城市有多少不同的用户SELECT city, COUNT(DISTINCT user_id) AS distinct_users FROM user_activity GROUP BY city;6.2 GROUP_CONCAT把多行数据拼到一格GROUP_CONCAT是一个容易被忽略但极其好用的聚合函数作用是把分组内的一列值拼成一个字符串。比如统计每个部门都有哪些员工SELECT department, GROUP_CONCAT(user_name) AS member_names FROM employees GROUP BY department;默认用逗号分隔也可以通过SEPARATOR指定分隔符SELECT department, GROUP_CONCAT(user_name ORDER BY user_id SEPARATOR | ) AS member_names FROM employees GROUP BY department;甚至可以搭配DISTINCT去重SELECT GROUP_CONCAT(DISTINCT tag ORDER BY tag SEPARATOR ,) FROM article_tags;GROUP_CONCAT的底层逻辑是把一分组内多行数据聚合为一个中间字符串所以要关注拼接结果的长度默认上限是group_concat_max_len在 MySQL 8.0 中默认值通常为 1024。数据量大的时候结果会被静默截断看起来像是数据丢失。解决方法是临时调大这个变量SET SESSION group_concat_max_len 102400;这个坑我很早以前踩过那次是拼一个商品的全部图片地址拼了两百多个字符后突然断了排查了很久才发现是默认长度限制。官方文档里写的这个参数影响所有GROUP_CONCAT结果如果业务上要拼很长的字段记得在会话级别或全局级别把它调大。7. 函数嵌套和组合一个综合实战例子7.1 案例统计月度订单金额报表函数单独拎出来都不难但实际写SQL的时候往往是好几个函数嵌套在一起。我写一个综合案例——统计2024年1月到3月每个月的订单总量、总金额并且把金额展示成带千分位的格式同时附带较上月增长/下降的标记。假设有一张orders表字段为order_id、created_atdatetime、amountdecimal。SELECT DATE_FORMAT(created_at, %Y-%m) AS month, COUNT(*) AS order_cnt, FORMAT(SUM(amount), 2) AS total_amount, CASE WHEN SUM(amount) LAG(SUM(amount)) OVER (ORDER BY DATE_FORMAT(created_at, %Y-%m)) THEN 增长 WHEN SUM(amount) LAG(SUM(amount)) OVER (ORDER BY DATE_FORMAT(created_at, %Y-%m)) THEN 下降 ELSE 持平 END AS trend FROM orders WHERE created_at 2024-01-01 AND created_at 2024-04-01 GROUP BY DATE_FORMAT(created_at, %Y-%m) ORDER BY month;这里出现了三层嵌套DATE_FORMAT(created_at, %Y-%m)把 datetime 截成月份字符串同时用于GROUP BY、SELECT、ORDER BYSUM(amount)做聚合汇总FORMAT(sum_amount, 2)格式化千分位显示CASE WHEN配合窗口函数LAG()计算环比方向。这种SQL在实际报表里非常典型。如果把它拆成在应用层实现你得先查出全量订单然后在Java/C里循环、按月份分组、累加金额、格式化、计算环比代码量至少多三倍而且数据量大时还有内存压力。这就是为什么我说SQL函数用得好的工程师做起报表来效率是碾压式的。7.2 面试题里那些函数综合体很多看似刁钻的MySQL面试题本质上是把几个函数组合在一起考。我整理几个高频的第一题如何判断一个字符串是否是纯数字这个问题的坑在于不能只用REGEXP因为空字符串经过CAST会变成 0。比较稳妥的写法是SELECT name, CASE WHEN name REGEXP ^[0-9]$ AND name NOT LIKE THEN 纯数字 ELSE 非纯数字 END AS is_numeric FROM users;如果同时要判断能转成合法的日期、小数、负数等判断条件会更复杂但核心思路一样正则先验证格式再考虑边界情况。第二题如何计算环比和同比难点在于关联上一个月的数据。用窗口函数LAG()最简单也可以用关联子查询。比如计算每个部门本月和上月的员工数变化SELECT department, month, cnt, cnt - LAG(cnt) OVER (PARTITION BY department ORDER BY month) AS growth FROM ( SELECT department, DATE_FORMAT(hire_date, %Y-%m) AS month, COUNT(*) AS cnt FROM employees GROUP BY department, DATE_FORMAT(hire_date, %Y-%m) ) t;这个题目经常和生产报表需求混在一起如果对窗口函数不熟用自连接也可以实现但SQL会复杂一截。第三题如何求中位数MySQL 没有内置MEDIAN()聚合函数所以要用窗口函数模拟给每个分组内的值编号取中间位置的编号对应值。SELECT department, AVG(salary) AS median_salary FROM ( SELECT department, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary) AS rn, COUNT(*) OVER (PARTITION BY department) AS cnt FROM employees ) t WHERE rn IN (FLOOR((cnt 1) / 2), CEIL((cnt 1) / 2)) GROUP BY department;这个解法同时用到了ROW_NUMBER()、COUNT()窗口函数、FLOOR、CEIL、AVG是函数组合考察的经典案例。奇数个取中间偶数个取中间两个的平均逻辑完全正确。8. 用函数时最容易忽略的三个性能与写法细节8.1 对索引列使用函数索引就废了这是函数使用中最大的性能杀手没有之一。先看一条典型慢查询SELECT * FROM orders WHERE DATE_FORMAT(created_at, %Y-%m-%d) 2024-05-10;表面上写得很工整实际执行时会全表扫描。原因很简单created_at字段只要被DATE_FORMAT包了一层MySQL 就无法直接比较索引里的原始值必须先对每一行的created_at做一次格式化转换再比较结果索引自然失效。正确写法是把函数用在等号右侧或者写成范围查询-- 写法一右侧用函数 SELECT * FROM orders WHERE created_at STR_TO_DATE(2024-05-10, %Y-%m-%d); -- 写法二范围查询推荐 SELECT * FROM orders WHERE created_at 2024-05-10 00:00:00 AND created_at 2024-05-11 00:00:00;范围查询是最高效的写法因为它适合走索引范围扫描而且语义清晰跨天边界也不容易出错。这个优化改动虽小但在千万级订单表上可以极大提升查询速度。做性能优化的时候我排查 SQL 的第一动作就是看条件列有没有被函数包裹。8.2 SELECT 别名在 WHERE 和 GROUP BY 里的使用限制MySQL 的语法规则里SELECT子句中定义的别名可以在GROUP BY和ORDER BY里使用但不能在WHERE里使用。-- 合法 SELECT DATE_FORMAT(created_at, %Y-%m) AS month, COUNT(*) AS cnt FROM orders GROUP BY month ORDER BY cnt DESC; -- 不合法WHERE里不能用SELECT别名month SELECT DATE_FORMAT(created_at, %Y-%m) AS month, COUNT(*) AS cnt FROM orders WHERE month 2024-05 GROUP BY month;为什么WHERE不能使用别名因为SQL语句的逻辑执行顺序里WHERE在SELECT计算别名之前就已经执行了别名到WHERE执行时根本还不存在。而GROUP BY和ORDER BY在SELECT之后执行所以可以直接用别名。理解这个顺序比死记规则更有用。8.3 隐式类型转换和字符集问题MySQL 在对不同类型的值比较时会做隐式类型转换。比如字符串字段和数字比较SELECT * FROM users WHERE phone 13800001111;phone是VARCHAR等号右边是数字MySQL 会把字段值转成数字再比较。一旦转换索引大概率失效还可能因为部分字符串无法转成数字而匹配到奇怪的结果。这种坑防不胜防我的经验是写条件时保持两边类型一致。如果要查整数先把参数转成字符串SELECT * FROM users WHERE phone 13800001111;字符集问题同理。如果连接字符串和表字段的字符集不一致MySQL 会在比较时做隐式转码结果同样是索引失效。遇到乱码或者查询不走索引检查SHOW CREATE TABLE的默认字符集再检查连接参数是否设置了characterEncodingutf8基本能定位到九成问题。我在实际项目里还发现一个规律凡是涉及函数导致的性能问题往往不会在开发环境暴露因为数据量小、执行计划看不出差别等上了生产库百万级数据立刻原形毕露。所以写SQL的时候就要有意识地贯彻索引列保持纯净的原则不要指望事后再通过执行计划慢慢排查。写在最后函数这块硬骨头值得花时间嚼透学 MySQL 函数没有捷径但也不需要死记硬背。我自己的体会是把函数分成字符串、数值、日期、流程控制、聚合五类先在本地建一张小表每个函数跑两遍记住输出结果然后在业务里真正碰到问题时优先想这个需求能不能用SQL函数解决再动手查文档。用上一次印象就深刻一次比对着教程抄二十遍都管用。如果这篇文章对你有帮助建议重点把DATE_FORMAT、CASE WHEN、GROUP_CONCAT、TIMESTAMPDIFF这几个函数练熟——它们是日常报表和面试里出现频率最高的。等你能把函数和GROUP BY、窗口函数灵活组合的时候写复杂统计SQL的底气就会完全不一样了。
返回列表