首页
/ CS-Notes SQL 练习:16 道经典 SQL 面试题的完整题解与本地调试实战

CS-Notes SQL 练习:16 道经典 SQL 面试题的完整题解与本地调试实战

2026-09-06 17:38:13作者:申梦珏Efrain

本文基于 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 EXISTSCREATE TABLEINSERT 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 Email
1 john@example.com
2 bob@example.com
3 john@example.com

要求删除重复的邮件地址,只保留 Id 最小的那条记录:

Id Email
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 Email
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.scoreCOUNT(*) 把同分记录数了一遍又一遍,导致名次被抬高。期望的排名语义是"相同分数放在同一排名,且相同分数只占一个位置"(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 写法正确性的可靠方式。

登录后查看全文
热门项目推荐
相关项目推荐