资讯详情

资讯详情

MySQL窗口函数实战:分组统计下的排名、占比、累计与环比

做MySQL分组统计大多数人第一反应就是GROUP BY。这个思路本身没错但需求一旦细化就很折磨人分组之后还想保留每一行的明细给组内排个名次算一下某条记录占了整个组的多少比例或者看组内逐日累计值GROUP BY一聚合就把行压没了只能子查询、自连接、临时表轮番上阵。MySQL 8.0引入的窗口函数正是为了解决这种既要分组计算、又要保留明细的别扭场景而设计的——它能在不合并行的前提下把分组计算结果追加到每一行上。这篇内容会围绕窗口函数做分组这个主题从场景定位、语法拆解讲到Top N、占比、累计、环比这些高频需求最后还会附上我实际踩过的坑和排查记录。如果你对GROUP BY已经很熟但窗口函数还停留在听说过、不太会用的阶段这篇文章可以帮你直接把这块补上。读完之后你大概率会把手写相关子查询的习惯丢掉。1. 窗口函数到底解决了什么问题1.1 一个让GROUP BY抓狂的需求具体场景是这样的某公司销售系统里有一张销售流水表领导要按区域统计每个销售员的汇总业绩并按区域排名次排名结果还要和销售员、区域这些明细信息一起展示。用GROUP BY的话第一步能拿到区域、销售员、汇总金额但排名这个动作GROUP BY本身做不了得自己想办法。比较传统也最让人头疼的一种写法是用相关子查询数同区域内业绩比我高的人有几个加一就是我的排名。SQL写出来长这样SELECT t.region, t.salesperson, t.total_amount, (SELECT COUNT(*) 1 FROM ( SELECT region, salesperson, SUM(amount) AS total_amount FROM sales_record GROUP BY region, salesperson ) t2 WHERE t2.region t.region AND t2.total_amount t.total_amount ) AS ranking FROM ( SELECT region, salesperson, SUM(amount) AS total_amount FROM sales_record GROUP BY region, salesperson ) t;这段SQL能跑但问题很明显第一可读性太差维护的人看到一个嵌套一个的子查询就头大第二相关子查询是逐行执行的外层有多少销售员内层就被执行多少次数据量一大性能立刻崩第三遇到并列名次还得额外想办法按并列处理逻辑再叠一层。用窗口函数重写之后同样的需求变成这样SELECT region, salesperson, total_amount, ROW_NUMBER() OVER (PARTITION BY region ORDER BY total_amount DESC) AS ranking FROM ( SELECT region, salesperson, SUM(amount) AS total_amount FROM sales_record GROUP BY region, salesperson ) t;两段代码放在一起对比后者肉眼可见地简洁。窗口函数把按区域分组和组内排名两个逻辑拆得清清楚楚后续要改排序规则、改排名方式都只需要动OVER子句里的参数不需要动外层结构。1.2 窗口函数的核心思路按组计算但行不合并用生活化的方式理解窗口函数可以把它比喻成在一叠单据上盖章。GROUP BY的分组是把一堆单据按区域分成若干叠每叠最后合并成一行汇总数字窗口函数的分组虽然也会先把单据分成一叠一叠但它不会把这些单据叠在一起压扁而是给这一叠贴一张标签比如本叠合计100万、本叠里你排第2然后把这叠里的每一张原始单据原样还给你。所以窗口函数最根本的特点就是结果集会保留每一行原始记录同时每行附带一组在PARTITION BY分区范围内计算出来的值。因为这个特点它特别适合做明细分组统计混合展示的场景。比如订单列表页既要显示每一笔订单的金额又要显示当前这张订单所属区域的订单总额、所属客户的累计消费金额这种需求用老写法往往要JOIN两张以上临时表而窗口函数一条SQL就能表达清楚。MySQL从8.0版本开始正式支持窗口函数MariaDB从10.2开始也有。如果你还在用5.7或者更早的版本只能靠变量模拟或者先升级再说这个我在后面踩坑实录那一节会详细说怎么处理。窗口函数不是锦上添花的语法糖它是数据库在处理组内计算这个维度上补齐的一块重要能力升级版本带来的收益非常直接。1.3 用不用窗口函数的判断清单什么时候该用窗口函数什么时候老老实实用GROUP BY我整理了一个快速判断清单按这个思路选型基本不会错需要组内排名比如每个区域内销售员排名用窗口函数。需要组内累计、移动平均比如按日期累加销售额用窗口函数。需要明细行附带组汇总值比如每笔订单后面都显示所属区域的订单总额用窗口函数。需要前后行对比、环比用LAG、LEAD这类偏移窗口函数。只需要每个分组一条汇总报表不需要明细GROUP BY足够。需要对分组结果做整体过滤且过滤条件和每一行明细无关考虑HAVING而不是窗口函数。这个清单不是教条但它能帮你快速判断该往哪个方向写。窗口函数解决的是在分组计算的参照系下行的维度保持完整的问题GROUP BY解决的是把多行压缩成一行的问题两者目标不同很多时候还需要配合使用。最典型的组合就是先用GROUP BY把明细聚合成原子指标再在外面套窗口函数做组内排名、占比、累计这在后面完整案例里会看到。2. 语法与函数分类拆解2.1 一条SQL看全语法骨架窗口函数的标准语法可以压缩成一行来记忆窗口函数(参数) OVER ( [PARTITION BY 分组列1, 分组列2, ...] [ORDER BY 排序列 ASC/DESC] [ROWS/RANGE BETWEEN 帧起点 AND 帧终点] )三个部分各自的作用不一样。PARTITION BY指定按哪些列分组逻辑上和GROUP BY的分组概念相似但它不会合并行ORDER BY指定组内排序方式对排名类函数来说它决定名次顺序对聚合类窗口函数来说它还会改变默认的计算范围这个陷阱后面会重点讲帧定义是在排好序的分区内进一步限定参与计算的行的区间不写的时候有默认规则也可以写成ROWS BETWEEN的形式做精确控制。给一个最基础也最好理解的例子统计每个区域的总销售额同时把每行订单都保留下来SELECT region, salesperson, sale_date, amount, SUM(amount) OVER (PARTITION BY region) AS region_total FROM sales_record;这条SQL跑出来行数和平时的SELECT完全一样只是每行多了个region_total列同一个区域内所有行的region_total值相同。先跑通这个例子后面加ORDER BY、加帧定义、换函数都只是在这个骨架上的变化而已。2.2 PARTITION BY和GROUP BY到底差在哪很多人把PARTITION BY当成另一种GROUP BY这个理解对了一半两者差着关键几步。先看执行时机SQL的逻辑执行顺序大致是FROM → WHERE → GROUP BY → HAVING → 窗口函数 → DISTINCT → ORDER BY → LIMIT。窗口函数在GROUP BY和HAVING之后、最终输出之前执行所以它能看到分组之后的结果而GROUP BY看不到未聚合的明细行。具体差异可以看这张对照表对比点GROUP BYPARTITION BY 窗口函数结果行数每个分组一行原始行数不变SELECT列的限制只能出现分组列和聚合函数原始列、窗口结果都能出现计算参照系整个结果集的组分区内的行集合与WHERE的关系WHERE可以先过滤再分组窗口函数的结果不能写在WHERE里典型输出汇总报表明细行 组统计值这个差异有一个很实用的推论窗口函数可以和GROUP BY同时出现在一条SQL里前提是窗口函数使用的是GROUP BY之后的结果。比如算每个区域的销售总额再按总额给区域排名可以这么写SELECT region, SUM(amount) AS total_amount, RANK() OVER (ORDER BY SUM(amount) DESC) AS region_rank FROM sales_record GROUP BY region;这里窗口函数拿到的输入是GROUP BY已经汇总好的每区域一行的结果集所以ORDER BY里可以直接写SUM(amount)。这种组合写法非常常见做区域排名、部门排名时比先套一层子查询更清爽也是先聚合、再开窗思想的雏形。2.3 ORDER BY和窗口框架最容易翻车的地方聚合类窗口函数SUM、AVG、MAX、MIN、COUNT有一个很容易被忽略的默认行为OVER里写了ORDER BY和不写ORDER BY计算结果可能完全不同。因为一旦OVER里出现ORDER BY聚合窗口函数的默认帧就会从整个分区变成从分区起点到当前行也就是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW相当于把聚合变成了累计。举个例子假设某个区域内三笔订单金额分别是1200、1500、800-- 写法AOVER里没有ORDER BY对整个分区计算每行都得到区域总额 SELECT sale_date, amount, SUM(amount) OVER (PARTITION BY region) AS region_total FROM sales_record; -- 写法BOVER里有ORDER BY默认变成累计值 SELECT sale_date, amount, SUM(amount) OVER (PARTITION BY region ORDER BY sale_date) AS running_total FROM sales_record;写法A中每行拿到的都是整个区域的总和3500写法B则是按日期排序后的累计值第一行是1200第二行是12001500第三行是12001500800。两段SQL只差一个ORDER BY业务含义就完全不同这属于典型的语法没错、语义错了的坑。更隐蔽的是RANGE和ROWS的区别。RANGE是按排序值划定范围如果排序日期有重复同一天的记录会被一起纳入当前累计帧看起来像跳档ROWS BETWEEN是按物理行严格划分不受重复值影响。所以做累计值时我基本会显式写成ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW而不是依赖默认行为这样语义更清晰也不会被数据里的重复值干扰。帧定义常用的几种写法我整理成了表格帧写法含义ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW从分区第一行到当前行ROWS BETWEEN 2 PRECEDING AND CURRENT ROW当前行和前两行ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING当前行和后两行ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING整个分区RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW按排序值划定到当前排序值为止还需要提醒一句ROW_NUMBER、RANK这些排名类函数本身不认帧定义它们总是拿整个分区做排名帧定义影响的主要是SUM、AVG这类聚合窗口函数。写SQL之前先搞清楚自己用的是哪类函数很多莫名其妙的计算结果就能提前避免。3. 分组场景实战排名、占比、累计、环比3.1 排名类函数三兄弟怎么选排名类窗口函数有三个ROW_NUMBER、RANK、DENSE_RANK。新手最容易纠结它们到底什么区别用一个例子就能说清楚。假设某区域两名销售员业绩都是100分、第三名是98分三种函数的结果是原始分数ROW_NUMBERRANKDENSE_RANK10011110021198332区别一句话总结ROW_NUMBER不管并列按顺序硬编号RANK遇到并列会跳号DENSE_RANK遇到并列不跳号名次紧凑排列。实际应用里如果要每组取Top N并且每人只能占一个名额用ROW_NUMBER最合适因为即使两条记录业绩相同也会强制给出不同序号保证每组稳定取到N条数据。如果希望并列的记录显示同样的名次、后面的名次空出来用RANK如果希望名次连续紧凑比如奖金按名次档位发放用DENSE_RANK。我处理每个区域业绩前2名这类需求时默认用ROW_NUMBER它不会因为并列而让结果集多出记录。很多团队会纠结到底哪个函数才是标准排名其实没有唯一答案关键看业务怎么定义并列。把三种函数都跑一遍对比输出结果五分钟就能确定用哪个比在会议室争论半天有效率。3.2 聚合窗口函数在分组里的典型应用SUM、AVG、COUNT这些平时总跟在GROUP BY后面的聚合函数一旦加上OVER子句就成了聚合窗口函数在分组场景里的玩法非常多。它们的共同点是计算的参照系是分区内的行但输出时每一行原样保留这是GROUP BY给不了的。第一个典型应用是组内占比。领导想看每笔订单在所属区域里占了多大份额原始表一行都不能少可以这么写SELECT region, salesperson, sale_date, amount, amount / SUM(amount) OVER (PARTITION BY region) AS region_share FROM sales_record;每行的amount除以该区域总金额就是这条订单的区域占比。如果用GROUP BY做这个需求需要先算区域总金额再JOIN回原表两步操作临时表的关联条件一多就容易出错。窗口函数把算总和的逻辑直接嵌在每一行旁边逻辑清晰得多。第二个典型应用是组内累计。统计每个销售员从年初到当前月份的累计销售额这是典型的组内跑累计SELECT salesperson, sale_date, amount, SUM(amount) OVER (PARTITION BY salesperson ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_amount FROM sales_record;PARTITION BY salesperson保证每个销售员独立看自己的累计ORDER BY sale_date决定累计方向ROWS BETWEEN把参与计算的范围限定到当前行。这里如果把帧定义省略遇到同一天多笔记录时结果可能和直觉不一致所以显式写帧是最稳的写法。第三个典型应用是移动平均。判断一个区域短期销售走势可以用最近三笔订单的平均金额做平滑SELECT region, sale_date, amount, AVG(amount) OVER (PARTITION BY region ORDER BY sale_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_avg_3 FROM sales_record;移动平均在零售、流量分析里非常常用窗口函数几乎是最简洁的实现方式。换成老写法得先给每行标上行号再自连接到前两行复杂度完全不在一个量级。3.3 偏移类函数做组内对比LAG和LEAD是一对很直观的函数作用分别是取分区内排好序后当前行前面第N行的值、后面第N行的值。默认N1也就是取上一行或下一行也可以显式指定偏移量。做环比时LAG是绝对主角。比如每个销售员要和自己上一个月的销售额做对比判断业绩是涨是跌可以这样写SELECT salesperson, mon, month_amount, LAG(month_amount) OVER (PARTITION BY salesperson ORDER BY mon) AS prev_month_amount, month_amount - LAG(month_amount) OVER (PARTITION BY salesperson ORDER BY mon) AS diff_amount FROM ( SELECT salesperson, DATE_FORMAT(sale_date, %Y-%m) AS mon, SUM(amount) AS month_amount FROM sales_record GROUP BY salesperson, DATE_FORMAT(sale_date, %Y-%m) ) t;这里有一个很关键的点LAG的PARTITION BY必须按销售员分ORDER BY按月份排这样上一行才真正代表这个人上一个月的业绩。有些人写环比时用全局排序取出来的上一条记录是别人家的结果完全错了。窗口函数的分区维度决定行与行之间的关系这是比函数本身更重要的设计决策。如果当前行没有上一行比如某销售员进入系统的第一个月LAG会返回NULL。这个NULL可以直接参与计算变成NULL也可以用IFNULL兜底成0或者配合COALESCE处理具体口径要看业务需求但一定要提前想清楚因为NULL出现在报表里很容易被忽略。4. 完整案例区域月度业绩报表从需求到落地4.1 需求背景与建表前面把知识点拆完了下面走一遍完整案例看看窗口函数在真实分组需求里是怎么组合使用的。原始需求是销售负责人要看每个区域每月的销售员业绩排行同时要看每个人在区域内的业绩占比、从1月到当前月的累计金额、以及和上月相比的增减额最后还要快速找到每个区域每月Top 2销售员。我先把演示表建出来模拟某公司的销售流水CREATE TABLE sales_record ( id INT PRIMARY KEY AUTO_INCREMENT, region VARCHAR(20), salesperson VARCHAR(20), category VARCHAR(20), amount DECIMAL(10,2), sale_date DATE ); INSERT INTO sales_record (region, salesperson, category, amount, sale_date) VALUES (区域A, 销售员A, 数码, 1200.00, 2024-01-05), (区域A, 销售员A, 数码, 800.00, 2024-02-11), (区域A, 销售员B, 数码, 1500.00, 2024-01-18), (区域A, 销售员B, 办公, 900.00, 2024-02-02), (区域B, 销售员C, 数码, 2000.00, 2024-01-09), (区域B, 销售员C, 数码, 1100.00, 2024-02-14), (区域B, 销售员D, 办公, 600.00, 2024-01-25), (区域B, 销售员D, 数码, 1300.00, 2024-02-20), (区域C, 销售员E, 数码, 1800.00, 2024-01-12), (区域C, 销售员E, 办公, 700.00, 2024-02-08);表结构不复杂重点是流水明细。注意我没有在表里存月份字段因为月份统计口径完全可以用DATE_FORMAT从日期里现算这也符合实际业务中明细表不带冗余汇总字段的规范。4.2 第一步先算月度基础明细无论后面要接多少个窗口函数第一步永远是先把最外层逻辑需要的原子指标算清楚。这里原子指标就是每个销售员每个月的销售总额。SELECT region, salesperson, DATE_FORMAT(sale_date, %Y-%m) AS mon, SUM(amount) AS month_amount FROM sales_record GROUP BY region, salesperson, DATE_FORMAT(sale_date, %Y-%m) ORDER BY region, salesperson, mon;regionsalespersonmonmonth_amount区域A销售员A2024-011200.00区域A销售员A2024-02800.00区域A销售员B2024-011500.00区域A销售员B2024-02900.00区域B销售员C2024-012000.00区域B销售员C2024-021100.00区域B销售员D2024-01600.00区域B销售员D2024-021300.00区域C销售员E2024-011800.00区域C销售员E2024-02700.00这一步先把GROUP BY的工作做完窗口函数下一步再叠加整个结构会非常清晰。这也是我推荐的处理顺序能先用GROUP BY算好的原子指标先收成一行窗口函数负责在已经收好的行集上做跨行、跨组的计算而不是让窗口函数直接对着原始流水做各种复杂操作。这样出问题的时候能快速判断是汇总错了还是窗口函数算错了。4.3 第二步叠加排名和区域内占比有了月度基础表之后区域排名和占比可以直接在上面加两个窗口函数不需要再JOIN任何表SELECT region, salesperson, mon, month_amount, ROW_NUMBER() OVER (PARTITION BY region, mon ORDER BY month_amount DESC) AS rank_in_region, month_amount / SUM(month_amount) OVER (PARTITION BY region, mon) AS share_in_region FROM ( SELECT region, salesperson, DATE_FORMAT(sale_date, %Y-%m) AS mon, SUM(amount) AS month_amount FROM sales_record GROUP BY region, salesperson, DATE_FORMAT(sale_date, %Y-%m) ) t ORDER BY region, mon, rank_in_region;PARTITION BY region, mon的意思是每个区域的每个月单独成一个分区。每个分区内排一次名、算一次总和再拿每条记录除以这个总和占比就出来了。以区域A 2024年1月为例那个分区只有销售员B的1500和销售员A的1200总额2700所以销售员B占比约55.56%、排名1销售员A占比约44.44%、排名2。这种窗口函数叠加在GROUP BY结果之上的写法非常符合SQL的逻辑执行顺序子查询先把明细聚合成月度记录外层窗口函数在已经聚合好的数据集上按区域和月份分区计算清晰且高效。如果反过来让窗口函数直接算原始流水还要先处理日期格式化和重复行的问题逻辑会乱很多。4.4 第三步加累计和环比输出最终报表最后一个需求是累计和环比。累计的口径是销售员本人从1月到当前月份的累计金额所以PARTITION BY必须用salespersonORDER BY用mon环比是拿这个月和这个人的上月比用LAG就能取到上一行的值。SELECT region, salesperson, mon, month_amount, ROW_NUMBER() OVER (PARTITION BY region, mon ORDER BY month_amount DESC) AS rank_in_region, month_amount / SUM(month_amount) OVER (PARTITION BY region, mon) AS share_in_region, SUM(month_amount) OVER (PARTITION BY salesperson ORDER BY mon ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS accumulated_amount, LAG(month_amount) OVER (PARTITION BY salesperson ORDER BY mon) AS prev_month_amount, month_amount - LAG(month_amount) OVER (PARTITION BY salesperson ORDER BY mon) AS diff_amount FROM ( SELECT region, salesperson, DATE_FORMAT(sale_date, %Y-%m) AS mon, SUM(amount) AS month_amount FROM sales_record GROUP BY region, salesperson, DATE_FORMAT(sale_date, %Y-%m) ) t ORDER BY region, mon, rank_in_region;注意这里有四个窗口函数同时出现每个都有自己的PARTITION BY和ORDER BY互不干扰。排名和占比按区域月份分区累计和环比按销售员分区这是这条SQL最值得体会的地方窗口函数的维度是各自定义的完全可以在一行SQL里混用多个不同分组维度。如果要再输出每个区域每月Top 2把上面这条SQL套一层子查询在WHERE里过滤rank_in_region2就行。因为窗口函数不能直接进WHERE必须借助派生表或者公共表表达式SELECT region, salesperson, mon, month_amount, rank_in_region, share_in_region, accumulated_amount, diff_amount FROM ( SELECT region, salesperson, mon, month_amount, ROW_NUMBER() OVER (PARTITION BY region, mon ORDER BY month_amount DESC) AS rank_in_region, month_amount / SUM(month_amount) OVER (PARTITION BY region, mon) AS share_in_region, SUM(month_amount) OVER (PARTITION BY salesperson ORDER BY mon ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS accumulated_amount, LAG(month_amount) OVER (PARTITION BY salesperson ORDER BY mon) AS prev_month_amount, month_amount - LAG(month_amount) OVER (PARTITION BY salesperson ORDER BY mon) AS diff_amount FROM ( SELECT region, salesperson, DATE_FORMAT(sale_date, %Y-%m) AS mon, SUM(amount) AS month_amount FROM sales_record GROUP BY region, salesperson, DATE_FORMAT(sale_date, %Y-%m) ) base ) filtered WHERE rank_in_region 2 ORDER BY region, mon, rank_in_region;到这一步领导要的报表从数据到逻辑全部落地总共就几条SQL没有一次自连接没有一次相关子查询性能和维护成本都远优于传统写法。整个演变过程就是先GROUP BY出原子明细再不断叠加窗口函数的一步步展开这也是我写这类需求时的标准套路。5. 高频踩坑与排查实录5.1 ERROR 3593窗口函数不能直接用在WHERE里刚上手窗口函数时我就犯过这个错想取每组第一名直接写SELECT region, salesperson, amount, ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS rn FROM sales_record WHERE rn 1;MySQL直接报错提示窗口函数不能出现在WHERE子句中。原因在于逻辑执行顺序WHERE在窗口函数计算之前就执行了这时候rn压根还不存在MySQL自然不会让你引用它。正确的做法是套一层派生表或者用WITH公共表表达式把窗口结果先算出来再在外面过滤WITH ranked AS ( SELECT region, salesperson, amount, ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS rn FROM sales_record ) SELECT region, salesperson, amount FROM ranked WHERE rn 1;这个错误非常典型几乎每个窗口函数初学者都会撞上。后来我养成了一个习惯凡是窗口函数的结果要参与过滤就先包一层不要和WHERE硬刚。也别想把窗口函数塞进HAVING或JOIN的ON条件里逻辑执行顺序决定了它还没算出来任何前置阶段都用不了它。5.2 忘了PARTITION BY整个结果集变成一个组排查别人写的SQL时我见过最普遍的问题就是漏写PARTITION BY。比如想写组内排名结果只写了ORDER BYSELECT region, salesperson, amount, RANK() OVER (ORDER BY amount DESC) AS global_rank FROM sales_record;这种写法不是语法错但结果是从全表角度排名而不是从区域内部排名。如果业务要的是每个区域的排名这就是典型的逻辑错误而且很难通过报错看出来。我排查这类问题的办法是先跑一条只含ROW_NUMBER的SQL数一下每个分区的起始行名次是不是1如果只有整个结果集第一个行名次是1基本可以确定是漏了PARTITION BY或者分区键写错了。另一个相关的坑是PARTITION BY里列的顺序。如果需求是区域月份的排名PARTITION BY region, mon和PARTITION BY mon, region计算结果其实一样因为分区只是集合划分顺序不影响成员。但为了读SQL的人不混淆建议保持和业务口径一致的书写顺序比如业务说的是按区域再按月就写region在前、mon在后少让同事产生疑问。5.3 MySQL 5.7或更老版本没有窗口函数怎么办窗口函数在MySQL 8.0才正式提供很多存量项目还跑在5.7上遇到组内排名需求确实没有现成语法。早期的经典替代方案是用用户变量模拟思路是先按区域和金额排序然后用变量记住上一个区域每读一行判断如果区域变了就从1重新记否则继续累加。SELECT region, salesperson, total_amount, rank : IF(cur_region region, rank 1, 1) AS rn, cur_region : region AS dummy FROM ( SELECT region, salesperson, SUM(amount) AS total_amount FROM sales_record GROUP BY region, salesperson ORDER BY region, total_amount DESC ) t CROSS JOIN (SELECT cur_region : NULL, rank : 0) vars;这个方案能跑但有两个隐含前提内层查询必须保证先按region排好序再按金额降序排因为变量依赖这个顺序MySQL对SELECT列表里变量赋值的求值顺序有历史包袱8.0之后这种行为已经被官方标记为不推荐依赖。所以我给的建议很直接如果条件允许优先把数据库升级到8.0及以上别把时间花在维护变量模拟SQL上如果确实不能升级这个方案也只能作为过渡别指望它在复杂SQL里稳定运行。5.4 性能优化索引、排序与执行计划窗口函数不是魔法它计算的前提是对每个分区做排序所以性能问题大多出在排序上。用EXPLAIN看执行计划如果出现Using filesort基本说明窗口函数的ORDER BY没有用上索引。优化思路有三个方向。第一尽量让PARTITION BY和ORDER BY的列有复合索引支撑例如经常按(region, sale_date)做分区排序就建一个region加sale_date的联合索引。第二减少不必要的全分区帧比如不需要整个分区都参与计算时通过ROWS BETWEEN把计算范围缩小减少参与计算的行数。第三注意不要在子查询里随便加DISTINCT窗口函数结果配合DISTINCT时MySQL有时会多一次去重排序开销不小。还有一个容易被忽略的点不要在PARTITION BY或ORDER BY里套函数比如ORDER BY DATE_FORMAT(sale_date, %Y-%m)列一旦被函数包住索引基本失效。如果必须按月排序要么单独维护一个月份字段要么接受这个写法带来的排序代价看数据量再权衡。窗口函数本身不是性能杀手真正需要盯住的是底层的排序和扫描范围。6. 一点实操心得窗口函数刚进入MySQL生态的时候很多人觉得它只是高级语法糖用不用都行。我自己用下来最深的体会是它彻底改变了处理分组计算类需求的思考方式以前拿到问题先想要不要拆两张临时表、要不要自连接现在会先想这需求到底关注哪个维度、要在哪个分区内算。GROUP BY和窗口函数不是替代关系而是分工关系GROUP BY负责把行压缩成报表窗口函数负责在不动行的前提下叠加分组视角的计算。最后分享一个小习惯我写复杂窗口SQL从来不直接一坨写完而是先写一个最内层的GROUP BY基础查询确认原子指标没问题再一层层往上加排名、占比、累计、偏移函数每加一层就先看结果是否符合预期。这么做的好处是出问题的时候能立刻定位是汇总错了还是窗口函数算错了而不是对着几百行SQL从头排查。如果你刚开始接触窗口函数也可以用这个节奏从最简单的SUM(amount) OVER(PARTITION BY region)开始逐步加入排序、帧定义、多函数组合会比一次写完整条复杂SQL踏实得多。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →