力扣 SQL 基础 50 题:题解(题目名称+题号+原题链接+题目复述+答案+解析)
【力扣 SQL 50 题】力扣 SQL 50 题
01. 可回收且低脂的产品
- 题号:1757
- 原题链接:1757. 可回收且低脂的产品
📝 题目复述
本题要求从一张名为 Products 的产品表中筛选出符合特定条件的记录。
表结构定义:
product_id(int): 产品主键。low_fats(enum): 低脂标志。取值为 'Y'(是)或 'N'(否)。recyclable(enum): 可回收标志。取值为 'Y'(是)或 'N'(否)。
目标任务:
我们需要写一条查询语句,找出那些既是低脂产品(low_fats 为 'Y')又是可回收产品(recyclable 为 'Y')的所有 product_id。
🔑 答案
SELECT product_id
FROM Products
WHERE low_fats = 'Y' AND recyclable = 'Y';
💡 解析
- WHERE 多条件组合:当我们需要筛选出的数据必须同时满足多个过滤标准时,使用逻辑运算符
AND。 - 筛选流程:SQL 引擎会逐行扫描
Products表,只有当该行的low_fats列和recyclable列的值同时等于 'Y' 时,该行的product_id才会进入结果集。 - 注意:这里的 'Y' 是字符串(ENUM类型),在书写 SQL 时必须用单引号括起来。
02. 寻找用户推荐人
- 题号:584
- 原题链接:584. 寻找用户推荐人
📝 题目复述
我们要处理的是一张 Customer 用户信息表。
表结构定义:
id(int): 用户主键。name(varchar): 用户姓名。referee_id(int): 推荐人 ID。注意:此列可以包含空值 (NULL),表示该用户不是被别人推荐来的。
目标任务: 请查询所有推荐人 ID 不等于 2 的用户姓名。
🔑 答案
SELECT name
FROM Customer
WHERE referee_id <> 2 OR referee_id IS NULL;
💡 解析
- 核心陷阱:NULL 的逻辑计算:这是 SQL 新手最容易出错的地方。在 SQL 中,
NULL不代表“0”,也不代表“空字符串”,它代表“未知”。 - 三值逻辑:
2 <> 2结果是FALSE。1 <> 2结果是TRUE。NULL <> 2结果是UNKNOWN(未知)。
- 筛选原理:
WHERE只会选取结果为TRUE的记录。由于NULL <> 2的结果不是TRUE,所以如果不显式加上OR referee_id IS NULL,系统会自动排除掉那些推荐人为 NULL 的用户。
03. 大的国家
- 题号:595
- 原题链接:595. 大的国家
📝 深度题目复述
表 World 存储了全球各国家的信息。
表结构定义:
name(varchar): 国家名。continent(varchar): 洲名。area(int): 面积。population(int): 人口。gdp(bigint): 国内生产总值。
目标任务:
查询符合以下任一条件的国家名称 (name)、人口 (population) 和面积 (area):
- 面积至少为 300 万平方公里(
area >= 3,000,000)。 - 人口至少为 2500 万(
population >= 25,000,000)。
🔑 答案
SELECT name, population, area
FROM World
WHERE area >= 3000000 OR population >= 25000000;
💡 解析
- OR 运算符:与
AND相反,OR只需要满足多个条件中的其中一个,该行记录就会被选中。 - 多字段选择:题目明确要求输出
name,population,area三列,因此在SELECT后面我们要按序罗列这些列名,中间用逗号分隔,不要直接写SELECT *。 - 进阶优化(Tip):虽然本题
OR可以通过,但在面试中,面试官有时会问及性能。对于大数量级的表,使用UNION有时比OR能更好地利用索引。
04. 文章浏览 I
- 题号:1148
- 原题链接:1148. 文章浏览 I
📝 题目复述
数据记录在 Views 视图记录表中。
表结构定义:
article_id(int): 文章 ID。author_id(int): 作者 ID。viewer_id(int): 阅读者 ID。view_date(date): 阅读日期。
目标任务: 我们需要找出所有自己看过自己写的文章的作者 ID。
- “看自己文章”的判断标准是:这一行的
author_id等于viewer_id。 - 要求 1:结果需要去重(如果某作者看了自己多篇文章,只需列出一次)。
- 要求 2:结果必须按照 ID 升序排列。
🔑 答案
SELECT DISTINCT author_id AS id
FROM Views
WHERE author_id = viewer_id
ORDER BY id ASC;
💡 解析
- DISTINCT 去重:由于同一个作者可能多次阅读自己的文章,表里会出现重复的 ID,必须加上
DISTINCT保证每个 ID 在结果里只出现一次。 - 字段对比过滤:
WHERE author_id = viewer_id是本题的业务逻辑核心。 - ORDER BY 排序:
ASC代表升序(从 1 到 100),它是默认设置,即便不写ASC结果也是升序的,但写上后代码可读性更佳。 - 别名 alias:题目要求结果列名为
id,我们用AS id对author_id进行重命名。
05. 无效的推文
- 题号:1683
- 原题链接:1683. 无效的推文
📝 题目复述
处理一张名为 Tweets 的推文表。
表结构定义:
tweet_id(int): 推文唯一 ID。content(varchar): 推文的正文内容。
目标任务:
筛选出所有“无效”的推文 ID。
无效的定义是:推文正文 content 中包含的字符数严格大于 15。
🔑 答案
SELECT tweet_id
FROM Tweets
WHERE CHAR_LENGTH(content) > 15;
💡 解析
- 长度函数的区别(重点):
LENGTH(string):返回字符串的字节数。对于多字节字符(如中文、特殊符号),结果可能大于字符数。CHAR_LENGTH(string):返回字符串的字符数。- 面试点:在 MySQL 环境中,题目考查的是字数限制,建议养成使用
CHAR_LENGTH的习惯,以规避编码带来的差异风险(虽然此题主要考查英文内容)。
- 比较运算符:
> 15表示“大于 15”,不包含 15 本身。
收到!接下来的这 5 道题是 SQL 入门中最重要的**连接(JOIN)和分组聚合(GROUP BY)**部分,也是实际面试和工作中频率最高的基础技能。
06. 使用唯一标识码替换员工ID
- 题号:1378
- 原题链接:1378. 使用唯一标识码替换员工ID
📝 题目复述
本题涉及到两张表的信息关联。 表结构定义:
Employees(员工表):包含id(主键) 和name(员工姓名)。EmployeeUNI(唯一标识表):包含id(和员工表对应) 和unique_id(唯一标识码)。
目标任务:
我们需要展示每个用户的 unique_id 和 name。
注意点: 如果某个员工在 EmployeeUNI 表中没有唯一的标识码,那么在结果中其 unique_id 应当显示为 null,但该员工的名字依然需要出现在结果中。
🔑 答案
SELECT
eu.unique_id,
e.name
FROM Employees e
LEFT JOIN EmployeeUNI eu ON e.id = eu.id;
💡 解析
- 左外连接 (LEFT JOIN):这是本题的魂。如果我们用普通的
JOIN(即 INNER JOIN),那些没有唯一码的员工会被直接删掉。LEFT JOIN的逻辑是:以左表(Employees)为主,即使右表(EmployeeUNI)匹配不到,也会保留左表的记录,找不到的列补为NULL。 - 表别名:
Employees e给表取了个外号e,这在连接多个表时可以大大简化 SQL 的长度,避免字段歧义。
07. 产品销售分析 I
- 题号:1068
- 原题链接:1068. 产品销售分析 I
📝 题目复述
查询产品的销售明细,数据分布在两张表中。 表结构定义:
Sales(销售表):包含sale_id,product_id,year(销售年份),quantity(数量) 和price(单价)。Product(产品基础信息表):包含product_id(主键) 和product_name(产品名)。
目标任务:
请查询 Sales 表中每一笔销售记录对应的 product_name (产品名称)、year (销售年份) 和 price (价格)。
🔑 答案
SELECT
p.product_name,
s.year,
s.price
FROM Sales s
JOIN Product p ON s.product_id = p.product_id;
💡 解析
- 内连接 (JOIN):这里使用普通的内连接即可,因为我们要查询的是“每一笔销售”,由于
product_id是产品表的主键且通常销售数据对应的产品必定存在,使用JOIN可以取出两个表交集部分的匹配数据。 - SELECT 列的选择:注意是从
Product表里取名字,从Sales表里取年份和价格。
08. 进店却未进行交易的顾客
- 题号:1581
- 原题链接:1581. 进店却未进行交易的顾客
📝 题目复述
我们需要统计那些进店了却没买东西的倒霉蛋,涉及两张记录表。 表结构定义:
Visits(访客表):包含visit_id(进店 ID) 和customer_id(顾客 ID)。Transactions(交易表):包含transaction_id,visit_id(对应进店 ID) 和amount(金额)。
目标任务:
有些顾客进了店(在 Visits 表中有记录),但一次钱都没付(在 Transactions 表里没记录)。找出这些顾客的 customer_id,以及他们光逛不买的次数 (count_no_trans)。
🔑 答案
SELECT
v.customer_id,
COUNT(v.visit_id) AS count_no_trans
FROM Visits v
LEFT JOIN Transactions t ON v.visit_id = t.visit_id
WHERE t.transaction_id IS NULL
GROUP BY v.customer_id;
💡 解析
- 左连接过滤法:这是找“不存在于 A 表但在 B 表中”的数据的经典方案。我们用
Visits左连Transactions,如果右表没对应的交易,那右表的字段transaction_id必然为NULL。 - WHERE 子句:筛选出这些
t.transaction_id IS NULL的记录。 - 聚合分组:因为我们要统计每个客户不买东西的次数,所以必须对
customer_id进行GROUP BY(分组),然后用COUNT计算记录行数。
09. 上升的温度
- 题号:197
- 原题链接:197. 上升的温度
📝 题目复述
比较同一张表中不同日期的数值。 表结构定义:
Weather(天气表):包含id(主键),recordDate(日期) 和temperature(当日气温)。
目标任务:
找出所有气温比昨天更高的日期的 id。
🔑 答案
SELECT w1.id
FROM Weather w1
JOIN Weather w2 ON DATEDIFF(w1.recordDate, w2.recordDate) = 1
WHERE w1.Temperature > w2.Temperature;
💡 解析
- 自连接 (Self Join):因为我们需要把今天的温度和昨天的温度进行对比,数据全在同一张表里,所以把
Weather表分别取名为w1(代表今天)和w2(代表昨天)。 - 日期计算函数
DATEDIFF:在 MySQL 中,DATEDIFF(end, start)返回的是天数之差。我们需要让w1的日期减去w2的日期正好等于 1。 - 注意点:不能直接用
w1.date = w2.date + 1,因为在 SQL 中处理日期必须使用专门的日期操作函数。
10. 每台机器的进程平均运行时间
- 题号:1661
- 原题链接:1661. 每台机器的进程平均运行时间
📝 题目复述
通过日志表计算系统处理时长。 表结构定义:
Activity(活动表):包含machine_id(机器 ID),process_id(进程 ID),activity_type(枚举:'start' 或 'end') 和timestamp(时间戳,以秒为单位)。
目标任务:
每个进程都包含 start 和 end 两条记录。一个进程的耗时 = end 时间戳 - start 时间戳。请计算每台机器上所有进程运行的平均时间,结果保留 3 位小数。
🔑 答案
SELECT
a1.machine_id,
ROUND(AVG(a2.timestamp - a1.timestamp), 3) AS processing_time
FROM Activity a1
JOIN Activity a2 ON a1.machine_id = a2.machine_id
AND a1.process_id = a2.process_id
AND a1.activity_type = 'start'
AND a2.activity_type = 'end'
GROUP BY a1.machine_id;
💡 解析
- 将行转列(连接法):原始数据里“开始”和“结束”是在不同的两行。为了算减法,我们把
Activity自己连自己,通过a1锁定 start,a2锁定 end,这样在一行内就能计算a2.timestamp - a1.timestamp了。 - AVG 与 ROUND:
AVG用于求出该机器所有进程的平均耗时;ROUND(..., 3)用于按照题目要求截取三位小数。 - 连接条件的细化:除了机器 ID 对应外,还必须确保
process_id是一致的,否则会乱套。
11. 员工奖金
- 题号:577
- 原题链接:577. 员工奖金
📝 题目复述
查找奖金低于 1000 的员工信息。 表结构定义:
Employee(员工表):包含empId(主键),name,supervisor,salary。Bonus(奖金表):包含empId(外键,指向员工表),bonus(奖金金额)。
目标任务:
查询所有奖金少于 1000 的员工的姓名 (name) 及其奖金 (bonus)。
注意点: 如果员工在奖金表中没有记录,其实际奖金可以视为 0,因此这些员工也应该被选出来(其奖金显示为 null 亦可)。
🔑 答案
SELECT e.name, b.bonus
FROM Employee e
LEFT JOIN Bonus b ON e.empId = b.empId
WHERE b.bonus < 1000 OR b.bonus IS NULL;
💡 解析
- LEFT JOIN 确保不丢人:并不是每个员工都有奖金,普通的
JOIN会剔除没有奖金的人。我们要确保所有员工都被考虑到,必须使用LEFT JOIN。 - NULL 值判断再次出现:如果某人在奖金表中没有数据,
b.bonus的值就是NULL。在 SQL 逻辑中,NULL < 1000的判断结果既不是 True 也不是 False,而是 Unknown。所以必须显式加上OR b.bonus IS NULL,才能找回这部分人。
12. 学生们参加各科测试的次数
- **题号:**1280
- 原题链接:1280. 学生们参加各科测试的次数
📝 题目复述
这是一个涉及三张表连接的复杂查询。 表结构定义:
Students(学生表):包含student_id(主键) 和student_name。Subjects(课程表):包含subject_name(主键)。Examinations(考试记录表):包含student_id和subject_name(不带主键,一行代表参加过一次考试)。
目标任务:
查询出每个学生参加每一门科目测试的次数。
关键难点: 如果某个学生从来没参加过某门课的考试,也要在结果中显示为 0,不能跳过该组合。结果按学生 ID 和科目名排序。
🔑 答案
SELECT
s.student_id,
s.student_name,
sub.subject_name,
COUNT(e.subject_name) AS attended_exams
FROM Students s
CROSS JOIN Subjects sub
LEFT JOIN Examinations e ON s.student_id = e.student_id AND sub.subject_name = e.subject_name
GROUP BY s.student_id, s.student_name, sub.subject_name
ORDER BY s.student_id, sub.subject_name;
💡 解析
- 笛卡尔积 (CROSS JOIN):这是解决“每个学生必须对应每个科目”的银弹。
Students CROSS JOIN Subjects会生成所有学生与所有科目的全部排列组合。 - LEFT JOIN 挂载考试数据:在上述全量组合的基础上,左连接考试表。这样,即便某生没考过某课,那一行记录依然存在。
- COUNT() 的对象:这里千万不要写
COUNT(*),要写COUNT(e.subject_name)。因为COUNT(*)会统计行数(即便数据是 NULL 也是 1 行),而COUNT(列名)只会统计非 NULL 的个数。 - 多字段分组:由于结果集包含学生信息和课程名,必须按这三个字段一起进行
GROUP BY。
13. 至少有5个直接下属的经理
- 题号:570
- 原题链接:570. 至少有5个直接下属的经理
📝 题目复述
在单一表中通过父子关系找出符合特定计数的记录。 表结构定义:
Employee(员工表):包含id(主键),name,department和managerId(上级的 ID)。
目标任务: 找出那些至少有五个直接下属的经理的名字。
🔑 答案
SELECT name
FROM Employee
WHERE id IN (
SELECT managerId
FROM Employee
GROUP BY managerId
HAVING COUNT(id) >= 5
);
💡 解析
- 子查询逻辑:先去查谁是那个“拥有 5 个以上小弟”的人。我们对
managerId进行分组,然后使用HAVING过滤出记录数(下属人数)大于等于 5 的那些 ID。 - HAVING 子句:
WHERE作用于每一行,而HAVING作用于聚合后的每一组。这里必须用HAVING来过滤统计出来的数字。 - 内连接法 (Alternative):这题也可以通过
JOIN自身来解,但在逻辑层级清晰度上,子查询在本题更直观。
14. 确认率
- 题号:1934
- 原题链接:1934. 确认率
📝 题目复述
计算每个用户的消息确效率。 表结构定义:
Signups(注册表):包含user_id,time_stamp。Confirmations(确认表):包含user_id,time_stamp,action(枚举值: 'confirmed' 或 'timeout')。
目标任务:
确认率定义为:已确认的消息数 / 总请求次数。如果没有请求,则确效率为 0。请计算每个用户的确效率,保留两位小数。
🔑 答案
SELECT
s.user_id,
ROUND(IFNULL(AVG(c.action = 'confirmed'), 0), 2) AS confirmation_rate
FROM Signups s
LEFT JOIN Confirmations c ON s.user_id = c.user_id
GROUP BY s.user_id;
💡 解析
- 聚合中的逻辑运算 (MySQL 黑科技):
AVG(c.action = 'confirmed')非常巧妙。当 action 等于 'confirmed' 时,括号内结果为 1,否则为 0。对其取平均值,正好就是确效率。 - IFNULL 处理无记录用户:如果一个用户只有注册信息但没有发送确认请求,
AVG会返回NULL。通过IFNULL(val, 0)将这些人的确认率强制转换为 0。 - 分组细节:我们要的是“每个”用户的确效率,所以以
Signups表为左表进行左连接,确保覆盖所有注册用户。
15. 有趣的电影
- 题号:620
- 原题链接:620. 有趣的电影
📝 题目复述
基本的行过滤与排序练习。 表结构定义:
cinema(电影表):包含id,movie,description(对电影的描述),rating(评分)。
目标任务: 筛选出那些:
id是奇数。- 描述
description不是 "boring"(不乏味)。 - 结果按照评分
rating从高到低(降序)排列。
🔑 答案
SELECT id, movie, description, rating
FROM cinema
WHERE id % 2 != 0 AND description != 'boring'
ORDER BY rating DESC;
💡 解析
- 取模运算
%:判断奇数最常用的办法是id % 2 != 0。 - 不等于号:在 SQL 中表示“不等于”,可以用
!=也可以用<>。 - 排序顺序:升序是
ASC,降序是DESC。题目要求从高到低评分,必须写DESC。
16. 平均售价
- 题号:1251
- 原题链接:1251. 平均售价
📝 题目复述
计算每种产品在一段时期内的平均售价。 表结构定义:
Prices(价格表):包含product_id,start_date,end_date,price(该时间段内的价格)。UnitsSold(销量表):包含product_id,purchase_date,units(购买数量)。
目标任务:
平均售价的计算公式为:该产品总销售额 / 总销售数量。请查出每个产品的平均售价,并保留两位小数。
注意点: 关联条件除了 product_id,销量表中的 purchase_date 必须落在价格表的 start_date 和 end_date 之间。如果某产品没有销售记录,则其平均售价为 0。
🔑 答案
SELECT
p.product_id,
IFNULL(ROUND(SUM(p.price * u.units) / SUM(u.units), 2), 0) AS average_price
FROM Prices p
LEFT JOIN UnitsSold u ON p.product_id = u.product_id
AND u.purchase_date BETWEEN p.start_date AND p.end_date
GROUP BY p.product_id;
💡 解析
- 带范围的 JOIN:这道题的
ON条件不再仅仅是等于号,而是使用了BETWEEN ... AND ...,这是连接条件非常灵活的一种体现。 - 加权平均值计算:平均单价不是价格的平均值,而是总额除以总数。公式为
SUM(单价 * 数量) / SUM(数量)。 - 空值与保留小数:
IFNULL(..., 0)处理没有销量的情况。ROUND(..., 2)用于保留题目要求的两位小数。
17. 项目员工 I
- 题号:1075
- 原题链接:1075. 项目员工 I
📝 题目复述
查询每个项目所属员工的平均工龄。 表结构定义:
Project(项目表):包含project_id和employee_id。Employee(员工表):包含employee_id,name,experience_years(工龄)。
目标任务:
统计每个项目的 project_id 以及其参与员工的平均工龄 average_years。结果保留两位小数。
🔑 答案
SELECT
p.project_id,
ROUND(AVG(e.experience_years), 2) AS average_years
FROM Project p
JOIN Employee e ON p.employee_id = e.employee_id
GROUP BY p.project_id;
💡 解析
- 聚合分组基础:先通过
project_id对记录进行分堆。 - AVG 函数:直接对该组内的
experience_years列应用AVG即可得到平均值。 - 连接选择:这里使用
JOIN即可,题目暗示了能被统计到项目的员工一定存在于员工表中。
18. 各赛事用户注册率
- 题号:1633
- 原题链接:1633. 各赛事用户注册率
📝 题目复述
计算每个赛事中有多少比例的用户进行了注册。 表结构定义:
Users(用户表):包含user_id,user_name。Register(注册表):包含contest_id,user_id。
目标任务:
对于每一个赛事,计算注册了该赛事的人数占总人数(Users 表总行数)的百分比。
结果排序要求: 按照百分比 percentage 降序排列;如果百分比相同,再按 contest_id 升序排列。
🔑 答案
SELECT
contest_id,
ROUND(COUNT(user_id) * 100 / (SELECT COUNT(*) FROM Users), 2) AS percentage
FROM Register
GROUP BY contest_id
ORDER BY percentage DESC, contest_id ASC;
💡 解析
- 子查询求分母:注册率的分母是全表总人数,这是一个固定的常数,因此在表达式里嵌入
(SELECT COUNT(*) FROM Users)来动态获取。 - 百分数计算:不要忘了乘以 100。
- 排序技巧:在
ORDER BY中,可以根据计算出的字段别名percentage进行排序,多级排序用逗号隔开。
19. 查询结果的质量和占比
- 题号:1211
- 原题链接:1211. 查询结果的质量和占比
📝 题目复述
这是一个衡量查询服务质量的任务。 表结构定义:
Queries(查询表):包含query_name(查询类型),result,position(排序位置,1 到 500),rating(评分,1 到 5)。
目标任务:
计算每个 query_name 的以下两个指标:
- 质量 (quality):该查询下所有 (
rating / position) 的平均值。 - 劣质查询占比 (poor_query_percentage):在该查询下,评分
rating小于 3 的记录占总查询记录的百分比。 以上两个结果均保留两位小数。
🔑 答案
SELECT
query_name,
ROUND(AVG(rating / position), 2) AS quality,
ROUND(SUM(IF(rating < 3, 1, 0)) * 100 / COUNT(*), 2) AS poor_query_percentage
FROM Queries
WHERE query_name IS NOT NULL
GROUP BY query_name;
💡 解析
- 混合计算:SQL 允许在聚合函数(如
AVG)中写算术表达式,系统会先对每行算rating / position,再取总平均。 - 条件统计 (SUM+IF):
SUM(IF(rating < 3, 1, 0))是一个非常实用的套路。如果满足条件则给 1,否则给 0,求和结果就是满足条件的总件数。 - 判空处理:题目数据中可能存在
query_name为空的情况,如果不加筛选会导致分组中多出一个空项,建议使用WHERE排除。
20. 每月交易 I
- 题号:1193
- 原题链接:1193. 每月交易 I
📝 题目复述
按照月份统计支付信息。 表结构定义:
Transactions(交易表):包含id,country(国家),state(交易状态:'approved' 已批准或 'declined' 已拒绝),amount(金额),trans_date(交易日期)。
目标任务: 编写一条 SQL 统计每个月、每个国家的:
- 总交易次数。
- 已批准(
approved)的总交易次数。 - 总交易金额。
- 已批准的总交易金额。
🔑 答案
SELECT
DATE_FORMAT(trans_date, '%Y-%m') AS month,
country,
COUNT(*) AS trans_count,
SUM(IF(state = 'approved', 1, 0)) AS approved_count,
SUM(amount) AS trans_total_amount,
SUM(IF(state = 'approved', amount, 0)) AS approved_total_amount
FROM Transactions
GROUP BY month, country;
💡 解析
- 日期格式化函数
DATE_FORMAT:因为原始日期包含具体的日(如 2019-12-18),为了实现“按月统计”,我们通过格式化将所有日期统一为YYYY-MM的形式,然后参与GROUP BY。 - 条件统计技巧升级:
- 求“已批准次数”:
SUM(state = 'approved')在 MySQL 中成立(True 会变成 1),但标准的写法是使用IF。 - 求“已批准总金额”:通过
IF(state = 'approved', amount, 0)。当交易不是 approved 时,算金额的时候加上 0。
- 求“已批准次数”:
- 多列分组:
GROUP BY month, country表示只有月份和国家都相同的记录才会归为一堆进行聚合计算。
21. 即时食物配送 II
- 题号:1174
- 原题链接:1174. 即时食物配送 II
📝 题目复述
统计每位客户首次订单中“即时订单”的占比。 表结构定义:
Delivery(配送表):包含delivery_id,customer_id,order_date(下单日期),customer_pref_delivery_date(客户期望配送日期)。
概念说明:
- 即时订单:如果下单日期 = 期望配送日期。
- 计划订单:如果下单日期 != 期望配送日期。
- 首单:每个客户
order_date最早的那一笔订单(题目保证每个客户首单日期唯一)。
目标任务: 找出所有客户首单中,“即时订单”所占的百分比,保留两位小数。
🔑 答案
SELECT
ROUND(
SUM(order_date = customer_pref_delivery_date) * 100 / COUNT(*),
2
) AS immediate_percentage
FROM Delivery
WHERE (customer_id, order_date) IN (
SELECT customer_id, MIN(order_date)
FROM Delivery
GROUP BY customer_id
);
💡 解析
- 锁定首单记录:使用子查询
SELECT customer_id, MIN(order_date) ... GROUP BY customer_id选出每个人的最早下单日期,并配合IN子句对主表进行过滤。 - 计算占比:在被筛选出的首单数据集中,使用
SUM(条件)统计符合“即时订单”的数量。在 MySQL 中,order_date = customer_pref_delivery_date为真返回 1,否则返回 0。 - 计算与取整:
* 100 / COUNT(*)得到百分比,再用ROUND(..., 2)保留两位小数。
22. 游戏玩法分析 IV
- 题号:550
- 原题链接:550. 游戏玩法分析 IV
📝 题目复述
计算玩家在首次登录后,“次日再次登录”的比例。 表结构定义:
Activity(活动表):包含player_id,device_id,event_date(登录日期),games_played。
目标任务: 报告在首次登录后的第二天再次登录的玩家人数占总玩家人数的比例,结果保留两位小数。
🔑 答案
SELECT
ROUND(COUNT(a2.player_id) / COUNT(a1.player_id), 2) AS fraction
FROM (
SELECT player_id, MIN(event_date) AS first_login
FROM Activity
GROUP BY player_id
) a1
LEFT JOIN Activity a2 ON a1.player_id = a2.player_id
AND DATEDIFF(a2.event_date, a1.first_login) = 1;
💡 解析
- 确定首次登录日期:通过派生表
a1找出每个人的MIN(event_date)。 - 次日留存匹配:将首登表
a1与原始表Activity(命名为a2) 进行LEFT JOIN。连接条件是player_id相等且a2.event_date比first_login正好晚 1 天(使用DATEDIFF = 1)。 - 统计分子与分母:
COUNT(a1.player_id):计算所有玩家的总数(分母)。COUNT(a2.player_id):由于是LEFT JOIN,如果玩家第二天没来,a2的字段会是NULL。COUNT(列名)恰好不统计NULL值,从而得到“连续登录的人数”。
23. 每位教师所授课程的数目
- 题号:2356
- 原题链接:2356. 每位教师所授课程的数目
📝 题目复述
统计教师独立教授的不同科目数量。 表结构定义:
Teacher(教师表):包含teacher_id,subject_id(课程 ID),dept_id(部门 ID)。
目标任务:
请查询每一位教师 (teacher_id) 教授的不同科目数量。
🔑 答案
SELECT
teacher_id,
COUNT(DISTINCT subject_id) AS cnt
FROM Teacher
GROUP BY teacher_id;
💡 解析
- 分组计数:按
teacher_id进行GROUP BY是常规操作。 - 去重计数:同一个老师可能在不同的部门 (
dept_id) 教同一门课。为了计算“不同科目的数量”,必须在COUNT函数内部使用DISTINCT关键字。
24. 查询近30天活跃用户数
- 题号:1141
- 原题链接:1141. 查询近30天活跃用户数
📝 题目复述
计算特定日期区间内的每日日活人数。 表结构定义:
Activity(活动表):包含user_id,session_id,activity_date(活动日期),activity_type(枚举值)。
目标任务: 统计截止到 2019-07-27(包含当天)的 30 天内,每日的活跃用户数。如果一个用户在一天内多次登录,只计算一次。
🔑 答案
SELECT
activity_date AS day,
COUNT(DISTINCT user_id) AS active_users
FROM Activity
WHERE activity_date BETWEEN '2019-06-28' AND '2019-07-27'
GROUP BY activity_date;
💡 解析
- 日期筛选区间:题目要求的区间是 2019-07-27 向前推 30 天,即 2019-06-28 到 2019-07-27。在 SQL 中使用
BETWEEN '开始日期' AND '结束日期'。 - 日活 (UV) 计算:按日期分组后,通过
COUNT(DISTINCT user_id)来确保同一用户即便有多条日志也只计数为 1。
25. 产品销售分析 III
- 题号:1070
- 原题链接:1070. 产品销售分析 III
📝 题目复述
查询每个产品在其第一年销售时的详细数据。 表结构定义:
Sales(销售表):包含sale_id,product_id,year,quantity,price。Product(产品表):包含product_id,product_name。
目标任务:
查询每个产品的 ID (product_id)、第一年销售的年份 (first_year)、对应的数量 (quantity) 以及单价 (price)。
🔑 答案
SELECT
product_id,
year AS first_year,
quantity,
price
FROM Sales
WHERE (product_id, year) IN (
SELECT product_id, MIN(year)
FROM Sales
GROUP BY product_id
);
💡 解析
- 筛选最小年份:由于同一产品在一年内可能有多笔订单,我们需要找出每个产品第一次出现的那个
year。首先用子查询通过GROUP BY product_id和MIN(year)确定每个产品的起始年。 - 二元组合过滤:使用
WHERE (product_id, year) IN (...)。这是一个非常有用的技巧,它能确保我们匹配到的是该产品对应的那个具体的最小年份,而不是单纯匹配 ID 或年份。 - 注意点:题目只要求第一年(不论该年内有多少笔订单)的数据。如果该产品在第一年里卖了 3 次,结果里应展示这 3 行。因此通过 IN 筛选主表能保留所有第一年的交易记录。
26. 超过5名学生的课
- 题号:596
- 原题链接:596. 超过5名学生的课
📝题目复述
筛选出选课人数达到一定标准的课程。 表结构定义:
Courses表:包含student(学生名) 和class(课程名)。(student, class) 是该表的主键组合。
目标任务: 列出所有选修学生人数大于或等于 5 的课程。
🔑 答案
SELECT class
FROM Courses
GROUP BY class
HAVING COUNT(student) >= 5;
💡 解析
- 分组基础:因为我们要统计每门课的人数,所以必须对
class进行GROUP BY。 - HAVING 与 WHERE 的区别:
WHERE在分组前筛选,它无法直接处理像COUNT()这种聚合后的统计值。HAVING在分组后执行,用于过滤符合条件的“组”。
- 统计逻辑:
COUNT(student)统计出每个组里有多少学生,满足条件则输出该课程名。
27. 寻找各站点的粉丝数
- 题号:1729
- 原题链接:1729. 寻找各站点的粉丝数
📝题目复述
统计社交平台中每位用户的粉丝总数。 表结构定义:
Followers表:包含user_id(用户 ID) 和follower_id(粉丝 ID)。(user_id, follower_id) 是表的主键。
目标任务:
对于每个用户,返回其粉丝人数。结果需按 user_id 升序排列。
🔑 答案
SELECT user_id, COUNT(follower_id) AS followers_count
FROM Followers
GROUP BY user_id
ORDER BY user_id ASC;
💡 解析
- GROUP BY 用户:为了汇总每位用户的状态,以
user_id作为分组列。 - 基础聚合:
COUNT(follower_id)即可算出粉丝总数。由于 (user_id, follower_id) 是主键,天然不重复,不需要在此额外写DISTINCT。 - 排序:明确题目要求的升序排列。
28. 最大的单数
- 题号:619
- 原题链接:619. 最大的单数
📝题目复述
寻找数据集中只出现过一次的数字里的最大值。 表结构定义:
MyNumbers表:仅包含一列num(整数)。
目标任务:
“单数”指在表中只出现过一次的数字。请找出这些单数中最大的一个。如果不存在这样的数字,则输出 null。
🔑 答案
SELECT MAX(num) AS num
FROM (
SELECT num
FROM MyNumbers
GROUP BY num
HAVING COUNT(num) = 1
) AS single_nums;
💡 解析
- 分层处理逻辑:
- 第一步:内部子查询。通过
GROUP BY num配合HAVING COUNT(num) = 1,筛选出所有只出现过一次的数字。 - 第二步:外部聚合。在子查询筛选出的“单数列表”里,使用
MAX(num)挑出最大值。
- 第一步:内部子查询。通过
- NULL 值特性:在 SQL 中,对一个空的结果集执行
MAX()聚合运算,结果会自动返回NULL,这完美符合题目要求的边缘情况。
29. 买过所有产品的客户
- 题号:1045
- 原题链接:1045. 买过所有产品的客户
📝题目复述
查找出完成了全品种购买的“大满贯”客户。 表结构定义:
Customer表:包含customer_id和product_key。注意,客户可能对同一产品有多次购买记录。Product表:包含product_key,记录了公司销售的所有产品种类。
目标任务:
查询买过 Product 表中所有类型产品的客户 ID。
🔑 答案
SELECT customer_id
FROM Customer
GROUP BY customer_id
HAVING COUNT(DISTINCT product_key) = (SELECT COUNT(*) FROM Product);
💡 解析
- 动态比较:买齐所有产品的标志是“该客户购买的不同产品 ID 数量”等于“产品库的总产品数”。
- 子查询作常数:使用
(SELECT COUNT(*) FROM Product)获取当前全品类到底有多少个。 - DISTINCT 的重要性:因为客户可能买了同一个东西多次,在统计其购买品种数时,必须用
COUNT(DISTINCT product_key)进行去重计数,否则计算结果会偏差。
30. 每位经理下属员工数量
- 题号:1731
- 原题链接:1731. 每位经理下属员工数量
📝题目复述
汇总经理级员工及其管辖团队的相关信息。 表结构定义:
Employees表:包含employee_id(主键),name,reports_to(经理 ID),age。
核心逻辑:
- 经理是指至少有一名员工向其汇报(即
employee_id出现在某人的reports_to列中)。
目标任务:
查询每位经理的 employee_id、name、其直接下属的人数 reports_count、以及下属的平均年龄(平均年龄需四舍五入到最近的整数)。结果需按 employee_id 排序。
🔑 答案
SELECT
e1.employee_id,
e1.name,
COUNT(e2.employee_id) AS reports_count,
ROUND(AVG(e2.age)) AS average_age
FROM Employees e1
JOIN Employees e2 ON e1.employee_id = e2.reports_to
GROUP BY e1.employee_id, e1.name
ORDER BY e1.employee_id;
💡 解析
- 自连接关系绑定:我们将
Employees表视为两张虚拟表:e1(代表经理):它的employee_id是管理者 ID。e2(代表员工):它的reports_to是员工上司。- 通过
e1.employee_id = e2.reports_to将两级关系对齐。
- 筛选经理:使用内连接
JOIN。如果某人没有下属(其 ID 没在reports_to出现过),连接后就不会产生对应记录。 - 数值处理:
COUNT(e2.employee_id):计算经理 e1 的所有下属数量。ROUND(AVG(e2.age)):先算平均年龄,再用ROUND()进行四舍五入取整。
- 多列分组:
GROUP BY e1.employee_id, e1.name确保查询的非聚合列都能合法输出。
31. 员工的直属部门
- 题号:1789
- 原题链接:1789. 员工的直属部门
📝题目复述
在一个员工可能属于多个部门的情况下,确定他们的直属部门。 表结构定义:
Employee表:包含employee_id(员工 ID),department_id(部门 ID),primary_flag(直属标志)。primary_flag是一个枚举值,'Y' 表示该部门是直属部门,'N' 表示不是。
筛选规则:
- 如果一个员工只属于一个部门,那么该部门就是其直属部门(无论
primary_flag是什么)。 - 如果一个员工属于多个部门,则其
primary_flag为 'Y' 的部门是其直属部门。
🔑 答案
SELECT employee_id, department_id
FROM Employee
WHERE primary_flag = 'Y'
UNION
SELECT employee_id, department_id
FROM Employee
GROUP BY employee_id
HAVING COUNT(department_id) = 1;
💡 解析
- 分情况汇总:这道题最清晰的方法是把两种符合条件的情况“拼”在一起。
- 情况 1:找出所有显式标注了
primary_flag = 'Y'的记录。 - 情况 2:找出那些在表中只出现过一次(即只属于一个部门)的员工。
- 情况 1:找出所有显式标注了
- UNION 运算符:使用
UNION将两个查询结果集合并。UNION会自动去除两个结果集中的重复行(例如某个员工只有一个部门且正好该标记也是 'Y')。 - 分组过滤:在第二个查询中,通过
GROUP BY employee_id并使用HAVING COUNT(*) = 1来锁定那些“没得选”只能选这唯一部门的员工。
32. 三角形判断
- 题号:610
- 原题链接:610. 三角形判断
📝题目复述
根据三条边的长度判断是否能组成三角形。 表结构定义:
Triangle表:包含x,y,z三个整数列,代表三条边的长度。
目标任务:
判断每一行数据中的 x, y, z 是否能组成一个三角形。
- 三角形的形成规则:任意两边之和大于第三边。
🔑 答案
SELECT *,
IF(x + y > z AND x + z > y AND y + z > x, 'Yes', 'No') AS triangle
FROM Triangle;
💡 解析
- 流程控制函数
IF:- 语法:
IF(expr1, expr2, expr3)。如果表达式expr1为真,返回expr2,否则返回expr3。 - 这里我们直接利用三角形的几何性质作为逻辑判断。
- 语法:
- CASE WHEN 替代方案:你也可以使用更通用的 SQL 语法
CASE WHEN condition THEN 'Yes' ELSE 'No' END。 - 多条件逻辑:注意必须三个条件同时满足(用
AND连接),任意一组边不符合规则都无法组成三角形。
33. 经理已离职的员工
- 题号:1978
- 原题链接:1978. 经理已离职的员工
📝题目复述
查找管理层变动后导致“孤立”的低薪员工。 表结构定义:
Employees表:包含employee_id(主键),name,manager_id(经理的 ID),salary。
目标任务:
找出同时满足以下条件的员工 employee_id:
- 员工的月薪严格少于 $30000。
- 该员工有经理(
manager_id不为空),但这位经理的employee_id已经不在Employees表中了(说明经理离职了)。 - 结果按
employee_id排序。
🔑 答案
SELECT employee_id
FROM Employees
WHERE salary < 30000
AND manager_id IS NOT NULL
AND manager_id NOT IN (SELECT employee_id FROM Employees)
ORDER BY employee_id;
💡 解析
- 子查询筛选:
SELECT employee_id FROM Employees会拿到当前全公司所有在职人员的 ID 集合。 - NOT IN 逻辑:
manager_id NOT IN (...)表示该员工所汇报的经理 ID 不在这个在职人员名单里。 - 复合条件:注意题干中的每一个限定词:“薪水少于 30000” 对应
salary < 30000,“有经理” 对应manager_id IS NOT NULL。
34. 交换座位
- 题号:626
- 原题链接:626. 交换座位
📝题目复述
在教室座位表中,交换相邻两位同学的座位。 表结构定义:
Seat表:包含id(自增) 和student(学生姓名)。
目标任务: 交换相邻两个学生的座位。
id为 1 和 2 的人交换位置。id为 3 和 4 的人交换位置。- 特殊规则:如果最后一位同学的
id是奇数(即总人数为奇数,没人能跟他换),则该同学的座位保持不变。
🔑 答案
SELECT
CASE
WHEN MOD(id, 2) != 0 AND id = (SELECT MAX(id) FROM Seat) THEN id
WHEN MOD(id, 2) != 0 THEN id + 1
ELSE id - 1
END AS id,
student
FROM Seat
ORDER BY id;
💡 解析
- 重塑 ID 的逻辑:我们的目标是把名字对应的 ID 改掉。
- 如果 ID 是奇数且不是最后一名:那他就去偶数座,即
id + 1。 - 如果 ID 是偶数:那他就去前面的奇数座,即
id - 1。 - 如果 ID 是奇数且正好是最大 ID:保持原状。
- 如果 ID 是奇数且不是最后一名:那他就去偶数座,即
- MOD 运算:
MOD(id, 2) != 0用于识别奇数。 - 排序:名字没换,只是 ID 数值变了,最后一定要用
ORDER BY id让座位表恢复正常的数字顺序显示。
35. 电影评分
- 题号:1341
- 原题链接:1341. 电影评分
📝题目复述
分析用户的点评行为和电影的受欢迎程度。 表结构定义:
Movies(电影表):包含movie_id,title(电影名)。Users(用户表):包含user_id,name(用户名)。MovieRating(评分表):包含movie_id,user_id,rating,created_at(点评日期)。
目标任务: 需要返回两个查询结果,并放在一个列里(列名可以任意):
- 查找评论电影数量最多的用户名称。如果有多个用户并列,返回字典序较小的那个名。
- 查找 2020 年 2 月平均评分最高的电影名称。如果有多个并列,返回字典序较小的电影名。
🔑 答案
(SELECT u.name AS results
FROM MovieRating mr
JOIN Users u ON mr.user_id = u.user_id
GROUP BY mr.user_id
ORDER BY COUNT(*) DESC, u.name ASC
LIMIT 1)
UNION ALL
(SELECT m.title AS results
FROM MovieRating mr
JOIN Movies m ON mr.movie_id = m.movie_id
WHERE DATE_FORMAT(mr.created_at, '%Y-%m') = '2020-02'
GROUP BY mr.movie_id
ORDER BY AVG(mr.rating) DESC, m.title ASC
LIMIT 1);
💡 解析
- UNION ALL 的巧妙应用:两个查询看似无关,但题目要求结果合在一个字段。用
UNION ALL可以保留所有查询结果,哪怕这两个名字碰巧一样。 - Top 1 的筛选套路:使用
ORDER BY排序列,再通过LIMIT 1取出最顶尖的一条。 - 并列排序逻辑:
ORDER BY COUNT(*) DESC, name ASC。第一个条件保“最多/最高”,第二个条件(名字字典序)在并列时选首字母最靠前的。 - 日期过滤:
DATE_FORMAT(created_at, '%Y-%m') = '2020-02'用来精准锁定特定的月份进行平均值统计。
36. 餐馆营业额增长
- 题号:1321
- 原题链接:1321. 餐馆营业额增长
📝题目复述
我们需要计算餐馆收入的滚动窗口统计数据。 表结构定义:
Customer表:包含customer_id,name,visited_on(访问日期),amount(消费金额)。
目标任务: 计算客户在每一个 7 天窗口内的消费总额和平均金额(当前日期 + 过去 6 天)。
- 统计应从表中最早日期的第 7 天开始。
average_amount需保留两位小数。- 结果按
visited_on升序排列。
🔑 答案
SELECT
visited_on,
amount,
ROUND(amount / 7, 2) AS average_amount
FROM (
SELECT
visited_on,
SUM(SUM(amount)) OVER (ORDER BY visited_on ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS amount,
DENSE_RANK() OVER (ORDER BY visited_on) AS rnk
FROM Customer
GROUP BY visited_on
) t
WHERE rnk >= 7;
💡 解析
- 两层聚合:首先通过
GROUP BY visited_on把每一天的总金额算出来。 - 窗口函数滚动求和:
SUM(SUM(amount)) OVER (ORDER BY visited_on ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)。这里的核心是指定窗口范围:当前行及前面的 6 行(总共 7 天)。 - 起始点过滤:题目要求从有记录以来的第 7 天开始统计(因为前 6 天凑不满 7 天的窗口)。我们通过
DENSE_RANK()标记天数的排名,在外面嵌套查询时过滤掉rnk < 7的数据。
37. 好友申请 II :谁的好友最多
- 题号:602
- 原题链接:602. 好友申请 II :谁的好友最多
📝题目复述
找出社交平台中拥有好友最多的用户。 表结构定义:
RequestAccepted表:包含requester_id(申请者),accepter_id(接受者),accept_date(接受日期)。- 一条记录代表两个人成为了好友。
目标任务: 找出拥有最多好友的人及其好友数目。 注意点: 一个用户的好友既可以是申请过来的,也可以是别人申请他接受的。
🔑 答案
SELECT id, COUNT(*) AS num
FROM (
SELECT requester_id AS id FROM RequestAccepted
UNION ALL
SELECT accepter_id AS id FROM RequestAccepted
) t
GROUP BY id
ORDER BY num DESC
LIMIT 1;
💡 解析
- 数据的双向合并:在
RequestAccepted表中,每一行意味着两个人都多了一个好友。为了统计每个 ID 的好友总数,需要用UNION ALL把这两列垂直堆叠在一起,统一成一列id。 - 聚合与排序:对堆叠后的
id列进行GROUP BY并执行COUNT(*),这就是每个人的总好友数。 - 取最高值:按照数量
DESC降序排列,利用LIMIT 1取出最顶尖的一位。
38. 2016年的投资
- 题号:585
- 原题链接:585. 2016年的投资
📝题目复述
根据两个筛选条件计算 2016 年的总投保金额。 表结构定义:
Insurance表:包含pid,tiv_2015,tiv_2016,lat(纬度),lon(经度)。
目标任务:
统计符合以下两个条件的投保人 2016 年总投保金额 (tiv_2016):
- 投保人在 2015 年的投保金额 (
tiv_2015) 至少与另一位投保人相同。 - 投保人所处的城市位置 (
lat,lon) 必须是唯一的(不能和其他投保人重复)。
🔑 答案
SELECT ROUND(SUM(tiv_2016), 2) AS tiv_2016
FROM Insurance
WHERE tiv_2015 IN (
SELECT tiv_2015
FROM Insurance
GROUP BY tiv_2015
HAVING COUNT(*) > 1
)
AND (lat, lon) IN (
SELECT lat, lon
FROM Insurance
GROUP BY lat, lon
HAVING COUNT(*) = 1
);
💡 解析
- 条件 1 处理(重复值):利用子查询,对
tiv_2015分组,找出计数大于 1 的值,意味着有人和你投了同样多的钱。 - 条件 2 处理(唯一值):利用子查询,对坐标组合
(lat, lon)分组,找计数等于 1 的记录,确保经纬度组合没重复。 - 结果聚合:将满足这两个
IN条件的tiv_2016相加并保留两位小数。
39. 部门工资前三高的员工
- 题号:185
- 原题链接:185. 部门工资前三高的员工
📝题目复述
找出每个部门中收入最高的三个“打工人”。 表结构定义:
Employee表:包含id,name,salary,departmentId。Department表:包含id,name(部门名称)。
目标任务: 公司的高管希望找出每个部门工资排名前三高的员工。注意:如果有多个人的工资相同,排名会并列。如果有并列的情况,前三名的结果中可能会出现多于三个人的记录。
🔑 答案
SELECT d.name AS Department, e.name AS Employee, e.salary AS Salary
FROM (
SELECT
departmentId, name, salary,
DENSE_RANK() OVER(PARTITION BY departmentId ORDER BY salary DESC) AS rnk
FROM Employee
) e
JOIN Department d ON e.departmentId = d.id
WHERE e.rnk <= 3;
💡 解析
- 窗口函数选择 (DENSE_RANK):这是本题最关键的一点。
DENSE_RANK在处理并列名次时,接下来的排名会是连续的(比如 1, 1, 2)。这符合题目要求的“找出工资前三高”的逻辑,即即使有很多人领最高工资,只要这个数字本身排在前三,对应的人就要显示出来。 - 分区排名:
PARTITION BY departmentId用于在每个部门内独立计算排名。 - 多表联查:在子查询得到排名后,将结果与
Department表连接以获取部门的具体名称,并过滤出rnk <= 3的记录。
40. 修复表中的名字
- 题号:1667
- 原题链接:1667. 修复表中的名字
📝题目复述
统一表中的人名格式。 表结构定义:
Users表:包含user_id和name。
目标任务:
将 name 统一格式化为:第一个字符大写,其余字符小写。结果返回修复后的 user_id 和 name,并按 user_id 排序。
🔑 答案
SELECT user_id,
CONCAT(UPPER(LEFT(name, 1)), LOWER(SUBSTR(name, 2))) AS name
FROM Users
ORDER BY user_id;
💡 解析
- 字符截取:
LEFT(name, 1)获取首个字符。SUBSTR(name, 2)获取从第 2 个位置开始到结尾的所有字符。
- 大小写转换:
UPPER(...)强制首字符大写。LOWER(...)强制剩余部分小写。
- 字符串拼接:使用
CONCAT(...)将修补好的两部分重新连起来,得到符合要求的完整姓名。
41. 按日期分组销售产品
- 题号:1484
- 原题链接:1484. 按日期分组销售产品
📝题目复述
统计每日销售的产品名称及种类。 表结构定义:
Activities表:包含sell_date(销售日期) 和product(产品名称)。
目标任务: 查找每个日期销售的不同产品的数量及名称。
- 销售产品名称应按词典序排列。
- 销售产品名称中间用逗号分隔。
- 结果按
sell_date升序排列。
🔑 答案
SELECT
sell_date,
COUNT(DISTINCT product) AS num_sold,
GROUP_CONCAT(DISTINCT product ORDER BY product SEPARATOR ',') AS products
FROM Activities
GROUP BY sell_date
ORDER BY sell_date;
💡 解析
- 聚合与去重:同一天可能卖出多次同一个产品,因此
COUNT(DISTINCT product)确保统计的是种类的数量。 - 字符串聚合函数
GROUP_CONCAT:- 它是 MySQL 专门用来将多行数据合并为一个字符串的工具。
- 在函数内部,我们可以直接使用
DISTINCT进行去重,使用ORDER BY进行组内排序。 SEPARATOR ','指定了合并时的分隔符。
- 结果输出:按日期分组后,该函数会自动处理该组下所有的产品名称拼接。
42. 列出指定时期内价格上升的产品
- 题号:1327
- 原题链接:1327. 列出指定时期内各产品订单之和
📝题目复述
筛选出在特定月份订购量达到要求的商品。 表结构定义:
Products表:包含product_id,product_name(产品名) 和product_category(类别)。Orders表:包含product_id,order_date(订单日期) 和unit(数量)。
目标任务: 要求获取在 2020 年 2 月份下单量不少于 100 的所有产品的名称和单位数量。
🔑 答案
SELECT p.product_name, SUM(o.unit) AS unit
FROM Products p
JOIN Orders o ON p.product_id = o.product_id
WHERE DATE_FORMAT(o.order_date, '%Y-%m') = '2020-02'
GROUP BY p.product_id
HAVING SUM(o.unit) >= 100;
💡 解析
- 日期过滤:利用
DATE_FORMAT(order_date, '%Y-%m')锁定 2020 年 2 月的数据。也可以用o.order_date BETWEEN '2020-02-01' AND '2020-02-29'。 - 内连接与分组:通过
product_id连接两张表,保证能拿到对应的产品名称,随后根据产品进行分组统计总数。 - 聚合后过滤:对于
SUM(unit)的结果筛选,必须使用HAVING子句,不能直接写在WHERE中。
43. 查找拥有有效邮箱的用户
- 题号:1517
- 原题链接:1517. 查找拥有有效邮箱的用户
📝题目复述
通过正则表达式识别格式正确的邮箱。 表结构定义:
Users表:包含user_id,name,mail(邮件地址)。
合法的邮件格式定义:
- 前缀部分:以字母开头。之后可以包含字母(大写或小写)、数字、下划线
_、句点.或/和破折号-。 - 域名部分:必须以
@leetcode.com结尾。
🔑 答案
SELECT user_id, name, mail
FROM Users
WHERE mail REGEXP '^[a-zA-Z][a-zA-Z0-9_.-]*@leetcode\\.com$';
💡 解析
REGEXP运算符:MySQL 使用正则表达式匹配字符串的强大工具。- 正则分解:
^:匹配字符串的开头。[a-zA-Z]:确保第一个字符必须是字母。[a-zA-Z0-9_.-]*:后面的字符可以是字母、数字、下划线、点或横杠,*代表 0 个或多个。@leetcode\\.com$:以特定的字符串结尾。在正则中.代表任意字符,所以要用\\.对其进行转义,表示真实的“点”。
44. 删除重复的电子邮件
- 题号:196
- 原题链接:196. 删除重复的电子邮件
📝题目复述
执行数据清理操作,保留重复项中 ID 最小的记录。 表结构定义:
Person表:包含id和email。
目标任务:
编写一个 DELETE 语句,删除所有重复的电子邮件。在删除之后,每个电子邮件只能出现一次,且需要保留 id 最小的那条记录。
🔑 答案
DELETE p1
FROM Person p1, Person p2
WHERE p1.email = p2.email AND p1.id > p2.id;
💡 解析
- 自连接删除逻辑:这是最直观的写法。我们将
Person表看成两个完全一样的副本p1和p2。 - 寻找该删的数据:当两行的邮件相同(
p1.email = p2.email),但p1的 ID 更大(p1.id > p2.id)时,说明p1是那个冗余的记录。 - 语法点:
DELETE p1指定了执行删除操作的是p1代表的那部分记录。
45. 第二高的薪水
- 题号:176
- 原题链接:176. 第二高的薪水
📝题目复述
处理数据库中的排序及特殊值返回逻辑。 表结构定义:
Employee表:包含id和salary(薪水)。
目标任务:
查询并返回 Employee 表中第二高的薪水。如果不存在第二高的薪水(例如表中只有一条记录),查询应返回 null。
🔑 答案
SELECT (
SELECT DISTINCT salary
FROM Employee
ORDER BY salary DESC
LIMIT 1 OFFSET 1
) AS SecondHighestSalary;
💡 解析
- 去重与排序:可能有多个员工拿同样的最高薪水,所以先
DISTINCT salary去重,再DESC降序排列。 - 精准定位:
LIMIT 1 OFFSET 1的意思是跳过第一个(最高薪水),取后面的一行数据(即第二高)。 - 强制 NULL 返回:本题的一个精髓在于:将查询放在
SELECT ( ... )子句中作为单值输出。如果子查询没有找到任何行(例如 offset 跳过后没数据了),它会产生一个空值,由于在外层作为表达式使用,查询结果会自动变成单行的NULL。如果你直接运行里面的查询,结果可能是完全没有行。
46. 连续出现的数字
- 题号:180
- 原题链接:180. 连续出现的数字
📝题目复述
查找序列中至少连续出现三次的数字。 表结构定义:
Logs表:包含id(自增主键) 和num(数字)。
目标任务: 找出所有至少连续出现三次的数字。要求返回的数字结果不能重复。
🔑 答案
SELECT DISTINCT l1.num AS ConsecutiveNums
FROM Logs l1, Logs l2, Logs l3
WHERE l1.id = l2.id - 1
AND l2.id = l3.id - 1
AND l1.num = l2.num
AND l2.num = l3.num;
💡 解析
- 多表自连接:因为
id是自增的,我们可以利用Logs表的三个实例(l1, l2, l3)通过id的差值建立对应关系:l1是第一个,l2是第二个(l1.id + 1),l3是第三个(l2.id + 1)。 - 值相等判断:在确认了三条记录在 ID 上是连续的基础上,判断它们的值
num是否全部相等。 - 去重:如果一个数字连续出现了四次,那么会有两组三连满足条件,所以使用
DISTINCT确保每个数字只返回一次。 - 进阶注脚:本题也常使用窗口函数
LEAD()或LAG()来解决,通过对比当前行及其后两行的数据来判断是否连续。
47. 指定日期的产品价格
- 题号:1164
- 原题链接:1164. 指定日期的产品价格
📝题目复述
在价格历史表中查找特定日期的商品最新价格。 表结构定义:
Products表:包含product_id,new_price(变更后的价格),change_date(价格变更日期)。
目标任务: 查找所有产品在 2019-08-16 的价格。 核心规则:
- 如果一个产品在 2019-08-16 之前或当天有变动,其价格应为截止到该日期的最后一次变动价格。
- 如果该产品在 2019-08-16 之前没有任何变动记录,则其价格默认为 10。
🔑 答案
SELECT product_id, new_price AS price
FROM Products
WHERE (product_id, change_date) IN (
SELECT product_id, MAX(change_date)
FROM Products
WHERE change_date <= '2019-08-16'
GROUP BY product_id
)
UNION
SELECT DISTINCT product_id, 10 AS price
FROM Products
WHERE product_id NOT IN (
SELECT product_id
FROM Products
WHERE change_date <= '2019-08-16'
);
💡 解析
- 第一部分(有变动):
- 通过子查询
MAX(change_date)锁定每个产品在指定日期前的最后一次变更时间。 - 使用
(product_id, change_date) IN ...的二元组合匹配主表。
- 通过子查询
- 第二部分(无变动):
- 使用
NOT IN过滤出那些在 2019-08-16 前没有任何历史记录的产品。 - 将价格统一设为常量 10。
- 使用
- 结果合并:利用
UNION将上述两种逻辑产生的结果集纵向合并。
48. 最后一个能进入电梯的人
- 题号:1204
- 原题链接:1204. 最后一个能进入电梯的人
📝题目复述
计算在承重限制下,电梯名单中最后一名能够进入的乘客姓名。 表结构定义:
Queue表:包含person_id,person_name,weight,turn(排队序号,从小到大代表登机顺序)。
限制条件:
- 电梯最大承重为 1000 公斤。
- 乘客按照
turn升序依次登机。
目标任务: 查询谁是最后一个登机且总重量(含其本身)未超过 1000 公斤的人。
🔑 答案
SELECT person_name
FROM (
SELECT person_name,
SUM(weight) OVER(ORDER BY turn) AS cumulative_weight
FROM Queue
) t
WHERE cumulative_weight <= 1000
ORDER BY cumulative_weight DESC
LIMIT 1;
💡 解析
- 累计求和 (Running Total):使用窗口函数
SUM(weight) OVER(ORDER BY turn)计算到当前乘客为止的所有乘客的总重量。 - 筛选重量界限:在外部查询中筛选出
cumulative_weight <= 1000的所有人。 - 寻找转折点:将符合条件的人按照重量降序排列 (
DESC),那么第一个出现的(也就是LIMIT 1)必然是能够登上的最后一个人。
49. 按分类统计薪水
- 题号:1907
- 原题链接:1907. 按分类统计薪水
📝题目复述
对薪资进行区间分档并统计每档的账户数量。 表结构定义:
Accounts表:包含account_id和income(月薪)。
档位定义:
- "Low Salary":薪水严格小于 $20000。
- "Average Salary":薪水在 [
50000] 之间(包含边界)。
- "High Salary":薪水严格大于 $50000。
目标任务: 统计各档位的账户数。如果某个档位下没有账户,结果也必须显示该档位名称并将数量记为 0。
🔑 答案
SELECT 'Low Salary' AS category, SUM(income < 20000) AS accounts_count FROM Accounts
UNION
SELECT 'Average Salary' AS category, SUM(income BETWEEN 20000 AND 50000) AS accounts_count FROM Accounts
UNION
SELECT 'High Salary' AS category, SUM(income > 50000) AS accounts_count FROM Accounts;
💡 解析
- 攻克“显示 0”的需求:普通的
CASE WHEN + GROUP BY有一个缺陷:如果某区间完全没数据,该区间在结果里就会消失。为了满足显示 0 的要求,最稳妥的办法是手动列出这三个UNION。 - 统计计数:在每个
SELECT分句中,直接使用SUM(条件判断)。例如SUM(income < 20000)在 MySQL 中会统计 true(1)的个数,得到账户数量。 - 数据拼接:将三个独立的区间查询通过
UNION堆叠,确保结果一定包含三行记录。
50. 每个员工每天上班的总时间
- 题号:1741
- 原题链接:1741. 计算每个员工每天上班的总时间
📝题目复述
通过日志表计算员工单日打卡的净时长。 表结构定义:
Employees表:包含emp_id,event_day(日期),in_time(入职/进入时间,单位:分钟),out_time(离职/退出时间,单位:分钟)。
目标任务: 计算每个员工在每一天花在办公室的总时长。单位为分钟。
- 单次停留时长 =
out_time-in_time。 - 同一人同一天可能会有多次进出,需要进行累计。
🔑 答案
SELECT
event_day AS day,
emp_id,
SUM(out_time - in_time) AS total_time
FROM Employees
GROUP BY event_day, emp_id;
💡 解析
- 分组维度:我们需要的是“某员工”在“某一天”的总时长。因此
GROUP BY需要同时包含日期和员工 ID。 - 算术运算与聚合:在 SQL 中可以先算
out_time - in_time,再套上SUM()函数进行累加求和,算出全天的总工作时长。 - 结果字段命名:根据题目要求,使用
AS day和AS total_time对结果列进行重命名以符合预期。
经典MySQL知识
曼迪匹艾公司福利 135人发布
