ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

力扣1565 SQL题全解:月度统计中的分组去重与日期过滤

力扣1565 SQL题全解:月度统计中的分组去重与日期过滤 1. 拿到力扣1565先别急着写SQL题目到底在考什么力扣1565这道题标题叫“按月统计订单数与顾客数”属于数据库题库里非常经典的入门题。我最初刷到这道题的时候觉得太简单了——不就是GROUP BY加两个COUNT嘛。但真正上手之后发现里面藏着不少值得琢磨的细节尤其是年份过滤、去重统计、分组字段这三个点几乎每个都是新手必踩的坑。这篇文章我就从题目本身出发把这道题从读题到落地完整拆一遍再把同类月度统计题在真实业务里的写法一起聊清楚。1.1 原题表格和输出先看懂再动手题目给了一张订单表Orders结构是这样的字段名类型说明order_idint订单ID主键order_datedate下单日期customer_idint顾客IDinvoiceint发票金额示例数据长这样order_idorder_datecustomer_idinvoice12020-09-151013022020-09-171025032020-10-061016542020-10-2010325052021-01-01101100注意最后一条order_date是 2021 年的这是个烟雾弹。题目要求只统计2020 年的数据。期望输出monthorder_countcustomer_count9221022拆开看这个结果9 月份有两笔订单order_id 为 1 和 2顾客分别是 101 和 102所以订单数是 2顾客数也是 210 月份有两笔订单order_id 为 3 和 4但这两笔订单的顾客是 101 和 103所以订单数还是 2顾客数也是 2。如果 10 月份两笔订单都属于同一个顾客那订单数依然是 2但customer_count就变成 1 了。这就是题目里 unique customers 的含义也是整道题最容易翻车的地方。1.2 三个隐藏考点任何一个都容易踩坑第一层考点是日期筛选。题目明确写了 for 2020但很多人的第一版 SQL 往往忘掉WHERE条件直接把所有日期的数据都统计进去。这个错误特别隐蔽因为示例数据里只有一条 2021 年的记录你要是没注意看结果就是 9 月、10 月、1 月三行跟预期输出对不上还半天找不到原因。第二层考点是去重计数。customer_count统计的是不重复的顾客数不是订单数。这在 SQL 里对应的就是COUNT(DISTINCT customer_id)。有的人会写成COUNT(customer_id)这样同一个顾客下两笔单就会被算成两个顾客。数据一多这个误差会被放大得很离谱。第三层考点是分组和排序。题目要求按月份升序排列所以GROUP BY之后要记得ORDER BY。另外分组字段是用MONTH(order_date)还是DATE_FORMAT(order_date, %Y-%m)会影响最终展示的“月份”长什么样。输出里的month是数字 9、10还是字符串 2020-09取决于你用的函数。力扣的判定对这两种写法都接受但如果你想输出数字月份老老实实用MONTH()最直接。1.3 为什么这道题适合作为SQL入门题我后来把这道题推荐给好几个刚学 SQL 的朋友理由是它覆盖了数据分析里最基础也最常用的四个操作筛选WHERE、分组GROUP BY、聚合COUNT、排序ORDER BY。这四个操作凑到一起就是一张最简版报表。你在真实工作中写月度订单报表、用户活跃报表核心逻辑跟这题完全一样。把这道题吃透后面遇到再复杂的报表需求至少知道第一步该往哪个方向想。2. 标准解法拆解三条SQL讲透月度统计2.1 最直接的解法YEAR MONTH 组合先上最标准的写法SELECT MONTH(order_date) AS month, COUNT(order_id) AS order_count, COUNT(DISTINCT customer_id) AS customer_count FROM Orders WHERE YEAR(order_date) 2020 GROUP BY MONTH(order_date) ORDER BY month;这个解法在力扣上能直接通过逻辑链路也清晰先用WHERE过滤出 2020 年的订单再按月份分组然后分别统计订单数和顾客数最后按月份排序。这里有个细节值得说COUNT(order_id)和COUNT(*)在这道题里结果一样因为order_id是主键不存在空值。但如果你是个严谨的人建议保留COUNT(order_id)这种写法它明确告诉你统计的是“非空订单 ID 的个数”语义更清楚。要知道如果某天表结构改了order_id允许为空了COUNT(*)会把空行也数进去那结果就是错的。2.2 SQL执行顺序别被代码顺序骗了很多新手看这段 SQL 会有一个困惑明明是SELECT在最前面为什么执行的时候不是先执行它这里必须把 SQL 的逻辑执行顺序讲明白否则你后面写复杂查询会一直觉得“为什么结果跟我代码顺序想的不一样”。完整顺序是FROM→WHERE→GROUP BY→HAVING→SELECT→ORDER BY。也就是说系统先拿到Orders表然后根据WHERE条件过滤行接着按GROUP BY的字段分组再对每组执行聚合函数最后才轮到SELECT去投影需要的列ORDER BY在所有操作之后做最终排序。回到上面这个例子WHERE YEAR(order_date) 2020先把 2021 年的订单剔除剩下 4 行数据然后GROUP BY MONTH(order_date)把 9 月的两行分到一组10 月的两行分到另一组每组分别执行COUNT得到order_count和customer_count最后ORDER BY month把 9 排在 10 前面。理解了这个顺序你就知道为什么不能在WHERE里写COUNT(...) 1这种条件——因为WHERE执行的时候分组还没发生聚合函数根本没法用。2.3 另一种常用套路DATE_FORMAT除了YEAR() MONTH()DATE_FORMAT是另一种常见写法SELECT DATE_FORMAT(order_date, %Y-%m) AS month, COUNT(order_id) AS order_count, COUNT(DISTINCT customer_id) AS customer_count FROM Orders WHERE order_date BETWEEN 2020-01-01 AND 2020-12-31 GROUP BY DATE_FORMAT(order_date, %Y-%m) ORDER BY month;这段代码在力扣上同样能过。它跟第一种写法的区别在于输出的month是 2020-09、2020-10 这种带年份的字符串而不是单纯的数字 9、10。实际业务里我更喜欢用这种带年份的格式因为月度报表跨年时不会出现“2020年9月”和“2021年9月”分不清的情况。但要注意一个陷阱如果order_date是datetime类型BETWEEN 2020-01-01 AND 2020-12-31是不包含2020-12-31 23:59:59 之后的数据的。更稳妥的写法是WHERE order_date 2020-01-01 AND order_date 2021-01-01这个左闭右开的区间写法无论字段是date还是datetime都不会漏数据。我在实际工作中写时间过滤条件默认就是用这种形式算是一个肌肉记忆了。2.4 COUNT(DISTINCT) 到底做了什么COUNT(DISTINCT customer_id)这个语法看起来简单背后其实做了去重操作。你可以把它理解为先在每个分组内部收集所有customer_id去掉重复值再数剩下的个数。以 10 月为例原始分组里customer_id是 [101, 103]去重后还是两个如果 10 月有两笔订单都来自 101那分组里就是 [101, 101]去重后只有一个customer_count就是 1。这个函数在业务里的意义非常直接算“有几个用户下了单”而不是“下了几单”。这两个数据经常一起出现在报表里用来算“人均下单量”。比如某个月订单数是 100顾客数是 50那人均下单就是 2。如果这里写错了去重人均数据就是错的后面的决策也会跟着歪。所以我每次写统计类 SQL都会反复检查这个指标到底需不需要去重需要的话DISTINCT加在哪个字段上3. 刷题中最容易犯的四个错误3.1 忘记过滤年份把2021年的订单也算进去了我在力扣讨论区见过不止一个人问“为什么我的结果多了一行 1 月份的数据”点开代码一看WHERE条件整个没写。这种错误在题目示例里尤其容易犯因为示例数据只有一条 2021 年的记录藏在最后一行不仔细看题目描述根本注意不到。解决思路是养成读题先划条件的习惯。看到 for 2020 这种描述第一时间就要想到WHERE里要加年份限制。我个人的做法是读完题先在草稿纸上写下三个问题查哪张表过滤什么条件按什么分组三个问题都答上来再动手写代码。3.2 计数字段没加DISTINCT这个错误比上一个更隐蔽。语法上完全没错也不报错但结果就是不对。比如把COUNT(DISTINCT customer_id)写成COUNT(customer_id)9 月的结果还是一样的因为 9 月两个顾客各下一单但换一组数据如果有 10 个顾客每人下了 5 单正确结果customer_count应该是 10错误写法会给你 50。差得不是一星半点。怎么避免我的经验是题目里出现 “unique”“distinct”“不重复” 这类词你就得条件反射般地去找DISTINCT。力扣英文版原题里写的是 number of unique orders and the number of unique customers中文版翻译成“订单数与顾客数”但“顾客数”隐含了去重含义。翻译一简化反而容易让人忽略这个关键信息。3.3 分组字段和排序字段不一致有些人的代码是这样写的GROUP BY MONTH(order_date) ORDER BY order_date;这个写法在 MySQL 里可能不报错取决于ONLY_FULL_GROUP_BY模式但结果排序是乱的。ORDER BY order_date不是按月份排而是按某条具体订单的日期排这在分组查询里根本没有意义。正确做法是ORDER BY month排序字段必须来自分组结果。顺便说一句MySQL 有个历史遗留问题默认情况下允许SELECT里出现不在GROUP BY中的字段比如上面那个ORDER BY order_date也能跑。但力扣的判题环境一般开了ONLY_FULL_GROUP_BY一旦你写了非分组字段直接报错。这个报错信息挺唬人的我第一次遇到的时候还以为是语法问题查了半天才发现是分组字段使用不规范。3.4 日期边界条件写错用BETWEEN写日期过滤时如果原表字段是datetimeBETWEEN 2020-01-01 AND 2020-12-31会漏掉 2020-12-31 当天 00:00:00 之后的所有记录。这是一个经典的边界问题。我知道的解决办法有三个用WHERE order_date 2020-01-01 AND order_date 2021-01-01左闭右开最稳妥。用WHERE YEAR(order_date) 2020简单直观但无法利用索引后面细说。用WHERE order_date BETWEEN 2020-01-01 AND 2020-12-31 23:59:59能行但字符串里写时间上限不够优雅。在力扣这道题里order_date是date类型没有时间部分BETWEEN写法不会出错。但真实业务表里datetime类型太常见了为了保险建议从一开始就养成写左闭右开区间的习惯。错误类型错误写法正确写法忘记过滤年份没有WHERE条件WHERE YEAR(order_date) 2020顾客数没去重COUNT(customer_id)COUNT(DISTINCT customer_id)排序字段错误ORDER BY order_dateORDER BY month日期边界漏数据BETWEEN 2020-01-01 AND 2020-12-31datetime时 2020-01-01 AND 2021-01-014. 从题目到业务月度统计的真实场景4.1 没有订单的月份报表上要不要补零力扣这道题直接GROUP BY MONTH(order_date)如果某个月没有任何订单这个月根本不会出现在结果里。这在刷题场景下没问题因为题目只要求输出有数据的月份。但真实业务里老板看月度报表的时候最关心的问题之一就是“哪个月没业绩”或者“哪个月用户流失了”。如果结果里直接不显示那个月一眼看过去还以为每个月都有单量实际上可能某个月份是 0。我把这类需求叫“补零报表”。补齐的思路是先有一个包含所有月份的维度表再左联订单统计结果WITH RECURSIVE months AS ( SELECT 1 AS month UNION ALL SELECT month 1 FROM months WHERE month 12 ) SELECT m.month, COALESCE(t.order_count, 0) AS order_count, COALESCE(t.customer_count, 0) AS customer_count FROM months m LEFT JOIN ( SELECT MONTH(order_date) AS month, COUNT(order_id) AS order_count, COUNT(DISTINCT customer_id) AS customer_count FROM Orders WHERE YEAR(order_date) 2020 GROUP BY MONTH(order_date) ) t ON m.month t.month ORDER BY m.month;这段逻辑也不复杂先用递归 CTE 生成 1 到 12 月的数字序列再跟订单统计结果左连接没有数据的月份用COALESCE补成 0。这样出来的报表12 个月一行不缺每个月的订单数、顾客数一目了然。这个技巧在真实业务里比直接GROUP BY更实用建议记下来。4.2 数据量大时怎么写性能更好力扣刷题只关心结果对不对不关心数据量。但真实环境里的订单表动辄上千万行这时候WHERE YEAR(order_date) 2020这种写法就很尴尬了——它会对order_date字段做函数运算导致索引失效全表扫描。我帮别人优化月度报表的时候看到这种写法都会建议改成范围查询WHERE order_date 2020-01-01 AND order_date 2021-01-01这样order_date上的索引能直接命中查询效率提升是数量级的。还有一个点GROUP BY MONTH(order_date)在数据量大时会让数据库临时计算月份如果表特别大可以考虑在表中冗余一个order_month字段入库时直接算好存进去查询时直接GROUP BY order_month空间换时间。4.3 一份月度订单报表的SQL长什么样结合上面两个技巧一段更贴近真实业务的月度订单统计大概是这样的SELECT DATE_FORMAT(order_date, %Y-%m) AS month, COUNT(order_id) AS order_count, COUNT(DISTINCT customer_id) AS customer_count, SUM(invoice) AS total_invoice FROM Orders WHERE order_date 2020-01-01 AND order_date 2021-01-01 GROUP BY DATE_FORMAT(order_date, %Y-%m) ORDER BY month;比力扣原题多了一个SUM(invoice)用来统计月度销售额。这笔力扣题目要求的“订单数与顾客数”多了一个维度但整体结构一模一样筛选、分组、聚合、排序。你可以把力扣 1565 当成这个真实需求的最小原型考试和工作的差距就是在这个原型上不断加维度、加指标、加边界处理。5. 从1565到更多力扣SQL题刷题路径参考5.1 力扣SQL题的通用模板刷了力扣数据库题库里的几十道题之后我发现大部分题都有一个固定套路可以总结成三步先读表结构再看输出要求最后套模板。模板是长这样的SELECT 分组字段(通常是日期或分类), 聚合指标1(COUNT/SUM/AVG), 聚合指标2(COUNT(DISTINCT 字段)) FROM 表名 WHERE 筛选条件(注意时间范围) GROUP BY 分组字段 HAVING 对聚合结果的筛选(可选) ORDER BY 排序字段(可选);力扣 1565 是这个模板最基础的版本没有HAVING没有JOIN没有子查询干净利落。等你把这个模板用熟了再去做带JOIN、带窗口函数的题会发现核心逻辑其实没变只是零件多了几个。5.2 关联热搜里的1875题和其他经典题最近力扣热搜里有个 1875 题从标题“将雇员相同的分组”来看核心也是GROUP BY之后按某种条件筛选分组只是它用了HAVING来过滤分组后的聚合结果。这类题就是在通用模板上加了HAVING条件。而热题 100 里的“买股票的最佳时机”其实不是 SQL 题而是动态规划/贪心算法的经典题。它跟 1565 的共同点在于先读懂题意再抽象出核心状态。SQL 题的核心状态是分组和聚合算法题的核心状态是“截至某一天的最大收益”。看起来八竿子打不着但刷题思维是一致的——把复杂问题拆成已知套路。至于“力扣热题100 python”说明现在很多刷题党在用 Python 刷算法题。但 SQL 题还是建议直接在数据库环境里写别用 ORM 或 Pandas 模拟因为 SQL 的声明式思维和 Python 的命令式思维是两回事。力扣数据库题库本身就是免费的 SQL 练习场直接练就好。5.3 面试官喜欢追问的三个角度把 1565 作为面试题抛出去面试官通常不会满足于“你写对了”他会顺着往下追问。我整理了几个高频追问方向第一个追问“如果订单表有百万行数据你的写法还能跑吗”这考的是索引意识答案就是上面说的避免对索引字段做函数运算用范围查询代替YEAR()函数。第二个追问“如果我想统计每个顾客每个月的订单数SQL 怎么改”答案是把GROUP BY改成GROUP BY MONTH(order_date), customer_id如果需要用窗口函数还要考虑分区和排序。第三个追问“如何找出 2020 年每个月都有下单的顾客”这题就要用GROUP BY customer_id加上HAVING COUNT(DISTINCT MONTH(order_date)) 12了。你看1565 的基础在那里再怎么变都跳不出分组聚合的框架。这些追问我在面试里被问到过好多次每次都能从最简单的统计题引申出索引、去重、分组筛选好几个知识点。如果把 1565 吃透了这些追问其实就是在一层一层剥你已经掌握的知识点而已。最后说一点我自己的体会。SQL 题跟算法题不一样它没有那么多花哨的数据结构核心就是对“集合”的理解怎么把一张表拆成多个分组怎么对每个分组做计算怎么把结果拼成你想要的样子。力扣 1565 这道题虽然简单但它是理解“集合思维”的最佳起点。我在带新人写报表 SQL 的时候经常让他们先把这道题默写一遍写对了再去碰真正的业务数据。把基础打扎实后面的路反而走得快。
返回列表