
干了这么多年数据库我有个很深的感受很多同事写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(*)统计的是结果集的总行数不管某列是否为NULLCOUNT(列名)只统计该列非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) testexample.com;这条SQL里LOWER(email)把email列的函数结果作为比较对象数据库无法使用email列上的索引只能在每一行上先执行LOWER()函数再做匹配这就变成了全表扫描。解决办法有两个。一是修改数据本身比如建表时就把邮箱统一存成小写从源头避免这个问题。二是新建一个生成列或者函数索引。MySQL 8.0和PostgreSQL都支持在索引上直接使用函数像CREATE INDEX idx_email_lower ON users((LOWER(email)))这样查询就能走索引了。我个人的原则是能不改数据就不改数据能改查询就不动表结构。如果既不能改数据也不能建索引那就在WHERE两侧各做一次函数让索引列保持原始形态比如WHERE email LOWER(TestExample.com)。这样右侧的输入值先转小写左侧的列不动索引就还能用上。4. 数值函数不只是round和abs4.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 BYSUM就变成了从窗口起始行到当前行的累积值行为完全不同。这是窗口函数最容易翻车的地方。6.2 经典窗口函数rank、row_number、sum over、lag/lead窗口函数不止有SUMCOUNT、AVG、MAX、MIN都可以配合OVER使用。除此之外有几类专门针对窗口的函数实用性极高。第一组是排序函数ROW_NUMBER()、RANK()、DENSE_RANK()。它们的区别是ROW_NUMBER()按顺序编号相同值的行也会被分配不同的编号。RANK()相同值的行编号相同但下一个不同的值会跳过编号产生空档。DENSE_RANK()相同值的行编号相同但下一个不同的值不跳号。用实际数据举例假设两行业绩都是100一行是90。ROW_NUMBER()输出1, 2, 3RANK()输出1, 1, 3DENSE_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 ONOracle和PostgreSQL也都有对应的计划查看方式。看执行计划时重点看两个地方一是type字段是不是ALL全表扫描二是key字段有没有用到索引。这两列能告诉你SQL慢的根源到底在不在函数。我个人在实际操作中的体会是排查慢SQL时最忌讳一上来就加索引。先把SQL语句里函数、类型转换、隐式转换这些隐藏问题排除掉再谈加索引。很多时候把WHERE条件里的函数挪个位置或者把LIKE %xxx%改成等值匹配性能问题就解决了一大半。函数这东西说到底就是SQL的积木。你掌握的积木越多搭出来的数据分析方案就越灵活。比起背公式更建议你多去写、多去用。建议手头备一份自己主力数据库的函数手册遇到不确定的就查。写SQL的功力不是一天练成的但你如果把这五类函数都主动用一遍那些曾经让你挠头的统计需求大部分都能顺手解决。最后分享一个小技巧每次新接一个写SQL的任务先花两分钟想想它属于哪类场景——是要聚合压缩要清洗字段还是跨行计算。想清楚这个事情你的SQL质量会有一个明显的提升。