SQL 算法笔记 - MuxiaoWF跳到主要内容

SQL 算法笔记

LeetCode 上 SQL50 的算法笔记

周日 8月 09 2026
7562 字 · 45 分钟

SQL 算法笔记

通用思维套路总览

看到什么特征 → 用什么 SQL 技巧

题目特征首选技巧典型题目
两个表关联取数据INNER JOIN / LEFT JOIN175, 1068, 1378
保留左表全部行(无匹配返回 NULL)LEFT JOIN175, 577, 1378, 1581
同一表比较(员工 vs 经理)SELF JOIN(自连接)181, 570, 1731, 1978
找重复值GROUP BY + HAVING COUNT > 1182, 196
分组后过滤GROUP BY + HAVING596, 570, 1045, 1084
分组后统计GROUP BY + COUNT/SUM/AVG511, 1693, 1729, 2356
排名 / Top N窗口函数 RANK / DENSE_RANK185, 1341
累计求和 / 滑动窗口SUM() OVER(ORDER BY ROWS BETWEEN)1204, 1321
条件聚合(分情况统计)SUM(IF(…)) 或 SUM(CASE WHEN)1193, 1661, 1393
合并多个查询结果UNION / UNION ALL1795, 1907, 1164
找「所有」/「全部」HAVING COUNT(DISTINCT) = 总数1045
比较前一天/连续 N 天SELF JOIN + DATEDIFF197, 180, 550
无匹配也要返回LEFT JOIN + IS NULL577, 581, 1978
字符串模式匹配LIKE / REGEXP1527, 1517, 1683
列转行UNION ALL1795
行转列 / 拼接GROUP_CONCAT1484
条件赋值CASE WHEN610, 626, 627
日期格式化DATE_FORMAT1193, 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, area
FROM World
WHERE 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 name
FROM Customer
WHERE referee_id != 2 OR referee_id IS NULL;

执行流程

idnamereferee_id判断结果
1WillNULLNULL != 2 → NULL(不是 true)→ 但 IS NULL → ✅输出
2JaneNULL同上 → ✅输出
3Alex22 != 2 → false → ❌不输出
4Bill33 != 2 → true → ✅输出
5Zack11 != 2 → true → ✅输出

⚠️ 关键陷阱referee_id != 2 不会匹配 NULL!NULL 参与比较结果是 NULL(不是 true),必须用 IS NULL 单独处理


0168. 无效的推文 Easy

SELECT tweet_id
FROM Tweets
WHERE LENGTH(content) > 15;
tweet_idcontentLENGTH结果
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 cinema
WHERE description != 'boring' AND id % 2 = 1
ORDER BY rating DESC;

执行流程

idmoviedescriptionrating→ WHERE→ ORDER BY rating DESC
1Wargreat 3D8.9✅ odd + not boring8.9
2Scienceboring8.4❌ boring-
3IrishNOT boring7.0✅ odd + not boring7.0
4Ice SongFantacy8.6❌ even-
5House cardInteresting9.1✅ odd + not boring9.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.state
FROM Person p
LEFT JOIN Address a ON p.personId = a.personId;

执行流程

Person 表

personIdfirstNamelastName
1AllenWang
2BobAlice
3ZackSy

Address 表

addressIdpersonIdcitystate
12NYCNY
23BostonMA

LEFT JOIN 结果

firstNamelastNamecitystate
AllenWangNULLNULL
BobAliceNYCNY
ZackSyBostonMA

Allen 在 Address 表无匹配 → city 和 state 为 NULL。LEFT JOIN 保证了 Person 表全部行都保留。


0181. 超过经理收入的员工 Easy(自连接)

表结构Employee(id, name, salary, managerId)

题目:找收入比经理高的员工。

SELECT e1.name AS Employee
FROM Employee e1
JOIN Employee e2 ON e1.managerId = e2.id
WHERE e1.salary > e2.salary;

执行流程

Employee 表(一张表充当两个角色)

idnamesalarymanagerId
1Joe700003
2Henry800004
3Sam60000NULL
4Max90000NULL

自连接后(e1=员工, e2=经理)

e1.name(员工)e1.salarye2.name(经理)e2.salarye1.salary > e2.salary?
Joe70000Sam60000✅ 70000 > 60000
Henry80000Max90000❌ 80000 < 90000

最终结果

Employee
Joe

核心点:同一张表 JOIN 自身,用不同别名区分员工和经理角色


0577. 员工奖金 Easy(LEFT JOIN + IS NULL)

表结构

  • Employee(empId, name, supervisor, salary)
  • Bonus(empId, bonus)

题目:报告奖金 < 1000 或没有奖金的员工。

SELECT name, bonus
FROM Employee e
LEFT JOIN Bonus b ON e.empId = b.empId
WHERE b.bonus < 1000
OR b.bonus IS NULL;

执行流程

Employee 表

empIdnamesupervisorsalary
1BradNULL5000
2John14000
3Dan13000
4Thomas12000

Bonus 表

empIdbonus
2500
3NULL
42000

LEFT JOIN 后

namebonus判断
BradNULLIS NULL → ✅
John500500 < 1000 → ✅
DanNULLIS NULL → ✅
Thomas20002000 ≥ 1000 → ❌

最终结果

namebonus
BradNULL
John500
DanNULL

⚠️ b.bonus < 1000 不匹配 NULL,必须加 OR b.bonus IS NULL


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

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

执行流程

EmployeesEmployeeUNILEFT JOIN 结果
id=1, Aliceid=1, unique_id=10unique_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.price
FROM Sales s
JOIN Product p ON s.product_id = p.product_id;

执行流程

SalesProductJOIN 结果
sale_id=1, product_id=100, year=2008, price=5000product_id=100, NokiaNokia, 2008, 5000
sale_id=2, product_id=100, year=2009, price=5000product_id=100, NokiaNokia, 2009, 5000
sale_id=7, product_id=200, year=2011, price=7000product_id=200, AppleApple, 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_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;

执行流程

VisitsLEFT JOIN TransactionsWHERE IS NULLGROUP 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_idcount_no_trans
91
301
542

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_exams
FROM Students s
JOIN Subjects su -- 先做笛卡尔积
LEFT JOIN Examinations e -- 再左连考试表
ON e.student_id = s.student_id AND e.subject_name = su.subject_name
GROUP BY s.student_id, su.subject_name
ORDER 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_age
FROM Employees e1
JOIN Employees e2 ON e1.reports_to = e2.employee_id
GROUP BY e1.reports_to
ORDER BY e2.employee_id;

执行流程

Employees 表

employee_idnamereports_toage
9HercyNULL43
6Alice931
4Bob936
2Omer624

自连接(e1=下属, e2=经理)

e1.name(下属)e1.agee2.employee_id(经理)e2.name(经理)
Alice319Hercy
Bob369Hercy
Omer246Alice

GROUP BY e1.reports_to 后

employee_idnamereports_countaverage_age
9Hercy2ROUND((31+36)/2) = 34
6Alice124

1978. 上级经理已离职的公司员工 Easy(LEFT JOIN + IS NULL)

SELECT e1.employee_id
FROM Employees e1
LEFT JOIN Employees e2 ON e1.manager_id = e2.employee_id
WHERE e1.salary < 30000
AND e2.employee_id IS NULL
AND e1.manager_id IS NOT NULL
ORDER BY e1.employee_id;

执行流程

e1(员工)e1.manager_ide1.salarye2(经理)匹配e2 IS NULL?manager_id IS NOT NULL?结果
3, Mary125000(id=1 存在)❌ 经理未离职
7, Robert9920000(id=99 不存在)
11, Brad528000(id=5 不存在)
13, JasonNULL15000(无匹配)❌ 无经理

三个条件缺一不可: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_login
FROM Activity
GROUP BY player_id;

执行流程

原始 Activity 表→ GROUP BY player_id→ MIN(event_date)
1, 2, 2016-03-01, 5player_id=1: {2016-03-01, 2016-05-02}2016-03-01
1, 2, 2016-05-02, 6player_id=2: {2017-06-25}2017-06-25
2, 3, 2017-06-25, 1player_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 Email
FROM Person
GROUP BY email
HAVING COUNT(email) > 1;

执行流程

Person 表

idemail
1a@b.com
2c@d.com
3a@b.com

GROUP BY email 后

emailCOUNTHAVING COUNT > 1?
a@b.com2
c@d.com1

最终结果a@b.com

核心点:GROUP BY 分组后,HAVING 过滤出 COUNT > 1 的组(即重复的邮箱)


0596. 超过 5 名学生的课 Easy

表结构Courses(student, class)

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

执行流程

Courses 表

studentclass
AMath
BEnglish
CMath
DBiology
EMath
FMath
GMath
HMath

GROUP BY class 后

classCOUNT(DISTINCT student)>= 5?
Math6
English1
Biology1

最终结果Math


0570. 至少有5名直接下属的经理 Medium(自连接 + GROUP BY + HAVING)

SELECT e1.name
FROM Employee e1
JOIN Employee e2 ON e1.Id = e2.managerId
GROUP BY e1.Id
HAVING 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 → Stephen

1045. 买下所有产品的客户 Medium(HAVING COUNT = 子查询)

表结构

  • Customer(customer_id, product_key)
  • Product(product_key)
SELECT customer_id
FROM Customer
GROUP BY customer_id
HAVING 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_name
FROM Product p
JOIN Sales s ON p.product_id = s.product_id
GROUP BY s.product_id
HAVING MIN(s.sale_date) >= '2019-01-01'
AND MAX(s.sale_date) <= '2019-03-31';

执行流程

product_id所有 sale_dateMINMAX都在春季?
12019-02-17, 2019-02-252019-02-172019-02-25
22019-02-01, 2019-04-042019-02-012019-04-04❌ (4月超出)
32019-03-102019-03-102019-03-10

核心点:用 MIN/MAX 判断所有销售记录是否都在目标范围内


1587. 银行账户概要 II Easy(GROUP BY + HAVING SUM)

SELECT u.name, SUM(t.amount) AS balance
FROM Users u
JOIN Transactions t ON u.account = t.account
GROUP BY t.account
HAVING SUM(t.amount) > 10000;

执行流程

UsersTransactionsJOIN + GROUP BYHAVING > 10000
account=1, Aliceaccount=1, +7000Alice: 7000+7000=14000
account=2, Bobaccount=1, +7000Bob: 3000-5000=-2000
account=2, +3000
account=2, -5000

最终结果

namebalance
Alice14000

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_amount
FROM Transactions
GROUP BY month, country;

执行流程

原始 Transactions 表

idcountrystateamounttrans_date
121USapproved10002019-01-18
122USdeclined20002019-01-19
123USapproved30002019-01-27
124DEapproved20002019-01-14

GROUP BY (month, country) 后

monthcountrytrans_countapproved_counttrans_totalapproved_total
2019-01US3SUM(1,0,1)=260001000+3000=4000
2019-01DE1SUM(1)=120002000

核心点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_loss
FROM Stocks
GROUP BY stock_name;

执行流程

stock_nameoperationpriceCASE 结果
LeetcodeBuy1000-1000
LeetcodeSell9000+9000
CoronaBuy3000-3000
CoronaSell1580+1580

GROUP BY stock_name 后

stock_namecapital_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 products
FROM Activities
GROUP BY sell_date;

执行流程

原始 Activities→ GROUP BY sell_date→ GROUP_CONCAT
2020-05-30, Headphone2020-05-30: {Headphone, Basketball, PC}2020-05-30: “Basketball,Headphone,PC”
2020-06-01, Pencil2020-06-01: {Pencil, Bathing}2020-06-01: “Bathing,Pencil”
2020-06-02, Mask2020-06-02: {Mask, Bathing}2020-06-02: “Bathing,Mask”
2020-05-30, BasketballCOUNT(DISTINCT)=3
2020-06-01, BathingCOUNT(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_partners
FROM DailySales
GROUP 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=0leads={0,1,1} → DISTINCT={0,1}unique_partners=2 (0,1)
2020-12-8, Toyota, lead=1, partner=2partners={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=0leads={0,0} → DISTINCT={0}unique_partners=2 (0,1)

四、子查询(IN / EXISTS / 相关子查询)

0619. 只出现一次的最大数字 Easy

表结构MyNumbers(num)(无主键,可能有重复)

SELECT MAX(num) AS num
FROM (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_2016
FROM Insurance
WHERE (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 Salary
FROM Employee e
JOIN Department d ON e.departmentId = d.id
WHERE (SELECT COUNT(DISTINCT e2.salary)
FROM Employee e2
WHERE e.salary < e2.salary
AND e.departmentId = e2.departmentId) < 3;

执行流程

Employee 表

idnamesalarydeptId
1Joe850001
2Henry800002
3Sam600002
4Max900001
5Janet690001
6Randy850001

对每个员工,相关子查询计算「同部门中工资比我高的不同工资金额数」

员工部门子查询:同部门比我高的不同 salaryCOUNT< 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_id
FROM Employee
WHERE 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_name
FROM (SELECT person_name,
turn,
SUM(weight) OVER(ORDER BY turn) AS sum_weight
FROM Queue) AS wei
WHERE sum_weight <= 1000
ORDER BY turn DESC LIMIT 1;

执行流程

turnperson_nameweightSUM() OVER(ORDER BY turn) 累计
1Alice250250
2Bob350250+350=600
3Alex400600+400=1000
4John3001000+300=1300
5Winston5001300+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_amount
FROM (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 ranked
WHERE 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 results
FROM (SELECT name, RANK() OVER(ORDER BY COUNT(title) DESC, name) AS rk
FROM uion
GROUP BY user_id) AS max_user
WHERE rk = 1
UNION ALL
SELECT title AS results
FROM (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_title
WHERE 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 triangle
FROM Triangle;

执行流程

xyzx+y>z?x+z>y?y+z>x?全满足?triangle
13153028>30 ❌43>15 ✅45>13 ✅No
10201530>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 id
END
AS id, student
FROM Seat ORDER BY id;

执行流程

原始 idstudentid % 2总行数CASE 结果新 id
1Abbot15奇数且≠5 → id+12
2Doris05偶数 → id-11
3Emerson15奇数且≠5 → id+14
4Green05偶数 → id-13
5Jeames15奇数且=5(最后一行) → id5

ORDER BY id 后

idstudent
1Doris
2Abbot
3Green
4Emerson
5Jeames

核心点:子查询 COUNT(*) 判断总行数,最后一行如果是奇数 id 则保持不变


0627. 变更性别 Easy(UPDATE + CASE)

UPDATE Salary
SET sex = CASE WHEN sex = 'm' THEN 'f' WHEN sex = 'f' THEN 'm' END;

执行流程

idnamesex(前)→ CASE →sex(后)
1Am→ ff
2Bf→ mm
3Cf→ mm
4Dm→ ff

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_count
FROM Accounts
UNION
SELECT 'Average Salary', SUM(IF(income >= 20000 AND income <= 50000, 1, 0))
FROM Accounts
UNION
SELECT '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 price
FROM Products
WHERE store1 IS NOT NULL
UNION ALL
SELECT product_id, 'store2' AS store, store2 AS price
FROM Products
WHERE store2 IS NOT NULL
UNION ALL
SELECT product_id, 'store3' AS store, store3 AS price
FROM Products
WHERE 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 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
-- 无变更记录的产品:价格为默认 10
SELECT product_id, 10 AS price
FROM Products
WHERE (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 fraction
FROM 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.id
FROM Weather w1
JOIN Weather w2 ON DATEDIFF(w1.recordDate, w2.recordDate) = 1
WHERE 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 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;

执行流程

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 = 1

1517. 查找拥有有效邮箱的用户 Easy(REGEXP)

SELECT *
FROM Users
WHERE 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_idnamemail匹配?
1Winstonwinston@leetcode.com
2Jonathanjonathanisgreat❌ 无 @
3Annabellebella-@leetcode.com
4Sallysally.come@leetcode.com
5Marwanquarz#2020@leetcode.com❌ # 不允许
6Daviddavid1@gmail.com❌ 域名不对
7GeorgeGeorge@leetcode.com❌ G 大写(utf8mb4_bin 区分大小写)

1527. 患某种疾病的患者 Easy(LIKE 模糊匹配)

表结构Patients(patient_id, patient_name, conditions)

题目:找患 I 类糖尿病(DIAB1 开头)的患者。

SELECT *
FROM Patients
WHERE conditions LIKE '% DIAB1%'
OR conditions LIKE 'DIAB1%';

执行流程

patient_idconditions匹配方式结果
1DIAB100LIKE ‘DIAB1%’ ✅(开头)输出
2SADIAB100❌ 不匹配(SAD 不是空格分隔)不输出
3ASDIAB1❌ 同上不输出
4FR DIAB100LIKE ’% DIAB1%’ ✅(前面有空格)输出
5SAD DIAB100LIKE ’% DIAB1%’ ✅输出

⚠️ LIKE '%DIAB1%' 会误匹配 SADIAB100!必须用 '% DIAB1%'(前有空格)或 'DIAB1%'(在开头)


1667. 修复表中的名字 Easy(字符串函数)

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

执行流程

user_idname(原始)SUBSTRING(name,1,1)UPPERSUBSTRING(name,2)LOWERCONCAT
1aLICEaALICEliceAlice
2bOBbBOBobBob

九、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_count
FROM tab
JOIN tab AS tab2
ON tab.user_id = tab2.user_id AND tab.category < tab2.category
GROUP BY tab.category, tab2.category
HAVING customer_count >= 3
ORDER 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_rate
FROM Signups s
LEFT JOIN Confirmations c ON s.user_id = c.user_id
GROUP BY s.user_id;

执行流程

SignupsLEFT JOIN ConfirmationsactionIF(confirmed)COUNT(action)比率
user=3(3, confirmed)confirmed → 1
user=3(3, timeout)timeout → 0SUM=1COUNT=21/2=0.5
user=7(7, timeout)timeout → 0
user=7(7, timeout)timeout → 0SUM=0COUNT=20/2=0
user=6(无匹配)NULLSUM=NULLCOUNT=0IFNULL(NULL,0)=0

最终结果

user_idconfirmation_rate
30.50
70.00
60.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_ggg
FROM Samples;

模式解析

模式LIKE/REGEXP含义
以 ATG 开头LIKE 'ATG%'% 匹配任意后续字符
以 TAA/TAG/TGA 结尾REGEXP 'TAA$|TAG$|TGA$'$ 锚定结尾,| 是或
包含 ATATLIKE '%ATAT%'% 在两端匹配任意前后缀
包含 GGGLIKE '%GGG%'同上

执行示例

sample_iddna_sequencehas_starthas_stophas_atathas_ggg
1ATGCGATATGGGTAATAG1 (ATG开头)1 (TAG结尾)1 (含ATAT)1 (含GGG)
2ATGCGGTAA11 (TAA结尾)00
3CGCGCG0000

1683. 无效的推文 Easy

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

0196. 删除重复的电子邮箱 Easy(DELETE + 自连接)

表结构Person(id, email)

DELETE
p1 FROM Person p1
JOIN 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 自然就写出来了。


感谢您的阅读!如果可以,给俺点些关注吧~

SQL 算法笔记

周日 8月 09 2026
7562 · 45 分钟
封面
示例歌曲
示例艺术家
封面
示例歌曲
示例艺术家
0:00 / 0:00