力扣 SQL 基础 50 题:题解(题目名称+题号+原题链接+题目复述+答案+解析)

【力扣 SQL 50 题】力扣 SQL 50 题

01. 可回收且低脂的产品

📝 题目复述

本题要求从一张名为 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';

💡 解析

  1. WHERE 多条件组合:当我们需要筛选出的数据必须同时满足多个过滤标准时,使用逻辑运算符 AND
  2. 筛选流程:SQL 引擎会逐行扫描 Products 表,只有当该行的 low_fats 列和 recyclable 列的值同时等于 'Y' 时,该行的 product_id 才会进入结果集。
  3. 注意:这里的 'Y' 是字符串(ENUM类型),在书写 SQL 时必须用单引号括起来。

02. 寻找用户推荐人

📝 题目复述

我们要处理的是一张 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;

💡 解析

  1. 核心陷阱:NULL 的逻辑计算:这是 SQL 新手最容易出错的地方。在 SQL 中,NULL 不代表“0”,也不代表“空字符串”,它代表“未知”。
  2. 三值逻辑
    • 2 <> 2 结果是 FALSE
    • 1 <> 2 结果是 TRUE
    • NULL <> 2 结果是 UNKNOWN(未知)。
  3. 筛选原理WHERE 只会选取结果为 TRUE 的记录。由于 NULL <> 2 的结果不是 TRUE,所以如果不显式加上 OR referee_id IS NULL,系统会自动排除掉那些推荐人为 NULL 的用户。

03. 大的国家

📝 深度题目复述

World 存储了全球各国家的信息。 表结构定义:

  • name (varchar): 国家名。
  • continent (varchar): 洲名。
  • area (int): 面积。
  • population (int): 人口。
  • gdp (bigint): 国内生产总值。

目标任务: 查询符合以下任一条件的国家名称 (name)、人口 (population) 和面积 (area):

  1. 面积至少为 300 万平方公里(area >= 3,000,000)。
  2. 人口至少为 2500 万(population >= 25,000,000)。

🔑 答案

SELECT name, population, area
FROM World
WHERE area >= 3000000 OR population >= 25000000;

💡 解析

  1. OR 运算符:与 AND 相反,OR 只需要满足多个条件中的其中一个,该行记录就会被选中。
  2. 多字段选择:题目明确要求输出 name, population, area 三列,因此在 SELECT 后面我们要按序罗列这些列名,中间用逗号分隔,不要直接写 SELECT *
  3. 进阶优化(Tip):虽然本题 OR 可以通过,但在面试中,面试官有时会问及性能。对于大数量级的表,使用 UNION 有时比 OR 能更好地利用索引。

04. 文章浏览 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;

💡 解析

  1. DISTINCT 去重:由于同一个作者可能多次阅读自己的文章,表里会出现重复的 ID,必须加上 DISTINCT 保证每个 ID 在结果里只出现一次。
  2. 字段对比过滤WHERE author_id = viewer_id 是本题的业务逻辑核心。
  3. ORDER BY 排序ASC 代表升序(从 1 到 100),它是默认设置,即便不写 ASC 结果也是升序的,但写上后代码可读性更佳。
  4. 别名 alias:题目要求结果列名为 id,我们用 AS idauthor_id 进行重命名。

05. 无效的推文

📝 题目复述

处理一张名为 Tweets 的推文表。 表结构定义:

  • tweet_id (int): 推文唯一 ID。
  • content (varchar): 推文的正文内容。

目标任务: 筛选出所有“无效”的推文 ID。 无效的定义是:推文正文 content 中包含的字符数严格大于 15

🔑 答案

SELECT tweet_id
FROM Tweets
WHERE CHAR_LENGTH(content) > 15;

💡 解析

  1. 长度函数的区别(重点)
    • LENGTH(string):返回字符串的字节数。对于多字节字符(如中文、特殊符号),结果可能大于字符数。
    • CHAR_LENGTH(string):返回字符串的字符数
    • 面试点:在 MySQL 环境中,题目考查的是字数限制,建议养成使用 CHAR_LENGTH 的习惯,以规避编码带来的差异风险(虽然此题主要考查英文内容)。
  2. 比较运算符> 15 表示“大于 15”,不包含 15 本身。

收到!接下来的这 5 道题是 SQL 入门中最重要的**连接(JOIN)分组聚合(GROUP BY)**部分,也是实际面试和工作中频率最高的基础技能。

06. 使用唯一标识码替换员工ID

📝 题目复述

本题涉及到两张表的信息关联。 表结构定义:

  • Employees (员工表):包含 id (主键) 和 name (员工姓名)。
  • EmployeeUNI (唯一标识表):包含 id (和员工表对应) 和 unique_id (唯一标识码)。

目标任务: 我们需要展示每个用户的 unique_idname注意点: 如果某个员工在 EmployeeUNI 表中没有唯一的标识码,那么在结果中其 unique_id 应当显示为 null,但该员工的名字依然需要出现在结果中。

🔑 答案

SELECT 
    eu.unique_id, 
    e.name
FROM Employees e
LEFT JOIN EmployeeUNI eu ON e.id = eu.id;

💡 解析

  1. 左外连接 (LEFT JOIN):这是本题的魂。如果我们用普通的 JOIN (即 INNER JOIN),那些没有唯一码的员工会被直接删掉。LEFT JOIN 的逻辑是:以左表(Employees)为主,即使右表(EmployeeUNI)匹配不到,也会保留左表的记录,找不到的列补为 NULL
  2. 表别名Employees e 给表取了个外号 e,这在连接多个表时可以大大简化 SQL 的长度,避免字段歧义。

07. 产品销售分析 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;

💡 解析

  1. 内连接 (JOIN):这里使用普通的内连接即可,因为我们要查询的是“每一笔销售”,由于 product_id 是产品表的主键且通常销售数据对应的产品必定存在,使用 JOIN 可以取出两个表交集部分的匹配数据。
  2. SELECT 列的选择:注意是从 Product 表里取名字,从 Sales 表里取年份和价格。

08. 进店却未进行交易的顾客

📝 题目复述

我们需要统计那些进店了却没买东西的倒霉蛋,涉及两张记录表。 表结构定义:

  • 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;

💡 解析

  1. 左连接过滤法:这是找“不存在于 A 表但在 B 表中”的数据的经典方案。我们用 Visits 左连 Transactions,如果右表没对应的交易,那右表的字段 transaction_id 必然为 NULL
  2. WHERE 子句:筛选出这些 t.transaction_id IS NULL 的记录。
  3. 聚合分组:因为我们要统计每个客户不买东西的次数,所以必须对 customer_id 进行 GROUP BY(分组),然后用 COUNT 计算记录行数。

09. 上升的温度

📝 题目复述

比较同一张表中不同日期的数值。 表结构定义:

  • 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;

💡 解析

  1. 自连接 (Self Join):因为我们需要把今天的温度和昨天的温度进行对比,数据全在同一张表里,所以把 Weather 表分别取名为 w1(代表今天)和 w2(代表昨天)。
  2. 日期计算函数 DATEDIFF:在 MySQL 中,DATEDIFF(end, start) 返回的是天数之差。我们需要让 w1 的日期减去 w2 的日期正好等于 1。
  3. 注意点:不能直接用 w1.date = w2.date + 1,因为在 SQL 中处理日期必须使用专门的日期操作函数。

10. 每台机器的进程平均运行时间

📝 题目复述

通过日志表计算系统处理时长。 表结构定义:

  • Activity (活动表):包含 machine_id (机器 ID), process_id (进程 ID), activity_type (枚举:'start' 或 'end') 和 timestamp (时间戳,以秒为单位)。

目标任务: 每个进程都包含 startend 两条记录。一个进程的耗时 = 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;

💡 解析

  1. 将行转列(连接法):原始数据里“开始”和“结束”是在不同的两行。为了算减法,我们把 Activity 自己连自己,通过 a1 锁定 start,a2 锁定 end,这样在一行内就能计算 a2.timestamp - a1.timestamp 了。
  2. AVG 与 ROUNDAVG 用于求出该机器所有进程的平均耗时;ROUND(..., 3) 用于按照题目要求截取三位小数。
  3. 连接条件的细化:除了机器 ID 对应外,还必须确保 process_id 是一致的,否则会乱套。

11. 员工奖金

📝 题目复述

查找奖金低于 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;

💡 解析

  1. LEFT JOIN 确保不丢人:并不是每个员工都有奖金,普通的 JOIN 会剔除没有奖金的人。我们要确保所有员工都被考虑到,必须使用 LEFT JOIN
  2. NULL 值判断再次出现:如果某人在奖金表中没有数据,b.bonus 的值就是 NULL。在 SQL 逻辑中,NULL < 1000 的判断结果既不是 True 也不是 False,而是 Unknown。所以必须显式加上 OR b.bonus IS NULL,才能找回这部分人。

12. 学生们参加各科测试的次数

📝 题目复述

这是一个涉及三张表连接的复杂查询。 表结构定义:

  • Students (学生表):包含 student_id (主键) 和 student_name
  • Subjects (课程表):包含 subject_name (主键)。
  • Examinations (考试记录表):包含 student_idsubject_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;

💡 解析

  1. 笛卡尔积 (CROSS JOIN):这是解决“每个学生必须对应每个科目”的银弹。Students CROSS JOIN Subjects 会生成所有学生与所有科目的全部排列组合。
  2. LEFT JOIN 挂载考试数据:在上述全量组合的基础上,左连接考试表。这样,即便某生没考过某课,那一行记录依然存在。
  3. COUNT() 的对象:这里千万不要写 COUNT(*),要写 COUNT(e.subject_name)。因为 COUNT(*) 会统计行数(即便数据是 NULL 也是 1 行),而 COUNT(列名) 只会统计非 NULL 的个数。
  4. 多字段分组:由于结果集包含学生信息和课程名,必须按这三个字段一起进行 GROUP BY

13. 至少有5个直接下属的经理

📝 题目复述

在单一表中通过父子关系找出符合特定计数的记录。 表结构定义:

  • Employee (员工表):包含 id (主键), name, departmentmanagerId (上级的 ID)。

目标任务: 找出那些至少有五个直接下属的经理的名字。

🔑 答案

SELECT name
FROM Employee
WHERE id IN (
    SELECT managerId
    FROM Employee
    GROUP BY managerId
    HAVING COUNT(id) >= 5
);

💡 解析

  1. 子查询逻辑:先去查谁是那个“拥有 5 个以上小弟”的人。我们对 managerId 进行分组,然后使用 HAVING 过滤出记录数(下属人数)大于等于 5 的那些 ID。
  2. HAVING 子句WHERE 作用于每一行,而 HAVING 作用于聚合后的每一组。这里必须用 HAVING 来过滤统计出来的数字。
  3. 内连接法 (Alternative):这题也可以通过 JOIN 自身来解,但在逻辑层级清晰度上,子查询在本题更直观。

14. 确认率

📝 题目复述

计算每个用户的消息确效率。 表结构定义:

  • 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;

💡 解析

  1. 聚合中的逻辑运算 (MySQL 黑科技)AVG(c.action = 'confirmed') 非常巧妙。当 action 等于 'confirmed' 时,括号内结果为 1,否则为 0。对其取平均值,正好就是确效率。
  2. IFNULL 处理无记录用户:如果一个用户只有注册信息但没有发送确认请求,AVG 会返回 NULL。通过 IFNULL(val, 0) 将这些人的确认率强制转换为 0。
  3. 分组细节:我们要的是“每个”用户的确效率,所以以 Signups 表为左表进行左连接,确保覆盖所有注册用户。

15. 有趣的电影

📝 题目复述

基本的行过滤与排序练习。 表结构定义:

  • cinema (电影表):包含 id, movie, description (对电影的描述), rating (评分)。

目标任务: 筛选出那些:

  1. id奇数
  2. 描述 description 不是 "boring"(不乏味)。
  3. 结果按照评分 rating 从高到低(降序)排列。

🔑 答案

SELECT id, movie, description, rating
FROM cinema
WHERE id % 2 != 0 AND description != 'boring'
ORDER BY rating DESC;

💡 解析

  1. 取模运算 %:判断奇数最常用的办法是 id % 2 != 0
  2. 不等于号:在 SQL 中表示“不等于”,可以用 != 也可以用 <>
  3. 排序顺序:升序是 ASC,降序是 DESC。题目要求从高到低评分,必须写 DESC

16. 平均售价

📝 题目复述

计算每种产品在一段时期内的平均售价。 表结构定义:

  • Prices (价格表):包含 product_id, start_date, end_date, price (该时间段内的价格)。
  • UnitsSold (销量表):包含 product_id, purchase_date, units (购买数量)。

目标任务: 平均售价的计算公式为:该产品总销售额 / 总销售数量。请查出每个产品的平均售价,并保留两位小数注意点: 关联条件除了 product_id,销量表中的 purchase_date 必须落在价格表的 start_dateend_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;

💡 解析

  1. 带范围的 JOIN:这道题的 ON 条件不再仅仅是等于号,而是使用了 BETWEEN ... AND ...,这是连接条件非常灵活的一种体现。
  2. 加权平均值计算:平均单价不是价格的平均值,而是总额除以总数。公式为 SUM(单价 * 数量) / SUM(数量)
  3. 空值与保留小数
    • IFNULL(..., 0) 处理没有销量的情况。
    • ROUND(..., 2) 用于保留题目要求的两位小数。

17. 项目员工 I

📝 题目复述

查询每个项目所属员工的平均工龄。 表结构定义:

  • Project (项目表):包含 project_idemployee_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;

💡 解析

  1. 聚合分组基础:先通过 project_id 对记录进行分堆。
  2. AVG 函数:直接对该组内的 experience_years 列应用 AVG 即可得到平均值。
  3. 连接选择:这里使用 JOIN 即可,题目暗示了能被统计到项目的员工一定存在于员工表中。

18. 各赛事用户注册率

📝 题目复述

计算每个赛事中有多少比例的用户进行了注册。 表结构定义:

  • 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;

💡 解析

  1. 子查询求分母:注册率的分母是全表总人数,这是一个固定的常数,因此在表达式里嵌入 (SELECT COUNT(*) FROM Users) 来动态获取。
  2. 百分数计算:不要忘了乘以 100。
  3. 排序技巧:在 ORDER BY 中,可以根据计算出的字段别名 percentage 进行排序,多级排序用逗号隔开。

19. 查询结果的质量和占比

📝 题目复述

这是一个衡量查询服务质量的任务。 表结构定义:

  • Queries (查询表):包含 query_name (查询类型), result, position (排序位置,1 到 500), rating (评分,1 到 5)。

目标任务: 计算每个 query_name 的以下两个指标:

  1. 质量 (quality):该查询下所有 (rating / position) 的平均值。
  2. 劣质查询占比 (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;

💡 解析

  1. 混合计算:SQL 允许在聚合函数(如 AVG)中写算术表达式,系统会先对每行算 rating / position,再取总平均。
  2. 条件统计 (SUM+IF)SUM(IF(rating < 3, 1, 0)) 是一个非常实用的套路。如果满足条件则给 1,否则给 0,求和结果就是满足条件的总件数。
  3. 判空处理:题目数据中可能存在 query_name 为空的情况,如果不加筛选会导致分组中多出一个空项,建议使用 WHERE 排除。

20. 每月交易 I

📝 题目复述

按照月份统计支付信息。 表结构定义:

  • Transactions (交易表):包含 id, country (国家), state (交易状态:'approved' 已批准或 'declined' 已拒绝), amount (金额), trans_date (交易日期)。

目标任务: 编写一条 SQL 统计每个月每个国家的:

  1. 总交易次数。
  2. 已批准(approved)的总交易次数。
  3. 总交易金额。
  4. 已批准的总交易金额。

🔑 答案

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;

💡 解析

  1. 日期格式化函数 DATE_FORMAT:因为原始日期包含具体的日(如 2019-12-18),为了实现“按月统计”,我们通过格式化将所有日期统一为 YYYY-MM 的形式,然后参与 GROUP BY
  2. 条件统计技巧升级
    • 求“已批准次数”:SUM(state = 'approved') 在 MySQL 中成立(True 会变成 1),但标准的写法是使用 IF
    • 求“已批准总金额”:通过 IF(state = 'approved', amount, 0)。当交易不是 approved 时,算金额的时候加上 0。
  3. 多列分组GROUP BY month, country 表示只有月份和国家都相同的记录才会归为一堆进行聚合计算。

21. 即时食物配送 II

📝 题目复述

统计每位客户首次订单中“即时订单”的占比。 表结构定义:

  • Delivery (配送表):包含 delivery_id, customer_id, order_date (下单日期), customer_pref_delivery_date (客户期望配送日期)。

概念说明:

  1. 即时订单:如果下单日期 = 期望配送日期。
  2. 计划订单:如果下单日期 != 期望配送日期。
  3. 首单:每个客户 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
);

💡 解析

  1. 锁定首单记录:使用子查询 SELECT customer_id, MIN(order_date) ... GROUP BY customer_id 选出每个人的最早下单日期,并配合 IN 子句对主表进行过滤。
  2. 计算占比:在被筛选出的首单数据集中,使用 SUM(条件) 统计符合“即时订单”的数量。在 MySQL 中,order_date = customer_pref_delivery_date 为真返回 1,否则返回 0。
  3. 计算与取整* 100 / COUNT(*) 得到百分比,再用 ROUND(..., 2) 保留两位小数。

22. 游戏玩法分析 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;

💡 解析

  1. 确定首次登录日期:通过派生表 a1 找出每个人的 MIN(event_date)
  2. 次日留存匹配:将首登表 a1 与原始表 Activity (命名为 a2) 进行 LEFT JOIN。连接条件是 player_id 相等且 a2.event_datefirst_login 正好晚 1 天(使用 DATEDIFF = 1)。
  3. 统计分子与分母
    • COUNT(a1.player_id):计算所有玩家的总数(分母)。
    • COUNT(a2.player_id):由于是 LEFT JOIN,如果玩家第二天没来,a2 的字段会是 NULLCOUNT(列名) 恰好不统计 NULL 值,从而得到“连续登录的人数”。

23. 每位教师所授课程的数目

📝 题目复述

统计教师独立教授的不同科目数量。 表结构定义:

  • 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;

💡 解析

  1. 分组计数:按 teacher_id 进行 GROUP BY 是常规操作。
  2. 去重计数:同一个老师可能在不同的部门 (dept_id) 教同一门课。为了计算“不同科目的数量”,必须在 COUNT 函数内部使用 DISTINCT 关键字。

24. 查询近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;

💡 解析

  1. 日期筛选区间:题目要求的区间是 2019-07-27 向前推 30 天,即 2019-06-28 到 2019-07-27。在 SQL 中使用 BETWEEN '开始日期' AND '结束日期'
  2. 日活 (UV) 计算:按日期分组后,通过 COUNT(DISTINCT user_id) 来确保同一用户即便有多条日志也只计数为 1。

25. 产品销售分析 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
);

💡 解析

  1. 筛选最小年份:由于同一产品在一年内可能有多笔订单,我们需要找出每个产品第一次出现的那个 year。首先用子查询通过 GROUP BY product_idMIN(year) 确定每个产品的起始年。
  2. 二元组合过滤:使用 WHERE (product_id, year) IN (...)。这是一个非常有用的技巧,它能确保我们匹配到的是该产品对应的那个具体的最小年份,而不是单纯匹配 ID 或年份。
  3. 注意点:题目只要求第一年(不论该年内有多少笔订单)的数据。如果该产品在第一年里卖了 3 次,结果里应展示这 3 行。因此通过 IN 筛选主表能保留所有第一年的交易记录。

26. 超过5名学生的课

📝题目复述

筛选出选课人数达到一定标准的课程。 表结构定义:

  • Courses 表:包含 student (学生名) 和 class (课程名)。(student, class) 是该表的主键组合。

目标任务: 列出所有选修学生人数大于或等于 5 的课程。

🔑 答案

SELECT class
FROM Courses
GROUP BY class
HAVING COUNT(student) >= 5;

💡 解析

  1. 分组基础:因为我们要统计每门课的人数,所以必须对 class 进行 GROUP BY
  2. HAVING 与 WHERE 的区别
    • WHERE 在分组前筛选,它无法直接处理像 COUNT() 这种聚合后的统计值。
    • HAVING 在分组后执行,用于过滤符合条件的“组”。
  3. 统计逻辑COUNT(student) 统计出每个组里有多少学生,满足条件则输出该课程名。

27. 寻找各站点的粉丝数

📝题目复述

统计社交平台中每位用户的粉丝总数。 表结构定义:

  • 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;

💡 解析

  1. GROUP BY 用户:为了汇总每位用户的状态,以 user_id 作为分组列。
  2. 基础聚合COUNT(follower_id) 即可算出粉丝总数。由于 (user_id, follower_id) 是主键,天然不重复,不需要在此额外写 DISTINCT
  3. 排序:明确题目要求的升序排列。

28. 最大的单数

📝题目复述

寻找数据集中只出现过一次的数字里的最大值。 表结构定义:

  • MyNumbers 表:仅包含一列 num (整数)。

目标任务: “单数”指在表中只出现过一次的数字。请找出这些单数中最大的一个。如果不存在这样的数字,则输出 null

🔑 答案

SELECT MAX(num) AS num
FROM (
    SELECT num
    FROM MyNumbers
    GROUP BY num
    HAVING COUNT(num) = 1
) AS single_nums;

💡 解析

  1. 分层处理逻辑
    • 第一步:内部子查询。通过 GROUP BY num 配合 HAVING COUNT(num) = 1,筛选出所有只出现过一次的数字。
    • 第二步:外部聚合。在子查询筛选出的“单数列表”里,使用 MAX(num) 挑出最大值。
  2. NULL 值特性:在 SQL 中,对一个空的结果集执行 MAX() 聚合运算,结果会自动返回 NULL,这完美符合题目要求的边缘情况。

29. 买过所有产品的客户

📝题目复述

查找出完成了全品种购买的“大满贯”客户。 表结构定义:

  • Customer 表:包含 customer_idproduct_key。注意,客户可能对同一产品有多次购买记录。
  • Product 表:包含 product_key,记录了公司销售的所有产品种类。

目标任务: 查询买过 Product 表中所有类型产品的客户 ID。

🔑 答案

SELECT customer_id
FROM Customer
GROUP BY customer_id
HAVING COUNT(DISTINCT product_key) = (SELECT COUNT(*) FROM Product);

💡 解析

  1. 动态比较:买齐所有产品的标志是“该客户购买的不同产品 ID 数量”等于“产品库的总产品数”。
  2. 子查询作常数:使用 (SELECT COUNT(*) FROM Product) 获取当前全品类到底有多少个。
  3. DISTINCT 的重要性:因为客户可能买了同一个东西多次,在统计其购买品种数时,必须用 COUNT(DISTINCT product_key) 进行去重计数,否则计算结果会偏差。

30. 每位经理下属员工数量

📝题目复述

汇总经理级员工及其管辖团队的相关信息。 表结构定义:

  • Employees 表:包含 employee_id (主键), name, reports_to (经理 ID), age

核心逻辑:

  • 经理是指至少有一名员工向其汇报(即 employee_id 出现在某人的 reports_to 列中)。

目标任务: 查询每位经理的 employee_idname、其直接下属的人数 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;

💡 解析

  1. 自连接关系绑定:我们将 Employees 表视为两张虚拟表:
    • e1 (代表经理):它的 employee_id 是管理者 ID。
    • e2 (代表员工):它的 reports_to 是员工上司。
    • 通过 e1.employee_id = e2.reports_to 将两级关系对齐。
  2. 筛选经理:使用内连接 JOIN。如果某人没有下属(其 ID 没在 reports_to 出现过),连接后就不会产生对应记录。
  3. 数值处理
    • COUNT(e2.employee_id):计算经理 e1 的所有下属数量。
    • ROUND(AVG(e2.age)):先算平均年龄,再用 ROUND() 进行四舍五入取整。
  4. 多列分组GROUP BY e1.employee_id, e1.name 确保查询的非聚合列都能合法输出。

31. 员工的直属部门

📝题目复述

在一个员工可能属于多个部门的情况下,确定他们的直属部门。 表结构定义:

  • Employee 表:包含 employee_id (员工 ID), department_id (部门 ID), primary_flag (直属标志)。
  • primary_flag 是一个枚举值,'Y' 表示该部门是直属部门,'N' 表示不是。

筛选规则:

  1. 如果一个员工只属于一个部门,那么该部门就是其直属部门(无论 primary_flag 是什么)。
  2. 如果一个员工属于多个部门,则其 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. 分情况汇总:这道题最清晰的方法是把两种符合条件的情况“拼”在一起。
    • 情况 1:找出所有显式标注了 primary_flag = 'Y' 的记录。
    • 情况 2:找出那些在表中只出现过一次(即只属于一个部门)的员工。
  2. UNION 运算符:使用 UNION 将两个查询结果集合并。UNION 会自动去除两个结果集中的重复行(例如某个员工只有一个部门且正好该标记也是 'Y')。
  3. 分组过滤:在第二个查询中,通过 GROUP BY employee_id 并使用 HAVING COUNT(*) = 1 来锁定那些“没得选”只能选这唯一部门的员工。

32. 三角形判断

📝题目复述

根据三条边的长度判断是否能组成三角形。 表结构定义:

  • 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;

💡 解析

  1. 流程控制函数 IF
    • 语法:IF(expr1, expr2, expr3)。如果表达式 expr1 为真,返回 expr2,否则返回 expr3
    • 这里我们直接利用三角形的几何性质作为逻辑判断。
  2. CASE WHEN 替代方案:你也可以使用更通用的 SQL 语法 CASE WHEN condition THEN 'Yes' ELSE 'No' END
  3. 多条件逻辑:注意必须三个条件同时满足(用 AND 连接),任意一组边不符合规则都无法组成三角形。

33. 经理已离职的员工

📝题目复述

查找管理层变动后导致“孤立”的低薪员工。 表结构定义:

  • Employees 表:包含 employee_id (主键), name, manager_id (经理的 ID), salary

目标任务: 找出同时满足以下条件的员工 employee_id

  1. 员工的月薪严格少于 $30000。
  2. 该员工有经理(manager_id 不为空),但这位经理的 employee_id 已经不在 Employees 表中了(说明经理离职了)。
  3. 结果按 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;

💡 解析

  1. 子查询筛选SELECT employee_id FROM Employees 会拿到当前全公司所有在职人员的 ID 集合。
  2. NOT IN 逻辑manager_id NOT IN (...) 表示该员工所汇报的经理 ID 不在这个在职人员名单里。
  3. 复合条件:注意题干中的每一个限定词:“薪水少于 30000” 对应 salary < 30000,“有经理” 对应 manager_id IS NOT NULL

34. 交换座位

📝题目复述

在教室座位表中,交换相邻两位同学的座位。 表结构定义:

  • 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;

💡 解析

  1. 重塑 ID 的逻辑:我们的目标是把名字对应的 ID 改掉。
    • 如果 ID 是奇数且不是最后一名:那他就去偶数座,即 id + 1
    • 如果 ID 是偶数:那他就去前面的奇数座,即 id - 1
    • 如果 ID 是奇数且正好是最大 ID:保持原状。
  2. MOD 运算MOD(id, 2) != 0 用于识别奇数。
  3. 排序:名字没换,只是 ID 数值变了,最后一定要用 ORDER BY id 让座位表恢复正常的数字顺序显示。

35. 电影评分

📝题目复述

分析用户的点评行为和电影的受欢迎程度。 表结构定义:

  • Movies (电影表):包含 movie_id, title (电影名)。
  • Users (用户表):包含 user_id, name (用户名)。
  • MovieRating (评分表):包含 movie_id, user_id, rating, created_at (点评日期)。

目标任务: 需要返回两个查询结果,并放在一个列里(列名可以任意):

  1. 查找评论电影数量最多的用户名称。如果有多个用户并列,返回字典序较小的那个名。
  2. 查找 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);

💡 解析

  1. UNION ALL 的巧妙应用:两个查询看似无关,但题目要求结果合在一个字段。用 UNION ALL 可以保留所有查询结果,哪怕这两个名字碰巧一样。
  2. Top 1 的筛选套路:使用 ORDER BY 排序列,再通过 LIMIT 1 取出最顶尖的一条。
  3. 并列排序逻辑ORDER BY COUNT(*) DESC, name ASC。第一个条件保“最多/最高”,第二个条件(名字字典序)在并列时选首字母最靠前的。
  4. 日期过滤DATE_FORMAT(created_at, '%Y-%m') = '2020-02' 用来精准锁定特定的月份进行平均值统计。

36. 餐馆营业额增长

📝题目复述

我们需要计算餐馆收入的滚动窗口统计数据。 表结构定义:

  • 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;

💡 解析

  1. 两层聚合:首先通过 GROUP BY visited_on 把每一天的总金额算出来。
  2. 窗口函数滚动求和SUM(SUM(amount)) OVER (ORDER BY visited_on ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)。这里的核心是指定窗口范围:当前行及前面的 6 行(总共 7 天)。
  3. 起始点过滤:题目要求从有记录以来的第 7 天开始统计(因为前 6 天凑不满 7 天的窗口)。我们通过 DENSE_RANK() 标记天数的排名,在外面嵌套查询时过滤掉 rnk < 7 的数据。

37. 好友申请 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;

💡 解析

  1. 数据的双向合并:在 RequestAccepted 表中,每一行意味着两个人都多了一个好友。为了统计每个 ID 的好友总数,需要用 UNION ALL 把这两列垂直堆叠在一起,统一成一列 id
  2. 聚合与排序:对堆叠后的 id 列进行 GROUP BY 并执行 COUNT(*),这就是每个人的总好友数。
  3. 取最高值:按照数量 DESC 降序排列,利用 LIMIT 1 取出最顶尖的一位。

38. 2016年的投资

📝题目复述

根据两个筛选条件计算 2016 年的总投保金额。 表结构定义:

  • Insurance 表:包含 pid, tiv_2015, tiv_2016, lat (纬度), lon (经度)。

目标任务: 统计符合以下两个条件的投保人 2016 年总投保金额 (tiv_2016):

  1. 投保人在 2015 年的投保金额 (tiv_2015) 至少与另一位投保人相同。
  2. 投保人所处的城市位置 (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. 条件 1 处理(重复值):利用子查询,对 tiv_2015 分组,找出计数大于 1 的值,意味着有人和你投了同样多的钱。
  2. 条件 2 处理(唯一值):利用子查询,对坐标组合 (lat, lon) 分组,找计数等于 1 的记录,确保经纬度组合没重复。
  3. 结果聚合:将满足这两个 IN 条件的 tiv_2016 相加并保留两位小数。

39. 部门工资前三高的员工

📝题目复述

找出每个部门中收入最高的三个“打工人”。 表结构定义:

  • 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;

💡 解析

  1. 窗口函数选择 (DENSE_RANK):这是本题最关键的一点。DENSE_RANK 在处理并列名次时,接下来的排名会是连续的(比如 1, 1, 2)。这符合题目要求的“找出工资前三高”的逻辑,即即使有很多人领最高工资,只要这个数字本身排在前三,对应的人就要显示出来。
  2. 分区排名PARTITION BY departmentId 用于在每个部门内独立计算排名。
  3. 多表联查:在子查询得到排名后,将结果与 Department 表连接以获取部门的具体名称,并过滤出 rnk <= 3 的记录。

40. 修复表中的名字

📝题目复述

统一表中的人名格式。 表结构定义:

  • Users 表:包含 user_idname

目标任务:name 统一格式化为:第一个字符大写,其余字符小写。结果返回修复后的 user_idname,并按 user_id 排序。

🔑 答案

SELECT user_id, 
       CONCAT(UPPER(LEFT(name, 1)), LOWER(SUBSTR(name, 2))) AS name
FROM Users
ORDER BY user_id;

💡 解析

  1. 字符截取
    • LEFT(name, 1) 获取首个字符。
    • SUBSTR(name, 2) 获取从第 2 个位置开始到结尾的所有字符。
  2. 大小写转换
    • UPPER(...) 强制首字符大写。
    • LOWER(...) 强制剩余部分小写。
  3. 字符串拼接:使用 CONCAT(...) 将修补好的两部分重新连起来,得到符合要求的完整姓名。

41. 按日期分组销售产品

📝题目复述

统计每日销售的产品名称及种类。 表结构定义

  • 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;

💡 解析

  1. 聚合与去重:同一天可能卖出多次同一个产品,因此 COUNT(DISTINCT product) 确保统计的是种类的数量。
  2. 字符串聚合函数 GROUP_CONCAT
    • 它是 MySQL 专门用来将多行数据合并为一个字符串的工具。
    • 在函数内部,我们可以直接使用 DISTINCT 进行去重,使用 ORDER BY 进行组内排序。
    • SEPARATOR ',' 指定了合并时的分隔符。
  3. 结果输出:按日期分组后,该函数会自动处理该组下所有的产品名称拼接。

42. 列出指定时期内价格上升的产品

📝题目复述

筛选出在特定月份订购量达到要求的商品。 表结构定义

  • 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;

💡 解析

  1. 日期过滤:利用 DATE_FORMAT(order_date, '%Y-%m') 锁定 2020 年 2 月的数据。也可以用 o.order_date BETWEEN '2020-02-01' AND '2020-02-29'
  2. 内连接与分组:通过 product_id 连接两张表,保证能拿到对应的产品名称,随后根据产品进行分组统计总数。
  3. 聚合后过滤:对于 SUM(unit) 的结果筛选,必须使用 HAVING 子句,不能直接写在 WHERE 中。

43. 查找拥有有效邮箱的用户

📝题目复述

通过正则表达式识别格式正确的邮箱。 表结构定义

  • Users 表:包含 user_id, name, mail (邮件地址)。

合法的邮件格式定义

  1. 前缀部分:以字母开头。之后可以包含字母(大写或小写)、数字、下划线 _、句点 . 或/和破折号 -
  2. 域名部分:必须以 @leetcode.com 结尾。

🔑 答案

SELECT user_id, name, mail
FROM Users
WHERE mail REGEXP '^[a-zA-Z][a-zA-Z0-9_.-]*@leetcode\\.com$';

💡 解析

  1. REGEXP 运算符:MySQL 使用正则表达式匹配字符串的强大工具。
  2. 正则分解
    • ^:匹配字符串的开头。
    • [a-zA-Z]:确保第一个字符必须是字母。
    • [a-zA-Z0-9_.-]*:后面的字符可以是字母、数字、下划线、点或横杠,* 代表 0 个或多个。
    • @leetcode\\.com$:以特定的字符串结尾。在正则中 . 代表任意字符,所以要用 \\. 对其进行转义,表示真实的“点”。

44. 删除重复的电子邮件

📝题目复述

执行数据清理操作,保留重复项中 ID 最小的记录。 表结构定义

  • Person 表:包含 idemail

目标任务: 编写一个 DELETE 语句,删除所有重复的电子邮件。在删除之后,每个电子邮件只能出现一次,且需要保留 id 最小的那条记录。

🔑 答案

DELETE p1
FROM Person p1, Person p2
WHERE p1.email = p2.email AND p1.id > p2.id;

💡 解析

  1. 自连接删除逻辑:这是最直观的写法。我们将 Person 表看成两个完全一样的副本 p1p2
  2. 寻找该删的数据:当两行的邮件相同(p1.email = p2.email),但 p1 的 ID 更大(p1.id > p2.id)时,说明 p1 是那个冗余的记录。
  3. 语法点DELETE p1 指定了执行删除操作的是 p1 代表的那部分记录。

45. 第二高的薪水

📝题目复述

处理数据库中的排序及特殊值返回逻辑。 表结构定义

  • Employee 表:包含 idsalary (薪水)。

目标任务: 查询并返回 Employee 表中第二高的薪水。如果不存在第二高的薪水(例如表中只有一条记录),查询应返回 null

🔑 答案

SELECT (
    SELECT DISTINCT salary
    FROM Employee
    ORDER BY salary DESC
    LIMIT 1 OFFSET 1
) AS SecondHighestSalary;

💡 解析

  1. 去重与排序:可能有多个员工拿同样的最高薪水,所以先 DISTINCT salary 去重,再 DESC 降序排列。
  2. 精准定位LIMIT 1 OFFSET 1 的意思是跳过第一个(最高薪水),取后面的一行数据(即第二高)。
  3. 强制 NULL 返回:本题的一个精髓在于:将查询放在 SELECT ( ... ) 子句中作为单值输出。如果子查询没有找到任何行(例如 offset 跳过后没数据了),它会产生一个空值,由于在外层作为表达式使用,查询结果会自动变成单行的 NULL。如果你直接运行里面的查询,结果可能是完全没有行。

46. 连续出现的数字

📝题目复述

查找序列中至少连续出现三次的数字。 表结构定义

  • 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;

💡 解析

  1. 多表自连接:因为 id 是自增的,我们可以利用 Logs 表的三个实例(l1, l2, l3)通过 id 的差值建立对应关系:l1 是第一个,l2 是第二个(l1.id + 1),l3 是第三个(l2.id + 1)。
  2. 值相等判断:在确认了三条记录在 ID 上是连续的基础上,判断它们的值 num 是否全部相等。
  3. 去重:如果一个数字连续出现了四次,那么会有两组三连满足条件,所以使用 DISTINCT 确保每个数字只返回一次。
  4. 进阶注脚:本题也常使用窗口函数 LEAD()LAG() 来解决,通过对比当前行及其后两行的数据来判断是否连续。

47. 指定日期的产品价格

📝题目复述

在价格历史表中查找特定日期的商品最新价格。 表结构定义

  • 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'
);

💡 解析

  1. 第一部分(有变动)
    • 通过子查询 MAX(change_date) 锁定每个产品在指定日期前的最后一次变更时间。
    • 使用 (product_id, change_date) IN ... 的二元组合匹配主表。
  2. 第二部分(无变动)
    • 使用 NOT IN 过滤出那些在 2019-08-16 前没有任何历史记录的产品。
    • 将价格统一设为常量 10。
  3. 结果合并:利用 UNION 将上述两种逻辑产生的结果集纵向合并。

48. 最后一个能进入电梯的人

📝题目复述

计算在承重限制下,电梯名单中最后一名能够进入的乘客姓名。 表结构定义

  • 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;

💡 解析

  1. 累计求和 (Running Total):使用窗口函数 SUM(weight) OVER(ORDER BY turn) 计算到当前乘客为止的所有乘客的总重量。
  2. 筛选重量界限:在外部查询中筛选出 cumulative_weight <= 1000 的所有人。
  3. 寻找转折点:将符合条件的人按照重量降序排列 (DESC),那么第一个出现的(也就是 LIMIT 1)必然是能够登上的最后一个人。

49. 按分类统计薪水

📝题目复述

对薪资进行区间分档并统计每档的账户数量。 表结构定义

  • Accounts 表:包含 account_idincome (月薪)。

档位定义

  • "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;

💡 解析

  1. 攻克“显示 0”的需求:普通的 CASE WHEN + GROUP BY 有一个缺陷:如果某区间完全没数据,该区间在结果里就会消失。为了满足显示 0 的要求,最稳妥的办法是手动列出这三个 UNION
  2. 统计计数:在每个 SELECT 分句中,直接使用 SUM(条件判断)。例如 SUM(income < 20000) 在 MySQL 中会统计 true(1)的个数,得到账户数量。
  3. 数据拼接:将三个独立的区间查询通过 UNION 堆叠,确保结果一定包含三行记录。

50. 每个员工每天上班的总时间

📝题目复述

通过日志表计算员工单日打卡的净时长。 表结构定义

  • 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;

💡 解析

  1. 分组维度:我们需要的是“某员工”在“某一天”的总时长。因此 GROUP BY 需要同时包含日期和员工 ID。
  2. 算术运算与聚合:在 SQL 中可以先算 out_time - in_time,再套上 SUM() 函数进行累加求和,算出全天的总工作时长。
  3. 结果字段命名:根据题目要求,使用 AS dayAS total_time 对结果列进行重命名以符合预期。
MySQL知识点整理 文章被收录于专栏

经典MySQL知识

全部评论

相关推荐

点赞 评论 收藏
分享
评论
3
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务