news 2026/10/20 8:35:01

SQL分桶统计补零:LeetCode 1435会话柱状图解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQL分桶统计补零:LeetCode 1435会话柱状图解析

做数据分析的人,应该对“按区间分桶统计”这类需求不陌生。拿到一张用户会话表,要统计每个时长区间里有多少记录,还要把没数据的区间也补成0,在SQL里确实能绊倒不少人。LeetCode的Database第1435题“制作会话柱状图”,就是专门考这个点的经典题目。

这道题的场景是这样的:有一张会话表Sessions,里面记录每次会话的唯一ID和持续时长duration(单位是秒)。现在需要把会话时长分成四个档次统计数量:[0-5>、[5-10>、[10-15>、15 or more,分别对应不足5分钟、5到10分钟、10到15分钟、15分钟及以上。重点在于,如果某一档一个会话都没有,这一档依然要出现在结果里,数量记0。

听起来不复杂,但你上手一写就会发现,直接把duration做CASE WHEN再GROUP BY,大概率会丢行。具体为什么会丢,用什么写法才能保证四行齐全,就是这篇文章要聊的重点。无论你是准备数据分析岗位的笔试题,还是工作中经常要做“按区间看分布”的统计,这个套路都值得收藏。

1. 题目解读与核心考点:这道题到底在考什么

1.1 先把表结构和分桶规则吃透

Sessions表结构非常简单,就两个字段。

字段名类型含义
session_idint会话ID,主键,唯一标识一次会话
durationint会话持续时长,单位是秒

示例数据长这样:

session_idduration
1120
2180
3300
4450
5900
61200
715
8720

按分钟看,120秒是2分钟,300秒是5分钟,720秒是12分钟。分桶规则是左闭右开的区间:

  • [0-5>:大于等于0分钟,小于5分钟
  • [5-10>:大于等于5分钟,小于10分钟
  • [10-15>:大于等于10分钟,小于15分钟
  • 15 or more:大于等于15分钟

注意一个很容易看漏的细节:正好5分钟,也就是duration等于300秒时,属于第二档[5-10>而不是第一档;10分钟、15分钟同理,分别落进第三档、第四档。左闭右开这个规则一旦踩错,整个统计表跟着错。

期望输出是这样的:

bintotal
[0-5>3
[5-10>2
[10-15>1
15 or more2

这四行是对着上面8条示例数据手工统计出来的:15秒、120秒、180秒落在[0-5>,300秒、450秒落在[5-10>,720秒落在[10-15>,900秒、1200秒落在15 or more。如果某一档没有会话,比如删掉720秒那条,[10-15>这一档依然要输出,数量为0。这就是题里埋的最大陷阱。

1.2 考点拆解:四个关键点

这道题在LeetCode数据库题库里难度不算高,但它同时考了四个非常基础又实用的点,组合起来就变精致了。

第一个是CASE WHEN条件分支。要把duration从秒换算成分钟,再映射到桶名称,同时必须处理好区间边界。

第二个是GROUP BY聚合与计数。根据桶名分组,数出每个桶里有多少条会话记录。只做到这一步远远不够,这是大多数人的第一反应,也是翻车的起点。

第三个是固定维度补全。桶列表是题目写死的四个区间,不是从数据里自然出现的值。只要数据里某个桶没有记录,纯GROUP BY就不会输出那一行。你必须额外造一张“桶全量表”,再通过连接把它带出来。这是题目真正想考的东西。

第四个是结果排序与NULL处理。输出四行的顺序是指定的,不是字典序;LEFT JOIN之后,没有数据的桶会得到NULL,还要安全地变成0。

这四个点单独拎出来都不难,但合到同一道题里,对SQL的综合能力就有要求了。尤其是第三个“固定维度补全”,在真实业务里极其常见——报表要求无论有没有数据都保留固定维度,没处理过这种需求的人,很容易被测试用例打回。

2. 解题思路对比:为什么直接GROUP BY会翻车

2.1 第一直觉:CASE WHEN + GROUP BY

大部分人上手的第一版代码长这样:

SELECT CASE WHEN duration / 60 < 5 THEN '[0-5>' WHEN duration / 60 < 10 THEN '[5-10>' WHEN duration / 60 < 15 THEN '[10-15>' ELSE '15 or more' END AS bin, COUNT(*) AS total FROM Sessions GROUP BY bin;

这个写法把duration换算成分钟,按区间打标,再按标签聚合。逻辑没毛病,跑起来也确实能算出数。问题在于,它返回的行数完全取决于数据里有几种桶标签。假如[10-15>这个区间恰好一个会话都没有,结果里就不会出现这一行。

还是用上面的8条示例数据做实验,把720秒那条删掉,数据库里只剩7条记录。上述SQL的输出会变成三行:

bintotal
[0-5>3
[5-10>2
15 or more2

期望输出是四行,缺的正是[10-15> 0这一行。题目要求所有桶为0也要输出,直接GROUP BY做不到。

2.2 为什么GROUP BY天然不适合这种场景

要理解这个问题,先说清楚GROUP BY的工作机制。SQL执行SELECT ... FROM Sessions GROUP BY bin时,会先按bin把真实存在的行分成一组一组,再对每组做聚合。分组键的取值来自数据本身,数据里没有的值,就不存在对应的组。

这和“固定维度报表”的需求正好相反。报表要求先有一张完整的维度表,比如四个桶的名称,再拿它和事实表碰撞,碰撞不到的地方填0。本质是“维度外连接事实”,而GROUP BY是“基于事实做切片”。后者天然会丢掉稀疏维度。

这个道理放到业务里很好懂:你有一张销售表,要按12个月统计销售额。如果6月一单都没有,GROUP BY月份的结果就只有11行。但报表必须显示12行,6月写0。做数据看板的人几乎每个月都会遇到这件事。

2.3 三种补全方案对比

既然纯GROUP BY不行,那就想办法把缺失的桶补上。常见思路有三种。

第一种是LEFT JOIN固定桶表。先用UNION ALL构造包含四个桶名的临时表,再把它作为左表,与分组统计结果做左连接。优点是逻辑清晰、性能好、通用性强,缺点是需要多写几行造桶SQL。

第二种是UNION ALL手动补零。先按GROUP BY统计,找出结果里缺了哪些桶,再用SELECT '[10-15>' AS bin, 0 AS total UNION ALL ...补进去。优点是直观,缺点是桶一多就变成体力活,而且你得提前知道数据里缺了哪些桶,程序化处理很别扭。

第三种是递归CTE生成桶。如果桶的规则是连续的数值区间,可以用递归CTE从0生成到某个上限,再拼接标签。优点是灵活可扩展,缺点是语法复杂,老版本MySQL还不支持递归。

从可维护性、通用性以及面试官的偏好来看,第一种都是首选。下面给出完整实现,这也是我认为最值得记住的一套模板。

3. 核心SQL实现与细节剖析:完整可跑的参考代码

3.1 参考SQL(MySQL 8.0+)

先把测试环境准备好:

CREATE TABLE Sessions ( session_id INT PRIMARY KEY, duration INT NOT NULL ); INSERT INTO Sessions (session_id, duration) VALUES (1, 120), (2, 180), (3, 300), (4, 450), (5, 900), (6, 1200), (7, 15), (8, 720);

然后是这个解题SQL:

WITH bin_table AS ( SELECT '[0-5>' AS bin UNION ALL SELECT '[5-10>' UNION ALL SELECT '[10-15>' UNION ALL SELECT '15 or more' ), sessions_with_bin AS ( SELECT session_id, CASE WHEN duration / 60 < 5 THEN '[0-5>' WHEN duration / 60 < 10 THEN '[5-10>' WHEN duration / 60 < 15 THEN '[10-15>' ELSE '15 or more' END AS bin FROM Sessions ) SELECT b.bin, COUNT(s.session_id) AS total FROM bin_table b LEFT JOIN sessions_with_bin s ON b.bin = s.bin GROUP BY b.bin ORDER BY FIELD(b.bin, '[0-5>', '[5-10>', '[10-15>', '15 or more');

执行结果:

bintotal
[0-5>3
[5-10>2
[10-15>1
15 or more2

如果把720秒那条删掉,也就是[10-15>区间没有会话,结果是这样:

bintotal
[0-5>3
[5-10>2
[10-15>0
15 or more2

四行齐全,空桶正确补0,这就是我们要的效果。

3.2 逐段拆解:每段代码为什么这么写

bin_table这段是“维度表”。它用四个UNION ALL把题目要求的桶名一行行列出来。UNION ALL和UNION的区别在于前者不去重,这里四个字符串本身就互不相同,用哪个都一样,但UNION ALL性能略好,也是大家更习惯的写法。

sessions_with_bin这段是“事实表打标”。CASE WHEN从最小的区间开始判断,满足条件就返回桶名,否则继续向下走。由于CASE WHEN是短路求值,判断顺序绝对不能乱,把< 5和< 10写反了,会让5分钟的会话错误落桶。

为什么用duration / 60 < 5而不是duration < 300?两者数学上完全等价,但除以60更直白,读者一眼就能看出是在按分钟比较。如果换成duration < 300,建议加注释说明“300秒=5分钟”,否则过两个月再回来看代码容易懵。

主查询这段是核心。bin_table作为左表LEFT JOIN打标结果,左边四行无论如何都会保留;右边能匹配上就带出session_id,匹配不上就是NULL。COUNT(s.session_id)统计的是非NULL的session_id个数,所以空桶得到的结果不是1而是0。这是整条SQL里最精妙也最容易写错的一步——如果手滑写成COUNT(*),空桶会输出1,因为LEFT JOIN之后那一行在物理上依然存在,只是右表字段全是NULL。初次接触这个写法的人,十个有九个会在这个地方踩坑。

最后的ORDER BY FIELD指定输出顺序。FIELD的第一个参数是待排序字段,后面按优先级列出一串候选值,MySQL会把排在前面的值放在结果集前面。它保证了输出永远是[0-5>、[5-10>、[10-15>、15 or more这个顺序。别小看这一步,如果不加排序,数据库可能返回多种顺序,很可能被判错。

3.3 其他数据库的写法适配

如果你用的是PostgreSQL,整体思路完全一样,只有两个小差异:一是把ORDER BY FIELD(...)改成ORDER BY CASE b.bin WHEN '[0-5>' THEN 1 WHEN '[5-10>' THEN 2 WHEN '[10-15>' THEN 3 ELSE 4 END;二是如果需要处理NULL,应使用COALESCE而不是IFNULL。但因为这里用的是COUNT(session_id),其实用不到NULL转换函数。其余CTE、UNION ALL、LEFT JOIN的语法完全通用。

如果是SQL Server,写法也几乎一致,把IFNULL换成ISNULL、FIELD换成CASE排序即可。如果数据库不支持CTE,例如老版本MySQL 5.6,可以把两个CTE整体改写成子查询,逻辑不变:

SELECT bins.bin, COUNT(s.session_id) AS total FROM ( SELECT '[0-5>' AS bin UNION ALL SELECT '[5-10>' UNION ALL SELECT '[10-15>' UNION ALL SELECT '15 or more' ) bins LEFT JOIN ( SELECT session_id, CASE WHEN duration / 60 < 5 THEN '[0-5>' WHEN duration / 60 < 10 THEN '[5-10>' WHEN duration / 60 < 15 THEN '[10-15>' ELSE '15 or more' END AS bin FROM Sessions ) s ON s.bin = bins.bin GROUP BY bins.bin ORDER BY FIELD(bins.bin, '[0-5>', '[5-10>', '[10-15>', '15 or more');

这一版的兼容性最好,也最容易迁移到不同SQL方言。如果你需要在一个老项目里跑类似逻辑,直接用这个就行。

4. 实操中的坑与排查技巧:这些细节最容易出错

4.1 边界值:5分钟到底属于哪个桶

题目给出的桶是左闭右开区间,[0-5>包含0分钟但排除5分钟。duration恰好等于300秒时,应该落在[5-10>而不是[0-5>。

对应CASE WHEN的写法必须是连续小于:

WHEN duration / 60 < 5 THEN '[0-5>' WHEN duration / 60 < 10 THEN '[5-10>' WHEN duration / 60 < 15 THEN '[10-15>' ELSE '15 or more'

如果手抖写成<=,比如第一句变成duration / 60 <= 5,那么5分钟会被分进第一个桶,后面的判断根本轮不到它。这种错误在数据上看起来只是“这里多个1、那里少个1”,但口径一旦错了,下游不管怎么用都是错的。

排查建议:构造一批正好落在300秒、600秒、900秒的测试数据,肉眼核对分桶结果。这三个点是三处边界,每个都必须单独验证一遍。

4.2 空桶的NULL与0陷阱

LEFT JOIN造出的空桶行,用COUNT(s.session_id)得到0,这是正确结果。但如果有人把函数改成COUNT(*),结果立刻变成1。这个细节我在实际排查中见过不止一次,最后定位到的问题都是COUNT写错。

顺带说一句,如果需求改成“没有会话的桶显示NULL而不是0”,SQL反而更简单:直接把COUNT(s.session_id)去掉,改成SUM(CASE WHEN s.session_id IS NULL THEN 0 ELSE 1 END),甚至直接不聚合、用字段把NULL带出来。但本题明确要求total为0,所以必须用COUNT来聚合并清掉NULL。

另外,如果duration字段本身允许NULL,需要在CASE WHEN里先处理。因为SQL里NULL参与比较会得到NULL而不是TRUE,每条duration为NULL的记录都会落入ELSE,被算成15 or more。稳妥的做法先单独过滤:CASE WHEN duration IS NULL THEN '未知' ...。这属于扩展思考,面试官追问时用得上。

4.3 分组与排序的兼容性

MySQL对GROUP BY的宽容度很高,允许select列表里出现没有参与分组或聚合的字段。但其他主流数据库的默认配置会直接报错,比如PostgreSQL。最严谨的写法是分组字段、排序字段、select字段尽量保持一致。我在上面的CTE版本里用GROUP BY b.bin,排序用ORDER BY FIELD(b.bin, ...),字段来源一致,就不会有歧义。

另一个容易忽略的坑是字符串排序。如果忘了ORDER BY,数据库很可能按字典序输出[0-5>、[10-15>、[5-10>、15 or more,和题目要求的顺序完全不一致。排序这一步绝不是可选项。

5. 从题目到业务:会话分桶统计模式化

5.1 一套模板打天下:固定标签表 + LEFT JOIN

把这道题的解法抽象一下,就是一套极其通用的SQL模式。

第一步,构造维度表,列出所有期望输出的分组标签。第二步,构造事实表,把原始行映射到某个标签上。第三步,LEFT JOIN加分组计数。第四步,按业务顺序排序。

这套模式可以套到大量需求上:你有一张用户表,要统计各年龄段的用户数量;有一张订单表,要统计各价格区间的订单笔数;有一张接口日志表,要统计各耗时区间的请求次数。全都长一个样子,差别只在第二步的CASE WHEN条件和第一步的标签枚举。

5.2 我实际用过的几个场景

有一回做产品埋点分析,要统计用户单次会话时长分布,判断用户在App里停留多久会出现明显流失。当时的表结构和这道题几乎一模一样,只是桶的边界改成了“0-1分钟、1-3分钟、3-5分钟、5-10分钟、10分钟以上”。我直接套模板,不到二十分钟就把结果交出去了。在此之前,我第一次做同类统计时用的是UNION ALL手动补零,只有五六个桶还能忍受,后面桶数增加到十几个,手动补零真的写到怀疑人生。

还有一次统计接口响应耗时的档位分布,要输出固定几个档位的请求量作为性能看板的数据源。需求方明确要求:某个档位即使0请求也必须保留,因为前端柱状图的x轴不能缺柱子。这就属于产品视角的固定维度,数据侧必须配合。固定标签表加LEFT JOIN是唯一干净的做法。

5.3 如果桶多了怎么扩展

实际工作里的分桶规则往往不止四个,可能是十个、二十个。UNION ALL一长串看起来特别笨重。如果桶的起点和终点是连续的,可以用递归CTE或数字表生成标签。比如生成0到15分钟、每分钟一个桶:

WITH RECURSIVE minute_range AS ( SELECT 0 AS minute UNION ALL SELECT minute + 1 FROM minute_range WHERE minute < 14 ) SELECT CONCAT('[', minute, '-', minute + 1, '>') AS bin FROM minute_range;

再和事实表LEFT JOIN,后面的逻辑完全不变。这就是前面提到的第三种方案,平时用不上,但当你需要快速扩展桶数量时,它会省下大量重复的UNION ALL代码。

另外,如果业务上还要按日期或渠道等第二维度同时分组,只需要把第二维度字段加进GROUP BY和SELECT,模板一样成立。唯一要注意的是维度表也要跟着扩展成“日期×桶”的笛卡尔积,别漏了组合。

最后分享一点个人体会。这类问题我在实际项目里遇到的频率,远高于LeetCode上很多所谓的难题。关键不在于背下这道题的答案,而是理解“固定维度必须显式造出来,再LEFT JOIN上去”这个思维模型。用熟之后,不管是四档、十二档还是几十档,不管是在MySQL还是PostgreSQL里,都能在几分钟之内把架子搭好。

建议看到这里的读者,把示例数据复制到本地环境里跑一遍,然后故意删掉一个区间的数据,看看输出是不是依然四行齐全。做完这个实验,这个知识点才算真正长在身上。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 8:34:46

ESP8266四足机器人DIY:硬件设计、舵机控制与步态算法实战

基于ESP8266的四足机器人的设计做硬件DIY的朋友&#xff0c;不管你是刚入门Arduino的新手&#xff0c;还是已经玩过几块开发板的老玩家&#xff0c;大概率都动过自己搞一台四足机器人的念头。为什么&#xff1f;因为四足机器人是性价比最高的运动机器人平台之一&#xff0c;它能…

作者头像 李华
网站建设 2026/10/20 8:33:59

主流AI大模型全景梳理与选型实战:从闭源API到开源本地部署

这两年AI大模型的迭代速度&#xff0c;说实话已经快到让人有点跟不上。前脚刚搞清楚“参数量越大越强”的说法不靠谱&#xff0c;后脚推理模型、多模态模型又成了新焦点&#xff1b;这边还在纠结用哪个API做业务&#xff0c;那边开源权重模型已经把本地部署的门槛拉到了单张消费…

作者头像 李华
网站建设 2026/10/20 8:33:56

长音频ASR模型部署与端侧AI胸针:端云协同语音识别实战

这两天我一直在等着看同一个技术方向的两条消息&#xff1a;一边是有大厂开源了专门面向长音频的语音识别模型 VibeVoice-ASR&#xff0c;官方说法是能直接支持一小时连续音频处理&#xff1b;另一边是某硬件厂商端出了一个只有追踪器大小的 AI 胸针&#xff0c;双摄像头加三颗…

作者头像 李华
网站建设 2026/10/20 8:33:26

2026正规的多账号矩阵管理工具,企业抖音账号集中管控方案

当企业账号从几个扩张到几十个&#xff0c;反复登录切换、内容排期冲突、数据分散、权限失控成为常态。与此同时&#xff0c;平台风控持续收紧&#xff0c;靠群控脚本批量操作账号的路已经走不通。2026年&#xff0c;企业需要的不是更多工具&#xff0c;而是一条建立在官方规则…

作者头像 李华
网站建设 2026/10/20 8:33:19

车载安全“一芯一证”落地:四级密钥体系与产线实践

做车载安全这行这么多年&#xff0c;“一芯一证”这个词这几年越来越高频。尤其是在整车E/E架构从分布式往域集中、中央计算演进之后&#xff0c;每一颗ECU上的安全芯片被赋予的信任职责&#xff0c;跟过去完全不是一个量级。过去大家觉得给ECU写个防盗匹配、做一次升级鉴权就差…

作者头像 李华
网站建设 2026/10/20 8:32:53

敏感数据AI处理路线图:本地模型、云端API与混合架构的合规落地

1. 这不是选择题&#xff0c;而是工作流重构的起点“2026隐私安全AI选型&#xff1a;本地大模型 vs 云端AI&#xff0c;敏感工作该怎么选&#xff1f;”——看到这个标题&#xff0c;我第一反应不是去查参数对比表&#xff0c;而是立刻翻出上周刚帮某金融合规团队做的一个真实案…

作者头像 李华