SQL 算法笔记
LeetCode 上 SQL50 的算法笔记
SQL 算法笔记
通用思维套路总览
看到什么特征 → 用什么 SQL 技巧
| 题目特征 | 首选技巧 | 典型题目 |
|---|---|---|
| 两个表关联取数据 | INNER JOIN / LEFT JOIN | 175, 1068, 1378 |
| 保留左表全部行(无匹配返回 NULL) | LEFT JOIN | 175, 577, 1378, 1581 |
| 同一表比较(员工 vs 经理) | SELF JOIN(自连接) | 181, 570, 1731, 1978 |
| 找重复值 | GROUP BY + HAVING COUNT > 1 | 182, 196 |
| 分组后过滤 | GROUP BY + HAVING | 596, 570, 1045, 1084 |
| 分组后统计 | GROUP BY + COUNT/SUM/AVG | 511, 1693, 1729, 2356 |
| 排名 / Top N | 窗口函数 RANK / DENSE_RANK | 185, 1341 |
| 累计求和 / 滑动窗口 | SUM() OVER(ORDER BY ROWS BETWEEN) | 1204, 1321 |
| 条件聚合(分情况统计) | SUM(IF(…)) 或 SUM(CASE WHEN) | 1193, 1661, 1393 |
| 合并多个查询结果 | UNION / UNION ALL | 1795, 1907, 1164 |
| 找「所有」/「全部」 | HAVING COUNT(DISTINCT) = 总数 | 1045 |
| 比较前一天/连续 N 天 | SELF JOIN + DATEDIFF | 197, 180, 550 |
| 无匹配也要返回 | LEFT JOIN + IS NULL | 577, 581, 1978 |
| 字符串模式匹配 | LIKE / REGEXP | 1527, 1517, 1683 |
| 列转行 | UNION ALL | 1795 |
| 行转列 / 拼接 | GROUP_CONCAT | 1484 |
| 条件赋值 | CASE WHEN | 610, 626, 627 |
| 日期格式化 | DATE_FORMAT | 1193, 1327 |
SQL 执行顺序(重要!)
FROM + JOIN → 先确定数据源和关联WHERE → 行级过滤GROUP BY → 分组HAVING → 组级过滤SELECT → 选择列 / 聚合 / 窗口函数ORDER BY → 排序LIMIT → 截取关键:WHERE 在 GROUP BY 之前,不能使用聚合函数;HAVING 在 GROUP BY 之后,可以使用聚合函数。
一、基础查询(WHERE / ORDER BY / LIMIT)
0595. 大的国家 Easy
表结构:World(name, continent, area, population, gdp)
题目:找面积 ≥ 300万 或 人口 ≥ 2500万 的国家。
SELECT name, population, areaFROM WorldWHERE area >= 3000000 OR population >= 25000000;执行流程:
| 原始 World 表 | → WHERE 过滤 |
|---|---|
| Afghanistan, Asia, 652230, 38928341, … | ✅ area≥300万 |
| Albania, Europe, 28748, 2837743, … | ❌ 都不满足 |
| Algeria, Africa, 2381741, 37100000, … | ✅ area≥300万 |
| Andorra, Europe, 468, 27457, … | ❌ 都不满足 |
| India, Asia, 3287590, 1324171354, … | ✅ area≥300万 |
核心点:WHERE 用 OR 连接两个独立条件,满足任一即可
0584. 寻找用户推荐人 Easy
表结构:Customer(id, name, referee_id)
题目:找推荐人不是 id=2 的客户(包括没有推荐人的)。
SELECT nameFROM CustomerWHERE referee_id != 2 OR referee_id IS NULL;执行流程:
| id | name | referee_id | 判断 | 结果 |
|---|---|---|---|---|
| 1 | Will | NULL | NULL != 2 → NULL(不是 true)→ 但 IS NULL → ✅ | 输出 |
| 2 | Jane | NULL | 同上 → ✅ | 输出 |
| 3 | Alex | 2 | 2 != 2 → false → ❌ | 不输出 |
| 4 | Bill | 3 | 3 != 2 → true → ✅ | 输出 |
| 5 | Zack | 1 | 1 != 2 → true → ✅ | 输出 |
⚠️ 关键陷阱:
referee_id != 2不会匹配 NULL!NULL 参与比较结果是 NULL(不是 true),必须用IS NULL单独处理
0168. 无效的推文 Easy
SELECT tweet_idFROM TweetsWHERE LENGTH(content) > 15;| tweet_id | content | LENGTH | 结果 |
|---|---|---|---|
| 1 | “Vote for Biden” | 14 | ❌ |
| 2 | “Let us make sure … (30字)” | 30 | ✅ |
0176. 第二高的薪水 Medium
表结构:Employee(id, salary)
题目:找第二高的不同薪水,不存在则返回 NULL。
SELECT (SELECT DISTINCT salary FROM Employee ORDER BY salary DESC LIMIT 1 OFFSET 1 ) AS SecondHighestSalary;执行流程:
原始 Employee 表 → DISTINCT 去重 → ORDER BY DESC → LIMIT 1 OFFSET 1┌────┬────────┐ ┌────────┐ ┌────────┐ ┌────────┐│ id │ salary │ │ salary │ │ salary │ │ salary │├────┼────────┤ ├────────┤ ├────────┤ ├────────┤│ 1 │ 100 │ │ 100 │ │ 400 │ │ 200 ││ 2 │ 200 │ → │ 200 │ → │ 300 │ → │ ││ 3 │ 300 │ │ 300 │ │ 200 │ │ ││ 4 │ 300 │ │ 400 │ │ 100 │ │ ││ 5 │ 400 │ └────────┘ └────────┘ └────────┘└────┴────────┘核心点:使用外层
SELECT (子查询)确保无结果时返回 NULL 而非空表
0620. 有趣的电影 Easy
SELECT *FROM cinemaWHERE description != 'boring' AND id % 2 = 1ORDER BY rating DESC;执行流程:
| id | movie | description | rating | → WHERE | → ORDER BY rating DESC |
|---|---|---|---|---|---|
| 1 | War | great 3D | 8.9 | ✅ odd + not boring | 8.9 |
| 2 | Science | boring | 8.4 | ❌ boring | - |
| 3 | Irish | NOT boring | 7.0 | ✅ odd + not boring | 7.0 |
| 4 | Ice Song | Fantacy | 8.6 | ❌ even | - |
| 5 | House card | Interesting | 9.1 | ✅ odd + not boring | 9.1 |
| 6 | … | … | … | … | … |
二、JOIN(内连接 / 左连接 / 自连接)
JOIN 类型对比
阴影 = 结果里出现的行来自哪里:INNER JOIN 只取两圆重叠部分;LEFT JOIN 取整个左圆(含重叠),右圆只有重叠部分算进去、没匹配的位置填 NULL。
INNER(内连接) LEFT(左连接)┌────┐ ┌────┐ ┌────┐ ┌────┐│ │ │ │ │████│ │ ││ ██│ │██ │ │████│ │██ ││ ██│ │██ │ │████│ │██ ││ │ │ │ │████│ │ │└────┘ └────┘ └────┘ └────┘ 左 右 左 右 左 右 左 右
用同一组数据看结果差异(A=左表 Person,B=右表 Address,连接键 id):
A 表(Person) B 表(Address)┌────┬──────┐ ┌────┬──────┐│ id │ name │ │ id │ city │├────┼──────┤ ├────┼──────┤│ 1 │ Allen│ │ 2 │ NYC ││ 2 │ Bob │ │ 3 │ Boston││ 3 │ Zack │ └────┴──────┘└────┴──────┘
① INNER JOIN(内连接)——只保留 A、B 都能按 id 匹配的行┌────┬──────┬──────┐│ id │ name │ city │├────┼──────┼──────┤│ 2 │ Bob │ NYC │ ← A.2 = B.2 ✅│ 3 │ Zack │ Boston│ ← A.3 = B.3 ✅└────┴──────┴──────┘(Allen 在 B 中无匹配 → 不出现)
② LEFT JOIN(左连接)——保留 A 的全部行,B 无匹配用 NULL 填充┌────┬──────┬──────┐│ id │ name │ city │├────┼──────┼──────┤│ 1 │ Allen│ NULL │ ← B 无匹配 → city 为 NULL│ 2 │ Bob │ NYC ││ 3 │ Zack │ Boston│└────┴──────┴──────┘
③ 自连接(SELF JOIN)——同一张表当两张用,用别名区分角色 Employee 里:员工.managerId = 经理.id(详见 0181 / 1731 / 0570)0175. 组合两个表 Easy
表结构:
Person(personId, firstName, lastName)Address(addressId, personId, city, state)
题目:报告每个人的姓名、城市和州,没有地址的也要返回。
SELECT p.firstName, p.lastName, a.city, a.stateFROM Person p LEFT JOIN Address a ON p.personId = a.personId;执行流程:
Person 表:
| personId | firstName | lastName |
|---|---|---|
| 1 | Allen | Wang |
| 2 | Bob | Alice |
| 3 | Zack | Sy |
Address 表:
| addressId | personId | city | state |
|---|---|---|---|
| 1 | 2 | NYC | NY |
| 2 | 3 | Boston | MA |
LEFT JOIN 结果:
| firstName | lastName | city | state |
|---|---|---|---|
| Allen | Wang | NULL | NULL |
| Bob | Alice | NYC | NY |
| Zack | Sy | Boston | MA |
Allen 在 Address 表无匹配 → city 和 state 为 NULL。LEFT JOIN 保证了 Person 表全部行都保留。
0181. 超过经理收入的员工 Easy(自连接)
表结构:Employee(id, name, salary, managerId)
题目:找收入比经理高的员工。
SELECT e1.name AS EmployeeFROM Employee e1 JOIN Employee e2 ON e1.managerId = e2.idWHERE e1.salary > e2.salary;执行流程:
Employee 表(一张表充当两个角色):
| id | name | salary | managerId |
|---|---|---|---|
| 1 | Joe | 70000 | 3 |
| 2 | Henry | 80000 | 4 |
| 3 | Sam | 60000 | NULL |
| 4 | Max | 90000 | NULL |
自连接后(e1=员工, e2=经理):
| e1.name(员工) | e1.salary | e2.name(经理) | e2.salary | e1.salary > e2.salary? |
|---|---|---|---|---|
| Joe | 70000 | Sam | 60000 | ✅ 70000 > 60000 |
| Henry | 80000 | Max | 90000 | ❌ 80000 < 90000 |
最终结果:
| Employee |
|---|
| Joe |
核心点:同一张表 JOIN 自身,用不同别名区分员工和经理角色
0577. 员工奖金 Easy(LEFT JOIN + IS NULL)
表结构:
Employee(empId, name, supervisor, salary)Bonus(empId, bonus)
题目:报告奖金 < 1000 或没有奖金的员工。
SELECT name, bonusFROM Employee e LEFT JOIN Bonus b ON e.empId = b.empIdWHERE b.bonus < 1000 OR b.bonus IS NULL;执行流程:
Employee 表:
| empId | name | supervisor | salary |
|---|---|---|---|
| 1 | Brad | NULL | 5000 |
| 2 | John | 1 | 4000 |
| 3 | Dan | 1 | 3000 |
| 4 | Thomas | 1 | 2000 |
Bonus 表:
| empId | bonus |
|---|---|
| 2 | 500 |
| 3 | NULL |
| 4 | 2000 |
LEFT JOIN 后:
| name | bonus | 判断 |
|---|---|---|
| Brad | NULL | IS NULL → ✅ |
| John | 500 | 500 < 1000 → ✅ |
| Dan | NULL | IS NULL → ✅ |
| Thomas | 2000 | 2000 ≥ 1000 → ❌ |
最终结果:
| name | bonus |
|---|---|
| Brad | NULL |
| John | 500 |
| Dan | NULL |
⚠️
b.bonus < 1000不匹配 NULL,必须加OR b.bonus IS NULL
1378. 使用唯一标识码替换员工ID Easy
SELECT euni.unique_id, e.nameFROM Employees e LEFT JOIN EmployeeUNI euni ON e.id = euni.id;执行流程:
| Employees | EmployeeUNI | LEFT JOIN 结果 |
|---|---|---|
| id=1, Alice | id=1, unique_id=10 | unique_id=10, Alice |
| id=2, Bob | (无匹配) | unique_id=NULL, Bob |
1068. 产品销售分析 I Easy(INNER JOIN)
表结构:
Sales(sale_id, product_id, year, quantity, price)Product(product_id, product_name)
SELECT p.product_name, s.year, s.priceFROM Sales s JOIN Product p ON s.product_id = p.product_id;执行流程:
| Sales | Product | JOIN 结果 |
|---|---|---|
| sale_id=1, product_id=100, year=2008, price=5000 | product_id=100, Nokia | Nokia, 2008, 5000 |
| sale_id=2, product_id=100, year=2009, price=5000 | product_id=100, Nokia | Nokia, 2009, 5000 |
| sale_id=7, product_id=200, year=2011, price=7000 | product_id=200, Apple | Apple, 2011, 7000 |
1581. 进店却未进行过交易的顾客 Easy(LEFT JOIN + IS NULL)
表结构:
Visits(visit_id, customer_id)Transactions(transaction_id, visit_id, amount)
SELECT v.customer_id, COUNT(v.customer_id) AS count_no_transFROM Visits v LEFT JOIN Transactions t ON v.visit_id = t.visit_idWHERE t.transaction_id IS NULLGROUP BY v.customer_id;执行流程:
| Visits | LEFT JOIN Transactions | WHERE IS NULL | GROUP BY |
|---|---|---|---|
| visit=1, customer=23 | + trans=1, amount=950 | ❌ 有交易 | - |
| visit=2, customer=9 | + NULL(无交易) | ✅ | customer=9, count=1 |
| visit=4, customer=30 | + NULL(无交易) | ✅ | customer=30, count=1 |
| visit=5, customer=54 | + NULL(无交易) | ✅ | customer=54, count=2 |
| visit=7, customer=54 | + NULL(无交易) | ✅ | (54 共 2 次) |
| visit=8, customer=96 | + trans=5, amount=540 | ❌ 有交易 | - |
最终结果:
| customer_id | count_no_trans |
|---|---|
| 9 | 1 |
| 30 | 1 |
| 54 | 2 |
1280. 学生们参加各科测试的次数 Easy(CROSS JOIN + LEFT JOIN)
表结构:
Students(student_id, student_name)Subjects(subject_name)Examinations(student_id, subject_name)
SELECT s.student_id, s.student_name, su.subject_name, COUNT(e.subject_name) AS attended_examsFROM Students s JOIN Subjects su -- 先做笛卡尔积 LEFT JOIN Examinations e -- 再左连考试表 ON e.student_id = s.student_id AND e.subject_name = su.subject_nameGROUP BY s.student_id, su.subject_nameORDER BY s.student_id, su.subject_name;执行流程:
Students × Subjects(笛卡尔积):┌─────────────┬──────────┐│ student_id │ subject │├─────────────┼──────────┤│ 1, Alice │ Math │ ← LEFT JOIN Exam: 找到1条 → count=1│ 1, Alice │ Physics │ ← LEFT JOIN Exam: 找到0条 → count=0│ 1, Alice │ Math │ ...│ 2, Bob │ Math ││ 2, Bob │ Physics │└─────────────┴──────────┘
→ LEFT JOIN Examinations(保留所有学生×科目组合)→ GROUP BY (student_id, subject_name)→ COUNT(e.subject_name) 只计非 NULL核心点:先 Students JOIN Subjects 生成「每个学生 × 每个科目」的全组合,再 LEFT JOIN 考试表计算参加次数
1731. 每位经理的下属员工数量 Easy(自连接)
SELECT e2.employee_id, e2.name, COUNT(e1.reports_to) AS reports_count, ROUND(AVG(e1.age), 0) AS average_ageFROM Employees e1 JOIN Employees e2 ON e1.reports_to = e2.employee_idGROUP BY e1.reports_toORDER BY e2.employee_id;执行流程:
Employees 表:
| employee_id | name | reports_to | age |
|---|---|---|---|
| 9 | Hercy | NULL | 43 |
| 6 | Alice | 9 | 31 |
| 4 | Bob | 9 | 36 |
| 2 | Omer | 6 | 24 |
自连接(e1=下属, e2=经理):
| e1.name(下属) | e1.age | e2.employee_id(经理) | e2.name(经理) |
|---|---|---|---|
| Alice | 31 | 9 | Hercy |
| Bob | 36 | 9 | Hercy |
| Omer | 24 | 6 | Alice |
GROUP BY e1.reports_to 后:
| employee_id | name | reports_count | average_age |
|---|---|---|---|
| 9 | Hercy | 2 | ROUND((31+36)/2) = 34 |
| 6 | Alice | 1 | 24 |
1978. 上级经理已离职的公司员工 Easy(LEFT JOIN + IS NULL)
SELECT e1.employee_idFROM Employees e1 LEFT JOIN Employees e2 ON e1.manager_id = e2.employee_idWHERE e1.salary < 30000 AND e2.employee_id IS NULL AND e1.manager_id IS NOT NULLORDER BY e1.employee_id;执行流程:
| e1(员工) | e1.manager_id | e1.salary | e2(经理)匹配 | e2 IS NULL? | manager_id IS NOT NULL? | 结果 |
|---|---|---|---|---|---|---|
| 3, Mary | 1 | 25000 | (id=1 存在) | ❌ | ✅ | ❌ 经理未离职 |
| 7, Robert | 99 | 20000 | (id=99 不存在) | ✅ | ✅ | ✅ |
| 11, Brad | 5 | 28000 | (id=5 不存在) | ✅ | ✅ | ✅ |
| 13, Jason | NULL | 15000 | (无匹配) | ✅ | ❌ | ❌ 无经理 |
三个条件缺一不可:salary < 30000 + 经理不存在(LEFT JOIN 后 IS NULL) + 有经理(IS NOT NULL)
三、GROUP BY + HAVING(分组与聚合)
WHERE vs HAVING 对比
WHERE:在分组前过滤行 HAVING:在分组后过滤组┌─────────────┐ ┌─────────────┐│ 原始数据 │ │ 分组后的结果 ││ WHERE 过滤 │ → GROUP BY → │ HAVING 过滤 │ → 最终结果│ (行级) │ │ (组级) │└─────────────┘ └─────────────┘不能用聚合函数 可以用聚合函数0511. 游戏玩法分析 I Easy
表结构:Activity(player_id, device_id, event_date, games_played)
题目:查询每位玩家第一次登录的日期。
SELECT player_id, MIN(event_date) AS first_loginFROM ActivityGROUP BY player_id;执行流程:
| 原始 Activity 表 | → GROUP BY player_id | → MIN(event_date) |
|---|---|---|
| 1, 2, 2016-03-01, 5 | player_id=1: {2016-03-01, 2016-05-02} | 2016-03-01 |
| 1, 2, 2016-05-02, 6 | player_id=2: {2017-06-25} | 2017-06-25 |
| 2, 3, 2017-06-25, 1 | player_id=3: {2016-03-02, 2018-07-03} | 2016-03-02 |
| 3, 1, 2016-03-02, 0 | ||
| 3, 4, 2018-07-03, 5 |
0182. 查找重复的电子邮箱 Easy
表结构:Person(id, email)
题目:找出重复的邮箱。
SELECT email AS EmailFROM PersonGROUP BY emailHAVING COUNT(email) > 1;执行流程:
Person 表:
| id | |
|---|---|
| 1 | a@b.com |
| 2 | c@d.com |
| 3 | a@b.com |
GROUP BY email 后:
| COUNT | HAVING COUNT > 1? | |
|---|---|---|
| a@b.com | 2 | ✅ |
| c@d.com | 1 | ❌ |
最终结果:a@b.com
核心点:GROUP BY 分组后,HAVING 过滤出 COUNT > 1 的组(即重复的邮箱)
0596. 超过 5 名学生的课 Easy
表结构:Courses(student, class)
SELECT classFROM CoursesGROUP BY classHAVING COUNT(DISTINCT student) >= 5;执行流程:
Courses 表:
| student | class |
|---|---|
| A | Math |
| B | English |
| C | Math |
| D | Biology |
| E | Math |
| F | Math |
| G | Math |
| H | Math |
GROUP BY class 后:
| class | COUNT(DISTINCT student) | >= 5? |
|---|---|---|
| Math | 6 | ✅ |
| English | 1 | ❌ |
| Biology | 1 | ❌ |
最终结果:Math
0570. 至少有5名直接下属的经理 Medium(自连接 + GROUP BY + HAVING)
SELECT e1.nameFROM Employee e1 JOIN Employee e2 ON e1.Id = e2.managerIdGROUP BY e1.IdHAVING COUNT(e2.Id) >= 5;执行流程:
Employee 表(自连接)e1=经理, e2=下属┌─────────────┬──────────────────┐│ e1.name(经理) │ e2.Id(下属) │├─────────────┼──────────────────┤│ Stephen │ 1, 2, 3, 4, 5 │ → COUNT=5 ✅│ Alice │ 6, 7 │ → COUNT=2 ❌└─────────────┴──────────────────┘→ GROUP BY e1.Id → HAVING COUNT >= 5 → Stephen1045. 买下所有产品的客户 Medium(HAVING COUNT = 子查询)
表结构:
Customer(customer_id, product_key)Product(product_key)
SELECT customer_idFROM CustomerGROUP BY customer_idHAVING COUNT(DISTINCT product_key) = (SELECT COUNT(*) FROM Product);执行流程:
Product 表共 3 个产品(product_key: 1, 2, 3)→ 子查询: SELECT COUNT(*) FROM Product = 3
Customer 表:┌─────────────┬────────────┐│ customer_id │ product_key│├─────────────┼────────────┤│ 1 │ 1 ││ 1 │ 2 ││ 1 │ 3 │ → COUNT(DISTINCT)=3 = 3 ✅│ 2 │ 1 ││ 2 │ 2 │ → COUNT(DISTINCT)=2 ≠ 3 ❌│ 3 │ 1 ││ 3 │ 2 ││ 3 │ 3 ││ 3 │ 1(重复) │ → COUNT(DISTINCT)=3 = 3 ✅└─────────────┴────────────┘
最终结果: customer_id = 1, 3核心点:COUNT(DISTINCT product_key) 去重后与 Product 总数比较
1084. 销售分析 III Easy(HAVING + MIN/MAX)
表结构:
Product(product_id, product_name, unit_price)Sales(seller_id, product_id, buyer_id, sale_date, quantity, price)
题目:找仅在 2019 春季售出的产品。
SELECT p.product_id, p.product_nameFROM Product p JOIN Sales s ON p.product_id = s.product_idGROUP BY s.product_idHAVING MIN(s.sale_date) >= '2019-01-01' AND MAX(s.sale_date) <= '2019-03-31';执行流程:
| product_id | 所有 sale_date | MIN | MAX | 都在春季? |
|---|---|---|---|---|
| 1 | 2019-02-17, 2019-02-25 | 2019-02-17 | 2019-02-25 | ✅ |
| 2 | 2019-02-01, 2019-04-04 | 2019-02-01 | 2019-04-04 | ❌ (4月超出) |
| 3 | 2019-03-10 | 2019-03-10 | 2019-03-10 | ✅ |
核心点:用 MIN/MAX 判断所有销售记录是否都在目标范围内
1587. 银行账户概要 II Easy(GROUP BY + HAVING SUM)
SELECT u.name, SUM(t.amount) AS balanceFROM Users u JOIN Transactions t ON u.account = t.accountGROUP BY t.accountHAVING SUM(t.amount) > 10000;执行流程:
| Users | Transactions | JOIN + GROUP BY | HAVING > 10000 |
|---|---|---|---|
| account=1, Alice | account=1, +7000 | Alice: 7000+7000=14000 | ✅ |
| account=2, Bob | account=1, +7000 | Bob: 3000-5000=-2000 | ❌ |
| account=2, +3000 | |||
| account=2, -5000 |
最终结果:
| name | balance |
|---|---|
| Alice | 14000 |
1193. 每月交易 I Medium(条件聚合 SUM + IF)
表结构:Transactions(id, country, state, amount, trans_date)
题目:按月和国家统计交易数、已批准交易数及金额。
SELECT DATE_FORMAT(trans_date, '%Y-%m') AS month, country, COUNT(state) 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_amountFROM TransactionsGROUP BY month, country;执行流程:
原始 Transactions 表:
| id | country | state | amount | trans_date |
|---|---|---|---|---|
| 121 | US | approved | 1000 | 2019-01-18 |
| 122 | US | declined | 2000 | 2019-01-19 |
| 123 | US | approved | 3000 | 2019-01-27 |
| 124 | DE | approved | 2000 | 2019-01-14 |
GROUP BY (month, country) 后:
| month | country | trans_count | approved_count | trans_total | approved_total |
|---|---|---|---|---|---|
| 2019-01 | US | 3 | SUM(1,0,1)=2 | 6000 | 1000+3000=4000 |
| 2019-01 | DE | 1 | SUM(1)=1 | 2000 | 2000 |
核心点:
SUM(IF(state='approved', 1, 0))实现条件计数——approved 记 1,否则记 0
1393. 股票的资本损益 Medium(CASE WHEN + SUM)
表结构:Stocks(stock_name, operation, operation_day, price)
SELECT stock_name, SUM(CASE WHEN operation = 'Buy' THEN -price WHEN operation = 'Sell' THEN price END) AS capital_gain_lossFROM StocksGROUP BY stock_name;执行流程:
| stock_name | operation | price | CASE 结果 |
|---|---|---|---|
| Leetcode | Buy | 1000 | -1000 |
| Leetcode | Sell | 9000 | +9000 |
| Corona | Buy | 3000 | -3000 |
| Corona | Sell | 1580 | +1580 |
GROUP BY stock_name 后:
| stock_name | capital_gain_loss |
|---|---|
| Leetcode | -1000 + 9000 = 8000 |
| Corona | -3000 + 1580 = -1420 |
Buy 取负(支出),Sell 取正(收入),SUM 求和得到净损益
1484. 按日期分组销售产品 Easy(GROUP_CONCAT)
表结构:Activities(sell_date, product)
SELECT sell_date, COUNT(DISTINCT product) AS num_sold, GROUP_CONCAT(DISTINCT product ORDER BY product) AS productsFROM ActivitiesGROUP BY sell_date;执行流程:
| 原始 Activities | → GROUP BY sell_date | → GROUP_CONCAT |
|---|---|---|
| 2020-05-30, Headphone | 2020-05-30: {Headphone, Basketball, PC} | 2020-05-30: “Basketball,Headphone,PC” |
| 2020-06-01, Pencil | 2020-06-01: {Pencil, Bathing} | 2020-06-01: “Bathing,Pencil” |
| 2020-06-02, Mask | 2020-06-02: {Mask, Bathing} | 2020-06-02: “Bathing,Mask” |
| 2020-05-30, Basketball | COUNT(DISTINCT)=3 | |
| 2020-06-01, Bathing | COUNT(DISTINCT)=2 | |
| 2020-05-30, PC | ||
| 2020-06-02, Bathing |
核心点:
GROUP_CONCAT(DISTINCT product ORDER BY product)去重 + 排序 + 逗号拼接
1693. 每天的领导和合伙人 Easy
SELECT date_id, make_name, COUNT(DISTINCT lead_id) AS unique_leads, COUNT(DISTINCT partner_id) AS unique_partnersFROM DailySalesGROUP BY date_id, make_name;执行流程:
| 原始 DailySales | → GROUP BY (date_id, make_name) | → COUNT(DISTINCT) |
|---|---|---|
| 2020-12-8, Toyota, lead=0, partner=1 | (2020-12-8, Toyota): | unique_leads=2 (0,1) |
| 2020-12-8, Toyota, lead=1, partner=0 | leads={0,1,1} → DISTINCT={0,1} | unique_partners=2 (0,1) |
| 2020-12-8, Toyota, lead=1, partner=2 | partners={1,0,2} → DISTINCT={0,1,2} | |
| 2020-12-7, Toyota, lead=0, partner=1 | (2020-12-7, Toyota): | unique_leads=1 (0) |
| 2020-12-7, Toyota, lead=0, partner=0 | leads={0,0} → DISTINCT={0} | unique_partners=2 (0,1) |
四、子查询(IN / EXISTS / 相关子查询)
0619. 只出现一次的最大数字 Easy
表结构:MyNumbers(num)(无主键,可能有重复)
SELECT MAX(num) AS numFROM (SELECT num FROM MyNumbers GROUP BY num HAVING COUNT(num) = 1) AS numbers;执行流程:
原始 MyNumbers: → GROUP BY num + HAVING COUNT=1: → MAX():┌──────┐ ┌──────┐ ┌──────┐│ num │ │ num │ │ num │├──────┤ ├──────┤ ├──────┤│ 8 │ │ 8 │ ← 出现1次 ✅ │ ││ 8 │ │ 6 │ ← 出现1次 ✅ │ 6 ││ 3 │ │ │ 3 出现2次 ❌ │ ││ 3 │ │ │ 1 出现3次 ❌ │ ││ 7 │ │ 7 │ ← 出现1次 ✅ │ ││ 6 │ └──────┘ └──────┘│ 6 ││ 1 ││ 1 ││ 1 │└──────┘0585. 2016年的投资 Medium(多重子查询)
表结构:Insurance(pid, tiv_2015, tiv_2016, lat, lon)
题目:2015 投保额与他人相同 + 城市坐标唯一 → 求 2016 投保额之和。
SELECT ROUND(SUM(tiv_2016), 2) AS tiv_2016FROM InsuranceWHERE (lat, lon) IN (SELECT lat, lon FROM Insurance GROUP BY lat, lon HAVING COUNT(pid) = 1) AND tiv_2015 IN (SELECT tiv_2015 FROM Insurance GROUP BY tiv_2015 HAVING COUNT(pid) > 1);执行流程:
原始 Insurance 表:┌─────┬──────────┬──────────┬──────┬──────┐│ pid │ tiv_2015 │ tiv_2016 │ lat │ lon │├─────┼──────────┼──────────┼──────┼──────┤│ 1 │ 10 │ 5 │ 10 │ 10 ││ 2 │ 20 │ 20 │ 20 │ 20 ││ 3 │ 10 │ 30 │ 20 │ 20 │ ← lat,lon 与 pid=2 相同!│ 4 │ 10 │ 40 │ 40 │ 40 │└─────┴──────────┴──────────┴──────┴──────┘
子查询1: 城市坐标唯一的 → (10,10) 和 (40,40) → pid=1, 4子查询2: 2015投保额重复的 → tiv_2015=10 (出现3次) → pid=1,3,4
两个条件取交集: pid=1 和 pid=4→ SUM(tiv_2016) = 5 + 40 = 45核心点:两个子查询分别筛选,WHERE 中 AND 取交集
0185. 部门工资前三高的所有员工 Hard(相关子查询)
表结构:
Employee(id, name, salary, departmentId)Department(id, name)
SELECT d.name AS Department, e.name AS Employee, e.salary AS SalaryFROM Employee e JOIN Department d ON e.departmentId = d.idWHERE (SELECT COUNT(DISTINCT e2.salary) FROM Employee e2 WHERE e.salary < e2.salary AND e.departmentId = e2.departmentId) < 3;执行流程:
Employee 表:
| id | name | salary | deptId |
|---|---|---|---|
| 1 | Joe | 85000 | 1 |
| 2 | Henry | 80000 | 2 |
| 3 | Sam | 60000 | 2 |
| 4 | Max | 90000 | 1 |
| 5 | Janet | 69000 | 1 |
| 6 | Randy | 85000 | 1 |
对每个员工,相关子查询计算「同部门中工资比我高的不同工资金额数」:
| 员工 | 部门 | 子查询:同部门比我高的不同 salary | COUNT | < 3? |
|---|---|---|---|---|
| Joe(85000) | 1 | {90000} | 1 | ✅ |
| Henry(80000) | 2 | {} | 0 | ✅ |
| Sam(60000) | 2 | {80000} | 1 | ✅ |
| Max(90000) | 1 | {} | 0 | ✅ |
| Janet(69000) | 1 | {85000, 90000} | 2 | ✅ |
| Randy(85000) | 1 | {90000} | 1 | ✅ |
核心点:相关子查询引用外层 e.salary 和 e.departmentId,COUNT(DISTINCT) 确保同薪金不重复计数
1789. 员工的直属部门 Easy
表结构:Employee(employee_id, department_id, primary_flag)
SELECT employee_id, department_idFROM EmployeeWHERE primary_flag = 'Y' OR employee_id IN (SELECT employee_id FROM Employee GROUP BY employee_id HAVING COUNT(employee_id) = 1);执行流程:
Employee 表:┌─────────────┬──────────────┬──────────────┐│ employee_id │ department_id│ primary_flag │├─────────────┼──────────────┼──────────────┤│ 1 │ 1 │ N │ ← 只有1个部门 → 子查询命中 ✅│ 2 │ 1 │ Y │ ← primary_flag=Y ✅│ 2 │ 2 │ N │ ← 不满足任何条件 ❌│ 3 │ 2 │ N │ ← 只有1个部门 → 子查询命中 ✅│ 4 │ 1 │ N │ ← 有2个部门,primary都不是Y → 看:│ 4 │ 2 │ N │ 子查询 COUNT=2≠1 ❌, primary≠Y ❌└─────────────┴──────────────┴──────────────┘
最终结果: (1,1), (2,1), (3,2)核心点:OR 连接两个条件——要么标记为主部门,要么只有一个部门
五、窗口函数(RANK / SUM OVER / ROWS BETWEEN)
窗口函数执行原理
普通 GROUP BY:每组只输出一行 窗口函数:每行都输出,但聚合范围是「窗口」┌────────────┐ ┌────────────────────────┐│ dept │ AVG │ │ emp │ dept │ salary │ AVG(dept) │├──────┼─────┤ ├─────┼──────┼────────┼──────────┤│ A │ 75 │ │ 1 │ A │ 80 │ 75 │ ← 同 dept 的平均│ B │ 60 │ │ 2 │ A │ 70 │ 75 │└────────────┘ │ 3 │ B │ 60 │ 60 │ └─────┴──────┴────────┴──────────┘1204. 最后一个能进入巴士的人 Medium(累计求和窗口)
表结构:Queue(person_id, person_name, weight, turn)
题目:找最后一个上车且总重 ≤ 1000 的人。
SELECT person_nameFROM (SELECT person_name, turn, SUM(weight) OVER(ORDER BY turn) AS sum_weight FROM Queue) AS weiWHERE sum_weight <= 1000ORDER BY turn DESC LIMIT 1;执行流程:
| turn | person_name | weight | SUM() OVER(ORDER BY turn) 累计 |
|---|---|---|---|
| 1 | Alice | 250 | 250 |
| 2 | Bob | 350 | 250+350=600 |
| 3 | Alex | 400 | 600+400=1000 |
| 4 | John | 300 | 1000+300=1300 |
| 5 | Winston | 500 | 1300+500=1800 |
WHERE sum_weight <= 1000 后:Alice(250), Bob(600), Alex(1000)
ORDER BY turn DESC LIMIT 1:取 turn 最大的 → Alex
核心点:
SUM() OVER(ORDER BY turn)对每一行计算从第一行到当前行的累计和
1321. 餐馆营业额变化增长 Medium(7天滚动窗口)
表结构:Customer(customer_id, name, visited_on, amount)
题目:计算 7 天滚动窗口的消费总额和日均值。
SELECT visited_on, amount, average_amountFROM (SELECT visited_on, SUM(amount) OVER(ORDER BY visited_on ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS amount, ROUND(AVG(amount) OVER(ORDER BY visited_on ROWS BETWEEN 6 PRECEDING AND CURRENT ROW), 2) AS average_amount, ROW_NUMBER() OVER(ORDER BY visited_on) AS rn FROM (SELECT visited_on, SUM(amount) AS amount FROM Customer GROUP BY visited_on) AS daily) AS rankedWHERE rn > 6;执行流程:
第1步:先按天聚合 visited_on | amount(当天总额) 2019-01-01 | 130 2019-01-02 | 110 2019-01-03 | 140 2019-01-04 | 100 2019-01-05 | 110 2019-01-06 | 130 2019-01-07 | 150 2019-01-08 | 120 2019-01-09 | 200
第2步:7天滚动窗口(ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)
visited_on | 窗口范围 | SUM | AVG | rn | >6? 2019-01-01 | [01-01] | 130 | 130.00 | 1 | ❌ 2019-01-02 | [01-01 ~ 01-02] | 240 | 120.00 | 2 | ❌ ... | 窗口不足7天 | ... | ... | .. | ❌ 2019-01-07 | [01-01 ~ 01-07] | 870 | 124.29 | 7 | ❌ 2019-01-08 | [01-02 ~ 01-08] | 860 | 122.86 | 8 | ✅ → 输出 2019-01-09 | [01-03 ~ 01-09] | 950 | 135.71 | 9 | ✅ → 输出核心点:
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW定义了包含当前行和前 6 行的 7 天窗口。ROW_NUMBER() > 6过滤掉窗口不足 7 天的前几行
1341. 电影评分 Medium(CTE + RANK + UNION ALL)
表结构:
Movies(movie_id, title)Users(user_id, name)MovieRating(movie_id, user_id, rating, created_at)
题目:① 评论最多的用户名 ② 2020年2月平均评分最高的电影名
WITH uion AS (SELECT u.user_id, u.name, m.title, mr.rating, mr.created_at FROM MovieRating mr JOIN Users u ON mr.user_id = u.user_id JOIN Movies m ON mr.movie_id = m.movie_id)SELECT name AS resultsFROM (SELECT name, RANK() OVER(ORDER BY COUNT(title) DESC, name) AS rk FROM uion GROUP BY user_id) AS max_userWHERE rk = 1UNION ALLSELECT title AS resultsFROM (SELECT title, RANK() OVER(ORDER BY AVG(rating) DESC, title) AS rk FROM uion WHERE created_at BETWEEN '2020-02-01' AND '2020-02-29' GROUP BY title) AS max_titleWHERE rk = 1;执行流程:
第1步:CTE uion — 三表 JOIN┌────────┬────────┬─────────────┬───────┬────────────┐│ user_id│ name │ title │ rating│ created_at │├────────┼────────┼─────────────┼───────┼────────────┤│ 3 │ Daniel │ Ice │ 5 │ 2020-02-14 ││ 2 │ Daniel │ Detention │ 3 │ 2020-02-12 ││ ... │ ... │ ... │ ... │ ... │└────────┴────────┴─────────────┴───────┴────────────┘
第2步:子查询1 — 用户评论数排名GROUP BY user_id → COUNT(title) → RANK() OVER(ORDER BY count DESC, name)┌────────┬───────┬─────┐│ name │ count │ rk │├────────┼───────┼─────┤│ Daniel │ 3 │ 1 │ ← WHERE rk=1 → 输出 "Daniel"│ Gloria │ 1 │ 2 │└────────┴───────┴─────┘
第3步:子查询2 — 2月电影平均评分排名WHERE created_at BETWEEN '2020-02-01' AND '2020-02-29'GROUP BY title → AVG(rating) → RANK() OVER(ORDER BY avg DESC, title)┌─────────────┬───────┬─────┐│ title │ avg │ rk │├─────────────┼───────┼─────┤│ Ice │ 4.5 │ 1 │ ← WHERE rk=1 → 输出 "Ice"│ Detention │ 3.0 │ 2 │└─────────────┴───────┴─────┘
第4步:UNION ALL 合并┌──────────┐│ results │├──────────┤│ Daniel ││ Ice │└──────────┘核心点:RANK() 处理平局(ORDER BY count DESC, name 确保平局取字典序小的)
六、CASE WHEN(条件表达式)
0610. 判断三角形 Easy
表结构:Triangle(x, y, z)
SELECT x, y, z, CASE WHEN x + y > z AND x + z > y AND y + z > x THEN 'Yes' ELSE 'No' END AS triangleFROM Triangle;执行流程:
| x | y | z | x+y>z? | x+z>y? | y+z>x? | 全满足? | triangle |
|---|---|---|---|---|---|---|---|
| 13 | 15 | 30 | 28>30 ❌ | 43>15 ✅ | 45>13 ✅ | ❌ | No |
| 10 | 20 | 15 | 30>15 ✅ | 25>20 ✅ | 35>10 ✅ | ✅ | Yes |
0626. 换座位 Medium
表结构:Seat(id, student) — id 从 1 连续
题目:交换相邻座位号,奇数个学生时最后一个不动。
SELECT CASE WHEN id % 2 = 0 THEN id - 1 WHEN id % 2 = 1 AND id != (SELECT COUNT(*) FROM Seat) THEN id + 1 ELSE idENDAS id, studentFROM Seat ORDER BY id;执行流程:
| 原始 id | student | id % 2 | 总行数 | CASE 结果 | 新 id |
|---|---|---|---|---|---|
| 1 | Abbot | 1 | 5 | 奇数且≠5 → id+1 | 2 |
| 2 | Doris | 0 | 5 | 偶数 → id-1 | 1 |
| 3 | Emerson | 1 | 5 | 奇数且≠5 → id+1 | 4 |
| 4 | Green | 0 | 5 | 偶数 → id-1 | 3 |
| 5 | Jeames | 1 | 5 | 奇数且=5(最后一行) → id | 5 |
ORDER BY id 后:
| id | student |
|---|---|
| 1 | Doris |
| 2 | Abbot |
| 3 | Green |
| 4 | Emerson |
| 5 | Jeames |
核心点:子查询
COUNT(*)判断总行数,最后一行如果是奇数 id 则保持不变
0627. 变更性别 Easy(UPDATE + CASE)
UPDATE SalarySET sex = CASE WHEN sex = 'm' THEN 'f' WHEN sex = 'f' THEN 'm' END;执行流程:
| id | name | sex(前) | → CASE → | sex(后) |
|---|---|---|---|---|
| 1 | A | m | → f | f |
| 2 | B | f | → m | m |
| 3 | C | f | → m | m |
| 4 | D | m | → f | f |
1907. 按分类统计薪水 Medium(UNION + IF)
表结构:Accounts(account_id, income)
题目:按 Low(<20000) / Average(20000-50000) / High(>50000) 统计账户数。
SELECT 'Low Salary' AS category, SUM(IF(income < 20000, 1, 0)) AS accounts_countFROM AccountsUNIONSELECT 'Average Salary', SUM(IF(income >= 20000 AND income <= 50000, 1, 0))FROM AccountsUNIONSELECT 'High Salary', SUM(IF(income > 50000, 1, 0))FROM Accounts;执行流程:
Accounts 表: income = [8000, 25000, 30000, 60000, 15000]
第1个 SELECT: 'Low Salary', SUM(IF(income<20000,1,0)) → 8000→1, 25000→0, 30000→0, 60000→0, 15000→1 → SUM=2
第2个 SELECT: 'Average Salary', SUM(IF(20000≤income≤50000,1,0)) → 8000→0, 25000→1, 30000→1, 60000→0, 15000→0 → SUM=2
第3个 SELECT: 'High Salary', SUM(IF(income>50000,1,0)) → 8000→0, 25000→0, 30000→0, 60000→1, 15000→0 → SUM=1
UNION 合并:┌──────────────────┬───────────────┐│ category │ accounts_count│├──────────────────┼───────────────┤│ Low Salary │ 2 ││ Average Salary │ 2 ││ High Salary │ 1 │└──────────────────┴───────────────┘核心点:UNION 确保三个类别都输出(即使某类别为 0),用字符串字面量作为分类名
七、UNION / UNION ALL(合并结果集)
UNION vs UNION ALL
UNION:去重合并 UNION ALL:不去重合并┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐│ A │ │ A │ │ A │ │ A ││ B │ │ C │ │ B │ │ C │└──────┘ └──────┘ └──────┘ └──────┘ ↓ ↓┌──────┐ ┌──────┐│ A │ ← 去重 │ A ││ B │ │ A │ ← 保留│ C │ │ B │└──────┘ │ C │ └──────┘1795. 每个产品在不同商店的价格 Easy(列转行)
表结构:Products(product_id, store1, store2, store3)
题目:将列转行,输出 (product_id, store, price)。
SELECT product_id, 'store1' AS store, store1 AS priceFROM ProductsWHERE store1 IS NOT NULLUNION ALLSELECT product_id, 'store2' AS store, store2 AS priceFROM ProductsWHERE store2 IS NOT NULLUNION ALLSELECT product_id, 'store3' AS store, store3 AS priceFROM ProductsWHERE store3 IS NOT NULL;执行流程:
原始 Products 表(宽表):┌────────────┬────────┬────────┬────────┐│ product_id │ store1 │ store2 │ store3 │├────────────┼────────┼────────┼────────┤│ 0 │ 95 │ 100 │ 105 ││ 1 │ 70 │ NULL │ 80 │└────────────┴────────┴────────┴────────┘
UNION ALL 后(长表):┌────────────┬────────┬───────┐│ product_id │ store │ price │├────────────┼────────┼───────┤│ 0 │ store1 │ 95 ││ 0 │ store2 │ 100 ││ 0 │ store3 │ 105 ││ 1 │ store1 │ 70 ││ 1 │ store3 │ 80 │ ← store2=NULL 被过滤└────────────┴────────┴───────┘核心点:UNION ALL 将宽表的多列转为长表的多行,
WHERE IS NOT NULL过滤无价格的商店
1164. 指定日期的产品价格 Medium(UNION + 子查询)
表结构:Products(product_id, new_price, change_date)
题目:找 2019-08-16 时所有产品的价格(初始价格=10)。
-- 有变更记录的产品:取 ≤ 2019-08-16 的最近一次价格SELECT product_id, new_price AS priceFROM ProductsWHERE (product_id, change_date) IN (SELECT product_id, MAX(change_date) FROM Products WHERE change_date <= '2019-08-16' GROUP BY product_id)UNION-- 无变更记录的产品:价格为默认 10SELECT product_id, 10 AS priceFROM ProductsWHERE (product_id, change_date) IN (SELECT product_id, MIN(change_date) FROM Products GROUP BY product_id HAVING MIN(change_date) > '2019-08-16')ORDER BY product_id;执行流程:
Products 表:┌────────────┬───────────┬─────────────┐│ product_id │ new_price │ change_date │├────────────┼───────────┼─────────────┤│ 1 │ 20 │ 2019-08-14 │ ← ≤ 08-16, 最近│ 2 │ 50 │ 2019-08-01 │ ← ≤ 08-16, 最近│ 1 │ 10 │ 2019-08-17 │ ← > 08-16│ 3 │ 30 │ 2019-08-19 │ ← 首次变更 > 08-16 → 默认 10└────────────┴───────────┴─────────────┘
子查询1: ≤ 08-16 的最近变更 → (1, 2019-08-14), (2, 2019-08-01) → product 1 → price=20, product 2 → price=50
子查询2: 首次变更 > 08-16 → (3, 2019-08-19) → product 3 → price=10 (默认)
UNION 合并:┌────────────┬───────┐│ product_id │ price │├────────────┼───────┤│ 1 │ 20 ││ 2 │ 50 ││ 3 │ 10 │└────────────┴───────┘八、字符串与日期函数
0550. 游戏玩法分析 IV Medium(DATEDIFF + 子查询)
表结构:Activity(player_id, device_id, event_date, games_played)
题目:首次登录第二天再次登录的玩家比率。
SELECT ROUND(COUNT(m.player_id) / COUNT(DISTINCT a.player_id), 2) AS fractionFROM Activity a LEFT JOIN (SELECT player_id, MIN(event_date) AS event_date FROM Activity GROUP BY player_id) m ON a.player_id = m.player_id AND DATEDIFF(a.event_date, m.event_date) = 1;执行流程:
第1步:子查询找首次登录日期 player_id=1 → 2016-03-01 player_id=2 → 2017-06-25 player_id=3 → 2016-03-02
第2步:LEFT JOIN 条件: 同玩家 + DATEDIFF=1(次日登录) Activity(1, 2016-03-02) JOIN m(1, 2016-03-01) → DATEDIFF=1 ✅ → m.player_id=1 Activity(1, 2016-03-01) JOIN m(1, 2016-03-01) → DATEDIFF=0 ❌ Activity(2, 2017-06-25) JOIN m(2, 2017-06-25) → DATEDIFF=0 ❌ Activity(3, 2018-07-03) JOIN m(3, 2016-03-02) → DATEDIFF=488 ❌
第3步:计算比率 COUNT(m.player_id) = 1(只有 player 1 次日登录) COUNT(DISTINCT a.player_id) = 3(总玩家数) fraction = 1/3 = 0.33核心点:LEFT JOIN 确保所有玩家都被计入分母,DATEDIFF(a, b) = a - b 的天数差
0197. 上升的温度 Easy(DATEDIFF + 自连接)
表结构:Weather(id, recordDate, temperature)
SELECT w1.idFROM Weather w1 JOIN Weather w2 ON DATEDIFF(w1.recordDate, w2.recordDate) = 1WHERE w1.temperature > w2.temperature;执行流程:
| w1 (今天) | w2 (昨天) | DATEDIFF | 温度比较 | 结果 |
|---|---|---|---|---|
| 2015-01-02, 25° | 2015-01-01, 10° | 1 ✅ | 25 > 10 ✅ | id=2 |
| 2015-01-03, 20° | 2015-01-02, 25° | 1 ✅ | 20 > 25 ❌ | - |
| 2015-01-04, 30° | 2015-01-03, 20° | 1 ✅ | 30 > 20 ✅ | id=4 |
0180. 连续出现的数字 Medium(自连接 ×3)
表结构:Logs(id, num)
SELECT DISTINCT l1.Num AS ConsecutiveNumsFROM Logs l1, Logs l2, Logs l3WHERE l1.Id = l2.Id - 1 AND l2.Id = l3.Id - 1 AND l1.Num = l2.Num AND l2.Num = l3.Num;执行流程:
Logs 表:┌────┬─────┐│ id │ num │├────┼─────┤│ 1 │ 1 ││ 2 │ 1 ││ 3 │ 1 │ ← 1 连续3次 ✅│ 4 │ 2 ││ 5 │ 1 ││ 6 │ 2 ││ 7 │ 2 │ ← 2 只连续2次 ❌└────┴─────┘
三表自连接条件: l1.id=l2.id-1 AND l2.id=l3.id-1→ 匹配: (id=1,num=1), (id=2,num=1), (id=3,num=1)→ l1.Num=l2.Num=l3.Num=1 ✅→ DISTINCT 后结果: ConsecutiveNums = 11517. 查找拥有有效邮箱的用户 Easy(REGEXP)
SELECT *FROM UsersWHERE mail REGEXP '^[a-zA-Z][a-zA-Z0-9_.-]*@leetcode\.com$' COLLATE utf8mb4_bin;正则解析:
^[a-zA-Z] → 以字母开头[a-zA-Z0-9_.-]* → 后跟任意数量的字母/数字/下划线/点/破折号@leetcode\.com$ → 以 @leetcode.com 结尾COLLATE utf8mb4_bin → 区分大小写(确保域名为小写)执行流程:
| user_id | name | 匹配? | |
|---|---|---|---|
| 1 | Winston | winston@leetcode.com | ✅ |
| 2 | Jonathan | jonathanisgreat | ❌ 无 @ |
| 3 | Annabelle | bella-@leetcode.com | ✅ |
| 4 | Sally | sally.come@leetcode.com | ✅ |
| 5 | Marwan | quarz#2020@leetcode.com | ❌ # 不允许 |
| 6 | David | david1@gmail.com | ❌ 域名不对 |
| 7 | George | George@leetcode.com | ❌ G 大写(utf8mb4_bin 区分大小写) |
1527. 患某种疾病的患者 Easy(LIKE 模糊匹配)
表结构:Patients(patient_id, patient_name, conditions)
题目:找患 I 类糖尿病(DIAB1 开头)的患者。
SELECT *FROM PatientsWHERE conditions LIKE '% DIAB1%' OR conditions LIKE 'DIAB1%';执行流程:
| patient_id | conditions | 匹配方式 | 结果 |
|---|---|---|---|
| 1 | DIAB100 | LIKE ‘DIAB1%’ ✅(开头) | 输出 |
| 2 | SADIAB100 | ❌ 不匹配(SAD 不是空格分隔) | 不输出 |
| 3 | ASDIAB1 | ❌ 同上 | 不输出 |
| 4 | FR DIAB100 | LIKE ’% DIAB1%’ ✅(前面有空格) | 输出 |
| 5 | SAD DIAB100 | LIKE ’% DIAB1%’ ✅ | 输出 |
⚠️
LIKE '%DIAB1%'会误匹配 SADIAB100!必须用'% DIAB1%'(前有空格)或'DIAB1%'(在开头)
1667. 修复表中的名字 Easy(字符串函数)
SELECT user_id, CONCAT(UPPER(SUBSTRING(name, 1, 1)), LOWER(SUBSTRING(name, 2))) AS nameFROM UsersORDER BY user_id;执行流程:
| user_id | name(原始) | SUBSTRING(name,1,1) | UPPER | SUBSTRING(name,2) | LOWER | CONCAT |
|---|---|---|---|---|---|---|
| 1 | aLICE | a | A | LICE | lice | Alice |
| 2 | bOB | b | B | OB | ob | Bob |
九、CTE 与复杂查询
3554. 查找类别推荐对 Hard(CTE + 自连接)
表结构:
ProductPurchases(user_id, product_id, quantity)ProductInfo(product_id, category, price)
题目:找同时购买两个类别的用户数 ≥ 3 的类别对。
WITH tab AS (SELECT pp.user_id, pi.category FROM ProductPurchases pp JOIN ProductInfo pi ON pp.product_id = pi.product_id)SELECT tab.category AS category1, tab2.category AS category2, COUNT(DISTINCT tab.user_id) AS customer_countFROM tab JOIN tab AS tab2 ON tab.user_id = tab2.user_id AND tab.category < tab2.categoryGROUP BY tab.category, tab2.categoryHAVING customer_count >= 3ORDER BY customer_count DESC, category1, category2;执行流程:
第1步:CTE tab — 关联购买记录和产品类别┌─────────┬──────────┐│ user_id │ category │├─────────┼──────────┤│ 1 │ A ││ 1 │ B ││ 2 │ A ││ 2 │ B ││ 3 │ A ││ 3 │ B ││ 4 │ A │ ← 只买了 A,没有 B└─────────┴──────────┘
第2步:自连接生成类别对(条件 category1 < category2 避免重复)tab JOIN tab ON user_id 相同 AND tab.category < tab2.category┌─────────┬───────────┬───────────┐│ user_id │ category1 │ category2 │├─────────┼───────────┼───────────┤│ 1 │ A │ B ││ 2 │ A │ B ││ 3 │ A │ B ││ 4 │ (无 B) │ │ ← 不匹配└─────────┴───────────┴───────────┘
第3步:GROUP BY (category1, category2) + HAVING ≥ 3┌───────────┬───────────┬────────────────┬──────┐│ category1 │ category2 │ COUNT(DISTINCT)│ ≥ 3? │├───────────┼───────────┼────────────────┼──────┤│ A │ B │ 3 │ ✅ │└───────────┴───────────┴────────────────┴──────┘核心点:
tab.category < tab2.category确保 (A,B) 和 (B,A) 只出现一次
1934. 确认率 Medium(LEFT JOIN + IF + IFNULL)
表结构:
Signups(user_id, time_stamp)Confirmations(user_id, time_stamp, action)
题目:每个用户的确认率 = confirmed 数 / 总请求数,无请求为 0。
SELECT s.user_id, ROUND(IFNULL(SUM(IF(c.action = 'confirmed', 1, 0)) / COUNT(c.action), 0), 2) AS confirmation_rateFROM Signups s LEFT JOIN Confirmations c ON s.user_id = c.user_idGROUP BY s.user_id;执行流程:
| Signups | LEFT JOIN Confirmations | action | IF(confirmed) | COUNT(action) | 比率 |
|---|---|---|---|---|---|
| user=3 | (3, confirmed) | confirmed → 1 | |||
| user=3 | (3, timeout) | timeout → 0 | SUM=1 | COUNT=2 | 1/2=0.5 |
| user=7 | (7, timeout) | timeout → 0 | |||
| user=7 | (7, timeout) | timeout → 0 | SUM=0 | COUNT=2 | 0/2=0 |
| user=6 | (无匹配) | NULL | SUM=NULL | COUNT=0 | IFNULL(NULL,0)=0 |
最终结果:
| user_id | confirmation_rate |
|---|---|
| 3 | 0.50 |
| 7 | 0.00 |
| 6 | 0.00 |
十、正则表达式与模式匹配
3475. DNA 模式识别 Medium(CASE WHEN + LIKE + REGEXP)
表结构:Samples(sample_id, dna_sequence, species)
SELECT sample_id, dna_sequence, species, CASE WHEN dna_sequence LIKE 'ATG%' THEN 1 ELSE 0 END AS has_start, CASE WHEN dna_sequence REGEXP 'TAA$|TAG$|TGA$' THEN 1 ELSE 0 END AS has_stop, CASE WHEN dna_sequence LIKE '%ATAT%' THEN 1 ELSE 0 END AS has_atat, CASE WHEN dna_sequence LIKE '%GGG%' THEN 1 ELSE 0 END AS has_gggFROM Samples;模式解析:
| 模式 | LIKE/REGEXP | 含义 |
|---|---|---|
| 以 ATG 开头 | LIKE 'ATG%' | % 匹配任意后续字符 |
| 以 TAA/TAG/TGA 结尾 | REGEXP 'TAA$|TAG$|TGA$' | $ 锚定结尾,| 是或 |
| 包含 ATAT | LIKE '%ATAT%' | % 在两端匹配任意前后缀 |
| 包含 GGG | LIKE '%GGG%' | 同上 |
执行示例:
| sample_id | dna_sequence | has_start | has_stop | has_atat | has_ggg |
|---|---|---|---|---|---|
| 1 | ATGCGATATGGGTAATAG | 1 (ATG开头) | 1 (TAG结尾) | 1 (含ATAT) | 1 (含GGG) |
| 2 | ATGCGGTAA | 1 | 1 (TAA结尾) | 0 | 0 |
| 3 | CGCGCG | 0 | 0 | 0 | 0 |
1683. 无效的推文 Easy
SELECT tweet_idFROM TweetsWHERE LENGTH(content) > 15;0196. 删除重复的电子邮箱 Easy(DELETE + 自连接)
表结构:Person(id, email)
DELETEp1 FROM Person p1JOIN Person p2 ON p1.email = p2.email AND p1.id > p2.id;执行流程:
删除前 Person 表:┌────┬───────────┐│ id │ email │├────┼───────────┤│ 1 │ john@mail │ ← 保留(id 最小)│ 2 │ bob@mail ││ 3 │ john@mail │ ← 删除(同 email,id 更大)└────┴───────────┘
自连接条件: p1.email = p2.email AND p1.id > p2.id→ p1(3, john@mail) JOIN p2(1, john@mail): email相同 AND 3>1 ✅ → DELETE p1→ p1(1, john@mail) JOIN p2(3, john@mail): 1>3 ❌ → 不删→ p1(2, bob@mail): 无同 email 的更小 id → 不删
删除后:┌────┬───────────┐│ id │ email │├────┼───────────┤│ 1 │ john@mail ││ 2 │ bob@mail │└────┴───────────┘核心点:
p1.id > p2.id确保保留 id 最小的那条记录
附录:SQL 常用函数速查
聚合函数
| 函数 | 说明 | 示例 |
|---|---|---|
| COUNT(*) | 计行数(含 NULL) | COUNT(*) FROM Users |
| COUNT(col) | 计非 NULL 行数 | COUNT(email) |
| COUNT(DISTINCT col) | 去重计数 | COUNT(DISTINCT user_id) |
| SUM(col) | 求和 | SUM(amount) |
| AVG(col) | 平均值 | AVG(rating) |
| MIN/MAX(col) | 最小/最大值 | MIN(event_date) |
条件表达式
| 写法 | 说明 |
|---|---|
IF(cond, a, b) | 条件为真返回 a,否则 b |
CASE WHEN cond THEN a ELSE b END | 多条件分支 |
IFNULL(a, b) | a 为 NULL 返回 b |
窗口函数
| 写法 | 说明 |
|---|---|
RANK() OVER(ORDER BY col) | 排名(同值同名次,跳号) |
DENSE_RANK() OVER(ORDER BY col) | 排名(同值同名次,不跳号) |
ROW_NUMBER() OVER(ORDER BY col) | 行号(不重复) |
SUM(col) OVER(ORDER BY col) | 累计求和 |
SUM(col) OVER(ORDER BY col ROWS BETWEEN N PRECEDING AND CURRENT ROW) | N+1 行滑动窗口 |
日期函数
| 函数 | 说明 | 示例 |
|---|---|---|
| DATEDIFF(a, b) | a-b 天数差 | DATEDIFF(d1, d2) = 1 |
| DATE_FORMAT(d, fmt) | 格式化日期 | DATE_FORMAT(d, '%Y-%m') |
| NOW() / CURDATE() | 当前时间/日期 |
字符串函数
| 函数 | 说明 |
|---|---|
| SUBSTRING(s, pos, len) | 截取子串 |
| UPPER(s) / LOWER(s) | 大写/小写 |
| CONCAT(s1, s2) | 拼接 |
| LENGTH(s) | 长度 |
| GROUP_CONCAT(col ORDER BY col) | 分组拼接 |
| REGEXP | 正则匹配 |
| LIKE | 模式匹配(% 任意,_ 单字符) |
💡 建议:SQL 考的不是复杂语法,而是把业务需求拆解成 SQL 执行步骤的能力。拿到题先想:① 需要哪些表?② 需要关联吗?③ 需要分组吗?④ 过滤条件在哪一步?⑤ 需要排序/截取吗?想清楚这五步,SQL 自然就写出来了。