尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
力扣1565 SQL题:按月统计订单与顾客数的分组去重实战解析
力扣 1565 这道 SQL 题题目全称叫“按月统计订单数与顾客数”在力扣数据库题库里属于最经典的一类分组汇总题。我第一次刷这道题时还觉得很轻松以为就是GROUP BY加COUNT(*)的事真正跑起来才发现日期格式化、客户去重、年份过滤一个个都是考点漏一个就得出错误结果。这道题其实模拟的是日常开发里特别常见的一个场景——从订单表里按月拉出唯一的订单数和顾客数整理成一张统计报表。相比很多烧脑的算法题它和实际工作的贴合度反而更高所以我个人强烈建议三类人不要跳过它刚开始学 SQL 的新手、准备数据库方向面试的求职者以及每天需要写报表查询的数据分析师和开发工程师。下面我按自己的刷题顺序把这题从题意、表结构、解题思路到标准答案完整过一遍。1. 这道 SQL 题到底在考什么为什么值得刷1.1 题目背景与需求速览力扣 1565 的题干其实很短核心就是一张Orders订单表表里包含订单编号、下单日期、客户编号、订单金额这四个字段。题目要求写一个 SQL 查询统计 2020 年每一个月的唯一订单数和唯一顾客数月份要按YYYY-MM的格式输出结果按月份排序。LeetCode 官方的示例数据大概是这样的order_idorder_datecustomer_idinvoice12020-07-3113022020-07-3024032020-07-3137042020-07-29410052020-06-101101062020-03-24210172020-03-25210282020-03-26110392020-04-062104期望的输出结果是monthorder_countcustomer_count2020-03322020-04112020-06112020-0744注意看2020 年 3 月一共有 3 条订单记录客户却只有 1 和 2 两个人所以订单数是 3顾客数是 2。这就是整道题的核心逻辑订单数按行计数顾客数必须去重。1.2 它在刷题计划里的真实位置很多人的刷题计划里永远只有算法题比如“力扣热题100”里那批高频题SQL 题直接被忽略掉这样其实很亏。数据库开发几乎是每个后端工程师的日常工作而像 1565 这种题考查的正好是面试里最常见的分组聚合和去重统计套路。我自己的体会是SQL 题不像算法题那样需要大量积累套路它更像是“语法熟练度”和“业务理解力”的组合练多了之后你看到“每个月”“每个部门”“每个渠道”这类关键词脑子里立刻就会反射出GROUP BY。这道题属于入门偏基础的水平但它的价值在于承上启下。你把这一题的逻辑彻底吃透之后再去碰同类型的分组统计题比如力扣里那道把雇员按相同属性分组的题思路基本是通的。所以我很建议把这个题放进你的力扣刷题攻略里当成 SQL 方向的第一批必刷题花半小时弄明白比草草刷十道重复的简单题更有收获。2. 读懂 Orders 表四个字段和三个隐藏考点2.1 字段类型决定了你能怎么写写 SQL 之前第一步永远是看表结构。Orders表里的四个字段各有用处order_id是订单编号也是表的主键。这里有一个很多人没注意的细节既然order_id是主键它就天然是唯一的理论上一张订单在表里只可能出现一次。所以题目里说的“唯一订单数”在这个表结构下其实和“订单总数”结果一样但题目专门用“唯一”两个字是在提醒你要有去重意识以后遇到订单号不唯一的表时COUNT(DISTINCT order_id)就是保命写法。order_date是下单日期字段类型是DATE精确到天。这个类型直接决定了我们可以放心使用日期格式化函数不用担心时分秒带来的边界问题。如果字段是DATETIME过滤条件就得格外小心后面我会专门讲。customer_id是客户编号同一个客户可以反复出现一个客户在同一个月份里可以下多笔订单这正是“顾客数”必须去重的原因。很多人在这里翻车直接COUNT(customer_id)结果把同一个客户统计了多次。invoice是订单金额本题用不到。但真实业务里这个字段往往才是老板最关心的订单数和顾客数只是辅助指标这个反差等你到了实际报表场景就会深有体会。2.2 需求里被你忽略的三个隐藏考点第一题目限定的是“2020 年”这意味着查询里必须有一个关于年份的过滤条件。你可以写WHERE YEAR(order_date) 2020也可以写区间过滤。我的建议是尽量用区间过滤因为把order_date包在YEAR()函数里会导致这一列无法走索引数据量大的时候性能差别很明显区间过滤则能保留索引的潜力。第二“唯一顾客数”要求你去重。同一个客户可能在某个月里下了 5 单如果输出结果里顾客数是 5那业务上是完全错误的因为顾客只有 1 个人。所以核心函数必须是COUNT(DISTINCT customer_id)而不是COUNT(customer_id)。第三月份输出格式必须是YYYY-MM比如2020-03。如果你直接用MONTH(order_date)输出就是3这样的月份数字如果直接按日期分组输出又会带上天。两种都不符合题目要求唯一的出路就是日期格式化。这三个点任何一个踩了最终输出的结果都不对。这也就是为什么我总说LeetCode 的 SQL 题不是“会写几条 SELECT”就能过的它非常考验你读懂题意的能力。3. 解题核心分组、去重、日期格式化3.1 GROUP BY 的工作方式先聊GROUP BY。你可以把订单表想象成一摞快递单现在要按月份把它们分到不同的抽屉里1 月的放一起2 月的放一起3 月的放一起这就是分组的过程。分组完成后每个抽屉里都剩下一个“代表”然后聚合函数就在每个抽屉里分别执行。具体到这个题GROUP BY DATE_FORMAT(order_date, %Y-%m)会把所有订单按照2020-03、2020-04这样的月份字符串分组。分组完之后再用COUNT(DISTINCT order_id)数每个抽屉里的订单数用COUNT(DISTINCT customer_id)数每个抽屉里的不同客户数。需要注意的是GROUP BY可以接列名、表达式也可以接序号。GROUP BY 1表示按SELECT里的第一个列分组GROUP BY month是按别名分组这种写法在 MySQL 里经常能跑通但并不是所有数据库都支持。为了稳妥我习惯在GROUP BY里写完整的表达式这一点在面试里很加分因为能体现你对不同数据库兼容性的理解。3.2 COUNT(DISTINCT) 与 COUNT(*) 的本质区别这三个函数考得极其频繁我在这里把它们彻底说清楚。COUNT(*)只统计一共有多少行不管行的内容是什么哪怕某些字段是NULL它也会把这一行算进去因为它的作用对象是“行”本身。COUNT(customer_id)是统计这一列里非NULL值的个数它会忽略掉customer_id为NULL的行但不会去重。COUNT(DISTINCT customer_id)则是先把这一列里所有不同的值挑出来再数一数有多少个。放到这个题里三者的差异非常明显。假设 2020 年 3 月有三笔订单客户分别是 2、2、1那么COUNT(*)是 3COUNT(customer_id)也是 3只有COUNT(DISTINCT customer_id)才是 2。业务上要的是“有多少个客户下了单”数字是 2所以前两种写法都会算错。COUNT(DISTINCT ...)执行时数据库内部通常需要维护一个去重集合数据量一大内存消耗和耗时都会明显上升。LeetCode 这种小数据量感觉不出来但如果你在真实环境里对一个大表跑COUNT(DISTINCT customer_id)就要考虑性能问题。实际方案可能变成先按客户维度去重成一个子表再在外面计数或者干脆用近似去重函数。但那是后话这道题用标准写法就好。3.3 月份格式化DATE_FORMAT 是最稳的选择在 MySQL 里要把日期转成YYYY-MM格式主流写法有几个DATE_FORMAT(order_date, %Y-%m)是最直白、可读性最好的写法。%Y代表四位年份%m代表两位月份输出结果就是2020-03这种标准字符串。LEFT(order_date, 7)也可以实现同样效果原理是把2020-03-26这个字符串从左边截 7 个字符得到2020-03。这个写法很取巧对纯DATE类型确实有效但对格式不统一的字符串日期就会翻车。SUBSTRING(order_date, 1, 7)和LEFT本质一样只是换个函数名。我在实际项目里绝大多数情况会用DATE_FORMAT因为它对语义表达最清晰任何人看到DATE_FORMAT(order_date, %Y-%m)都知道这是在格式化日期。LEFT虽然简洁但是隐含了对日期格式的假设。而且DATE_FORMAT以后想换成%Y、%Y-%m-%d、%Y-W%u之类的格式只要改一个参数就行维护成本低很多。4. 标准解法完整实现与逐行解读4.1 可以直接交上去的标准 SQL这道题的标准答案其实是很多解法社区里的共同写法MySQL 环境下可以直接通过SELECT DATE_FORMAT(order_date, %Y-%m) AS month, COUNT(DISTINCT order_id) AS order_count, COUNT(DISTINCT customer_id) AS customer_count 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;个别资料里也会把GROUP BY写成GROUP BY 1或者GROUP BY month。因为这题在 LeetCode 上的运行环境是 MySQL这些变体通常都能过。但我个人不推荐在标准解法里使用GROUP BY 1因为一旦SELECT里的列顺序变动序号分组的结果就会变可读性也很差。写上完整的表达式是更稳的习惯。4.2 逐行拆解每段代码第一行DATE_FORMAT(order_date, %Y-%m) AS month这是把订单日期转成月份字符串并起了一个别名month。这里要注意别名不能是month的保留字冲突问题在 MySQL 里month是可以作为别名的但如果你用的数据库对保留字更严格就需要换成month_name或者加反引号。第二行COUNT(DISTINCT order_id) AS order_count统计每个月的唯一订单数。因为order_id是主键所以这里的DISTINCT在实际执行时并不会带来额外开销但写成DISTINCT完全符合题意也提醒阅读者注意去重逻辑。第三行COUNT(DISTINCT customer_id) AS customer_count统计每个月的不同顾客数这是整道题最核心的业务逻辑也是最容易写错的一行。WHERE子句里的order_date 2020-01-01 AND order_date 2021-01-01是左闭右开的半开区间写法等价于“把 2020 年整年都包含进来但不包含 2021 年 1 月 1 日零点”。这种写法比BETWEEN 2020-01-01 AND 2020-12-31更安全因为如果字段是DATETIME类型BETWEEN会漏掉 2020 年 12 月 31 日 23:59:59 的记录而半开区间不会。GROUP BY DATE_FORMAT(order_date, %Y-%m)按格式化后的月份进行分组。这里的分组表达式必须和SELECT里的格式化表达式保持一致否则会出现“分组依据不明确”的报错。4.3 SQL 的逻辑执行顺序与别名问题很多人以为 SQL 是从上往下顺序执行的其实不是。一条标准查询的执行顺序大致是FROM→WHERE→GROUP BY→SELECT→ORDER BY也就是说数据库会先确定数据从哪张表来然后立即根据WHERE条件把 2020 年之外的数据过滤掉再对这个过滤后的结果集按月分组分组完成之后才轮到SELECT里的表达式做计算和别名最后才用ORDER BY排序。正因为执行顺序是这样的你才能在ORDER BY里直接使用month这个别名因为ORDER BY在SELECT之后执行。但你不能在WHERE里写WHERE month 2020-07因为WHERE执行时别名month还不存在数据库会直接报错“未知的列”。GROUP BY这里就有点特殊了。MySQL 对GROUP BY使用别名的支持相对宽松所以GROUP BY month在 LeetCode 上能跑通但为了写出更通用、更规范的 SQL我还是建议在GROUP BY里写完整的表达式或列名而不是别名。这一点你多写几个项目之后就会发现越“朴实”的写法越不容易踩坑。5. 踩坑实录这些错误可能你已经犯过5.1 语法与格式层面的典型错误第一个常见错误是在WHERE里用别名。很多新手习惯把查询拆成逻辑步骤先给月份起个名字再过滤于是写出了类似这样的语句SELECT DATE_FORMAT(order_date, %Y-%m) AS month, ... WHERE month 2020-07这种写法在 SQL Server 的一些版本里可以通过HAVING或者子查询绕过去但直接在WHERE里用别名绝大多数数据库都不认。我常见到有人因此纠结很久实际上是还没理解执行顺序。第二个常见错误是DATE_FORMAT的格式串写反。%Y是四位年份%m是两位月份如果你把%y-%M这种大小写搞混输出格式可能变成20-03或者20-March之类的怪东西。虽然也能跑通但结果完全不符合题目要求的YYYY-MM。第三个常见错误是GROUP BY与SELECT的表达式不一致。比如SELECT里写DATE_FORMAT(order_date, %Y-%m)GROUP BY里却写DATE_FORMAT(order_date, %Y%m)两者格式不一样分组依据不同结果就会乱掉。这种问题在开启ONLY_FULL_GROUP_BY模式的 MySQL 8.0 里会直接报错反而更容易被提前发现。第四个常见错误是漏掉ORDER BY。题目明确要求按月排序虽然 MySQL 的GROUP BY在很多情况下会按分组字段默认排序但这是执行计划顺手做的不是标准 SQL 的保证。如果换一个数据库输出顺序可能就变成随机的了所以ORDER BY month一定要显式写出来。5.2 业务逻辑与边界条件的典型错误首先是年份边界。如果只写WHERE order_date BETWEEN 2020-01-01 AND 2020-12-31对DATE类型没问题但一旦order_date是带时分秒的DATETIME最后一天 23 点到 24 点之间的订单就会被漏掉。反过来说如果写的是 2020-12-31加上DATETIME类型那 2020 年 12 月 31 日 23:59:59 之后到 2021 年之间的记录是否被包含就完全取决于数据库的边界判断。用 2020-01-01 AND 2021-01-01这样的半开区间就能彻底避免这个坑。其次是去重对象错误。正确答案是COUNT(DISTINCT customer_id)但有人会写成COUNT(DISTINCT order_id)或者干脆COUNT(*)。前者统计的是订单数后者统计的是行数如果同一个客户一个月里下了多笔订单这两种写法得出的顾客数都会偏大。再次是空月份问题。力扣 1565 的要求比较温和没有任何订单的月份可以不出现在结果里。但真实业务里老板往往希望看到每个月的数字哪怕那个月是 0不然报表上缺一个月视觉上就像是统计出错了。这个问题在 LeetCode 上不扣分但在实际工作中是大问题我在下一章会展开讲。最后还有一个容易被忽略的边界情况就是一张订单可能因为退款、取消等原因在表里出现多条记录。如果一张订单被拆成多行order_id不再唯一那么COUNT(DISTINCT order_id)才真正发挥价值去重后才是真实的订单数。这也是为什么题目反复强调“唯一订单数”的原因。6. 从力扣 1565 延伸到真实报表场景6.1 真实“按月统计”比 LeetCode 复杂在哪LeetCode 1565 的订单表只有四个字段需求也极简。但真实业务里同样的“按月统计订单数和顾客数”至少要再加三个维度渠道、地区、订单状态。老板真正想问的是“哪个渠道在增长”“哪个地区的顾客在流失”“退款订单占比多少”而不仅仅是总数。假设业务表里多了一个channel字段那查询就会变成按月份和渠道两维分组SELECT DATE_FORMAT(order_date, %Y-%m) AS month, channel, COUNT(DISTINCT order_id) AS order_count, COUNT(DISTINCT customer_id) AS customer_count FROM Orders WHERE order_date 2020-01-01 AND order_date 2021-01-01 GROUP BY DATE_FORMAT(order_date, %Y-%m), channel ORDER BY month, channel;这只是最基本的扩展。如果还要看每个月的下单顾客里有多少是回头客就需要用窗口函数或者自连接如果要看环比增长就需要把上个月的数据也挪进来做对比。到了这一步你已经不仅仅是写一条 SQL而是在设计一套报表逻辑。这也是我为什么一直强调基础题虽然简单但它背后撬动的业务场景非常庞大。6.2 用递归 CTE 把没有订单的月份也补出来力扣 1565 不要求输出没有订单的月份但实际工作中报表通常需要连续月份哪怕当月没有订单也要显示 0。最稳妥的做法是先生成一张月份表再和订单数据进行左连接。在 MySQL 8.0 里可以直接用递归 CTE 生成 2020 年每一月的起始日期WITH RECURSIVE month_series AS ( SELECT 2020-01-01 AS month_start UNION ALL SELECT DATE_ADD(month_start, INTERVAL 1 MONTH) FROM month_series WHERE month_start 2020-12-01 ) SELECT DATE_FORMAT(ms.month_start, %Y-%m) AS month, COUNT(DISTINCT o.order_id) AS order_count, COUNT(DISTINCT o.customer_id) AS customer_count FROM month_series ms LEFT JOIN Orders o ON o.order_date ms.month_start AND o.order_date DATE_ADD(ms.month_start, INTERVAL 1 MONTH) WHERE o.order_date 2020-01-01 OR o.order_date IS NULL GROUP BY ms.month_start ORDER BY ms.month_start;这个写法生成一个包含 12 行的月份序列再和订单表关联。没有订单的月份左连接后统计结果就是 0最终报表才能看到连续的趋势。我第一次在真实项目里需要这种“缺月补零”的报表时还没学会递归 CTE是拿 Python 在外部拼的月份列表麻烦得要命后来发现数据库本身就能解决一行 CTE 的事。6.3 关于力扣 SQL 刷题方式的一点个人建议最后聊聊刷题方式。很多人把力扣当成纯算法题库只盯着“力扣热题100”里的题目练这其实是一条腿走路。SQL 题就像英语里的单词量看起来不起眼但写报表、做数据分析、后端接口优化哪一样都离不开。我建议你把力扣里 SQL 题单独列成一条学习线每天花二十分钟做一两道和算法题穿插着来。刷 SQL 题时不建议背答案。你就算把这道 1565 的答案背得滚瓜烂熟遇到同样的表换个场景还是不会写。更好的做法是先自己动手写一遍跑通了再去看高赞解法对比自己和别人的思路差在哪里。比如这一题你可能会写成WHERE YEAR(order_date) 2020看了别人用区间过滤的写法才会意识到函数包住字段会影响索引使用。这种细节靠背是背不来的必须靠对比和复盘。另外LeetCode 的 SQL 运行环境以 MySQL 为主但不同数据库的语法多少有点差异。平时可以顺手用DATE_FORMAT、LEFT、SUBSTRING、TO_CHAR这些函数各写一遍同样的功能感受一下差异。真到了面试官面前你能说出“这个题在 MySQL 和 PostgreSQL 里分别怎么写”绝对比只给一个标准答案更能打动对方。这道题对我来说的意义不只是打通了 GROUP BY 和 COUNT DISTINCT 的常见组合更重要的是让我意识到SQL 刷题的核心不在于记住某道题的答案而在于建立一种“看标题就知道要分组看关键词就知道要去重”的反射能力。等你刷到一定数量再回头写报表、查数据速度和准度都会有质的提升。
RELATED

相关推荐

多商户SaaS进销存ERP源码落地:数据隔离与扫码库存设计要点

多商户SaaS进销存ERP源码落地:数据隔离与扫码库存设计要点

简介:2022年新版多商户多仓库带扫描云进销存系统ERP管理系统Saas营销版无限商户源码,专为需要实现多商户协同、多仓库调拨、扫码出入库及营销管理场景的开发者与企业用户设计。资源共1317个文件,压缩包大小21.38MB,包含509个PHP后…

📅 2026/9/25 16:46:41
基于SpringBoot的鲜花在线商城管理系统:技术栈、背景意义与核心代码

基于SpringBoot的鲜花在线商城管理系统:技术栈、背景意义与核心代码

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 1. 项目背景与意义 随着互联网和移动支付的普及,线上购物已成为人们日常生活的重要组成部分。鲜花作为一种具有时效性、季节性和情感属性的特殊商品&#x…

📅 2026/9/25 16:46:41
5 分钟打通!OpenClaw 连接 Ollama 本地模型,保姆级实操教程(TaoToken 统一 Key 配置版)

5 分钟打通!OpenClaw 连接 Ollama 本地模型,保姆级实操教程(TaoToken 统一 Key 配置版)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/25 16:41:41
MORE NEWS

更多资讯

📰

Higgsfield详解:AI视频生成项目原理、参数调优与实战避坑指南

最近总有人在群里问 higgsfield 到底是什么,有人以为这是物理考题,有人以为又冒出来一个新 AI 视频工具。我的回答是:两个方向都对,但当它以项目名的形式出现时,绝大多数情况下指的是一个主打 AI 视频生成的项目。我把…

📰

2026顺义搬家公司怎么选?正规搬家公司挑选技巧,告别中途加价套路

北京顺义租房换房、家庭迁居、别墅搬迁、企业搬迁需求持续走高,后沙峪、天竺、中央别墅区、顺义城区、机场周边等片区,每天都有大量居民和企业筹备搬家。但不少市民都踩过搬家行业的坑:网上看到低价报价,师傅上门后临时增加楼层费…

📰

PPTist 模板式 AIPPT 原理与模板制作实战:从类型标注到 AI 生成的全流程解析

前端企业应用 【免费下载链接】PPTist PowerPoint-ist(/pauəpɔintist/), An online presentation application that replicates most of the commonly used features of MS PowerPoint, allowing for the editing and presentation of PPT online. It …

📰

VoltAgent 工具体系实战指南:从 createTool 到 Toolkit 的完整工具箱构建

人工智能AI AgentAgent 框架后端多智能体RAG工具调用Agent 记忆 【免费下载链接】voltagent AI Agent Engineering Platform built on an Open Source TypeScript AI Agent Framework 项目地址: https://gitcode.com/gh_mirrors/vo/voltagent 点击查看 免费下载 本…

📰

Airtest Android 屏幕旋转处理深度解析:RotationWatcher 与 XYTransformer 原理与实战

测试质量保障计算机视觉 【免费下载链接】Airtest UI Automation Framework for Games and Apps 项目地址: https://gitcode.com/gh_mirrors/ai/Airtest 点击查看 免费下载 导读:本文基于 Airtest 官方 API 参考文档 airtest.core.android.rotation.rst…

📰

GPT Image Playground 预置配置 JSON 完全参考:3 种方式为团队批量注入 AI 绘图配置

GPT Image Playground 预置配置 JSON 完全参考:3 种方式为团队批量注入 AI 绘图配置 【免费下载链接】gpt_image_playground 基于 OpenAI gpt-image-2.5 API 的图片生成与编辑工具 项目地址: https://gitcode.com/gh_mirrors/gp/gpt_image_playground GPT Im…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬