SQL面试高频问题汇总与解析 – wiki基地


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: 在事务中设置一个保存点,可以回滚到该点。

4. 什么是主键(Primary Key)和外键(Foreign Key)?它们的作用是什么?

  • 解析:
    • 主键 (Primary Key):
      • 定义:表中唯一标识每一行记录的一个或一组列。
      • 特性:唯一性(不允许重复值)、非空性(不允许 NULL 值)。一个表只能有一个主键。
      • 作用:保证数据的完整性(实体完整性),作为行的唯一标识符,常用于表连接。
    • 外键 (Foreign Key):
      • 定义:一个表中的一个或一组列,其值引用(指向)另一个表的主键。
      • 作用:建立和维护两个表之间的关联关系,保证数据的参照完整性。确保外键列的值必须存在于被引用表的主键列中,或者为 NULL(如果允许)。
    • 举例:Orders 表中,CustomerID 列可以是一个外键,引用 Customers 表的主键 CustomerID。这确保了每个订单都关联一个有效的客户。

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. WHEREHAVING 子句的区别?

  • 解析:

    • 作用对象不同:
      • 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,因为 WHEREGROUP 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. UNIONUNION 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);

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)。但覆盖索引可以避免回表。

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 BYORDER BY 确保相关列有索引,尽量减少排序的数据量。
      • 使用 EXISTS 代替 IN(某些场景下): 特别是当子查询结果集较大时,EXISTS 通常性能更好(它找到第一个匹配就停止)。反之,IN 可能更优。
      • 减少子查询,考虑用 JOIN 代替: 非相关子查询有时可以改写为 JOIN,性能可能更好。
      • 合理使用 UNIONUNION 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 作为索引结构。

第四部分:事务与并发控制

考察对数据库事务特性和并发问题的理解。

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): 最高级别。强制事务串行执行,仿佛它们是一个接一个地执行。避免了脏读、不可重复读和幻读。
      • 实现: 通常通过加锁(如读写锁)实现。
      • 缺点: 并发性能极差,通常只在对数据一致性要求极高的场景下使用。

第五部分:数据库设计与范式

考察对数据库结构设计原则的理解。

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, 部门号, 部门名称)。这里“部门名称”依赖于“部门号”(非主键),而“部门号”依赖于“员工号”(主键),存在传递依赖。应将部门信息拆分到单独的部门表。
    • 更高范式: 还存在 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) 不强制要求返回值,可以通过输出参数(OUTINOUT)返回多个值或状态。主要用于执行一系列操作(可能包含 DML, DDL)。通常通过 CALLEXECUTE 命令调用。
      • 调用方式: 函数可以在 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;
        “`

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是最高薪之一

面试建议

  1. 清晰沟通: 理解问题,如有疑问及时向面试官确认需求和表结构。
  2. 思考过程: 边写 SQL 边解释你的思路,展示逻辑推理能力。
  3. 代码规范: 书写清晰、格式规范的 SQL 代码,使用有意义的别名。
  4. 考虑边界: 思考 NULL 值、空表、重复值等边界情况。
  5. 性能意识: 在回答复杂查询或优化问题时,体现对性能的关注。
  6. 实践为王: 平时多刷题,多在实际数据库环境中练习。

结语

SQL 面试的范围广泛,但核心知识点相对集中。掌握本文梳理的基础概念、熟练运用 DML 和 DQL 进行数据操作与查询、理解索引和性能优化的原理、了解事务和范式的基本概念,并能解决常见的场景问题,将大大增加你在 SQL 面试中脱颖而出的机会。祝你面试顺利!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注