ARTICLE DETAIL

资讯详情

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

零基础学SQL 19:报表累计求和总对不上?窗口函数 4 行搞定(附 3 个模板)

零基础学SQL 19:报表累计求和总对不上?窗口函数 4 行搞定(附 3 个模板) 做月度报表绕不开这三个问题截至这个月累计是多少跟上个月比涨了还是跌了这个月占了整体的百分之多少不会窗口函数的人一般有三条出路导到 Excel 手动拉一列、写自连接自己跟自己拼、或者用变量一行行累加。三条路都有同一个毛病——数据一多就慢而且容易算错错了还很难查。其实这三个问题SQL 里是同一个工具解决窗口函数。它跟 GROUP BY 最大的区别用一句话说清GROUP BY 把多行压成一行窗口函数不压行每一行都留下只是在旁边多算一列。所以明细 累计能同时出现在一张表里这也是它比 GROUP BY 更适合做报表的原因。下面从累计开始把三个场景一次讲完每个都不超过 4 行 SQL。一、先把练习表建好本系列所有文章都用下面这 4 张固定的表建一次就能跟着任意一篇练手。开头两行是建一个练习用的库已经有自己的库把这两行换成USE 你的库名;就行每张表先 DROP 再 CREATE重复运行不会报表已存在。-- SQL 系列统一练习表整段复制即可运行可重复执行CREATEDATABASEIFNOTEXISTSsql_practiceDEFAULTCHARACTERSETutf8mb4;USEsql_practice;DROPTABLEIFEXISTS员工表;CREATETABLE员工表(员工idINTPRIMARYKEY,姓名VARCHAR(20),部门VARCHAR(20),工资DECIMAL(10,2),邮箱VARCHAR(50),手机VARCHAR(20),入职日期DATE);INSERTINTO员工表(员工id,姓名,部门,工资,邮箱,手机,入职日期)VALUES(1,张三,技术部,9000.00,zhangsandemo.com,13800000001,2019-03-01),(2,李四,技术部,9000.00,lisidemo.com,13800000002,2020-07-15),(3,王五,销售部,9200.00,wangwudemo.com,13800000003,2018-01-10),(4,赵六,销售部,9200.00,zhaoliudemo.com,13800000004,2021-05-20);DROPTABLEIFEXISTS订单表;CREATETABLE订单表(订单idINTPRIMARYKEY,员工idINT,订单金额DECIMAL(10,2),下单时间DATETIME,付款时间DATETIME,状态VARCHAR(20));INSERTINTO订单表(订单id,员工id,订单金额,下单时间,付款时间,状态)VALUES(101,1,3000.00,2026-01-10 10:00:00,2026-01-10 10:05:00,已付款),(102,1,2500.00,2026-02-15 14:00:00,2026-02-15 14:10:00,已付款),(103,3,4000.00,2026-03-20 09:30:00,2026-03-20 09:40:00,已付款),(104,2,1500.00,2026-04-05 16:00:00,NULL,待付款);DROPTABLEIFEXISTS用户表;CREATETABLE用户表(用户idINTPRIMARYKEY,姓名VARCHAR(20),手机VARCHAR(20),邮箱VARCHAR(50),地址VARCHAR(100));INSERTINTO用户表(用户id,姓名,手机,邮箱,地址)VALUES(1,张三,13900000001,zhangsandemo.com,北京市朝阳区),(2,李四,13900000002,lisidemo.com,上海市浦东新区),(3,王五,13900000003,wangwudemo.com,广州市天河区);DROPTABLEIFEXISTS任务表;CREATETABLE任务表(任务idINTPRIMARYKEY,员工idINT,备注VARCHAR(100),状态VARCHAR(20));INSERTINTO任务表(任务id,员工id,备注,状态)VALUES(1,1,完成需求评审,已完成),(2,2,NULL,进行中),(3,3,修复线上bug,已完成),(4,4,NULL,待分配);订单表里 4 条订单分别在 1、2、3、4 月各一笔金额 3000、2500、4000、1500。数量不多但算累计、环比、占比都够用了写法跟几十万行的表完全一样。二、累计求和核心就 4 行需求按时间顺序算出截至每一笔订单的累计金额。SELECT订单id,下单时间,订单金额,SUM(订单金额)OVER(ORDERBY下单时间)AS累计金额FROM订单表;运行结果订单id下单时间订单金额累计金额1012026-01-103000.003000.001022026-02-152500.005500.001032026-03-204000.009500.001042026-04-051500.0011000.00每一行的累计金额都是从第一行加到自己这一行。101 只有自己 3000102 是 300025005500到最后一行正好是全部订单总额 11000。把这个写法拆成三块看就不会记混代码片段作用少写会怎样SUM(订单金额)要算的指标没有指标可算OVER声明这是窗口函数别分组报语法错误(ORDER BY 下单时间)累计的顺序从头加到自己变成总计每行都一样最后一行那句要重点记OVER 里写了 ORDER BYSUM 才叫累计不写它算的是总计。这不是差一点是两个完全不同的结果。后面第四个坑会再拿数据演示一次。三、分部门各自累计PARTITION BY上面的累计是从第一行加到最后如果需求变成每个部门各自累计呢比如技术部的累计走到 7000 就结束销售部要从头开始算不能接着技术部的数往下加。这时候就要用PARTITION BY——先在部门上把数据切开每一块单独累计SELECTe.部门,o.订单id,o.下单时间,o.订单金额,SUM(o.订单金额)OVER(PARTITIONBYe.部门ORDERBYo.下单时间)AS部门累计FROM订单表 oJOIN员工表 eONo.员工ide.员工idORDERBYe.部门,o.下单时间;运行结果部门订单id订单金额部门累计技术部1013000.003000.00技术部1022500.005500.00技术部1041500.007000.00销售部1034000.004000.00注意技术部的第三条累计到 7000 就停了300025001500销售部的 4000 没被算进来而销售部自己那一条累计就是 4000从头开始。PARTITION BY 部门 ORDER BY 下单时间读起来就是先按部门分成几堆每一堆内部按时间顺序累加。这两个关键词的分工可以直接背下来关键词管什么类比PARTITION BY数据怎么分组换组就重新开始分几摞牌ORDER BY组内按什么顺序累加每摞按顺序翻牌四、环比LAG() 往上取一行第二个常见问题跟上一个月比是涨了还是跌了。这类和上一行比的需求用LAG()。LAG(字段)的意思是取当前行的前一行那条记录的这个字段。换个通用说法就是上一笔是多少。SELECT订单id,下单时间,订单金额,LAG(订单金额)OVER(ORDERBY下单时间)AS上一笔金额,ROUND((订单金额-LAG(订单金额)OVER(ORDERBY下单时间))/LAG(订单金额)OVER(ORDERBY下单时间)*100,1)AS环比涨幅FROM订单表ORDERBY下单时间;运行结果订单id下单时间订单金额上一笔金额环比涨幅1012026-01-103000.00NULLNULL1022026-02-152500.003000.00-16.71032026-03-204000.002500.0060.01042026-04-051500.004000.00-62.5三个要注意的点第一行是 NULL因为它前面没有数据LAG 取不到值。这是正常现象不是写错了。要显示成首月之类的文字用IFNULL(上一笔金额, 0)兜底。公式就是本期 − 上期÷ 上期 × 100上期是 0 的时候环比列会变成 NULL而且数据库不报错。新开的产品上个月金额是 0除数就是 0MySQL 直接返回 NULL——语句执行成功但报表里这一格是空的很容易被当成数据没同步。想主动挡一道把分母写成NULLIF(上期, 0)SELECT订单id,订单金额,ROUND((订单金额-LAG(订单金额)OVER(ORDERBY下单时间))/NULLIF(LAG(订单金额)OVER(ORDERBY下单时间),0)*100,1)AS环比涨幅FROM订单表ORDERBY下单时间;NULLIF(上期, 0)就是上期等于 0 就当成 NULL。结果同样是 NULL但意图写在明面上接手你 SQL 的人一眼就知道这里考虑过除零。如果是按部门各自算环比把 OVER 补全成OVER (PARTITION BY 部门 ORDER BY 下单时间)就行逻辑和上一节的部门累计完全一致。五、占比OVER 里什么都不写第三个问题这个月占了整体的多少。这里的写法很反直觉——OVER 的括号里是空的SELECT订单id,订单金额,SUM(订单金额)OVER()AS总金额,ROUND(订单金额/SUM(订单金额)OVER()*100,1)AS占比FROM订单表;运行结果订单id订单金额总金额占比1013000.0011000.0027.31022500.0011000.0022.71034000.0011000.0036.41041500.0011000.0013.6OVER ()空括号的含义是不分区、不排序整个查询结果算一个窗口。所以每一行的总金额都挂着同一个数——11000然后再拿自己的金额去除它占比就出来了。这样写的好处是不需要再写一个子查询去求总额一张查询里既出明细又出占比。如果要算每个部门占全公司的比例在 OVER 里加上PARTITION BY 部门就是部门总额再除以OVER ()的全公司总额即可SELECTe.部门,SUM(o.订单金额)AS部门金额,SUM(SUM(o.订单金额))OVER()AS全公司金额,ROUND(SUM(o.订单金额)/SUM(SUM(o.订单金额))OVER()*100,1)AS占比FROM订单表 oJOIN员工表 eONo.员工ide.员工idGROUPBYe.部门;运行结果部门部门金额全公司金额占比技术部7000.0011000.0063.6销售部4000.0011000.0036.4SUM(SUM(订单金额)) OVER ()看着怪拆开就懂里层的SUM(订单金额)是 GROUP BY 的聚合先把每个部门压成一行外层的SUM(...) OVER ()在这个结果之上再开窗口算出全部部门的总和。先聚合、再开窗就是这个顺序。六、4 个必踩的坑这一节的四个坑都在 MySQL 8.0.40 上真机跑过报错原文照抄在下面。看到同样的报错提示可以直接对号入座。坑1OVER 里漏写 ORDER BY累计变总计需求是累计代码写成了这样-- 错误示范漏了 ORDER BY可直接运行不会报错SELECT订单id,订单金额,SUM(订单金额)OVER()AS累计金额FROM订单表;这段 SQL 完全合法能正常跑出结果所以它是四个坑里最难发现的那个订单id订单金额累计金额1013000.0011000.001022500.0011000.001034000.0011000.001041500.0011000.00四行的累计金额全是 11000。因为它算的是整个结果集的总和每一行都拿到同一个数根本不随时间递增。正确写法就是把 ORDER BY 补回去SELECT订单id,订单金额,SUM(订单金额)OVER(ORDERBY下单时间)AS累计金额FROM订单表;自查方法很简单跑完看一眼最后一列如果每一行都是同一个数那就是漏了 ORDER BY。坑2PARTITION BY 和 ORDER BY 的顺序写反顺序写反数据库会直接拦下来一条数据都不给你-- 错误示范顺序写反可直接运行会报错SELECT订单id,订单金额,SUM(订单金额)OVER(ORDERBY下单时间PARTITIONBY员工id)AS累计金额FROM订单表;报错原文ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near PARTITION BY 员工id) AS 累计金额这条报错怎么看1064 就是 MySQL 的语法错误代号重点在near PARTITION BY 员工id)这一截——它标的是从这个位置开始看不懂也就是要改的地方。窗口函数的固定顺序是OVER (PARTITION BY 分区字段 ORDER BY 排序字段)先分区再排序不能颠倒。记不住就记这个顺序先切块再排队。坑3想在 WHERE 里过滤窗口函数的结果很自然的一个想法算完排名之后只留第一名。-- 错误示范窗口函数不能出现在 WHERE 里可直接运行会报错SELECT订单id,订单金额,ROW_NUMBER()OVER(ORDERBY订单金额DESC)AS排名FROM订单表WHEREROW_NUMBER()OVER(ORDERBY订单金额DESC)1;报错原文ERROR 3593 (HY000): You cannot use the window function row_number in this context.这条报错怎么看3593 是 MySQL 专门给窗口函数用错了地方准备的代号。WHERE、GROUP BY、HAVING 里都不能直接用窗口函数原因还是执行顺序——WHERE 执行的时候SELECT 里那一列排名还没算出来当然没法拿它做过滤。改成在 WHERE 里写别名WHERE 排名 1也一样不行会报ERROR 1054 (42S22): Unknown column 排名 in where clause道理是同一个。正确做法是把它包成子查询在外层过滤SELECT*FROM(SELECT订单id,订单金额,ROW_NUMBER()OVER(ORDERBY订单金额DESC)AS排名FROM订单表)tWHERE排名1;子查询先算列外层再筛行这是窗口函数做过滤的标准套路。坑4排序字段有重复值时默认会把并列的行一起算这个坑最隐蔽而且它不报错、也不算错只是结果跟你脑子想的不一样。用标准订单表就能复现4 笔订单分别来自员工 1、1、2、3其中员工 1 有两笔——如果按员工id排序做累计排序字段就出现重复值了。-- 对比两种写法可直接运行SELECT订单id,员工id,订单金额,SUM(订单金额)OVER(ORDERBY员工id)AS默认框架累计,SUM(订单金额)OVER(ORDERBY员工idROWSBETWEENUNBOUNDEDPRECEDINGANDCURRENTROW)AS显式ROWS累计FROM订单表ORDERBY员工id,订单id;运行结果订单id员工id订单金额默认框架累计显式ROWS累计10113000.005500.003000.0010212500.005500.005500.0010421500.007000.007000.0010334000.0011000.0011000.00看第一行就明白了。按 MySQL 的默认规则员工 1 的两笔订单在员工id这个排序字段上值相同被当成同一个整体两行的累计都直接给到 5500而显式写出ROWS的那一列才是老老实实一行一行往下加第一行就是 3000。原因在于默认框架是RANGERANGE默认看排序字段的值值一样的行算作一组整组一起进窗口ROWS数行只看物理上的第几行不管值重不重复想让累计严格逐行递增就把框架写全SELECT订单id,订单金额,SUM(订单金额)OVER(ORDERBY下单时间ROWSBETWEENUNBOUNDEDPRECEDINGANDCURRENTROW)AS累计金额FROM订单表;ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW的意思是窗口从第一行UNBOUNDED PRECEDING到当前行CURRENT ROW一行一行算不管谁跟谁并列。日常做报表排序字段如果是时间戳这种基本不会重复的字段默认写法就够用。但只要排序字段可能重复按员工、按部门、按日期聚会时发现同一天有多笔就建议把 ROWS 写全别赌数据里没有并列值。七、3 个模板直接抄三个场景的写法整理成一张表做报表时对照着套需求核心写法关键点累计求和SUM(金额) OVER (ORDER BY 时间)必须有 ORDER BY否则变总计分组累计SUM(金额) OVER (PARTITION BY 组 ORDER BY 时间)先切块再排队顺序不能颠倒环比LAG(金额) OVER (ORDER BY 时间)第一行是 NULL属正常占比金额 / SUM(金额) OVER () * 100空括号整体MySQL 不需要写100.0排名过滤外层套子查询WHERE 排名 1窗口函数不能直接写在 WHERE 里通用套路可以总结成一句话先想清楚按什么切PARTITION BY、按什么排ORDER BY、要算什么SUM/LAG三个空填完SQL 就写完了。八、动手练三题用上面的订单表写出下面三个查询。写完再往下看答案。按时间顺序给出每笔订单的金额、上一笔订单的金额和环比涨幅。按部门分组算出每个部门的订单总额以及它占全部订单金额的百分比。算出每笔订单的累计金额并且保证排序时间相同的情况下也是严格逐行累加。第 1 题每笔订单的上一笔金额与环比涨幅SELECT订单id,订单金额,LAG(订单金额)OVER(ORDERBY下单时间)AS上一笔金额,ROUND((订单金额-LAG(订单金额)OVER(ORDERBY下单时间))/LAG(订单金额)OVER(ORDERBY下单时间)*100,1)AS环比涨幅FROM订单表ORDERBY下单时间;订单id订单金额上一笔金额环比涨幅1013000.00NULLNULL1022500.003000.00-16.71034000.002500.0060.01041500.004000.00-62.5第 2 题部门订单金额与占全公司的百分比SELECTe.部门,SUM(o.订单金额)AS部门金额,ROUND(SUM(o.订单金额)/SUM(SUM(o.订单金额))OVER()*100,1)AS占比FROM订单表 oJOIN员工表 eONo.员工ide.员工idGROUPBYe.部门;部门部门金额占比技术部7000.0063.6销售部4000.0036.4顺带一个常见报错如果 SELECT 里带了别的非聚合字段比如e.姓名而没写进 GROUP BY会报ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause ... this is incompatible with sql_modeonly_full_group_by。记住一条SELECT 里的非聚合字段要么进 GROUP BY要么被聚合函数包住。第 3 题严格逐行累加的累计金额SELECT订单id,订单金额,SUM(订单金额)OVER(ORDERBY下单时间ROWSBETWEENUNBOUNDEDPRECEDINGANDCURRENTROW)AS累计金额FROM订单表ORDERBY下单时间;订单id订单金额累计金额1013000.003000.001022500.005500.001034000.009500.001041500.0011000.00这组数据里下单时间没有重复所以结果跟不加 ROWS 完全一致。区别在于意图加上ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW就等于告诉数据库不管有没有并列都给我一行一行加。换成ORDER BY 员工id再跑一次就能看到不加时会变成 5500、5500、7000、11000见坑4。九、小结今天这 4 个写法解决的是报表里最高频的三类口径累计、环比、占比加上排名取第一这个经典需求基本覆盖日常取数的大半场景。回到最开始那句话——窗口函数和 GROUP BY 的区别不在语法在它不压行。只要接受了每一行都留着旁边多算一列这个设定OVER 里的两个空PARTITION BY、ORDER BY就都能想明白了。下一篇文章讲滑动平均不只是从头加到自己而是只看最近 3 个月。这时候要控制窗口的大小也就是ROWS BETWEEN的完整用法跟今天坑4 里那句话是同一个东西的延伸。如果这篇帮你把报表里的累计和环比理顺了可以收藏起来写 SQL 的时候对着模板套。
返回列表