做数据开发这些年我有个很深的体会很多人写SQL靠的是拼积木式的堆子查询业务逻辑稍微复杂一点SQL就膨胀得像一团乱麻性能也跟着崩。其实SQL 方法函数就是数据库内置的一套“瑞士军刀”聚合汇总、字符串加工、日期计算、类型转换、逻辑判断、窗口排名几乎所有的日常操作都能用它们干净利落地解决。这篇文章是这个系列的第一篇核心任务就是把SQL中最常用、最容易被低估的函数一次讲透不但讲用法还讲背后的设计逻辑和实战中的坑。我会把函数拆成聚合函数、字符串函数、日期函数、窗口函数、逻辑辅助函数这几个大族逐个讲清楚它们各自擅长什么、有什么使用限制、以及怎么避免最常见的性能陷阱。这篇文章适合两类人一类是刚把SELECT语法学明白、想系统补齐函数能力的新手另一类是写了不少SQL但全靠子查询和临时表硬扛、希望精简逻辑提升效率的开发者。读完之后你会发现很多原本要写几十行的逻辑其实一个函数就能解决。1. 函数家族全景图先看懂分类再动手1.1 为什么分类能决定你的SQL质量SQL函数看似零散但按处理对象的特征可以清晰分成几大家族聚合函数、字符串函数、日期时间函数、数学函数、转换函数再加上近些年数据库普遍支持的窗口函数和逻辑判断函数。先理解分类再动手不是学院派作风而是因为这些函数族的语法限制、使用场景和性能特征差异非常大。拿聚合函数和窗口函数来说两者都做求和、计数、平均值但前者会把多行数据折叠成一行后者却能在不折叠明细行的前提下同时输出汇总值。不理解这个底层区别你在处理“每行订单都显示该客户累计消费总额”这种需求时就会本能地去写相关子查询然后被性能折磨到怀疑人生。而字符串函数和日期函数大多是标量操作一行进一行出逻辑上简单但细节坑特别多比如字符长度计算、时区处理、NULL传播等。我见过太多人写SQL是遇到什么问题就百度什么函数用完就忘下次继续百度。这样效率极低。真正高效的做法是先花半天时间把你所用数据库的函数清单通读一遍建立一个“函数地图”以后遇到需求就知道该去哪一类里找工具。下面这张表是我日常工作中的简化版地图你可以直接抄走。函数家族典型代表核心用途最常犯的错聚合函数SUM、AVG、COUNT、MAX、MIN多行折叠汇总忘记处理NULLCOUNT(列)与COUNT(*)混淆字符串函数LEN/LENGTH、SUBSTRING、REPLACE、TRIM文本清洗与加工中文按字符还是按字节计算搞不清日期函数DATEADD、DATEDIFF、GETDATE时间的加减与差值直接比较字符串导致索引失效数学函数ROUND、ABS、CEILING、FLOOR数值处理浮点精度问题被忽略转换函数CAST、CONVERT类型转换隐式转换导致性能暴跌窗口函数ROW_NUMBER、RANK、SUM() OVER()不折叠行的排名与累计分区边界理解错误逻辑函数CASE WHEN、COALESCE、NULLIF条件分支与空值处理NULL判断用了而不是IS NULL后面所有的篇幅都会围绕这张地图展开逐类拆解。1.2 SQL函数执行的底层逻辑要真正用好SQL函数光记语法是不够的你得理解数据库执行一条带函数的SQL时函数的调用发生在哪个环节。这个理解直接决定了你写出的SQL能不能走索引。大多数标量函数如字符串、数学、日期处理是在行读取后被逐行调用的这意味着如果某个列上建有索引你对这个列使用函数加工后进行比较比如WHERE YEAR(create_time) 2024数据库通常无法直接利用该列的索引因为索引里存储的是原始值不是处理后的值。这就是所谓的“函数导致索引失效”。聚合函数则不一样它在扫描数据的过程中逐步累加数据库对聚合操作做了大量优化通常即使不走索引性能也在可接受范围内。窗口函数则在结果集确定之后进行二次计算它依赖排序和分区排序字段选得好不好直接决定性能下限。理解这些底层逻辑之后你在设计SQL的时候就会自然形成条件反射能用原始列比较就不套函数能用聚合函数解决就不写相关子查询能用窗口函数一次算出结果就不要用多条SQL拼业务。这些习惯比背一百个函数语法都管用。2. 聚合函数与分组统计的正确打开方式2.1 五大基础聚合的适用边界SUM、AVG、COUNT、MAX、MIN是SQL统计的地基但很多人从第一天起就在踩同一个坑NULL值的处理。举个最简单的例子COUNT(*)统计的是表中的行数而COUNT(column)统计的是该列非NULL值的个数。一张1000行的订单表如果备注列有400个NULLCOUNT(备注)的结果就是600。这不是函数出错了而是语义设计如此。问题在于很多新手在统计订单数量时习惯写成COUNT(订单编号)如果订单编号列碰巧存在NULL虽然正常设计不应该统计结果就会静默出错。AVG函数也有同样的特性它只对非NULL值求平均。假如一组销售记录里有三天没有成交记作NULL而不是0AVG(销售额)会把这三天直接忽略算出来的均值会比“实际均值”偏高。这里必须用COALESCE把NULL转成0再求平均或者明确用SUM(销售额)/COUNT(*)。理解NULL语义是聚合函数的第一课。MAX和MIN相对简单但要注意它们在处理字符串时按字典序处理日期时按时间先后。一旦类型不统一比如把日期存成了字符串2024/3/5和2024-03-05混在一起MAX出来的结果可能完全不是你想要的最大日期。2.2 GROUP BY与HAVING的组合技巧聚合函数真正的威力必须配合GROUP BY才能发挥出来。GROUP BY的语义是把一张表按指定列的值拆成多个小组然后每个组独立执行聚合函数。这里有个新手高频问题为什么SELECT列表里出现的非聚合列必须出现在GROUP BY里这是SQL的语法约束背后是逻辑一致性既然你已经把数据按某些列分组了那组内任何一行的该列值都是相同的数据库允许你直接显示它但必须声明这个分组依据。否则就会出现“这一列在组内有多个值你到底想显示哪一个”的歧义MySQL有一个宽松模式允许这种写法但取到的值是不确定的生产环境强烈不建议依赖这个行为。HAVING与WHERE的分工也常被混淆。WHERE在分组前过滤行HAVING在分组后过滤组。你要统计“订单金额大于5000的客户”这种先过滤行再分组的需求用WHERE你要统计“总消费超过10万的客户”必须先分组算SUM再过滤结果就得用HAVING SUM(amount) 100000。把条件误放到WHERE里直接报错因为WHERE执行时SUM还不存在。实操建议能用WHERE先过滤掉的行尽量不要留到HAVING阶段因为分组的数据量越小聚合计算越快。比如统计某地区客户的汇总情况就先把地区条件放WHERE而不是GROUP BY之后在HAVING里再过滤两者的结果可能相同但性能差别明显。2.3 空值、去重与隐藏的统计陷阱聚合函数里还有两个易被忽视的细节COUNT(DISTINCT column)和SUM(DISTINCT column)。前者的作用是统计去重后的非NULL值数量后者的作用是对唯一值求和。这个语法用起来要谨慎COUNT(DISTINCT)在大表上性能开销巨大因为它需要排序或哈希去重数据量到千万级别时如果你还想精确统计很可能让查询跑出分钟级别的响应。如果业务上可以接受近似值可以考虑用数据库自带的近似去重函数比如SQL Server的APPROX_COUNT_DISTINCT性能能提升一到两个数量级。另一个隐藏陷阱是聚合结果里的除零问题。AVG不会除零因为分母是行数但当你想算“订单完成率”这类自定义比例时SUM(CASE WHEN 状态完成 THEN 1 ELSE 0 END)1.0/COUNT()完全没有问题可一旦COUNT()结果为零整个表达式会直接报“除以零”的错误。稳妥做法是先判断分母是否为零或者用NULLIF(COUNT(), 0)把零转成NULL避免报错。3. 字符串函数实操清洗、拼接与格式统一3.1 必备字符串函数清单与示例字符串函数是数据清洗的主力军。在日常数据处理中我几乎每天都会用到CHARINDEX或INSTR、SUBSTRING、REPLACE、TRIM、LEN或LENGTH、UPPER/LOWER、CONCAT这几员大将。以SQL Server语法为例定位某个子串位置用CHARINDEX(关键字, 列名)然后配合SUBSTRING截取。比如从一段“订单号SO-2024-0012”的文本里提取订单号可以写成SUBSTRING(列名, CHARINDEX(, 列名) 1, LEN(列名))。这个组合是文本解析的万能套路很多看似复杂的字符串提取问题都能拆解成“定位截取”两步。REPLACE的用途更广泛清洗脏数据时批量替换换行符、空格、全角字符都非常好用。TRIM在SQL Server里默认只去空格MySQL的TRIM可以指定去掉任意字符。CONCAT比直接用拼接更安全因为CONCAT会把NULL当空字符串处理而字符串NULL的结果是NULL这点非常容易被忽略。这里给一个实际清洗场景导入的客户手机号混有空格和横线你想统一成纯数字格式。一条SQL就能搞定UPDATE 客户表 SET 手机号 REPLACE(REPLACE(手机号, , ), -, )。要注意如果手机号列本身有NULLUPDATE不会报错但也不会更新NULL还是NULL后续查询记得用IS NULL判断。3.2 字符串函数中那些防不胜防的坑字符串函数真正的坑不在于语法而在于不同数据库的方言差异。最典型的就是LEN和LENGTHSQL Server的LEN返回字符数且自动去掉末尾空格MySQL的LENGTH返回字节数一个中文字符在UTF-8下占3个字节。很多人从MySQL转SQL Server或者反过来经常在长度校验上翻车。另一个高频坑是字符串与数字比较时的隐式转换。比如你在查询条件里写WHERE 手机号 13800138000手机号列是字符串数据库会把列值一个个转成数字去比较。当数据量大时这会导致索引失效全表扫描。更危险的是如果业务上手机号可能存在前导零或特殊字符转换过程会直接报“转换失败”的错误。这种问题排查起来非常痛苦因为报错信息经常和使用场景完全不相关。我的建议是所有字符串与数字的比较都显式把数字改写成字符串字面量比如WHERE 手机号 13800138000从源头上杜绝隐式转换。第三个坑是大小写敏感性。不同数据库、不同排序规则下字符串等值比较是否区分大小写完全不一样。SQL Server默认排序规则通常不区分MySQL默认区分。如果你把用户输入的用户名直接拿去做WHERE匹配而表里存的是首字母大写的昵称MySQL下就可能查不到。稳妥的做法是统一用UPPER或LOWER规范化后再比较虽然会牺牲一点索引优势但逻辑上绝对可靠。3.3 去重的三种境界提到数据清洗不得不讲“去重”。热词里也有好几个人在搜“sql语句去重”。去重看起来简单但具体需求差别很大。最简单的是SELECT DISTINCT对整个结果集按所有选择的列去重。它的缺陷在于无法只对部分列去重而保留其他字段而且DISTINCT会触发排序或哈希表大时很慢。第二种是GROUP BY配合聚合函数适合需要对重复组做汇总统计的场景。比如统计同一用户的订单数、总金额GROUP BY用户ID天然就去掉了用户维度上的重复。第三种是窗口函数法适合“每组保留一条”且要保留最新或最完整记录的场景。比如每个用户有多条地址记录只想保留最近更新的一条先用ROW_NUMBER() OVER(PARTITION BY 用户ID ORDER BY 更新时间 DESC)编号再取编号为1的记录。这是目前解决复杂去重问题的最优解比写自连接清爽得多也更容易维护。这部分后面讲窗口函数时会细说。4. 日期时间函数计算、对比与索引权限4.1 日期函数的基本操作与场景日期是SQL世界里最容易出问题的数据类型。数据库内置的日期函数通常围绕三个目标获取当前时间、对日期做加减、计算两个日期间的差值。SQL Server里GETDATE()取当前数据库服务器时间SYSDATETIME()取更高精度的当前时间。MySQL里对应的是NOW()和CURDATE()。生产环境要注意这些函数返回的是数据库服务器所在时区的时间而不是应用服务器或用户的时间。如果数据库部署在云上时区配置不当你记录的时间就会比实际业务时间早几小时排查起来非常隐蔽。日期加减用DATEADD(datepart, number, date)比如DATEADD(DAY, -7, GETDATE())取七天前的日期。MySQL的写法是DATE_SUB(CURDATE(), INTERVAL 7 DAY)。日期差值用DATEDIFFSQL Server里DATEDIFF(DAY, 开始日期, 结束日期)返回跨越的天数它按“边界跨越次数”计算而不是按24小时。所以同一天内23点和次日凌晨1点这两个时间之间DATEDIFF(DAY, ...)的结果是1而如果你用结束时间减开始时间算小时差再换算天数结果是不到1天。这两种算法在不同业务场景下各有用途但你心里必须清楚自己在用哪种。4.2 日期比较的性能教训日期函数使用中最严重的性能问题是函数套在索引列上。比如WHERE YEAR(create_time) 2024如果create_time上有索引这个索引就完全失效了。数据库的优化器无法直接从索引的B树里定位“年份为2024”的范围因为索引里存的是完整时间戳不是年份。正确的写法是用范围比较WHERE create_time 2024-01-01 AND create_time 2025-01-01。这样优化器可以将查询下推到索引上走索引范围扫描。这两条SQL的结果几乎完全一样但性能在小表上看不出差别一旦上千万行就是毫秒级和秒级甚至分钟级的差别。我在多个项目里排查慢SQL时这种“函数套列”的写法占比相当高而且因为是历史代码遗留往往没人意识到问题在哪。改成范围查询后本来要跑30秒的报表变成了1秒内出结果这种成就感用几次就再也不想写函数套列了。另外日期字段的类型也建议直接使用数据库原生的DATE/DATETIME/TIMESTAMP而不是用VARCHAR。用字符串存日期在查询和排序上都能正确工作但数据库无法用最优的数值比较路径去处理它而且各种日期函数的参数校验也会在数据质量不好时疯狂报错。导出到其他系统时日期类型还能保留时区语义这一点在跨系统对接时尤其重要。4.3 时区与服务器时间带来的隐性问题还有一个很深的坑是TIMESTAMP与DATETIME或TIMESTAMP WITH TIME ZONE与普通TIMESTAMP的区别。在SQL Server里DATETIME2和DATETIME的区别只是精度但MySQL里TIMESTAMP会自动跟随服务器时区转换存储时转成UTC读取时转回本地时区而DATETIME原样存储。如果你的应用有跨时区业务比如跨境电商用DATETIME存“用户本地时间”再配合一个时区偏移字段可能是最可控的方案直接依赖TIMESTAMP自动转换一旦调整服务器时区或者数据迁移到不同机房历史数据的时间全都会变。日期函数还有一个我踩过很多次的坑日期格式化字符串在不同数据库里的占位符不一样。SQL Server用FORMAT(日期, yyyy-MM-dd HH:mm:ss)MySQL用DATE_FORMAT(日期, %Y-%m-%d %H:%i:%s)。同一个格式化需求换个数据库就要重写。所以如果你的代码层有ORM尽量在数据库返回原始日期类型后在应用层格式化而不是在SQL里做格式化这样至少迁移数据库时少改一堆SQL。5. 窗口函数不折叠行的进阶数据操作技巧5.1 ROW_NUMBER、RANK、DENSE_RANK三兄弟的区别窗口函数是SQL进阶路上绕不开的一关也是解决“每组前N条”“累计统计”“移动平均”等复杂业务需求的利器。窗口函数最大的特点是每一行仍然是结果集里独立的一行但在计算时能“看到”它所属分组的其他行从而得到聚合或排名结果。先讲排名三兄弟。ROW_NUMBER()按指定顺序给每个分组内的行依次编号编号连续且唯一。RANK()在出现并列值时会跳过后续的序号比如两个人并列第一第三个人编号是3而不是2。DENSE_RANK()则不会跳过并列第一之后下一个还是2。这三者的区别在业务上非常关键做榜单排名想要空出并列名的位置用RANK想要保持序号连续用DENSE_RANK只需要给每行一个唯一序号比如用于去重选最新记录就用ROW_NUMBER。举一个典型场景电商平台要取每个品类下销量前三的商品。写法是WITH Ranked AS (SELECT 品类, 商品, 销量, ROW_NUMBER() OVER(PARTITION BY 品类 ORDER BY 销量 DESC) AS rn FROM 销售表) SELECT ... FROM Ranked WHERE rn 3。这个查询语义清晰执行计划也比一堆自连接的写法高效。但要注意ORDER BY 销量 DESC时如果销量相同ROW_NUMBER的分配顺序是不确定的如果你希望结果稳定需要在ORDER BY里追加一个唯一字段比如加上商品ID。5.2 累计统计与移动计算的魅力窗口函数另一个强大的能力是聚合函数配合OVER()子句做累计计算。SUM(amount) OVER(PARTITION BY 客户ID ORDER BY 下单时间)就是计算每个客户按时间累计的消费额每一行都能看到“截至本行”的总和。这在财务对账、用户成长路径分析里用处极大。定义一个移动平均也极其方便AVG(价格) OVER(ORDER BY 日期 ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) 表示取当前行和之前6行的平均价格用来画趋势线很漂亮。这里有一个容易犯错的地方是窗口边界ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW表示从分组起点累加到当前行RANGE BETWEEN则按相同值分组。如果排序列上有重复值RANGE方式的计算结果可能与直觉不同建议在处理有时间戳等唯一值的场景下优先用ROWS。窗口函数的性能优化要点是PARTITION BY的分区字段最好有索引或与表的分布键一致ORDER BY字段决定排序开销排序数据量越小性能越好。在千万级大表上做窗口计算如果每个分组太大且分组键分布不均匀可能会出现数据倾斜某些Reduce节点长尾严重。实际生产中如果只是分组内前几条可以先用ROW_NUMBER过滤到很小的结果集再做其他关联能省下大量计算资源。6. 逻辑判断与辅助函数的组合拳6.1 CASE WHEN的多种用法CASE WHEN不是函数而是表达式但它和函数配合起来威力巨大。基本语法CASE WHEN 条件 THEN 结果 ELSE 兜底 END可以在SELECT里做条件列转换在WHERE里做复杂条件组合。SELECT里最典型的应用是把数值或代码翻译成业务标签CASE WHEN 状态 1 THEN 待支付 WHEN 状态 2 THEN 已支付 ELSE 已取消 END AS 状态描述。这个写法的好处是显示逻辑集中在SQL里应用层拿到结果直接展示省去了业务代码里一堆if-else。在聚合函数里用CASE做条件计数是我用得最多的高级技巧。SUM(CASE WHEN 销售额 10000 THEN 1 ELSE 0 END) 等价于 COUNT(IF(销售额 10000)) 的效果能在一个分组里同时统计多个维度的指标避免把同一份数据扫多遍。比如要按月份统计总订单数、大额订单数、退款订单数一条SQL就能完成三种聚合条件而不是拆成三个子查询再联表。6.2 COALESCE、NULLIF与类型转换的妙用COALESCE(值1, 值2, 值3...)函数返回第一个非NULL值这是空值处理的首选。业务场景极多查询客户联系方式时优先返回手机号没填就返回邮箱再没有就返回未知一行COALESCE搞定。它的优势在于比ISNULLSQL Server专用可移植性更强而且支持多个参数。把它和前一天讲的聚合函数NULL语义结合起来几乎能解决所有的空值业务诉求。NULLIF(表达式1, 表达式2)的作用是如果两个表达式相等返回NULL否则返回表达式1。它最常见的用法是处理除零SUM(价格)/NULLIF(COUNT(), 0)。当COUNT()为0时NULLIF把分母变成NULL整个除法的结果是NULL而不是报错然后在应用层判断结果是否为NULL做后续提示。还有一种巧妙的用法是做数据对比当两个值相等时返回NULL在多列对比场景中可以配合一些数据库特有的NULL行为来筛选差异列。CAST和CONVERT是类型转换的两元大将。CAST(字段 AS INT)用来把字符串转整数、把日期转DATE类型SQL Server的CONVERT可以附加样式参数做日期格式化。类型转换的坑在于转换失败的运行时错误。字符串列里混入一个非数字字符整列CAST就会直接爆“转换失败”错误。应对方案是先做数据质量检查SELECT COUNT(*) FROM 表 WHERE 列名 NOT LIKE %[0-9]% 定位非法值再决定是清洗还是跳过。6.3 隐式转换是慢SQL的隐形杀手上一节提到为什么显式转换比隐式转换好这里再展开一个层面。很多开发者以为数据库会自动处理类型不匹配比如拿字符串列和数字比较、日期列和字符串比较确实数据库会做隐式转换但转换的方向和是否能用上索引完全不由你控制。最常见的就是WHERE create_time 2024-01-01如果create_time是DATETIME类型而比较值是字符串优化器通常会把字符串转成DATETIME再走索引这个还算幸运但如果反过来WHERE 手机号 13800138000手机号列是VARCHAR优化器会把每行的列值转成数字再比较索引失效。所以在写SQL的时候好习惯是保持比较两端的类型一致。模型字段是字符串就传字符串是日期就传日期或日期范围尽量不要指望数据库的隐式转换帮你兜底。这一点在执行计划里都能看出来Table Scan加CONVERT_IMPLICIT运算就是典型的症状。7. 常见问题与排查技巧实录7.1 常见问题速查表每次带新人或者帮别人排查SQL问题我都发现很多问题反复出现。我总结了一张速查表覆盖SQL函数及方法使用中的高频问题你可以直接收藏症状大概率原因解决方案COUNT(*)结果和业务预期不一致表有重复行或NULL语义理解错误确认COUNT(DISTINCT)或加WHERE过滤字符串比较查不到数据大小写敏感性或隐藏空格用TRIM处理按需求统一UPPER/LOWER确认排序规则日期查询没走索引YEAR()、MONTH()等函数套在列上改写为范围比较 和 除法报“除以零”分母聚合结果为0用NULLIF(分母, 0)保护拼接字段部分为NULL导致整列NULL使用拼接字符串改用CONCAT或COALESCE先处理NULL窗口函数结果顺序不稳定ORDER BY字段内有重复值追加一个唯一字段作为次级排序类型转换查询报错字符串列混杂非法字符SELECT查非法值清洗后转换慢SQL出现CONVERT_IMPLICIT隐式类型转换显式CAST统一两端类型7.2 一个慢SQL优化的完整复盘上个月我在做报表系统优化时遇到一条SQL逻辑很简单按月统计每个区域的销售额、订单数和客单价。写法的结构基本是SELECT 区域, MONTH(下单时间) AS 月份, SUM(金额)... FROM 订单表 WHERE 下单时间 2024-01-01 GROUP BY 区域, MONTH(下单时间)。数据量800万行执行时间在30秒左右。看执行计划问题很明显WHERE条件本身可以走索引但SELECT里用了MONTH(下单时间)做分组导致分组操作需要对全量结果集做哈希索引预计算的优势无法发挥。我的改法是把月份字段拆出来在源表建模时冗余一个下单月份列或者查询时先通过日期范围把数据缩小到当月再分组。改后的SQL是SELECT 区域, 月份, SUM(金额) FROM 订单表 WHERE 下单时间 2024-01-01 AND 下单时间 2025-01-01 GROUP BY 区域, 月份。配合一个冗余的月份字段执行时间降到了3秒以内。这个案例的关键不是MONTH函数本身慢而是函数使用位置不当把索引和分组优化全堵死了。还有一次排查字符串去重慢的问题原因出在COUNT(DISTINCT 用户昵称)上用户昵称列很长且基数大去重排序开销惊人。后来改成先对用户昵称做HASH计算存一个短哈希列再对哈希列做COUNT(DISTINCT)性能提升明显。当然哈希存在碰撞风险精确场景下还是要权衡。7.3 实操心得函数不是越炫越好最后分享一点我自己的真实体会。SQL方法函数的数量极其庞大每个数据库还在不断新增特性但生产环境的核心原则永远是稳定和可维护。写函数时尽量选择语义清晰、跨数据库兼容性好的写法性能优化时先看执行计划再动手遇到函数相关的奇怪问题先检查NULL、类型、排序规则这老三样。我在实际工作中见过太多为了炫技使用复杂窗口函数结果半年后没人敢改这条SQL的情况。函数是用来服务业务逻辑的代码可读性永远排在第一位。如果一个COALESCE加一个CASE WHEN能解决90%的空值场景就没有必要为了用数据库的冷门特性而把逻辑写得神乎其神。SQL是团队协作的语言你能把函数用得让同事一眼看懂才是真正的功力。下一篇我准备深入讲字符串处理和JSON数据的函数组合用法如果你在平时遇到过类似上面提到的问题欢迎在评论区说说你的解法。