尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
SQL销售分析1082题实战:HAVING、子查询与窗口函数深度解析
刷到 SQL 1082 这套销售分析题的时候我一开始没太当回事以为就是拿两张表做几个 SELECT。结果真正动手写才发现它把 SQL 里几个最容易被忽视的点全踩了一遍分组汇总之后怎么和“最大值”比较、如何表达“买过 A 但没买过 B”这种集合逻辑、如何判断“所有记录都落在某个时间区间内”。这套题很适合刚学完 SELECT、JOIN 和 GROUP BY、想用业务场景练手的人也适合准备面试前把 HAVING、子查询、窗口函数一起过一遍。下面我按三道题拆开讲每一道都会给出建表语句、测试数据和最终查询再附上我实际踩过的一些坑。1. 三道销售分析题到底在考什么1.1 这类题目背后的共同套路销售分析 I 考的是分组求和再取极值销售分析 II 考的是集合判断翻译成业务语言就是“有 A 无 B”销售分析 III 考的是全量判断翻译成业务语言就是“所有销售记录都在某个时间段内一条都不能漏到外面”。搞清楚这三件事你就知道为什么同样是 GROUP BY HAVING写出来的条件差一点结果就差很多。销售分析 I 的 HAVING 是用来比“组和组之间的总额”销售分析 II 的 HAVING 是用来数“组内的标记行数”销售分析 III 的 HAVING 则是用来判断“组内有没有越界记录”。同一个语法在不同场景下的用法完全不同这也是这套题最值得刷的地方。1.2 先看清两张表的字段三道题共用两张表一张存产品主数据一张存销售明细。字段结构如下产品表 product字段类型说明product_idint产品编号主键product_namevarchar产品名称unit_priceint产品单价销售表 sales字段类型说明seller_idint销售者编号product_idint产品编号buyer_idint买家编号sale_datedate销售日期quantityint销售数量priceint该笔销售的总价这里要注意销售表没有专门设置唯一主键。这不是失误而是很多业务明细表的真实状态——同一个卖家、同一天、卖同一个产品可能产生多条记录。写 SQL 的时候不能默认它没有重复行后面我会专门说这个坑。1.3 计算口径SUM(price) 还是 SUM(quantity * price)很多新手到这一步就栽了。题目里的price字段到底是不是总价我按这个题型的惯例把price当作这一行销售记录已经算好的成交总额所以统计销售者总销售额时直接用SUM(price)。如果你的业务表里price存的是单价那就必须写成SUM(quantity * price)。这两者的差别不是一个小数点的问题而是结果可能差一个数量级。实际工作中遇到这种表一定要先和上游确认口径不要想当然。注意编码前先确认字段语义销售分析 I 的所有后续逻辑都建立在这个口径上。2. 建表脚本与测试数据准备2.1 把表结构先搭出来我在 MySQL 8.0 环境里练习建表脚本如下。为了演示方便索引先不建后面统一讨论性能。CREATE TABLE product ( product_id INT PRIMARY KEY, product_name VARCHAR(50), unit_price INT ); CREATE TABLE sales ( seller_id INT, product_id INT, buyer_id INT, sale_date DATE, quantity INT, price INT );再插入产品数据。为了避免涉及真实品牌我统一用 Alpha、Beta、Grocery 这三个代号INSERT INTO product VALUES (1, Alpha, 1000), (2, Beta, 1400), (3, Grocery, 200);2.2 销售分析 I 的测试数据这一题关注的是每个销售者的总销售额插入数据时故意做一个并列第一TRUNCATE TABLE sales; INSERT INTO sales VALUES (1, 1, 1, 2018-01-01, 5, 600), (1, 2, 2, 2018-02-01, 2, 300), (2, 1, 3, 2018-03-01, 1, 900), (3, 3, 4, 2018-04-01, 10, 200);计算一下seller 1 有 600 300 900seller 2 只有一笔 900seller 3 只有 200。也就是说最终结果要同时返回 seller 1 和 seller 2这才是这题最容易写错的地方。2.3 销售分析 II 的测试数据销售分析 II 要看买家买过哪些产品所以我重新清空销售表插入一组能区分三类买家的数据只买过 Alpha、只买过 Beta、两个都买过。DELETE FROM sales; INSERT INTO sales VALUES (1, 1, 1, 2018-05-01, 5, 600), -- buyer 1 买了 Alpha (2, 2, 2, 2018-06-01, 5, 1000), -- buyer 2 买了 Beta (3, 1, 3, 2019-01-01, 1, 100), -- buyer 3 买了 Alpha (3, 2, 3, 2019-02-01, 1, 200); -- buyer 3 又买了 Beta预期结果只有 buyer 1因为 buyer 3 虽然买过 Alpha但也买过 Beta不符合“没买过 Beta”的条件。2.4 销售分析 III 的测试数据销售分析 III 关心的是产品在 2019 年第一季度的销售情况。我重新构造数据产品 1 在 Q1 和 Q2 都有销售产品 2 只在 Q1 有销售产品 3 只在 2018 年有销售。DELETE FROM sales; INSERT INTO sales VALUES (1, 1, 1, 2019-02-01, 5, 600), -- product 1 在 Q1 有销售 (1, 1, 1, 2019-04-01, 5, 700), -- product 1 在 Q2 也有销售应排除 (2, 2, 2, 2019-01-15, 5, 1000), -- product 2 只在 Q1 有销售 (3, 3, 3, 2018-12-31, 2, 200); -- product 3 在 2018 年有销售应排除预期结果只有 product 2。为什么 product 1 明明第一季度也卖过却还是会被排除因为“只在某段时间内销售”意味着不能有任何一条记录落到区间外。3. 销售分析 I销售额最高且并列要全出3.1 从分组汇总开始第一眼看到“找销售额最高的销售者”大部分人都会先写分组查询SELECT seller_id, SUM(price) AS total_sales FROM sales GROUP BY seller_id;执行后你会得到三行临时结果seller_idtotal_sales190029003200问题在于答案不能只展示这个分组结果而是要筛选出 total_sales 最大的那一组。如果只是人工看一眼你当然知道是 900但 SQL 查询本身并不知道。3.2 用 HAVING 和子查询做“最大值”判断最大值本身也是一个聚合结果。你可以在 HAVING 里把它写成一个子查询SELECT seller_id FROM sales GROUP BY seller_id HAVING SUM(price) ( SELECT MAX(total_sales) FROM ( SELECT SUM(price) AS total_sales FROM sales GROUP BY seller_id ) t );这里的逻辑是先在最内层算出每个 seller 的总额再用 MAX 取出最大值最后回到外层 HAVING把总额等于这个最大值的 seller 筛出来。注意最内层的派生表必须加别名我写的是t。这个别名在很多数据库里不能不写否则直接报语法错误。另一个容易踩的坑是你可能会试图写HAVING SUM(price) MAX(SUM(price))这在 SQL 里行不通因为聚合函数不能在同一层直接嵌套。3.3 用窗口函数 RANK 更省心如果你用的数据库支持窗口函数比如 MySQL 8.0、PostgreSQL、SQL Server那可以用 RANK 来处理并列问题SELECT seller_id FROM ( SELECT seller_id, RANK() OVER (ORDER BY SUM(price) DESC) AS rk FROM sales GROUP BY seller_id ) ranked_sales WHERE rk 1;RANK 遇到并列会给出相同名次这就天然满足了“并列第一都要输出”的要求。如果业务上只需要任意一个可以改成 ORDER BY SUM(price) DESC LIMIT 1但这会忽略并列情况面试时建议先问清楚需求。3.4 这一题最容易出现的计算错误我见过不少人为了拿到产品名称把 product 表也 join 进来然后对 SUM(price) 求和。如果两张表的关联是 1 对 1 还好一旦 sales 表里同一个 seller 和同一个 product 有多条记录或者 product 表里有重复的 product_idjoin 就会让行数翻倍SUM 结果直接膨胀。实际上这一题根本不需要 join product 表因为seller_id和price都只在 sales 表里。只查单表就能解决的问题不要为了“看起来完整”去额外 join。4. 销售分析 II买过 Alpha 却不要 Beta 的买家4.1 第一个能跑版本条件聚合这道题是典型的集合判断。把每个买家的购买记录看成一个小集合你要的是集合里包含“Alpha”但不包含“Beta”。写成 SQL就是先按 buyer_id 分组然后在组内用条件聚合数标记SELECT s.buyer_id FROM sales s JOIN product p ON s.product_id p.product_id GROUP BY s.buyer_id HAVING SUM(CASE WHEN p.product_name Alpha THEN 1 ELSE 0 END) 0 AND SUM(CASE WHEN p.product_name Beta THEN 1 ELSE 0 END) 0;这个写法的好处是只扫一遍 sales 表思路也很直接满足 Alpha 条件的行加 1不满足加 0Beta 同理。最后“Alpha 的计数大于 0”保证买过 Alpha“Beta 的计数等于 0”保证没买过 Beta。如果是在 MySQL 里还可以简写成SUM(p.product_name Alpha) 0因为 MySQL 支持布尔表达式直接求和。但我更推荐写标准的 CASE WHEN方便迁移到其他数据库也方便别人一眼读懂。4.2 第二个版本NOT EXISTS条件聚合虽然简洁但有些人读起来不太习惯。这时候可以换成 NOT EXISTS 的版本SELECT DISTINCT s.buyer_id FROM sales s JOIN product p ON s.product_id p.product_id WHERE p.product_name Alpha AND NOT EXISTS ( SELECT 1 FROM sales s2 JOIN product p2 ON s2.product_id p2.product_id WHERE p2.product_name Beta AND s2.buyer_id s.buyer_id );这个版本的思路更贴近自然语言先找出所有买过 Alpha 的买家再排除那些存在 Beta 购买记录的买家。DISTINCT 是必要的因为同一个买家可能买过多次 Alpha。4.3 为什么尽量避开 NOT IN很多新手会写成buyer_id NOT IN (SELECT buyer_id FROM ... WHERE product_name Beta)。这在数据干净的时候没问题但一旦子查询结果里出现 NULLNOT IN 的行为会变成“整体为假”最终返回空结果。举个例子如果 Beta 商品存在一条 buyer_id 为 NULL 的销售记录那NOT IN (1, 2, NULL)对任何买家都不会返回 TRUE。而 NOT EXISTS 是逐行判断NULL 只会让那一行不匹配不会污染整个结果。生产环境里 buyer_id 不一定都是非空的所以我个人更推荐 NOT EXISTS 或条件聚合。4.4 这个思路还能怎么变这套逻辑在实际业务里非常常见。比如会员运营里要找出“买过 A 品类但没买过 B 品类”的用户或者风控场景里要识别“触发过规则 X 但没触发规则 Y”的账号。把条件聚合稍微改一下还能实现更多变体买过 Alpha 且买过 Beta两个条件都改成 0。只买过 Alpha没买过任何其他产品把COUNT(DISTINCT p.product_id) 1加进 HAVING。买过 Alpha 两次以上把SUM(CASE ... END) 1。这些变体在面试中经常会被追问建议自己动手跑一遍。5. 销售分析 III只在 2019 年第一季度卖过的商品5.1 用 COUNT 和条件聚合卡住全部记录“只在 2019 年第一季度销售”是一个全量判断要求产品的每一条销售记录都落在 2019-01-01 到 2019-03-31 之间。我习惯先写一个容易理解的版本SELECT p.product_id, p.product_name FROM product p JOIN sales s ON p.product_id s.product_id GROUP BY p.product_id, p.product_name HAVING COUNT(s.sale_date) 0 AND SUM(CASE WHEN s.sale_date 2019-01-01 AND s.sale_date 2019-04-01 THEN 1 ELSE 0 END) COUNT(s.sale_date);这里的核心等式是如果符合条件的销售记录数等于总销售记录数说明所有记录都在区间内一条都没有漏出去。COUNT(s.sale_date) 0这一行看起来多余但它非常重要。如果你改用 LEFT JOIN 实现没有销售记录的产品也会被带出来如果不加这条一个没有任何销售的产品会被误判为“符合条件”因为它所有 0 条记录确实都在区间内。这属于逻辑上的空集真值问题真实业务里一定要显式排除。5.2 用 MIN/MAX 做区间全量判断如果产品所有销售都在区间内那么最早销售日期和最近销售日期自然也都在区间内。反过来只要有一条记录在区间外MIN 或者 MAX 就会被拉出区间。所以可以简化成SELECT p.product_id, p.product_name FROM product p JOIN sales s ON p.product_id s.product_id GROUP BY p.product_id, p.product_name HAVING MIN(s.sale_date) 2019-01-01 AND MAX(s.sale_date) 2019-04-01;这个写法更短扫描次数也不会变多因为 MIN 和 MAX 都是分组聚合可以一次算出来。唯一需要注意的是如果 product 表里存在没有销售记录的孤立产品INNER JOIN 会把它们过滤掉所以不会误判。只有改成 LEFT JOIN 时才需要额外加 COUNT 条件。5.3 用 NOT EXISTS 写成人话版本如果团队里有人对聚合不太熟我推荐这个 NOT EXISTS 版本它最容易被读懂SELECT p.product_id, p.product_name FROM product p WHERE EXISTS ( SELECT 1 FROM sales s WHERE s.product_id p.product_id AND s.sale_date 2019-01-01 AND s.sale_date 2019-04-01 ) AND NOT EXISTS ( SELECT 1 FROM sales s WHERE s.product_id p.product_id AND (s.sale_date 2019-01-01 OR s.sale_date 2019-04-01) );第一个 EXISTS 负责保证产品在 Q1 有销售记录第二个 NOT EXISTS 负责保证不存在任何区间外的销售。这个写法最大的优点是把两个条件分开逻辑边界非常清楚排查问题的时候也方便注释。5.4 日期边界一定要用左闭右开写日期条件时不要图省事用sale_date BETWEEN 2019-01-01 AND 2019-03-31。如果 sale_date 是 DATE 类型这样写没问题但如果字段是 DATETIME 类型字符串2019-03-31会被隐式转换成2019-03-31 00:00:003 月 31 日当天任何大于零点的时间都会被漏掉。稳妥的写法是sale_date 2019-01-01 AND sale_date 2019-04-01这是典型的左闭右开区间也是日期范围查询里最不容易出错的写法。这个习惯一旦养成在统计周、月、季度时都能少踩很多坑。6. 从这三道题提炼出的 SQL 排查清单6.1 高频问题速查表我把这三道题里最容易出问题的地方整理成一张表方便你刷题或写业务 SQL 时对照现象可能原因对应的排查方向销售分析 I 的总销售额偏大不必要的 JOIN 让明细行重复检查是否只用了 sales 表是否缺 DISTINCT销售分析 I 只返回一个结果用了 LIMIT 1 而不是处理并列改用 RANK() 或 MAX 子查询销售分析 II 结果为空NOT IN 子查询里含 NULL改用 NOT EXISTS 或条件聚合销售分析 II 少人商品名称大小写或空格不一致统一用 UPPER/TRIM 后再比较销售分析 III 误把区间外产品算进来用了BETWEEN ... AND 2019-03-31改成 开始 AND 结束销售分析 III 把无销售产品算进来聚合条件少了 COUNT 0用 INNER JOIN 或加 EXISTS 限制6.2 性能优化建议在线题库的数据量小怎么写都能跑。真实业务里 sales 表可能是千万级甚至亿级这时候要注意几点。给 sales 表的关联字段和过滤字段建索引我会至少建这四个CREATE INDEX idx_sales_seller ON sales(seller_id); CREATE INDEX idx_sales_product ON sales(product_id); CREATE INDEX idx_sales_buyer ON sales(buyer_id); CREATE INDEX idx_sales_date ON sales(sale_date);还有一点GROUP BY 之前能不能提前用 WHERE 过滤能但要想清楚过滤条件是否会影响排除逻辑。比如销售分析 III如果提前把sale_date 2019-04-01过滤掉你确实只剩下了第一季度之前的记录但产品在第二季度是否有销售就再也看不见了排除条件等于白写。先保逻辑正确再谈优化。6.3 在真实业务里怎么变形这三道题看起来是题库题实际业务中非常常见销售分析 I 可以演变成“计算每个门店的销售额找出排名前几的门店”。销售分析 II 可以演变成“找出买过某活动商品但没有买过另一活动商品的用户做定向推送”。销售分析 III 可以演变成“统计只在活动期间产生过销售的 SKU评估活动是否吸引了全新商品”。我自己在实际工作中遇到频率最高的是第二类也就是“有 A 无 B”的需求。它的难点从来不是 JOIN而是把集合关系想清楚。建议你把这三种解题模板都写一遍尤其要试着自己构造边界数据比如 NULL、重复行、空表、并列值跑出来的结果才能帮你真正理解 SQL 的执行逻辑。
RELATED

相关推荐

eNSP模拟器ARP与ICMP抓包实战:从原理到排障全解析

eNSP模拟器ARP与ICMP抓包实战:从原理到排障全解析

1. 为什么要在模拟器里死磕ARP与ICMP刚入行做网络运维那会儿,我最怕的就是"ping不通"这三个字。用户说上不了网,你ping一下网关,不通;ping一下DNS,不通;再ping一下隔壁工位,还是不通。…

📅 2026/10/18 1:52:04
TCP Tahoe与Reno拥塞控制仿真实验深度解析:原理、参数与避坑指南

TCP Tahoe与Reno拥塞控制仿真实验深度解析:原理、参数与避坑指南

这个实验我前后做了两遍。第一遍照着实验指导书把仿真脚本敲完,跑出来两条曲线,乍一看都是锯齿,我还以为脚本写错了——TCP Tahoe与Reno的拥塞窗口变化看起来实在“太像了”。后来把参数逐个捋了一遍,又把trace文件里每一次丢包、…

📅 2026/10/18 1:47:03
王珊《数据库系统概论》第5版答案:关系代数与范式思维训练指南

王珊《数据库系统概论》第5版答案:关系代数与范式思维训练指南

简介:本资源是《数据库系统概论(第5版)》王珊、萨师煊主编教材的配套课后习题详细答案,面向高校计算机、软件工程、信息管理等专业本科生及数据库初学者,用于巩固关系模型、关系代数、SQL语言等核心理论知识&#xff0…

📅 2026/10/18 1:47:03
MORE NEWS

更多资讯

📰

无人机视角下YOLO目标检测与PyQt5界面开发实战

这个标题看着就像很多人正在做或准备做的方向:无人机视角下的目标检测,加上YOLO全家桶和PyQt5界面,等于一套能直接演示、能继续训练、能二次开发的完整工程。我去年正好完整走过一遍这个流程,从数据整理到训练调参,再到…

📰

alpha shape几何算法:从Delaunay剖分到任意维度点云边界提取

简介:面向 JavaScript 开发者的 alpha-shape 模块资源,能够计算任意维度点集的 alpha 形状(阿尔法形状),属于计算几何中常用的轮廓提取工具,适用于散点边界还原、凹包算法验证、二维或三维点云形态分析等场…

📰

Puppet Stack 实战:Puppet Master、Foreman 与 smart-proxy 集成安装与避坑指南

简介:这份资源面向需要搭建 Puppet 自动化运维体系的工程师与 DevOps 学习者,围绕 Puppet Master、The Foreman 与 smart-proxy 的安装和管理展开,帮助解决多角色 Puppet 架构部署与升级控制的问题。资源包共 60 个文件,约 64KB&a…

📰

STM32F407工程模板搭建:HAL库、启动文件与时钟配置全解析

简介:STM32F407工程模板是一套基于ARM Cortex-M4内核的预配置开发环境,面向嵌入式初学者和有经验的电子工程师,旨在解决STM32F407项目起步时环境搭建繁琐、外设初始化复杂的问题。压缩包共204个文件,体积仅1.12MB,包含…

📰

SPI、UART、I2C通信协议选型指南:从原理到实战的深度对比

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

📰

Linux下HTTP排障全指南:从报文解剖到抓包实战

先交代一个背景。很多年前我开始接手某台业务服务器的接口排查,遇到超时第一反应是去盯 TCP 层,看到一堆重传就怀疑机房网络,折腾一晚上毫无进展。后来旁边一位老同事用 tcpdump 按端口抓了完整会话,才发现问题出在一行 Connectio…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

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

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

📞 💬