CS-Notes SQL 练习:16 道经典 SQL 面试题的完整题解与本地调试实战
本文基于 CS-Notes 仓库中的 notes/SQL 练习.md 展开,完整覆盖文档中 16 道经典 SQL 练习题的题干、解法与可本地运行的 SQL Schema。读完本文后,你将掌握过滤、分组聚合、连接、子查询、排名等核心 SQL 技术点的实战写法,并能利用每题自带的建表脚本在本地 MySQL 环境中复现和调试全部解法。
一、文档定位与练习方法论
notes/SQL 练习.md 是 CS-Notes 数据库板块中面向面试的 SQL 实战文档,收录了 16 道经典题目,每题固定包含三部分:
- Description:题干说明,包含示例输入/输出数据表;
- Solution:参考解法及思路讲解;
- SQL Schema:用于在本地环境创建表结构并导入数据的脚本,方便在本地环境调试。
文档开头明确说明了 SQL Schema 的用途(见 notes/SQL 练习.md#L64-L66):"SQL Schema 用于在本地环境下创建表结构并导入数据,从而方便在本地环境调试。" 这也是本套练习的可复制性保证:每道题都可以按 DROP TABLE IF EXISTS → CREATE TABLE → INSERT INTO 三步脚本在任意 MySQL 实例中重建数据,然后直接执行 Solution 中的查询验证结果。
从题目分布看,16 道题完整覆盖了 SQL 面试的核心考点:
| 考察方向 | 题目 |
|---|---|
| 条件过滤 | 595 Big Countries、620 Not Boring Movies |
| 数据更新与删除(DML) | 627 Swap Salary、196 Delete Duplicate Emails |
| 分组聚合与 HAVING | 596 Classes More Than 5 Students、182 Duplicate Emails |
| 多表连接 | 175 Combine Two Tables、181 Employees Earning More Than Their Managers、183 Customers Who Never Order |
| 子查询与临时表 | 184 Department Highest Salary、176 Second Highest Salary |
| Top-N 与排名 | 177 Nth Highest Salary、178 Rank Scores |
| 高级技巧 | 180 Consecutive Numbers、626 Exchange Seats |
本仓库中与 SQL 练习配合使用的还有两份文档:notes/SQL 语法.md 提供从建表、查询、分组、连接、组合查询到视图、存储过程的完整语法速查(例如 GROUP BY 与 HAVING 的语义见 notes/SQL 语法.md#L364-L400,连接操作见 notes/SQL 语法.md#L428-L529),notes/MySQL.md 则深入 MySQL 索引(B+ Tree)、存储引擎与查询性能优化,适合作为练习之后的纵深延伸。
二、条件过滤类
595. Big Countries
题干给出 World 表:
| name | continent | area | population | gdp |
|---|---|---|---|---|
| Afghanistan | Asia | 652230 | 25500100 | 20343000 |
| Albania | Europe | 28748 | 2831741 | 12960000 |
| Algeria | Africa | 2381741 | 37100000 | 188681000 |
| Andorra | Europe | 468 | 78115 | 3712000 |
| Angola | Africa | 1246700 | 20609294 | 100990000 |
查找面积超过 3,000,000 或者人口数超过 25,000,000 的国家:
| name | population | area |
|---|---|---|
| Afghanistan | 25500100 | 652230 |
| Algeria | 37100000 | 2381741 |
注意条件是 OR 语义:阿富汗人口达标但面积未达标,同样入选。解法(见 notes/SQL 练习.md#L53-L62):
SELECT name,
population,
area
FROM
World
WHERE
area > 3000000
OR population > 25000000;
SQL Schema:
DROP TABLE IF EXISTS World;
CREATE TABLE World ( NAME VARCHAR ( 255 ), continent VARCHAR ( 255 ), area INT, population INT, gdp INT );
INSERT INTO World ( NAME, continent, area, population, gdp )
VALUES
( 'Afghanistan', 'Asia', '652230', '25500100', '203430000' ),
( 'Albania', 'Europe', '28748', '2831741', '129600000' ),
( 'Algeria', 'Africa', '2381741', '37100000', '1886810000' ),
( 'Andorra', 'Europe', '468', '78115', '37120000' ),
( 'Angola', 'Africa', '1246700', '20609294', '1009900000' );
注:Schema 中的 gdp 数值(如 203430000、1886810000)与题干表格示例存在量级差异,这不影响本题解法,因为 WHERE 条件只涉及 area 与 population 两列。
620. Not Boring Movies
cinema 表数据:
| id | movie | description | rating |
|---|---|---|---|
| 1 | War | great 3D | 8.9 |
| 2 | Science | fiction | 8.5 |
| 3 | irish | boring | 6.2 |
| 4 | Ice song | Fantacy | 8.6 |
| 5 | House card | Interesting | 9.1 |
要求查找 id 为奇数、并且 description 不是 boring 的电影,按 rating 降序输出,期望结果为 id 5 与 id 1 两行。解法同时使用了取模运算与多条件 AND(见 notes/SQL 练习.md#L173-L183):
SELECT
*
FROM
cinema
WHERE
id % 2 = 1
AND description != 'boring'
ORDER BY
rating DESC;
SQL Schema:
DROP TABLE IF EXISTS cinema;
CREATE TABLE cinema ( id INT, movie VARCHAR ( 255 ), description VARCHAR ( 255 ), rating FLOAT ( 2, 1 ) );
INSERT INTO cinema ( id, movie, description, rating )
VALUES
( 1, 'War', 'great 3D', 8.9 ),
( 2, 'Science', 'fiction', 8.5 ),
( 3, 'irish', 'boring', 6.2 ),
( 4, 'Ice song', 'Fantacy', 8.6 ),
( 5, 'House card', 'Interesting', 9.1 );
这道题涉及的三个基础语法点——WHERE 多条件组合、数值取模、ORDER BY 排序——在 notes/SQL 语法.md 的"八、排序"与"九、过滤"小节中均有对应说明:AND 优先级高于 OR,多列排序可以为每列单独指定 ASC/DESC(见 notes/SQL 语法.md#L190-L230)。
三、数据更新与删除类
627. Swap Salary
salary 表初始数据:
| id | name | sex | salary |
|---|---|---|---|
| 1 | A | m | 2500 |
| 2 | B | f | 1500 |
| 3 | C | m | 5500 |
| 4 | D | f | 500 |
要求只用一个 SQL 查询将 sex 字段反转(m ↔ f)。
本题的巧妙之处在于利用异或(XOR)的数学性质:两个相等的数异或结果为 0,而 0 与任何一个数异或结果为该数本身。由于 sex 字段只有 'f' 与 'm' 两个取值,因此(见 notes/SQL 练习.md#L108-L124):
'f' ^ ('m' ^ 'f') = 'm' ^ ('f' ^ 'f') = 'm'
'm' ^ ('m' ^ 'f') = 'f' ^ ('m' ^ 'm') = 'f'
将 sex 字段与 'm' ^ 'f' 的异或结果做异或,即可实现反转,无需 CASE WHEN 分支:
UPDATE salary
SET sex = CHAR ( ASCII(sex) ^ ASCII( 'm' ) ^ ASCII( 'f' ) );
SQL Schema:
DROP TABLE IF EXISTS salary;
CREATE TABLE salary ( id INT, NAME VARCHAR ( 100 ), sex CHAR ( 1 ), salary INT );
INSERT INTO salary ( id, NAME, sex, salary )
VALUES
( '1', 'A', 'm', '2500' ),
( '2', 'B', 'f', '1500' ),
( '3', 'C', 'm', '5500' ),
( '4', 'D', 'f', '500' );
196. Delete Duplicate Emails
Person 表:
| Id | |
|---|---|
| 1 | john@example.com |
| 2 | bob@example.com |
| 3 | john@example.com |
要求删除重复的邮件地址,只保留 Id 最小的那条记录:
| Id | |
|---|---|
| 1 | john@example.com |
| 2 | bob@example.com |
解法一:连接查询。 将表自身以两个别名 p1、p2 连接,凡 p1 的 Id 大于同邮箱下 p2 的 Id 的记录即为重复项,予以删除(见 notes/SQL 练习.md#L361-L369):
DELETE p1
FROM
Person p1,
Person p2
WHERE
p1.Email = p2.Email
AND p1.Id > p2.Id;
解法二:子查询。 先按 email 分组求出每组最小 id,删除不在此集合内的记录:
DELETE
FROM
Person
WHERE
id NOT IN (
SELECT id
FROM (
SELECT min( id ) AS id
FROM Person
GROUP BY email
) AS m
);
关键坑点:上述解法二特意多嵌套了一层 SELECT。如果不嵌套,直接写成下面这样,MySQL 会报错 You can't specify target table 'Person' for update in FROM clause(Error 1093):
DELETE
FROM
Person
WHERE
id NOT IN (
SELECT min( id ) AS id
FROM Person
GROUP BY email
);
这是因为 MySQL 不允许在 UPDATE/DELETE 的目标表上直接执行 FROM 子查询。额外的嵌套层会迫使优化器先将内层子查询物化为临时表,从而绕过该限制(错误原理与绕法参见 notes/SQL 练习.md#L388-L402)。
SQL Schema 与 182 题相同,见下文。
182. Duplicate Emails
Person 表:
| Id | |
|---|---|
| 1 | a@b.com |
| 2 | c@d.com |
| 3 | a@b.com |
查找重复的邮件地址,期望结果仅 a@b.com 一行。
解法是对 Email 分组后使用 COUNT 计数统计,结果大于等于 2 的即为重复邮件(见 notes/SQL 练习.md#L296-L309):
SELECT
Email
FROM
Person
GROUP BY
Email
HAVING
COUNT( * ) >= 2;
SQL Schema:
DROP TABLE IF EXISTS Person;
CREATE TABLE Person ( Id INT, Email VARCHAR ( 255 ) );
INSERT INTO Person ( Id, Email )
VALUES
( 1, 'a@b.com' ),
( 2, 'c@d.com' ),
( 3, 'a@b.com' );
四、分组聚合类
596. Classes More Than 5 Students
courses 表:
| student | class |
|---|---|
| A | Math |
| B | English |
| C | Math |
| D | Biology |
| E | Math |
| F | Computer |
| G | Math |
| H | Math |
| I | Math |
查找有五名及以上 student 的 class,期望结果为 Math。
解题关键是理解 WHERE 与 HAVING 的分工:对 class 列分组后,用 count 汇总函数统计每个分组的记录数,再用 HAVING 筛选——HAVING 针对分组进行筛选,而 WHERE 针对每个记录(行)进行筛选(见 notes/SQL 练习.md#L233-L246)。这与 notes/SQL 语法.md#L387 中"WHERE 过滤行,HAVING 过滤分组,行过滤应当先于分组过滤"的总结完全一致:
SELECT
class
FROM
courses
GROUP BY
class
HAVING
count( DISTINCT student ) >= 5;
使用 count(DISTINCT student) 而非 count(*) 可以保证同一学生重复记录时不会被重复计数。
SQL Schema:
DROP TABLE IF EXISTS courses;
CREATE TABLE courses ( student VARCHAR ( 255 ), class VARCHAR ( 255 ) );
INSERT INTO courses ( student, class )
VALUES
( 'A', 'Math' ),
( 'B', 'English' ),
( 'C', 'Math' ),
( 'D', 'Biology' ),
( 'E', 'Math' ),
( 'F', 'Computer' ),
( 'G', 'Math' ),
( 'H', 'Math' ),
( 'I', 'Math' );
184. Department Highest Salary
Employee 表:
| Id | Name | Salary | DepartmentId |
|---|---|---|---|
| 1 | Joe | 70000 | 1 |
| 2 | Henry | 80000 | 2 |
| 3 | Sam | 60000 | 2 |
| 4 | Max | 90000 | 1 |
Department 表:
| Id | Name |
|---|---|
| 1 | IT |
| 2 | Sales |
查找每个 Department 中收入最高者的信息:
| Department | Employee | Salary |
|---|---|---|
| IT | Max | 90000 |
| Sales | Henry | 80000 |
解法思路是构造一个包含各部门最大薪资的临时表:对 DepartmentId 分组并使用 MAX() 汇总函数取得最大薪资;之后用连接找到薪资等于该临时表最大薪资的员工(见 notes/SQL 练习.md#L660-L681):
SELECT
D.NAME Department,
E.NAME Employee,
E.Salary
FROM
Employee E,
Department D,
( SELECT DepartmentId, MAX( Salary ) Salary
FROM Employee
GROUP BY DepartmentId ) M
WHERE
E.DepartmentId = D.Id
AND E.DepartmentId = M.DepartmentId
AND E.Salary = M.Salary;
这里用 FROM 子句中的派生表(隐式连接)替代了显式 JOIN 写法,效果等价于先执行子查询物化为临时表 M,再与 Employee、Department 三表连接。该写法同时处理了一个隐含边界:同一部门多人并列最高薪资时会全部输出。
SQL Schema:
DROP TABLE IF EXISTS Employee;
CREATE TABLE Employee ( Id INT, NAME VARCHAR ( 255 ), Salary INT, DepartmentId INT );
DROP TABLE IF EXISTS Department;
CREATE TABLE Department ( Id INT, NAME VARCHAR ( 255 ) );
INSERT INTO Employee ( Id, NAME, Salary, DepartmentId )
VALUES
( 1, 'Joe', 70000, 1 ),
( 2, 'Henry', 80000, 2 ),
( 3, 'Sam', 60000, 2 ),
( 4, 'Max', 90000, 1 );
INSERT INTO Department ( Id, NAME )
VALUES
( 1, 'IT' ),
( 2, 'Sales' );
五、多表连接类
175. Combine Two Tables
Person 表结构(PersonId 为主键):
| Column Name | Type |
|---|---|
| PersonId | int |
| FirstName | varchar |
| LastName | varchar |
Address 表结构(AddressId 为主键):
| Column Name | Type |
|---|---|
| AddressId | int |
| PersonId | int |
| City | varchar |
| State | varchar |
要求查找 FirstName、LastName、City、State,且不管一个用户有没有填地址信息都要输出。
由于必须保留 Person 表中的全部行,即使 Address 表中没有对应记录,需要用左外连接并把 Person 表放在 LEFT JOIN 的左边(见 notes/SQL 练习.md#L443-L457):
SELECT
FirstName,
LastName,
City,
State
FROM
Person P
LEFT JOIN Address A
ON P.PersonId = A.PersonId;
若使用 INNER JOIN,未填地址的用户会被整体过滤掉。外连接保留左表未匹配行并以 NULL 填充右表列的行为,在 notes/SQL 语法.md#L491-L529 的"外连接"小节有完整的示例结果演示。
SQL Schema:
DROP TABLE IF EXISTS Person;
CREATE TABLE Person ( PersonId INT, FirstName VARCHAR ( 255 ), LastName VARCHAR ( 255 ) );
DROP TABLE IF EXISTS Address;
CREATE TABLE Address ( AddressId INT, PersonId INT, City VARCHAR ( 255 ), State VARCHAR ( 255 ) );
INSERT INTO Person ( PersonId, LastName, FirstName )
VALUES
( 1, 'Wang', 'Allen' );
INSERT INTO Address ( AddressId, PersonId, City, State )
VALUES
( 1, 2, 'New York City', 'New York' );
181. Employees Earning More Than Their Managers
Employee 表:
| Id | Name | Salary | ManagerId |
|---|---|---|---|
| 1 | Joe | 70000 | 3 |
| 2 | Henry | 80000 | 4 |
| 3 | Sam | 60000 | NULL |
| 4 | Max | 90000 | NULL |
查找薪资大于其经理薪资的员工信息(本例结果为 Joe)。
"员工-经理"关系存放在同一张表中,需要自连接(self join):将 Employee 以 E1、E2 两个别名连接,E1 是员工、E2 是其经理,连接条件 E1.ManagerId = E2.Id,比较条件 E1.Salary > E2.Salary(见 notes/SQL 练习.md#L499-L509):
SELECT
E1.NAME AS Employee
FROM
Employee E1
INNER JOIN Employee E2
ON E1.ManagerId = E2.Id
AND E1.Salary > E2.Salary;
自连接本质是内连接的特殊形式——连接的表是自身。notes/SQL 语法.md#L454-L478 的"自连接"小节给出了同场景(同部门员工查找)的子查询版本与自连接版本对照,并指出给表名取别名正是为了"简化 SQL 语句以及连接相同表"。ManagerId 为 NULL 的顶层管理者会因连接不成立而被自然排除,无需额外过滤。
SQL Schema:
DROP TABLE IF EXISTS Employee;
CREATE TABLE Employee ( Id INT, NAME VARCHAR ( 255 ), Salary INT, ManagerId INT );
INSERT INTO Employee ( Id, NAME, Salary, ManagerId )
VALUES
( 1, 'Joe', 70000, 3 ),
( 2, 'Henry', 80000, 4 ),
( 3, 'Sam', 60000, NULL ),
( 4, 'Max', 90000, NULL );
183. Customers Who Never Order
Customers 表:
| Id | Name |
|---|---|
| 1 | Joe |
| 2 | Henry |
| 3 | Sam |
| 4 | Max |
Orders 表:
| Id | CustomerId |
|---|---|
| 1 | 3 |
| 2 | 1 |
查找没有订单的顾客,期望结果为 Henry 与 Max。
解法一:左外连接。 顾客左连接订单表,从未下过单的顾客其右表列必为 NULL,用 O.CustomerId IS NULL 筛出(见 notes/SQL 练习.md#L567-L580):
SELECT
C.Name AS Customers
FROM
Customers C
LEFT JOIN Orders O
ON C.Id = O.CustomerId
WHERE
O.CustomerId IS NULL;
解法二:子查询。 直接用 NOT IN 排除所有下过单的顾客:
SELECT
Name AS Customers
FROM
Customers
WHERE
Id NOT IN (
SELECT CustomerId
FROM Orders
);
两种写法语义等价,"LEFT JOIN + IS NULL 反连接"与"NOT IN 子查询"是查找不存在记录的两种经典模式。
SQL Schema:
DROP TABLE IF EXISTS Customers;
CREATE TABLE Customers ( Id INT, NAME VARCHAR ( 255 ) );
DROP TABLE IF EXISTS Orders;
CREATE TABLE Orders ( Id INT, CustomerId INT );
INSERT INTO Customers ( Id, NAME )
VALUES
( 1, 'Joe' ),
( 2, 'Henry' ),
( 3, 'Sam' ),
( 4, 'Max' );
INSERT INTO Orders ( Id, CustomerId )
VALUES
( 1, 3 ),
( 2, 1 );
六、Top-N 与排名类
176. Second Highest Salary
Employee 表:
| Id | Salary |
|---|---|
| 1 | 100 |
| 2 | 200 |
| 3 | 300 |
查找工资第二高的员工,期望列名 SecondHighestSalary 输出 200。关键要求:没有找到时返回 null,而不是不返回数据(即结果必须恰好有一行)。
解法分两层:内层用 DISTINCT + ORDER BY Salary DESC + LIMIT 1, 1 取去重后的第二高薪;外层再套一层 SELECT——当内层结果为空时,外层查询仍然会输出一行 NULL(见 notes/SQL 练习.md#L729-L741):
SELECT
( SELECT DISTINCT Salary
FROM Employee
ORDER BY Salary DESC
LIMIT 1, 1 ) SecondHighestSalary;
这里 LIMIT 1, 1 表示跳过第 1 行、取第 2 行。LIMIT 双参数(offset, count)的语义在 notes/SQL 语法.md#L164-L188 中有说明:"第一个参数为起始行,从 0 开始;第二个参数为返回的总行数"。
SQL Schema:
DROP TABLE IF EXISTS Employee;
CREATE TABLE Employee ( Id INT, Salary INT );
INSERT INTO Employee ( Id, Salary )
VALUES
( 1, 100 ),
( 2, 200 ),
( 3, 300 );
177. Nth Highest Salary
将 176 推广为"查找工资第 N 高的员工",要求封装成函数。解法是把偏移量参数化:先令 N = N - 1(把"第 N 高"转换为"跳过 N-1 行"),再用同样的双层 SELECT 保证无结果时返回 NULL(见 notes/SQL 练习.md#L763-L779):
CREATE FUNCTION getNthHighestSalary ( N INT ) RETURNS INT BEGIN
SET N = N - 1;
RETURN (
SELECT (
SELECT DISTINCT Salary
FROM Employee
ORDER BY Salary DESC
LIMIT N, 1
)
);
END
SQL Schema 同 176。
注意:CREATE FUNCTION 是 MySQL 的过程式语法,依赖存储函数能力,属于 MySQL 方言写法而非标准 SQL,迁移到其他数据库时通常要改写。这一"各 DBMS 方言差异"的主题在 notes/SQL 语法.md 的函数一节中也有提醒:"各个 DBMS 的函数都是不相同的,因此不可移植"。
七、排名问题:178. Rank Scores(本文档剖析最深入的题目)
得分表:
| Id | Score |
|---|---|
| 1 | 3.50 |
| 2 | 3.65 |
| 3 | 4.00 |
| 4 | 3.85 |
| 5 | 4.00 |
| 6 | 3.65 |
要求将得分排序并统计排名,相同分数并列、且并列不占额外名次:
| Score | Rank |
|---|---|
| 4.00 | 1 |
| 4.00 | 1 |
| 3.85 | 2 |
| 3.65 | 3 |
| 3.65 | 3 |
| 3.50 | 4 |
核心思路:要统计某个 score 的排名,只要统计大于等于该 score 的 score 数量。用自连接找出每条记录对应的所有 S2.score >= S1.score 的记录:
| Id | score | 大于等于该 score 的 score 数量 | 排名 |
|---|---|---|---|
| 1 | 4.1 | 3 | 3 |
| 2 | 4.2 | 2 | 2 |
| 3 | 4.3 | 1 | 1 |
连接操作示例(见 notes/SQL 练习.md#L832-L843):
SELECT
*
FROM
Scores S1
INNER JOIN Scores S2
ON S1.score <= S2.score
ORDER BY
S1.score DESC, S1.Id;
连接结果中每个 S1.score 都对应多条记录:
| S1.Id | S1.score | S2.Id | S2.score |
|---|---|---|---|
| 3 | 4.3 | 3 | 4.3 |
| 2 | 4.2 | 2 | 4.2 |
| 2 | 4.2 | 3 | 4.3 |
| 1 | 4.1 | 1 | 4.1 |
| 1 | 4.1 | 2 | 4.2 |
| 1 | 4.1 | 3 | 4.3 |
按 S1 分组后对每个分组的记录数计数即得 Rank:
SELECT
S1.score 'Score',
COUNT(*) 'Rank'
FROM
Scores S1
INNER JOIN Scores S2
ON S1.score <= S2.score
GROUP BY
S1.id, S1.score
ORDER BY
S1.score DESC, S1.Id;
错误反例:上述 COUNT(*) 版本对无重复分数的数据没问题,但遇到并列分数就会出错。对以下输入:
| Id | score |
|---|---|
| 1 | 4.1 |
| 2 | 4.2 |
| 3 | 4.2 |
会得到错误结果:
| score | Rank |
|---|---|
| 4.2 | 2 |
| 4.2 | 2 |
| 4.1 | 3 |
而正确结果是:
| score | Rank |
|---|---|
| 4.2 | 1 |
| 4.2 | 1 |
| 4.1 | 2 |
原因是并列的 4.2 互相满足 S1.score <= S2.score,COUNT(*) 把同分记录数了一遍又一遍,导致名次被抬高。期望的排名语义是"相同分数放在同一排名,且相同分数只占一个位置"(Id=2 与 Id=3 并列第一,Id=1 应排第二而非第三)。因此计数时必须去重,改用 COUNT(DISTINCT S2.score)(见 notes/SQL 练习.md#L910-L924):
SELECT
S1.score 'Score',
COUNT( DISTINCT S2.score ) 'Rank'
FROM
Scores S1
INNER JOIN Scores S2
ON S1.score <= S2.score
GROUP BY
S1.id, S1.score
ORDER BY
S1.score DESC;
SQL Schema:
DROP TABLE IF EXISTS Scores;
CREATE TABLE Scores ( Id INT, Score DECIMAL ( 3, 2 ) );
INSERT INTO Scores ( Id, Score )
VALUES
( 1, 4.1 ),
( 2, 4.1 ),
( 3, 4.2 ),
( 4, 4.2 ),
( 5, 4.3 ),
( 6, 4.3 );
八、高级技巧类
180. Consecutive Numbers
Logs 表:
| Id | Num |
|---|---|
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 4 | 2 |
| 5 | 1 |
| 6 | 2 |
| 7 | 2 |
查找连续出现三次的数字,期望结果为 1。
解法是把 Logs 表自连接三次,令 L1、L2、L3 分别对应 id 连续递增的三行,且三行 Num 相同(见 notes/SQL 练习.md#L975-L988):
SELECT
DISTINCT L1.num 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;
DISTINCT 用于防止同一数字连续出现四次以上时重复输出。该写法假设 id 严格连续递增;若 id 存在空洞,需改用窗口函数思路(本题 Schema 数据满足该前提)。
SQL Schema:
DROP TABLE IF EXISTS LOGS;
CREATE TABLE LOGS ( Id INT, Num INT );
INSERT INTO LOGS ( Id, Num )
VALUES
( 1, 1 ),
( 2, 1 ),
( 3, 1 ),
( 4, 2 ),
( 5, 1 ),
( 6, 2 ),
( 7, 2 );
626. Exchange Seats
seat 表存储座位对应的学生:
| id | student |
|---|---|
| 1 | Abbot |
| 2 | Doris |
| 3 | Emerson |
| 4 | Green |
| 5 | Jeames |
要求交换相邻座位的两个学生;如果最后一个座位的 id 是奇数(即没有下一位可交换),则该座位不交换。期望结果:
| id | student |
|---|---|
| 1 | Doris |
| 2 | Abbot |
| 3 | Green |
| 4 | Emerson |
| 5 | Jeames |
解法使用多个 UNION 分三类处理(见 notes/SQL 练习.md#L1042-L1077):
## 处理偶数 id,让 id 减 1
## 例如 2,4,6,... 变成 1,3,5,...
SELECT
s1.id - 1 AS id,
s1.student
FROM
seat s1
WHERE
s1.id MOD 2 = 0 UNION
## 处理奇数 id,让 id 加 1。但是如果最大的 id 为奇数,则不做处理
## 例如 1,3,5,... 变成 2,4,6,...
SELECT
s2.id + 1 AS id,
s2.student
FROM
seat s2
WHERE
s2.id MOD 2 = 1
AND s2.id != ( SELECT max( s3.id ) FROM seat s3 ) UNION
## 如果最大的 id 为奇数,单独取出这个数
SELECT
s4.id AS id,
s4.student
FROM
seat s4
WHERE
s4.id MOD 2 = 1
AND s4.id = ( SELECT max( s5.id ) FROM seat s5 )
ORDER BY
id;
三个分支的划分保证了完备性:偶数 id 前移一格;奇数 id 且不是最大 id 的后移一格;奇数 id 且是最大 id 的原样保留。整个 UNION 语句末尾只能有一个 ORDER BY 且位于最外层——这一约束与 notes/SQL 语法.md#L530-L548 中组合查询的规范一致:"只能包含一个 ORDER BY 子句,并且必须位于语句的最后"。
SQL Schema:
DROP TABLE IF EXISTS seat;
CREATE TABLE seat ( id INT, student VARCHAR ( 255 ) );
INSERT INTO seat ( id, student )
VALUES
( '1', 'Abbot' ),
( '2', 'Doris' ),
( '3', 'Emerson' ),
( '4', 'Green' ),
( '5', 'Jeames' );
九、知识映射与延伸学习
将 16 道题用到的语法点回映到 notes/SQL 语法.md 的体系,可以得到一张"练习 → 语法速查"对照表:
| 语法点 | 使用它的题目 | 语法速查位置 |
|---|---|---|
| WHERE + AND/OR 过滤 | 595、620 | notes/SQL 语法.md#L201-L232 |
| ORDER BY 多列排序 | 620、178 | notes/SQL 语法.md#L188-L200 |
| GROUP BY + HAVING | 596、182、184、178 | notes/SQL 语法.md#L362-L401 |
| 汇总函数 COUNT/MAX/DISTINCT | 182、184、178 | notes/SQL 语法.md#L272-L293 |
| 子查询(标量/IN/派生表) | 175 无、183、176、177、184、626 | notes/SQL 语法.md#L402-L426 |
| 内连接 / 自连接 / 外连接 | 175、181、183、178、180 | notes/SQL 语法.md#L426-L529 |
| UNION 组合查询 | 626 | notes/SQL 语法.md#L528-L548 |
| LIMIT offset,count | 176、177 | notes/SQL 语法.md#L162-L188 |
| CREATE FUNCTION 存储函数 | 177 | notes/SQL 语法.md 存储过程小节(第十八节) |
练习之外的延伸路径:完成全部 16 题后,可以沿 notes/SQL 语法.md 补齐视图、存储过程、游标、触发器、事务管理、权限管理等面试常考点;再结合 notes/MySQL.md 理解 B+ Tree 索引结构、索引使用条件与 EXPLAIN 查询分析,就能把"会写 SQL"推进到"会分析 SQL 为什么快或慢";更宏观的表结构设计与范式问题则参见 notes/数据库系统原理.md。
最后强调本地调试流程:对任意一道题,先执行其 SQL Schema 脚本(DROP TABLE IF EXISTS 保证可重复执行),导入数据,运行 Solution 查询,与题干中的期望输出表逐行比对。由于所有题目数据量都在个位数行,结果可以直接肉眼验证,这是在没有 LeetCode 环境时检验 SQL 写法正确性的可靠方式。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00