SQL 面试高频问题汇总与解析(超 3000 字详解)
前言
在当今数据驱动的时代,无论是后端开发、数据分析、数据科学、测试,还是运维岗位,SQL(Structured Query Language)都已成为一项不可或缺的核心技能。因此,在技术面试中,SQL 相关的知识点考察几乎是必不可少的环节。面试官通过 SQL 问题,不仅考察候选人对语言本身的掌握程度,更能评估其逻辑思维能力、数据处理能力以及解决实际问题的能力。
本文旨在全面梳理 SQL 面试中出现频率最高的核心问题,并进行深入浅出的解析,涵盖基础概念、数据操作、复杂查询、性能优化、数据库设计等多个维度,辅以实例代码,帮助求职者系统性地复习 SQL 知识,从容应对面试挑战。
第一部分:基础概念与语法
这部分主要考察对 SQL 基本定义的理解,是后续所有操作的基础。
1. 什么是 SQL?它有什么特点?
- 解析:
- SQL(Structured Query Language)是一种专门用于管理关系数据库管理系统(RDBMS)或在关系数据流管理系统(RDSMS)中进行流处理的领域特定语言。
- 特点:
- 标准化: 尽管存在不同的数据库实现(如 MySQL, PostgreSQL, SQL Server, Oracle),但核心 SQL 语法是标准化的(如 ANSI SQL)。
- 非过程化/声明性: 用户只需指定“做什么”(what),而无需关心“怎么做”(how)。数据库系统会负责优化和执行查询。
- 集合导向: SQL 操作的对象是数据集合(表),操作的结果也是数据集合。
- 易学性: 相较于许多通用编程语言,SQL 的语法结构更接近自然语言,相对容易学习和使用。
- 功能丰富: 涵盖数据查询(DQL)、数据操纵(DML)、数据定义(DDL)、数据控制(DCL)等多种功能。
2. 什么是 RDBMS?请举例说明。
- 解析:
- RDBMS(Relational Database Management System)是指基于关系模型的数据库管理系统。数据以表格(Table)的形式存储,表格由行(Row/Record)和列(Column/Field)组成。表与表之间可以通过键(Key)建立关系。
- 核心特性: 数据结构化、支持事务(ACID)、数据一致性、低冗余(通过范式)。
- 常见 RDBMS 举例: MySQL, PostgreSQL, Oracle Database, Microsoft SQL Server, SQLite, IBM Db2.
3. SQL 主要包含哪些部分(或命令类型)?
- 解析: SQL 主要可以分为以下几个部分:
- DDL (Data Definition Language – 数据定义语言): 用于定义数据库的结构或模式。
CREATE
: 创建数据库、表、索引、视图、存储过程等。(CREATE DATABASE
,CREATE TABLE
)ALTER
: 修改数据库对象的结构。(ALTER TABLE ADD COLUMN
,ALTER TABLE MODIFY COLUMN
)DROP
: 删除数据库对象。(DROP TABLE
,DROP DATABASE
)TRUNCATE
: 快速删除表中的所有行,但保留表结构(通常比DELETE
快,且不可回滚或回滚有限)。
- DML (Data Manipulation Language – 数据操作语言): 用于管理数据库中的数据。
SELECT
: 从数据库中检索数据。INSERT
: 向表中插入新数据。UPDATE
: 修改表中的现有数据。DELETE
: 从表中删除数据行。
- DQL (Data Query Language – 数据查询语言): 严格来说
SELECT
是 DQL 的核心,有时 DQL 被单独列出或并入 DML。 - DCL (Data Control Language – 数据控制语言): 用于控制数据库访问权限。
GRANT
: 授予用户或角色权限。REVOKE
: 撤销用户或角色的权限。
- TCL (Transaction Control Language – 事务控制语言): 用于管理数据库事务。
COMMIT
: 永久保存事务中所做的更改。ROLLBACK
: 撤销当前事务中的更改。SAVEPOINT
: 在事务中设置一个保存点,可以回滚到该点。
- DDL (Data Definition Language – 数据定义语言): 用于定义数据库的结构或模式。
4. 什么是主键(Primary Key)和外键(Foreign Key)?它们的作用是什么?
- 解析:
- 主键 (Primary Key):
- 定义:表中唯一标识每一行记录的一个或一组列。
- 特性:唯一性(不允许重复值)、非空性(不允许 NULL 值)。一个表只能有一个主键。
- 作用:保证数据的完整性(实体完整性),作为行的唯一标识符,常用于表连接。
- 外键 (Foreign Key):
- 定义:一个表中的一个或一组列,其值引用(指向)另一个表的主键。
- 作用:建立和维护两个表之间的关联关系,保证数据的参照完整性。确保外键列的值必须存在于被引用表的主键列中,或者为 NULL(如果允许)。
- 举例: 在
Orders
表中,CustomerID
列可以是一个外键,引用Customers
表的主键CustomerID
。这确保了每个订单都关联一个有效的客户。
- 主键 (Primary Key):
5. 什么是唯一约束(Unique Constraint)?它和主键有什么区别?
- 解析:
- 唯一约束 (Unique Constraint): 保证列(或列组合)中的所有值都是唯一的。
- 与主键的区别:
- NULL 值: 唯一约束允许有一个 NULL 值(在某些数据库系统中允许多个 NULL,但标准 SQL 和多数系统只允许一个或零个),而主键不允许任何 NULL 值。
- 数量: 一个表可以有多个唯一约束,但只能有一个主键。
- 索引: 主键默认会创建聚簇索引(Clustered Index,在多数数据库中),而唯一约束通常默认创建非聚簇索引(Non-Clustered Index)。(索引类型可能因数据库系统而异)
- 目的: 主键的核心目的是唯一标识行,而唯一约束主要目的是强制某列(非标识列)的数据唯一性。
第二部分:数据操作与查询 (DML & DQL)
这部分是 SQL 面试的重中之重,考察实际的数据处理能力。
6. DELETE
, TRUNCATE
, DROP
命令的区别?
- 解析:
DELETE FROM table_name [WHERE condition];
- 属于 DML。
- 逐行删除,可以带
WHERE
子句删除满足条件的特定行。 - 会触发触发器(Triggers)。
- 可以回滚(如果在一个事务中)。
- 删除操作会记录日志,相对较慢。
- 不释放表占用的空间(高水位线 HWM 不会重置,除非数据库有特殊机制)。
TRUNCATE TABLE table_name;
- 属于 DDL。
- 删除表中的所有行,保留表结构(列、约束、索引等)。
- 通常非常快,因为它不逐行删除,而是直接释放数据页。
- 通常不触发触发器。
- 通常不可回滚或回滚机制有限(取决于数据库系统)。
- 重置高水位线,释放大部分表空间。
- 不能带
WHERE
子句。
DROP TABLE table_name;
- 属于 DDL。
- 完全删除表,包括表结构、数据、索引、约束、触发器等。
- 操作不可回滚(通常需要备份恢复)。
- 释放所有占用的空间。
7. WHERE
和 HAVING
子句的区别?
-
解析:
- 作用对象不同:
WHERE
子句作用于 行级别 的过滤,在数据分组(GROUP BY
)之前进行。它筛选原始表或连接后的表中的行。HAVING
子句作用于 组级别 的过滤,在数据分组(GROUP BY
)之后进行。它筛选由GROUP BY
产生的聚合结果组。
- 使用聚合函数:
WHERE
子句中 不能 直接使用聚合函数(如COUNT()
,SUM()
,AVG()
等)。HAVING
子句中 可以 使用聚合函数。
- 执行顺序:
FROM
->WHERE
->GROUP BY
-> 聚合函数计算 ->HAVING
->SELECT
->DISTINCT
->ORDER BY
->LIMIT
/OFFSET
.
- 作用对象不同:
-
示例: 查询平均薪资大于 5000 的部门及其平均薪资。
sql
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
HAVING AVG(salary) > 5000;这里不能用
WHERE AVG(salary) > 5000
,因为WHERE
在GROUP BY
之前执行,那时聚合结果还不存在。
8. 解释 SQL 中的各种 JOIN 类型及其区别 (INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL OUTER JOIN, CROSS JOIN)。
-
解析: JOIN 用于根据两个或多个表中的相关列组合行。
INNER JOIN
(内连接):- 返回两个表中联接字段相等的行。即只返回两个表中都匹配的行。
SELECT columns FROM table1 INNER JOIN table2 ON table1.column = table2.column;
LEFT JOIN
(左连接 / 左外连接):- 返回左表(
table1
)的所有行,以及右表(table2
)中匹配的行。 - 如果右表中没有匹配的行,则结果中右表的列将包含 NULL。
SELECT columns FROM table1 LEFT JOIN table2 ON table1.column = table2.column;
- 返回左表(
RIGHT JOIN
(右连接 / 右外连接):- 返回右表(
table2
)的所有行,以及左表(table1
)中匹配的行。 - 如果左表中没有匹配的行,则结果中左表的列将包含 NULL。
SELECT columns FROM table1 RIGHT JOIN table2 ON table1.column = table2.column;
- 返回右表(
FULL OUTER JOIN
(全外连接):- 返回左表和右表中所有匹配的行,以及左表和右表中不匹配的行。
- 当某一行在另一个表中没有匹配时,另一个表的列将包含 NULL。
- (注意:MySQL 不直接支持
FULL OUTER JOIN
,需要用LEFT JOIN UNION RIGHT JOIN
模拟)。 SELECT columns FROM table1 FULL OUTER JOIN table2 ON table1.column = table2.column;
CROSS JOIN
(交叉连接 / 笛卡尔积):- 返回左表中的每一行与右表中的每一行的组合。
- 结果集的行数等于左表行数乘以右表行数。
- 通常不需要
ON
子句(除非是旧式语法FROM table1, table2 WHERE ...
)。 SELECT columns FROM table1 CROSS JOIN table2;
9. UNION
和 UNION ALL
的区别?
-
解析: 两者都用于合并两个或多个
SELECT
语句的结果集。UNION
:- 合并结果集,并 自动去除重复 的行。
- 会对结果集进行排序以查找重复项,因此相对较慢。
-
UNION ALL
:- 合并结果集,包含所有 的行,不去重。
- 不需要排序去重,因此通常比
UNION
更快。
-
使用条件:
- 所有
SELECT
语句选择的列数必须相同。 - 对应列的数据类型必须兼容(或可以隐式转换)。
- 列的顺序也很重要。
- 所有
-
选择: 如果确定结果集中没有重复项,或者允许重复项存在,或者性能是首要考虑因素,应优先使用
UNION ALL
。如果需要去重,则使用UNION
。
10. 什么是子查询(Subquery)?有哪些类型?
- 解析:
- 子查询是嵌套在另一个 SQL 查询(主查询)中的查询。它可以出现在
SELECT
,FROM
,WHERE
,HAVING
子句中,以及INSERT
,UPDATE
,DELETE
语句中。 - 主要类型:
- 标量子查询 (Scalar Subquery): 返回单个值(一行一列)。可以用在需要单个值的地方,如
SELECT
列表或WHERE
子句的比较操作符右侧。
sql
SELECT employee_name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees); - 行子查询 (Row Subquery): 返回单行,但可能包含多列。通常用在
WHERE
子句中与行构造器比较。
sql
SELECT * FROM employees
WHERE (department_id, manager_id) = (SELECT department_id, manager_id FROM departments WHERE department_name = 'Sales'); - 列子查询 (Column Subquery): 返回单列,但可能包含多行。常用于
IN
,ANY
,ALL
操作符。
sql
SELECT product_name FROM products
WHERE product_id IN (SELECT product_id FROM order_details WHERE quantity > 100); - 表子查询 (Table Subquery): 返回多行多列,就像一个临时的表。常用于
FROM
子句中,需要给子查询一个别名。
sql
SELECT e.employee_name, d.avg_dept_salary
FROM employees e
JOIN (SELECT department_id, AVG(salary) AS avg_dept_salary
FROM employees
GROUP BY department_id) d
ON e.department_id = d.department_id; - 相关子查询 (Correlated Subquery): 子查询的执行依赖于外部查询的值。子查询会为外部查询处理的每一行都执行一次,性能通常较低。
sql
-- 查询每个部门中薪资最高的员工
SELECT employee_name, salary, department_id
FROM employees e1
WHERE salary = (SELECT MAX(salary)
FROM employees e2
WHERE e1.department_id = e2.department_id);
- 标量子查询 (Scalar Subquery): 返回单个值(一行一列)。可以用在需要单个值的地方,如
- 子查询是嵌套在另一个 SQL 查询(主查询)中的查询。它可以出现在
11. 什么是窗口函数(Window Function)?与聚合函数的区别是什么?
-
解析:
- 窗口函数: 对与当前行相关的 一组 表行(称为“窗口”)执行计算。与聚合函数不同,窗口函数 不 会将多行聚合成单行输出;它为结果集中的 每一行 都返回一个值。
- 与聚合函数的区别:
- 输出行数: 聚合函数(使用
GROUP BY
)会减少输出的行数(每组一行),而窗口函数保持原始行数不变。 - 计算范围: 聚合函数作用于整个分组,窗口函数作用于定义的“窗口”(可以是整个分区,也可以是基于当前行的滑动窗口)。
- 输出行数: 聚合函数(使用
- 基本语法:
function_name() OVER ([PARTITION BY column] [ORDER BY column] [frame_clause])
PARTITION BY
: 将行分成不同的分区,窗口函数在每个分区内独立计算。类似于GROUP BY
,但不合并行。ORDER BY
: 定义分区内行的顺序,对于排名函数和依赖顺序的窗口是必需的。frame_clause
: 定义窗口的范围(如ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)。
- 常见窗口函数:
- 排名函数:
ROW_NUMBER()
,RANK()
,DENSE_RANK()
,NTILE()
- 聚合窗口函数:
SUM() OVER (...)
,AVG() OVER (...)
,COUNT() OVER (...)
,MAX() OVER (...)
,MIN() OVER (...)
- 值函数:
LAG()
,LEAD()
,FIRST_VALUE()
,LAST_VALUE()
- 排名函数:
-
示例: 计算每个员工的薪资及其所在部门的平均薪资。
sql
SELECT
employee_name,
department_id,
salary,
AVG(salary) OVER (PARTITION BY department_id) AS avg_dept_salary
FROM employees;这里,
AVG(salary) OVER (PARTITION BY department_id)
为每一行计算其所属部门的平均薪资,而不会像GROUP BY
那样只输出每个部门一行。
12. RANK()
, DENSE_RANK()
, ROW_NUMBER()
的区别?
-
解析: 这三个都是常用的窗口排名函数,区别在于处理并列(ties)值的方式:
ROW_NUMBER()
:- 为分区内的每一行分配一个 连续唯一 的排名,即使值相同,排名也不同(通常基于
ORDER BY
的顺序,如果ORDER BY
列值相同,则排名可能不确定,除非有额外的排序键)。 - 示例:(1, 2, 3, 4) 对于值 (10, 20, 20, 30)
- 为分区内的每一行分配一个 连续唯一 的排名,即使值相同,排名也不同(通常基于
RANK()
:- 如果存在并列值,则分配 相同 的排名。下一个排名会 跳过 中间的序号。
- 示例:(1, 2, 2, 4) 对于值 (10, 20, 20, 30) – 排名 3 被跳过。
DENSE_RANK()
:- 如果存在并列值,则分配 相同 的排名。下一个排名是 紧随其后 的序号,不跳过。
- 示例:(1, 2, 2, 3) 对于值 (10, 20, 20, 30) – 排名 3 紧随排名 2。
-
选择:
- 需要唯一行号时用
ROW_NUMBER()
。 - 需要反映真实排名(并列同名次,后续名次跳跃)时用
RANK()
。 - 需要连续的排名(并列同名次,后续名次不跳跃)时用
DENSE_RANK()
。
- 需要唯一行号时用
第三部分:索引与性能优化
面试官通常会考察候选人对数据库性能的理解以及如何优化慢查询。
13. 什么是索引?为什么使用索引?索引的优缺点是什么?
- 解析:
- 索引 (Index): 数据库中一种特殊的数据结构(通常是 B-Tree 或其变种,如 B+Tree),用于 快速查找 表中具有特定值的行。它包含表中一个或多个列的值以及指向这些值所在行的物理地址(或指针)。
- 为什么使用索引:
- 提高查询速度: 主要目的是加速
SELECT
查询,尤其是带有WHERE
,JOIN ON
,ORDER BY
子句的查询。如果没有索引,数据库需要进行全表扫描(Full Table Scan),效率低下。 - 加速排序: 如果
ORDER BY
的列上有索引,数据库可能直接利用索引的有序性,避免额外的排序操作。 - 确保唯一性: 主键和唯一约束通常通过创建唯一索引来实现。
- 提高查询速度: 主要目的是加速
- 优点:
- 显著提高数据检索(查询)的速度。
- 通过唯一索引保证数据的唯一性。
- 加速表连接和排序操作。
- 缺点:
- 占用存储空间: 索引本身需要存储空间,有时甚至比数据本身还大。
- 降低 DML 操作速度: 对表进行
INSERT
,UPDATE
,DELETE
操作时,不仅要修改数据行,还需要 维护索引(增加、删除、更新索引项),这会增加额外的开销,降低 DML 操作的性能。 - 创建和维护成本: 创建索引需要时间,索引也需要定期维护(如重建、重组)。
14. 索引有哪些类型?聚簇索引和非聚簇索引的区别?
- 解析:
- 常见索引类型(逻辑分类):
- 主键索引: 针对主键列创建的索引,要求键值唯一且非空。
- 唯一索引: 针对唯一约束列创建的索引,要求键值唯一(可允许一个 NULL)。
- 普通索引(非唯一索引): 最基本的索引类型,无唯一性限制。
- 组合索引(复合索引): 在多个列上创建的索引。遵循“最左前缀”原则。
- 全文索引: 主要用于文本数据的搜索(如
MATCH AGAINST
)。 - 空间索引: 用于地理空间数据类型。
- 物理存储分类(重要):
- 聚簇索引 (Clustered Index):
- 定义: 表中数据行的物理存储顺序与索引键的逻辑顺序 相同。数据本身就是索引的一部分(通常在叶子节点存储完整数据行)。
- 特性: 一个表 只能有一个 聚簇索引(因为物理顺序只有一种)。通常主键默认是聚簇索引(在 MySQL InnoDB, SQL Server 中)。
- 优点: 基于聚簇键的范围查询和点查询非常快,因为相关数据物理上是连续的。
- 缺点: 插入和更新操作可能较慢,因为可能需要移动数据行来维护物理顺序。非聚簇索引查找可能需要两次查找(先找到聚簇键,再通过聚簇键找到数据)。
- 非聚簇索引 (Non-Clustered Index / Secondary Index):
- 定义: 索引的逻辑顺序与数据行的物理存储顺序 不同。索引的叶子节点通常存储索引键的值和指向对应数据行的 指针(如行 ID 或聚簇索引键)。
- 特性: 一个表可以有 多个 非聚簇索引。
- 优点: 插入和更新操作通常比聚簇索引快(只需要修改索引结构和指针)。
- 缺点: 基于非聚簇索引的查询可能需要额外的 I/O 操作来获取数据行(回表查询,Lookup)。但覆盖索引可以避免回表。
- 聚簇索引 (Clustered Index):
- 常见索引类型(逻辑分类):
15. 什么是覆盖索引(Covering Index)?
- 解析:
- 如果一个查询需要的所有列(
SELECT
,WHERE
,ORDER BY
,GROUP BY
中涉及的列)都包含在某个非聚簇索引中,那么数据库引擎可以直接从该索引获取所有需要的数据,而 无需 回到主表(聚簇索引)去查找完整的行数据。这个索引就称为该查询的 覆盖索引。 - 优点: 避免了回表查询(Table Lookup / Bookmark Lookup),减少了 I/O 操作,显著提高查询性能。
- 实现: 通常通过创建包含所有必要列的复合索引来实现。
- 如果一个查询需要的所有列(
16. 如何优化 SQL 查询?请列举一些常见的优化方法。
- 解析: SQL 优化是一个复杂的主题,涉及多个层面。常见方法包括:
- 索引优化:
- 为
WHERE
,JOIN
,ORDER BY
,GROUP BY
子句中频繁使用的列创建合适的索引。 - 创建复合索引时注意列的顺序(区分度高的列放前面,遵循最左前缀原则)。
- 利用覆盖索引避免回表。
- 定期检查并删除未使用或冗余的索引。
- 避免在索引列上使用函数或进行运算,这会导致索引失效。(
WHERE YEAR(date_col) = 2023
应改为WHERE date_col >= '2023-01-01' AND date_col < '2024-01-01'
)
- 为
- 查询语句优化:
- 避免
SELECT *
: 只选择需要的列,减少网络传输量和 I/O,更容易触发覆盖索引。 - 优化
JOIN
: 选择合适的 JOIN 类型,确保连接列上有索引,小表驱动大表(有时优化器会自动处理)。 - 优化
WHERE
子句:- 避免在
WHERE
子句中使用!=
或<>
操作符,尽量使用=
、>
、<
、BETWEEN
等。 - 避免使用
OR
连接条件(有时可改写为UNION ALL
或分拆查询)。 - 避免
NULL
值判断 (IS NULL
,IS NOT NULL
),如果可以,给列设置NOT NULL
约束并使用默认值。 - 对于
LIKE
查询,避免前导模糊匹配 (LIKE '%value'
),这会导致索引失效。后缀模糊匹配 (LIKE 'value%'
) 或特定情况下的全文索引更优。
- 避免在
- 优化
GROUP BY
和ORDER BY
: 确保相关列有索引,尽量减少排序的数据量。 - 使用
EXISTS
代替IN
(某些场景下): 特别是当子查询结果集较大时,EXISTS
通常性能更好(它找到第一个匹配就停止)。反之,IN
可能更优。 - 减少子查询,考虑用 JOIN 代替: 非相关子查询有时可以改写为 JOIN,性能可能更好。
- 合理使用
UNION
和UNION ALL
: 优先使用UNION ALL
避免不必要的去重排序。
- 避免
- 数据库结构优化:
- 适当的范式化: 减少数据冗余,但也可能增加 JOIN 操作。
- 反范式化: 在特定场景下,为提高查询性能,适当增加冗余(如添加汇总字段)。
- 选择合适的数据类型: 使用尽可能小且合适的数据类型。
- 使用查询分析工具:
EXPLAIN
(或EXPLAIN ANALYZE
): 分析查询的执行计划,找出性能瓶颈(如全表扫描、未使用索引、文件排序等)。- 数据库性能监控工具: 识别慢查询。
- 索引优化:
17. 什么是 B-Tree 索引和 B+Tree 索引?它们有什么区别?
-
解析: B-Tree 和 B+Tree 是数据库中最常用的索引结构。
- B-Tree (平衡多路搜索树):
- 每个节点(包括内部节点和叶子节点)都 同时存储键值和数据指针(或数据本身)。
- 所有叶子节点都在同一层,且不一定有指向兄弟节点的指针。
- 查找特定键值时,可能在内部节点就命中并返回。
-
B+Tree:
- 内部节点只存储键值(用于导航),不存储数据指针。数据指针(或完整数据)全部存储在叶子节点。
- 所有叶子节点通过指针连接,形成一个有序链表。
- 区别与优势(B+Tree 相对于 B-Tree):
- 范围查询更高效: 由于所有数据都在叶子节点,并且叶子节点是链表结构,进行范围查询时只需定位到起始叶子节点,然后沿链表顺序扫描即可,无需回溯。
- 查询性能更稳定: 所有查询最终都必须到达叶子节点才能获取数据指针,查询路径长度相对固定。
- 内部节点存储更多键值: 因为内部节点不存数据指针,相同大小的磁盘页可以容纳更多键值,使得树的高度更低,减少磁盘 I/O 次数。
- 更适合磁盘存储: B+Tree 的结构特点使其更利于磁盘预读(缓存)。
-
应用: 大多数关系型数据库(如 MySQL 的 InnoDB 存储引擎)使用 B+Tree 作为索引结构。
- B-Tree (平衡多路搜索树):
第四部分:事务与并发控制
考察对数据库事务特性和并发问题的理解。
18. 什么是数据库事务?事务的 ACID 特性是什么?
- 解析:
- 事务 (Transaction): 数据库管理系统执行过程中的一个 逻辑工作单元,它包含一个或多个数据库操作(读/写),这些操作要么 全部执行成功,要么 全部不执行(回滚)。事务是一个不可分割的操作序列。
- ACID 特性: 是保证事务可靠性的四个核心特性:
- 原子性 (Atomicity): 事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。如果事务中的任何一步失败,整个事务将被回滚到初始状态。
- 一致性 (Consistency): 事务必须使数据库从一个一致性状态转变到另一个一致性状态。事务执行的结果必须是使数据库保持合法状态(满足所有约束和规则)。
- 隔离性 (Isolation): 多个并发事务之间是隔离的,一个事务的执行不应被其他事务干扰。即一个事务所做的修改在最终提交前,对其他事务是不可见的(根据隔离级别不同,可见性程度不同)。
- 持久性 (Durability): 一旦事务成功提交,则它对数据库所做的更改便是 永久性 的,即使后续系统发生崩溃或故障,这些更改也不会丢失。通常通过写入日志(如 WAL – Write-Ahead Logging)来保证。
19. 解释数据库的隔离级别及其可能引发的问题。
-
解析: 隔离级别定义了一个事务在执行过程中,其操作对其他并发事务的可见程度。SQL 标准定义了四种隔离级别,级别越高,并发性能越差,但数据一致性越好。
- 读未提交 (Read Uncommitted): 最低级别。一个事务可以读取到另一个事务 尚未提交 的修改。
- 可能问题:脏读 (Dirty Read) – 读取到了未提交的数据,如果该事务回滚,则读取到的数据是无效的。
- 读已提交 (Read Committed): 一个事务只能读取到其他事务 已经提交 的修改。解决了脏读问题。这是大多数数据库(如 Oracle, SQL Server, PostgreSQL)的 默认 隔离级别。
- 可能问题:不可重复读 (Non-Repeatable Read) – 在同一个事务内,两次执行相同的查询,可能得到不同的结果,因为其他事务在此期间提交了更新。
- 可重复读 (Repeatable Read): 保证在同一个事务内,多次读取同一数据的结果是一致的。解决了不可重复读问题。这是 MySQL InnoDB 的 默认 隔离级别。
- 实现: 通常通过 MVCC(多版本并发控制)实现。读取操作会读取事务开始时的数据快照。
- 可能问题:幻读 (Phantom Read) – 在同一个事务内,两次执行相同的范围查询(如
SELECT ... WHERE condition
),第二次查询可能会返回 新增 的、符合条件的行(“幻象”行),因为其他事务在此期间插入了新数据并提交。MySQL InnoDB 通过 Next-Key Locking 在一定程度上解决了幻读问题。
- 可串行化 (Serializable): 最高级别。强制事务串行执行,仿佛它们是一个接一个地执行。避免了脏读、不可重复读和幻读。
- 实现: 通常通过加锁(如读写锁)实现。
- 缺点: 并发性能极差,通常只在对数据一致性要求极高的场景下使用。
- 读未提交 (Read Uncommitted): 最低级别。一个事务可以读取到另一个事务 尚未提交 的修改。
第五部分:数据库设计与范式
考察对数据库结构设计原则的理解。
20. 什么是数据库范式?常见的范式有哪些(1NF, 2NF, 3NF)?
-
解析:
- 数据库范式 (Database Normalization): 是设计关系数据库表结构的一系列规则或指南,其主要目的是 减少数据冗余、提高数据一致性、避免数据插入/更新/删除异常。范式级别越高,冗余越少,但可能需要更多的表和连接操作。
-
常见范式:
- 第一范式 (1NF – First Normal Form):
- 要求: 表中的每一列都是 原子性 的,不可再分。即每个字段只存储一个值,而不是列表或集合。
- 目的: 消除非原子列。所有关系数据库表天然满足 1NF。
- 第二范式 (2NF – Second Normal Form):
- 要求: 在满足 1NF 的基础上,表中的所有 非主键列 必须 完全依赖 于整个 主键(针对复合主键),而不能只依赖于主键的一部分。
- 目的: 消除部分依赖。如果主键是单列,则满足 1NF 的表自动满足 2NF。
- 例子: (订单号, 商品号) 作为复合主键,如果存在“商品名称”列,它只依赖于“商品号”,不依赖“订单号”,则不满足 2NF。应将商品信息拆分到单独的商品表。
- 第三范式 (3NF – Third Normal Form):
- 要求: 在满足 2NF 的基础上,表中的所有 非主键列 不得 传递依赖 于主键。即非主键列之间不能存在依赖关系(一个非主键列不能依赖于另一个非主键列)。
- 目的: 消除传递依赖。
- 例子: 员工表有 (员工号 PK, 部门号, 部门名称)。这里“部门名称”依赖于“部门号”(非主键),而“部门号”依赖于“员工号”(主键),存在传递依赖。应将部门信息拆分到单独的部门表。
- 第一范式 (1NF – First Normal Form):
-
更高范式: 还存在 BCNF(Boyce-Codd Normal Form)、4NF、5NF 等,要求更严格,但在实践中 3NF 通常足够。
21. 范式化有什么好处?什么时候会考虑反范式化设计?
- 解析:
- 范式化的好处:
- 减少数据冗余: 相同信息只存储一次。
- 提高数据一致性: 修改数据时只需修改一处,不易出错。
- 避免更新异常: 如修改一个信息时需要更新多行。
- 避免插入异常: 如无法插入某些信息,除非关联的其他信息也存在。
- 避免删除异常: 如删除一行导致有用信息的丢失。
- 使数据库结构更清晰、更易于维护。
- 反范式化 (Denormalization):
- 定义: 为了提高查询性能,故意 违反某些范式规则,增加数据冗余。
- 考虑场景:
- 当严格遵循范式导致 频繁且复杂的 JOIN 操作,严重影响查询性能时。
- 在 读多写少 的应用场景中,查询性能是首要考虑因素。
- 构建数据仓库或报表系统时,经常需要汇总和冗余数据以加速分析。
- 方法: 添加冗余列、创建汇总表等。
- 代价: 增加了数据冗余,可能导致数据不一致(需要额外的机制维护数据同步),增加了存储空间,降低了写操作的性能。需要权衡利弊。
- 范式化的好处:
第六部分:其他常见问题
22. 什么是视图(View)?使用视图有什么好处?
- 解析:
- 视图 (View): 一个 虚拟表,其内容由一个 SQL 查询定义。它本身不存储数据(除非是物化视图),数据来源于其基于的基础表。视图就像一个窗口,通过它可以查看或操作基础表中的数据。
- 好处:
- 简化复杂查询: 将复杂的 JOIN 或聚合查询封装在视图中,用户只需查询简单的视图。
- 数据安全性: 可以通过视图限制用户能访问的数据行和列,只暴露部分数据,隐藏敏感信息。
- 逻辑数据独立性: 即使基础表的结构发生变化(如添加列),只要视图的定义不受影响,依赖于视图的应用程序就无需修改。
- 数据重用和一致性: 定义一次,多处使用,确保逻辑一致性。
23. 什么是存储过程(Stored Procedure)和函数(Function)?它们有什么区别?
- 解析:
- 存储过程和函数: 都是预先编译好并存储在数据库中的一组 SQL 语句集合,可以接受参数、执行逻辑、并返回结果。
- 共同优点: 提高性能(预编译)、减少网络流量(调用名称而非长 SQL)、代码重用、封装业务逻辑、提高安全性(通过权限控制)。
- 主要区别:
- 返回值:
- 函数 (Function) 必须有一个返回值(标量值或表类型)。主要用于计算并返回结果。可以在
SELECT
语句中像内置函数一样直接调用。 - 存储过程 (Stored Procedure) 不强制要求返回值,可以通过输出参数(
OUT
或INOUT
)返回多个值或状态。主要用于执行一系列操作(可能包含 DML, DDL)。通常通过CALL
或EXECUTE
命令调用。
- 函数 (Function) 必须有一个返回值(标量值或表类型)。主要用于计算并返回结果。可以在
- 调用方式: 函数可以在 SQL 表达式中使用,存储过程不能。
- 用途侧重: 函数侧重于计算和返回值,存储过程侧重于执行复杂的业务逻辑和数据操作流程。
- 返回值:
24. 如何查找表中的重复记录?如何删除重复记录(保留一条)?
- 解析:
- 查找重复记录(基于某些列,如 col1, col2):
sql
SELECT col1, col2, COUNT(*)
FROM your_table
GROUP BY col1, col2
HAVING COUNT(*) > 1; - 删除重复记录(保留一条,通常保留 ID 最小或最大的那条):
- 方法一:使用子查询和
MIN()
或MAX()
ID (假设有唯一 ID 列id
)
sql
DELETE FROM your_table
WHERE id NOT IN (
SELECT MIN(id) -- 或 MAX(id)
FROM your_table
GROUP BY col1, col2 -- 基于哪些列判断重复
); -
方法二:使用窗口函数
ROW_NUMBER()
(推荐,通常更高效)
“`sql
— CTE (Common Table Expression) 写法
WITH RowNumCTE AS (
SELECT
id, — 或者其他可以唯一标识行的列
ROW_NUMBER() OVER(PARTITION BY col1, col2 ORDER BY id) as rn — 按重复列分组,按id排序编号
FROM your_table
)
DELETE FROM RowNumCTE WHERE rn > 1; — 删除编号大于1的记录(即重复记录)— 注意:直接从 CTE 删除可能在某些数据库版本/类型中不支持,
— 可能需要改写为 DELETE … FROM your_table JOIN RowNumCTE …
— 或者先查询出要删除的 id 列表,再执行 DELETE。
— 例如,在 MySQL 中可能这样写:
DELETE t1 FROM your_table t1
INNER JOIN (
SELECT id, ROW_NUMBER() OVER(PARTITION BY col1, col2 ORDER BY id) as rn
FROM your_table
) t2 ON t1.id = t2.id
WHERE t2.rn > 1;
“`
- 方法一:使用子查询和
- 查找重复记录(基于某些列,如 col1, col2):
25. 请写一个 SQL 查询,找出每个部门薪资最高的员工信息。
-
解析: 这是考察复杂查询和分组/排序能力的经典问题。
- 方法一:使用相关子查询
sql
SELECT e.employee_id, e.employee_name, e.department_id, e.salary
FROM employees e
WHERE e.salary = (
SELECT MAX(salary)
FROM employees e2
WHERE e.department_id = e2.department_id
);
-- 如果有多个员工薪资相同且最高,都会被选出。 - 方法二:使用窗口函数
RANK()
或DENSE_RANK()
(推荐)
sql
WITH RankedEmployees AS (
SELECT
employee_id,
employee_name,
department_id,
salary,
RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) as salary_rank
-- 或者 DENSE_RANK()
FROM employees
)
SELECT employee_id, employee_name, department_id, salary
FROM RankedEmployees
WHERE salary_rank = 1;
-- 使用 RANK() 或 DENSE_RANK() 可以处理并列最高薪的情况。 - 方法三:使用
LEFT JOIN
(特定场景,可能效率不高)
sql
SELECT e1.employee_id, e1.employee_name, e1.department_id, e1.salary
FROM employees e1
LEFT JOIN employees e2
ON e1.department_id = e2.department_id AND e1.salary < e2.salary
WHERE e2.employee_id IS NULL; -- 如果找不到同部门更高薪的,说明e1是最高薪之一
- 方法一:使用相关子查询
面试建议
- 清晰沟通: 理解问题,如有疑问及时向面试官确认需求和表结构。
- 思考过程: 边写 SQL 边解释你的思路,展示逻辑推理能力。
- 代码规范: 书写清晰、格式规范的 SQL 代码,使用有意义的别名。
- 考虑边界: 思考 NULL 值、空表、重复值等边界情况。
- 性能意识: 在回答复杂查询或优化问题时,体现对性能的关注。
- 实践为王: 平时多刷题,多在实际数据库环境中练习。
结语
SQL 面试的范围广泛,但核心知识点相对集中。掌握本文梳理的基础概念、熟练运用 DML 和 DQL 进行数据操作与查询、理解索引和性能优化的原理、了解事务和范式的基本概念,并能解决常见的场景问题,将大大增加你在 SQL 面试中脱颖而出的机会。祝你面试顺利!