干了这么多年数据库,我有个很深的感受:很多同事写SQL的水平,基本靠复制粘贴加改表名,一到统计、清洗、跨行计算的场景就卡壳。不是不够聪明,而是对SQL的函数体系没有建立起完整的认知。SQL函数这东西,说难难在细节,说简单其实也就那么几大类:聚合、字符串、数值、日期,再加上近些年越来越重要的窗口函数。把这五类吃透,你写SQL的档次直接上一个台阶。
这篇文章就是做这件事的。我会把每一类函数掰开揉碎了讲,不光告诉你函数是干嘛的,还会讲清楚为什么用、什么时候用、用了之后要注意什么坑。适合刚入行的数据分析师、后端开发,也适合写了几年SQL但一直靠“面向搜索编程”的老油条,如果你想系统补一下SQL函数这门课,这篇文章可以直接当手册用。
这篇文章涉及的内容我都基于实际生产环境验证过,每个函数都来自真实业务场景,不是在文档里背出来的。接下来我们直接进入正题。
1. 先搞懂SQL函数的分类逻辑
1.1 为什么必须先分清函数类型
很多初学者喜欢把SQL函数当字典背,背了忘,忘了背,回头一问还是不会用。问题就出在没搞懂函数分类背后的逻辑。
SQL函数的分类,本质上是按“数据操作维度”来划分的。聚合函数解决的是“多行变一行”的问题,比如统计总数、平均值、最大最小值;字符串函数解决的是“文本怎么改”的问题,比如截取、拼接、替换;数值函数解决的是“数字怎么算”的问题,比如四舍五入、取绝对值、求余数;日期函数解决的是“时间怎么处理”的问题,比如提取年月日、计算日期差;窗口函数解决的则是“跨行计算但不合并行”的问题,也就是你既要看到每一行的明细数据,又要同时看到分组统计的结果。
这么一分类你就明白了:你不是在背函数,你是在选工具。碰到什么样的数据需求,就在对应的工具箱里挑合适的工具。给这个分类逻辑打个比方:聚合函数像榨汁机,一堆水果进去,出来一杯果汁;窗口函数像在水果上贴标签,每个水果都保留,但标签上写着整箱水果的平均重量。
1.2 数据库方言差异:你以为的函数,换个数据库可能就没了
这一节必须放在最前面说,因为90%的新手踩的第一个坑就是方言问题。
SQL有标准,但每家数据库厂商都有自己的小动作。同样是取字符串长度,MySQL里是LENGTH(),Oracle里是LENGTH()没错,但SQL Server里就得写LEN()。同样是拼接字符串,MySQL直接CONCAT(),Oracle在旧版本里用||符号,SQL Server里也支持+。同样是取当前日期,MySQL是NOW(),SQL Server是GETDATE(),Oracle是SYSDATE。
我的习惯是,每个函数先搞清楚它属于哪一类,再记住它在自己主力数据库里的写法。不要试图把所有数据库的函数差异全记下来,那不现实。你在公司用什么数据库,就把那一家的函数手册吃透,其他数据库的写法了解个大概,能看懂就行。从经验来看,在一个团队里,数据库类型基本是固定的,与其学一堆用不上的方言,不如把手头这个用精。
2. 聚合函数:从count到group by,看懂统计的口径
2.1 五个最常用的聚合函数
聚合函数是SQL里使用频率最高的一类函数,核心作用就是“压缩数据”。常用的是这五个:
COUNT():统计行数SUM():求和AVG():求平均值MAX():求最大值MIN():求最小值
这里有几个细节很多人没注意。COUNT(*)和COUNT(列名)含义完全不同。COUNT(*)统计的是结果集的总行数,不管某列是否为NULL;COUNT(列名)只统计该列非NULL的行数。我见过有同事用COUNT(列名)去统计总数,结果遇到空值直接漏数据,排查了半天才反应过来。SUM()和AVG()天然会忽略NULL值,但AVG()如果样本里全是NULL,会返回NULL而不是0,这个在做报表时容易出问题,需要配合COALESCE()兜底。
再就是MAX()和MIN()对日期类型也适用,可以直接取最大日期和最小日期,不用先转字符串。
SELECT COUNT(*) AS total_orders, COUNT(discount) AS orders_with_discount, SUM(amount) AS total_amount, AVG(amount) AS avg_amount, MAX(created_at) AS latest_order, MIN(created_at) AS earliest_order FROM orders;这段SQL同时统计了总订单数、有折扣的订单数、成交总额、客单价以及最早和最新的下单时间,一个查询就把核心业务指标全都拎出来了。
2.2 group by + having:聚合查询的黄金组合
聚合函数单独用的时候,是把整张表压缩成一行。但实际业务里,你更多时候是要按某个维度分组统计。这时候就得请出GROUP BY。
GROUP BY的逻辑你可以理解为“分组后再压缩”。它先按指定的列把数据分成若干组,然后每组再用聚合函数压缩成一行。比如按省份统计订单总额:
SELECT province, SUM(amount) AS total_amount FROM orders GROUP BY province;这个查询跑完之后,每个省份只保留一行,字段就是省份名和该省的订单总额。这里有一个隐藏逻辑:SELECT子句里只要出现了聚合函数之外的普通列,这个列就必须出现在GROUP BY里。比如上面例子里的province,你一旦用GROUP BY province分组,SELECT里就只能放province和聚合函数,不能在SELECT里放customer_name,因为customer_name在分组后不属于任何一个确定的组内成员。很多数据库(比如MySQL)在未开启ONLY_FULL_GROUP_BY模式时不会报错,但它返回的结果是随机的,这种隐患比报错更吓人。
再说HAVING。HAVING和WHERE长得像,但地位完全不同。WHERE是分组之前对原始行做过滤,HAVING是分组之后对分组结果做过滤。所以WHERE里不能写聚合函数,HAVING里专门写聚合条件。如果你想筛选出订单总额超过10000的省份,就得这么写:
SELECT province, SUM(amount) AS total_amount FROM orders GROUP BY province HAVING SUM(amount) > 10000;我在实际开发里踩过这样的坑:一开始把过滤条件全塞进HAVING里,结果因为HAVING无法利用索引,查询效率直线下降。后来才意识到,能用WHERE过滤掉的数据,一定要在WHERE阶段干掉,HAVING只保留必须用聚合结果才能判断的条件。这个顺序对性能影响很大,尤其在大表上,能甩开几倍的差距。
2.3 聚合函数使用中的常见误区
聚合函数看着简单,实际用起来踩坑率极高。第一个误区就是COUNT和SUM混用。COUNT统计行数,SUM统计数值总和,两者语义完全不同。统计“多少笔订单”用COUNT(*),统计“订单总金额”用SUM(amount)。我看到过有人写SUM(*)——这在语法上根本过不了,但也有人用COUNT(amount)去替代SUM(amount),当amount为0或固定值时可能碰巧对,但业务数据一变就全错。
第二个误区是聚合函数嵌套。有些数据库支持两层聚合嵌套,比如AVG(SUM(amount)),但这容易把人绕晕。更好的写法是用子查询或者CTE先把内层聚合做好,再在外面做第二次聚合。
WITH daily_total AS ( SELECT order_date, SUM(amount) AS daily_amount FROM orders GROUP BY order_date ) SELECT AVG(daily_amount) AS avg_daily_amount FROM daily_total;这段SQL先算出每天的订单总额,再求这些日总额的平均值,逻辑一清二楚,比在一条SQL里硬套聚合嵌套可读性强得多。
第三个误区是GROUP BY的列顺序问题。GROUP BY支持多列,比如GROUP BY province, city,它先按省份分组,再在省份内按城市分组。列的先后顺序不会影响查询结果,但会影响中间分组过程和索引利用率。如果表上有联合索引(province, city),那么GROUP BY province, city能很好地利用索引的有序性;反过来写GROUP BY city, province就无法有效利用这个索引了。即使是聚合查询,索引设计经验依然管用。
3. 字符串函数:数据清洗和字段拼装的主力
3.1 必须背下来的字符串函数清单
字符串函数是数据清洗的一把刀,也是日常开发里用得最频繁的一类。不用全背,但这几个你必须滚瓜烂熟:
CONCAT()/CONCAT_WS():字符串拼接。CONCAT_WS可以指定分隔符,拼接多个字段时特别好用。SUBSTRING()/SUBSTR():截取子串,按位置截取。LEFT()/RIGHT():从左边或右边取固定长度的字符。LENGTH()/CHAR_LENGTH():计算字符串长度。中文场景下要特别注意字节数和字符数的区别。UPPER()/LOWER():大小写转换,常用于不区分大小写的比较。TRIM()/LTRIM()/RTRIM():去空格,处理用户输入的数据时几乎是必需品。REPLACE():替换字符串中的指定内容。LOCATE()/INSTR():查找子串位置,一般配合SUBSTRING使用。LPAD()/RPAD():用指定字符填充到固定长度,最典型的用途是把订单号补成统一位数。
SELECT CONCAT_WS('-', province, city, district) AS full_address, LEFT(phone, 3) AS phone_prefix, UPPER(email) AS upper_email, TRIM(username) AS clean_username, REPLACE(description, '(已删除)', '') AS cleaned_description FROM users;这段SQL把字符串函数的使用场景全串起来了:地址拼接、手机号截取、邮箱统一大写、用户名去空格、描述内容替换。
3.2 实战:用字符串函数做字段清洗
我在之前的项目里碰到过一个很典型的问题。上线了一个老系统导出的用户数据,里面的手机号千奇百怪:有的带空格、有的带+86前缀、有的中间有横线分隔,还有一些干脆是中文括号包起来的。如果直接拿这些数据去匹配订单表,一条都对不上。
我当时写了一段清洗SQL,用REGEXP_REPLACE配合TRIM把手机号统一格式:
UPDATE users SET phone = REGEXP_REPLACE(phone, '[^0-9]', '') WHERE phone IS NOT NULL;这个操作把非数字字符全部干掉,去掉空格、加号、横线之后,再配合LEFT(phone, 11)截取前11位,基本就把手机号统一起来了。不过这里有个细节要注意,用户量大的时候,一次性UPDATE整张表会锁表,影响线上业务。我当时是把清洗逻辑拆成批次执行的,每批只处理5000条,把对线上环境的影响降到最低。
处理字符串数据时,我的经验是:先抽样,再清洗,最后验证。不要拿到数据就写UPDATE,一定要先SELECT出来看各类脏数据的长相,再决定清洗规则。你永远想象不到用户会在一个手机号字段里填出什么花样。
3.3 字符串函数与索引:一个容易忽略的坑
用字符串函数有个非常隐蔽的性能坑:在WHERE条件里对索引列使用函数,会让索引失效。比如:
SELECT * FROM users WHERE LOWER(email) = 'test@example.com';这条SQL里,LOWER(email)把email列的函数结果作为比较对象,数据库无法使用email列上的索引,只能在每一行上先执行LOWER()函数,再做匹配,这就变成了全表扫描。
解决办法有两个。一是修改数据本身,比如建表时就把邮箱统一存成小写,从源头避免这个问题。二是新建一个生成列或者函数索引。MySQL 8.0和PostgreSQL都支持在索引上直接使用函数,像CREATE INDEX idx_email_lower ON users((LOWER(email))),这样查询就能走索引了。
我个人的原则是:能不改数据就不改数据,能改查询就不动表结构。如果既不能改数据也不能建索引,那就在WHERE两侧各做一次函数,让索引列保持原始形态,比如WHERE email = LOWER('Test@Example.com')。这样右侧的输入值先转小写,左侧的列不动,索引就还能用上。
4. 数值函数:不只是round和abs
4.1 数学函数、舍入函数、类型转换
数值函数看起来是五类里最不起眼的,但它在金融、电商、报表场景中用得极其频繁。核心的数值函数有这么几组:
- 舍入类:
ROUND(x, d)四舍五入,FLOOR(x)向下取整,CEIL(x)向上取整。 - 绝对值与符号:
ABS(x)取绝对值,SIGN(x)返回正负号。 - 取余与整除:
MOD(x, y)取余数,DIV整除。 - 常用数学函数:
POWER(x, y)幂运算、SQRT(x)平方根、EXP(x)指数、LN(x)自然对数。 - 类型转换:
CAST(x AS TYPE)、CONVERT(x, TYPE),比如把字符串转成数值、把数值转成小数。
其中ROUND和TRUNCATE的差别要特别注意。ROUND(3.14159, 2)返回3.14,这是四舍五入;TRUNCATE(3.14159, 2)返回3.14,但这只是截断,不管第三位小数是几,都直接砍掉。统计报表时如果金额需要分毫不差,就必须确认舍入规则是四舍五入还是截断,否则月底对账时会差出一分钱。
还有一个容易被忽略的细节:浮点数的精度问题。用ROUND处理浮点数,可能会出现诡异结果。比如ROUND(2.675, 2)在一些数据库里返回的是2.67而不是2.68,原因在于2.675在二进制浮点里存的其实是2.67499999...。遇到这种问题,建议先用CAST把小数字段转换成高精度DECIMAL类型,再做舍入运算。
SELECT ROUND(price * quantity, 2) AS amount, CAST(ROUND(price * quantity, 2) AS DECIMAL(10, 2)) AS precise_amount, FLOOR(total_amount / 100) AS hundred_count, MOD(total_amount, 100) AS remainder FROM order_items;4.2 用数值函数处理真实业务数据
数值函数在业务里的用法,我自己印象最深的是优惠券分摊场景。一套订单有多个商品,每个商品分摊多少优惠金额,需要精确到分,还要保证分摊之后总额不减不增。
我当时的解法是:先按商品金额占比粗算,再用ROUND保留两位小数,最后用“尾差归一”的思路,把由于舍入产生的差额加到最后一个商品上。核心就是先用SUM算出总额,再逐项计算分摊金额,用ROUND取两位,之后用总金额 - SUM(各分摊金额)算出尾差,加到最后一个商品上。这样既用了SUM聚合,也用了ROUND数值处理。
另外,CAST和CONVERT在数据格式清洗时也经常用到。比如接口传过来的金额字段是字符串"99.90",需要转成数值才能做运算:
SELECT CAST('99.90' AS DECIMAL(10, 2)) + 0.10;字符串转数值时要注意,如果字符串里混入了非数字字符比如"99.90元",CAST会直接报错或者返回0。稳妥的做法是先REGEXP_REPLACE把非数字字符清理干净,再CAST。这点在从Excel、文本文件导入数据时尤其常见,值得养成习惯。
5. 日期函数:统计报表的时间轴核心
5.1 日期获取、格式化、加减计算
日期函数是日常报销统计、用户活跃分析、订单流水处理的绝对核心。我把常用的日期函数分成三类:获取类、格式化类、计算类。
- 获取类:
CURRENT_DATE()取当前日期、CURRENT_TIME()取当前时间、NOW()取当前日期时间。 - 格式化类:
DATE_FORMAT(date, '%Y-%m-%d')按指定格式输出日期、YEAR(date)、MONTH(date)、DAY(date)提取年月日。 - 计算类:
DATE_ADD(date, INTERVAL 1 DAY)加一天、DATE_SUB(date, INTERVAL 1 MONTH)减一个月、DATEDIFF(date1, date2)计算两个日期相差的天数。
这里有一个很经典的错误,很多人以为DATEDIFF是“date1减去date2”还是反过来无所谓,实际上DATEDIFF(date1, date2)在MySQL里返回的是date1 - date2的含义,也就是DATEDIFF('2025-03-01', '2025-02-01')返回28,反过来就是-28。在SQL Server里,DATEDIFF(day, date1, date2)的参数顺序又完全不一样,它要求把时间单位放第一位。所以写日期差的函数时,一定要先确认你用的数据库的参数习惯,不然查出来的正负号反了,月报数据就全乱了。
日期格式化的UNIX_TIMESTAMP()函数也值得一提。它可以把日期转成Unix时间戳,适合做时间范围的比较,配合FROM_UNIXTIME()可以从时间戳转回日期。时间戳的好处是跨数据库可移植性强,缺点是肉眼不可读,排错时得先转回日期格式才能看懂。
5.2 日期函数实战:按月、按周统计
按月统计是报表类需求里最常见的。写法通常是这样:
SELECT DATE_FORMAT(order_date, '%Y-%m') AS month, COUNT(*) AS order_count, SUM(amount) AS total_amount FROM orders WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01' GROUP BY DATE_FORMAT(order_date, '%Y-%m') ORDER BY month;这段SQL把订单日期格式化成YYYY-MM的形式,按月份分组统计。这里有个关键点:WHERE条件里没有对order_date做任何函数操作,直接用的原始范围比较,这样可以最大限度利用索引。而GROUP BY子句里用了DATE_FORMAT,这个无法避免,毕竟是按格式化后的月份分组。
按周统计也是常见需求。MySQL里可以用YEARWEEK(order_date, 1)来算年份和周数,第二个参数1表示周从周一开始。SQL Server则用DATEPART(week, order_date)。写周统计有个比较麻烦的边界问题:跨年那几天到底算上一年的最后一周还是新一年的第一周?不同数据库给的答案不一样,甚至不同参数得到的答案也不一样。我一般是这样解决的:在分组前先定好一个“周起始日”的基准,然后统一用DATE_ADD和WEEKDAY函数来做归一化。
5.3 日期函数在慢SQL里的隐形杀手
日期函数用不对,是慢SQL的高发区。最典型的错误是在WHERE条件里对日期列套函数。比如:
SELECT * FROM orders WHERE DATE_FORMAT(order_date, '%Y-%m-%d') = '2025-03-01';这个查询,看似只是把日期格式化后比较,但它对order_date用了函数,导致数据库没法走order_date上的索引,只能把全表的order_date都格式化一遍再做匹配。数据量一大,这个查询的耗时直接从毫秒级变成秒级甚至分钟级。
正确的写法是:
SELECT * FROM orders WHERE order_date >= '2025-03-01' AND order_date < '2025-03-02';这两种写法在结果上几乎等价,但执行效率天差地别。第二种写法保留了order_date的原始形态,能直接命中索引。
如果查询条件是“最近30天”,也要注意别写成DATE_SUB(CURDATE(), INTERVAL 30 DAY)放在列上,要放在比较值的一侧:
SELECT * FROM orders WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY);这样order_date列本身没有加函数,索引依然能用。
6. 窗口函数:跨行计算的终极解法
6.1 窗口函数语法骨架:over + partition by + order by
窗口函数是这几年SQL面试和实战的常客,也是很多人觉得最难的部分。实际上窗口函数的语法骨架非常固定,记住三个关键字就行:OVER()、PARTITION BY、ORDER BY。
OVER():定义窗口范围,划出要计算的“窗口”。PARTITION BY:按哪些列分组,相当于窗口内部的“分组”。ORDER BY:窗口内的排序规则,决定计算的顺序。
拿一个最常见的例子来说,我想给每笔订单算一个“到目前为止的累计销售额”。用聚合函数得写子查询,用窗口函数就是一行:
SELECT order_id, order_date, amount, SUM(amount) OVER (PARTITION BY DATE_FORMAT(order_date, '%Y-%m') ORDER BY order_date) AS monthly_cumulative_amount FROM orders;这个查询里,PARTITION BY DATE_FORMAT(order_date, '%Y-%m')表示按月切开窗口,ORDER BY order_date表示窗口内按日期排序,SUM(amount) OVER(...)表示对窗口内从第一行到当前行的金额做累加。每行都保留了自己的明细数据,同时又多了一列累计值。这就是窗口函数和聚合函数最本质的区别:聚合函数压缩行数,窗口函数不压缩行数。
有人可能会问,ORDER BY在窗口函数里到底有什么用。默认情况下,如果不写ORDER BY,窗口范围是整个分组的所有行,SUM会对整个分组求和。一旦写了ORDER BY,SUM就变成了从窗口起始行到当前行的累积值,行为完全不同。这是窗口函数最容易翻车的地方。
6.2 经典窗口函数:rank、row_number、sum over、lag/lead
窗口函数不止有SUM,COUNT、AVG、MAX、MIN都可以配合OVER使用。除此之外有几类专门针对窗口的函数,实用性极高。
第一组是排序函数,ROW_NUMBER()、RANK()、DENSE_RANK()。它们的区别是:
ROW_NUMBER():按顺序编号,相同值的行也会被分配不同的编号。RANK():相同值的行编号相同,但下一个不同的值会跳过编号产生空档。DENSE_RANK():相同值的行编号相同,但下一个不同的值不跳号。
用实际数据举例,假设两行业绩都是100,一行是90。ROW_NUMBER()输出1, 2, 3;RANK()输出1, 1, 3;DENSE_RANK()输出1, 1, 2。这三个函数的区别写SQL时最容易模糊,面试也爱问,务必要分清。
SELECT employee_name, sales_amount, ROW_NUMBER() OVER (ORDER BY sales_amount DESC) AS row_num, RANK() OVER (ORDER BY sales_amount DESC) AS rank_num, DENSE_RANK() OVER (ORDER BY sales_amount DESC) AS dense_rank_num FROM sales;第二组是跨行引用函数,LAG(列名, N)取当前行往前第N行的值,LEAD(列名, N)取当前行往后第N行的值。这个在做同环比分析时特别有用,比如算每笔订单相对于上一笔订单的金额变化:
SELECT order_id, order_date, amount, LAG(amount, 1) OVER (ORDER BY order_date) AS previous_amount, amount - LAG(amount, 1) OVER (ORDER BY order_date) AS amount_change FROM orders;这个SQL直接算出每笔订单和上一笔的差额,不用自连接,不用子查询,一句搞定。我第一次用LAG替代自连接做同环比的时候,SQL的长度缩了一大截,执行效率也快了不是一点半点。
6.3 窗口函数 vs 聚合函数:什么时候用哪个
判断用窗口函数还是聚合函数,有一个非常简单的标准:你需不需要看到明细行。
如果你只需要分组统计结果,比如每个省份的订单总额,各省一行就行,用GROUP BY+ 聚合函数;如果你需要每行明细都保留,同时还得带上分组统计值,比如每笔订单的金额和所在省份的平均订单金额,用窗口函数。
窗口函数还有一个无法被替代的场景:取组内TopN。比如每个部门工资最高的前3名员工。这个需求用普通聚合函数几乎无从下手,但用窗口函数就非常优雅:
WITH ranked_emp AS ( SELECT employee_name, department, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn FROM employees ) SELECT employee_name, department, salary FROM ranked_emp WHERE rn <= 3;先用一个CTE算排名,再在外面过滤排名小于等于3的。这个写法是窗口函数的经典应用,也是面试中的高频考题。
不过窗口函数也不是万能药。它最大的问题是可读性比较差,一条SQL里如果同时出现四五个窗口函数,排错的时候会很痛苦。我的经验是窗口函数不要嵌套太多,如果逻辑过于复杂,宁可用CTE拆成多段,每段承担一个清晰的任务。代码长一点没关系,但一定要让人能看懂。
7. SQL函数相关的常见问题排查速查表
7.1 高频问题:为什么我的SQL这么慢
函数用不好,最常见的表现就是慢SQL。我梳理了几个高频场景:
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
| 大表查询直接全表扫描 | WHERE条件里对索引列用了函数 | 把函数移到比较值一侧,或建函数索引 |
| 字段类型不匹配,索引失效 | 字符串列与数值列直接比较 | 用CAST显式转换,而不是依赖隐式转换 |
ORDER BY乱序导致排序开销大 | 排序列没走索引 | 确认排序列上有索引,或减少排序数据量 |
| 分页查询越到后面越慢 | LIMIT offset过大 + 无索引排序 | 改用游标分页或基于索引的WHERE条件分页 |
| 聚合查询结果集过大 | GROUP BY粒度过细 | 先按粗粒度聚合,再在应用层二次聚合 |
这里面,函数导致的索引失效是最隐蔽的。你在WHERE子句里对列做了任何微小的运算,数据库优化器就可能放弃索引。除了函数,对列做隐式类型转换也会有类似问题。比如WHERE varchar_col = 123,有些数据库会把列转为数字,导致索引失效。要先确认列的定义类型,必要时写WHERE varchar_col = CAST(123 AS VARCHAR(11)),让列保持原样。
7.2 从热搜词里提炼的几个高频疑问
我注意到最近不少人搜索“SQL语句去重”“SQL去除空值”“SQL MD5加密函数”这些词。去重和去空,本质上都和函数使用习惯有关。
去重有两种写法:SELECT DISTINCT和GROUP BY。DISTINCT适合对整行或多列组合去重,GROUP BY适合在去重的同时做聚合计算。这里有个性能小技巧:如果只需要去重后的某一列,用SELECT DISTINCT col没问题;但如果需要顺带统计数量,用GROUP BY col配合COUNT(1)效率往往更稳定。
去除空值可以用WHERE col IS NOT NULL,但更灵活的做法是用COALESCE(col, 0)把空值替换成默认值。COALESCE支持多个参数,比如COALESCE(col1, col2, col3, 0),它会从左到右找第一个非NULL的值。这在处理多列可能为空的数据时极其好用。
至于“SQL MD5加密函数”,倒不是让你在业务里存明文密码时用。MD5是一种哈希函数,MySQL里直接MD5('字符串')就能生成32位的十六进制哈希。但这里我要提醒一句:MD5已经不推荐用于密码存储了,有更安全的哈希算法可选。在写SQL时可以用MD5来做一些去重判断、数据校验,但不要把它当作安全方案来依赖。
再补充一点,慢SQL优化时最好养成先看执行计划的习惯,不要凭感觉猜。MySQL里用EXPLAIN SELECT ...,SQL Server里用SET SHOWPLAN_ALL ON,Oracle和PostgreSQL也都有对应的计划查看方式。看执行计划时重点看两个地方:一是type字段是不是ALL(全表扫描),二是key字段有没有用到索引。这两列能告诉你SQL慢的根源到底在不在函数。
我个人在实际操作中的体会是,排查慢SQL时最忌讳一上来就加索引。先把SQL语句里函数、类型转换、隐式转换这些隐藏问题排除掉,再谈加索引。很多时候,把WHERE条件里的函数挪个位置,或者把LIKE '%xxx%'改成等值匹配,性能问题就解决了一大半。
函数这东西,说到底就是SQL的积木。你掌握的积木越多,搭出来的数据分析方案就越灵活。比起背公式,更建议你多去写、多去用。建议手头备一份自己主力数据库的函数手册,遇到不确定的就查。写SQL的功力不是一天练成的,但你如果把这五类函数都主动用一遍,那些曾经让你挠头的统计需求,大部分都能顺手解决。最后分享一个小技巧:每次新接一个写SQL的任务,先花两分钟想想它属于哪类场景——是要聚合压缩,要清洗字段,还是跨行计算。想清楚这个事情,你的SQL质量会有一个明显的提升。