一、引言:为什么数据库是面试的必考点
无论是校招还是社招,无论是后端开发、大数据还是运维岗位,数据库几乎都是面试中绕不开的一环。面试官考察的不只是「会写 SQL」,更重要的是你对存储原理、索引结构、事务特性、并发控制、性能优化等底层机制的掌握程度。本文系统整理了从基础概念到高阶优化、从 MySQL 到 Redis 的高频面试题,配以原理讲解、代码示例和对比表格,帮助你在面试前完成一次体系化的复习。
提示:本文约 2 万字,建议按目录顺序通读一遍,再针对薄弱环节重点复习。每个小节都给出了「考点分析」和「标准回答思路」,可以直接作为答题模板。
二、数据库基础概念高频题
2.1 什么是数据库、数据库管理系统和数据库系统
数据库(Database,DB)是指长期存储在计算机内、有组织、可共享的数据集合。数据库管理系统(Database Management System,DBMS)是位于用户与操作系统之间的一层数据管理软件,负责数据的组织、存储、维护和检索,例如 MySQL、Oracle、PostgreSQL。数据库系统(DBS)则是由数据库、DBMS、应用程序、数据库管理员(DBA)和用户共同构成的完整系统。
可以把三者的关系类比为:数据库是「仓库里的货物」,DBMS 是「仓库管理系统」,数据库系统是「整个仓储运营体系」。
2.2 关系型数据库和非关系型数据库的区别
| 对比维度 | 关系型数据库(RDBMS) | 非关系型数据库(NoSQL) |
|---|---|---|
| 数据模型 | 二维表,行列结构,模式固定 | 键值、文档、列族、图等多种模型 |
| 代表产品 | MySQL、Oracle、SQL Server、PostgreSQL | Redis、MongoDB、HBase、Cassandra、Neo4j |
| 事务支持 | ACID 强事务 | 多数只提供弱一致性或 BASE |
| 扩展方式 | 以纵向扩展为主,分库分表复杂 | 天然适合横向扩展 |
| 查询语言 | 标准 SQL | 各产品自有 API,如 Redis 命令、Mongo 查询 |
| 适用场景 | 金融、订单、ERP 等强一致场景 | 缓存、海量日志、社交关系、高并发读 |
回答要点:不要简单说「SQL 和 NoSQL 谁更好」,而要强调场景匹配。关系型擅长复杂事务和多表关联;NoSQL 擅长高并发、海量数据和灵活结构。实践中常见的是「MySQL 做主存储 + Redis 做缓存」的混合架构。
2.3 什么是三范式?为什么有时要反范式设计
第一范式(1NF):字段不可再分,保证原子性。第二范式(2NF):在满足 1NF 基础上,消除非主属性对主键的部分函数依赖。第三范式(3NF):在满足 2NF 基础上,消除非主属性对主键的传递函数依赖。
范式设计的目的是减少数据冗余、避免更新异常。但在实际互联网业务中,为了减少多表 Join、提升查询性能,常常会进行反范式设计,例如在订单表中冗余用户昵称、在统计表中预聚合数据。本质是用「冗余换性能」,需要在一致性和查询效率之间权衡。
2.4 主键、外键、唯一键和索引的区别
- 主键(Primary Key):唯一标识一行记录,非空且唯一,一张表只能有一个。
- 外键(Foreign Key):用于建立表与表之间的引用关系,保证引用完整性。
- 唯一键(Unique Key):保证列值唯一,但允许有多个 NULL(不同数据库对 NULL 的处理略有差异)。
- 索引(Index):为加速查询而建立的数据结构,可以有多个,不要求唯一。
注意:主键和唯一键都会自动创建唯一索引,但索引是一种存储结构,主键/唯一键是一种约束,二者概念要区分清楚。
2.5 什么是视图、存储过程和触发器
视图(View)是一条被保存的查询语句,本身不存储数据,可简化复杂查询、隔离底层表结构、控制数据访问权限。存储过程(Stored Procedure)是预编译的一组 SQL 语句的集合,可减少网络传输、提升执行效率,但可移植性较差。触发器(Trigger)是在特定表上发生 INSERT、UPDATE、DELETE 时自动执行的程序,多用于审计、日志同步和约束增强,但过多触发器会降低 DML 性能、增加维护成本。
三、SQL 语法与查询高频题
3.1 SQL 的执行顺序是什么
这是非常高频的面试题。SQL 的关键字书写顺序与执行顺序并不一致,标准执行顺序如下:
- FROM:确定数据来源,执行笛卡尔积和关联。
- ON:执行连接条件过滤。
- JOIN:添加外部行(外连接时)。
- WHERE:对行进行条件过滤。
- GROUP BY:分组。
- HAVING:对分组结果过滤。
- SELECT:选取需要的列。
- DISTINCT:去重。
- ORDER BY:排序。
- LIMIT:限制返回行数。
理解了执行顺序,很多问题就迎刃而解:比如WHERE 不能使用 SELECT 里的别名(因为 WHERE 先于 SELECT 执行),而ORDER BY 可以使用别名(它执行得更晚)。
3.2 WHERE 和 HAVING 的区别
| 对比项 | WHERE | HAVING |
|---|---|---|
| 作用对象 | 作用于行(分组前的原始数据) | 作用于组(分组后的聚合结果) |
| 执行阶段 | GROUP BY 之前 | GROUP BY 之后 |
| 能否使用聚合函数 | 不能 | 能,如 COUNT、SUM |
| 典型用途 | 初步行过滤,减少数据量 | 对分组结果的二次过滤 |
3.3 各种 JOIN 的区别
以 A、B 两张表为例:
- INNER JOIN:只返回两表匹配的行。
- LEFT JOIN:返回左表全部行,右表无匹配则为 NULL。
- RIGHT JOIN:返回右表全部行,左表无匹配则为 NULL。
- FULL OUTER JOIN:返回两表所有行,无匹配的一侧补 NULL(MySQL 不直接支持,可用 UNION 模拟)。
- CROSS JOIN:笛卡尔积,返回两表所有组合。
-- 查询所有用户及其订单(无订单的用户也显示) SELECT u.id, u.name, o.order_no FROM user u LEFT JOIN orders o ON u.id = o.user_id; -- 查询没有下过单的用户 SELECT u.* FROM user u LEFT JOIN orders o ON u.id = o.user_id WHERE o.id IS NULL;3.4 UNION 和 UNION ALL 的区别
UNION会对结果集去重并排序;UNION ALL不去重、不排序,直接把两个结果集拼接,性能更高。如果能确认结果没有重复,应优先使用 UNION ALL。另外 UNION 要求两个查询的列数相同、对应列类型兼容。
3.5 什么是子查询?相关子查询和非相关子查询的区别
子查询是嵌套在另一个查询中的 SELECT。按是否依赖外层查询可分为:
- 非相关子查询:内层查询独立执行一次,结果供外层使用。例如
SELECT * FROM t WHERE id IN (SELECT id FROM t2 WHERE status=1)。 - 相关子查询:内层查询引用外层查询的列,外层每处理一行,内层都要重新执行一次。例如 EXISTS 关联查询。
相关子查询可能带来性能问题,实际开发中常改写为 JOIN 来优化。
3.6 COUNT(1)、COUNT(*) 和 COUNT(列名) 的区别
COUNT(*)和COUNT(1)都是统计所有行的数量,在 MySQL 的 InnoDB 引擎中二者性能基本没有差别。COUNT(列名)只统计该列非 NULL的值的个数。若要判断「某列有多少个非空值」,只能用 COUNT(列名)。
3.7 什么是窗口函数?常见的窗口函数有哪些
窗口函数(Window Function)可以在不改变结果集行数的情况下,对某一「窗口」内的行进行聚合计算,从而同时保留明细和汇总信息。相比 GROUP BY,它不会把多行压缩成一行,非常适合做排名、移动平均、累计求和等分析。
-- 按部门对员工薪资排名 SELECT name, department, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn, RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rk, DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dr FROM employee;常见窗口函数:ROW_NUMBER(不重复编号)、RANK(并列跳号)、DENSE_RANK(并列不跳号)、SUM/AVG/COUNT(聚合)、LEAD/LAG(前后行)等。关键语法是OVER (PARTITION BY ... ORDER BY ...)。
3.8 TRUNCATE、DELETE 和 DROP 的区别
| 操作 | 类型 | 是否可回滚 | 能否带 WHERE | 自增值 | 速度 |
|---|---|---|---|---|---|
| DELETE | DML | 可回滚(事务内) | 能 | 不重置 | 慢,逐行删 |
| TRUNCATE | DDL | 不可回滚 | 不能 | 重置 | 快,整表释放 |
| DROP | DDL | 不可回滚 | 不能 | - | 最快,删除表结构 |
TRUNCATE 本质是删除整张表再重建结构,所以速度很快且不可回滚;DELETE 是逐行删除,属于 DML,可以在事务中回滚。
四、索引原理与优化高频题
4.1 什么是索引?为什么能提升查询效率
索引是一种用于快速查找的数据结构,相当于书的目录。没有索引时,数据库需要全表扫描;建立索引后,可以通过索引快速定位到目标数据,把时间复杂度从 O(n) 降低到接近 O(log n)。
索引虽然加快查询,但也有代价:占用额外磁盘空间、降低写入性能(每次 INSERT/UPDATE/DELETE 都要同步维护索引)。因此索引并非越多越好。
4.2 B 树和 B+ 树的区别?为什么 MySQL 用 B+ 树
B 树的每个节点既存储键也存储数据,查找可能在非叶子节点就命中;B+ 树的所有数据都存储在叶子节点,非叶子节点只存键和指针,并且叶子节点之间通过双向链表连接,形成一个有序链表。
MySQL 的 InnoDB 选择 B+ 树,核心原因有三个:
- 更矮更胖:非叶子节点不存数据,单个节点能容纳更多键,树高更低,磁盘 IO 次数更少。
- 范围查询友好:叶子节点有序且链表相连,范围扫描、排序非常高效。
- 查询性能稳定:任何查找都必须到叶子节点,查询路径长度一致。
4.3 聚簇索引和非聚簇索引的区别
| 对比项 | 聚簇索引(聚集索引) | 非聚簇索引(二级索引) |
|---|---|---|
| 数据存储 | 叶子节点直接存整行数据 | 叶子节点存索引键 + 主键值 |
| 数量 | 一张表只能有一个 | 一张表可以有多个 |
| 默认选择 | InnoDB 默认用主键 | 除主键外的其他索引 |
| 查找流程 | 找到叶子即拿到数据 | 找到主键后还需回表 |
在 InnoDB 中,如果没有显式定义主键,会优先选择第一个非空唯一索引;若都没有,则生成隐藏的 row_id 作为聚簇索引键。二级索引查找时,先定位到主键,再通过主键回表查询整行,这就是回表。
4.4 什么是回表?如何避免回表
回表是指通过二级索引查到主键后,还需要回到聚簇索引中查询完整行数据的过程。回表会带来额外的磁盘 IO,可以用覆盖索引(Covering Index)来避免:即查询所需的所有列恰好都在索引中,引擎直接返回结果,无需回表。
-- 假设存在联合索引 idx(user_id, status, create_time) -- 这条 SQL 需要回表,因为查询了索引外的列 total_amount SELECT user_id, status, create_time, total_amount FROM orders WHERE user_id = 123; -- 只查索引中的列,触发覆盖索引,不用回表 SELECT user_id, status, create_time FROM orders WHERE user_id = 123;4.5 最左前缀原则是什么
对于联合索引idx(a, b, c),其索引结构是按 a、b、c 的顺序依次排序的。查询时只有从最左边列开始、且中间不能跳过,才能命中索引:
- 能命中:
WHERE a=1、WHERE a=1 AND b=2、WHERE a=1 AND b=2 AND c=3。 - 部分命中:
WHERE a=1 AND c=3(只能用 a,c 无法利用)。 - 无法命中:
WHERE b=2、WHERE c=3(未从最左边开始)。
因此设计联合索引时,应把区分度高、查询频繁且常在等值条件中的列放在前面。
4.6 索引失效的常见场景
- 对索引列使用函数或运算:
WHERE DATE(create_time) = '2026-10-07'会导致索引失效,应改写为范围条件。 - 隐式类型转换:字符串列与数字比较,如
WHERE phone = 13800138000,会被隐式转换导致索引失效,应写成WHERE phone = '13800138000'。 - 前导模糊查询:
LIKE '%keyword'无法命中索引,LIKE 'keyword%'才能正常使用索引。 - 违反最左前缀原则:联合索引查询时跳过最左边列,无法整体命中索引。
- 使用负向条件:
NOT IN、!=、<>通常导致索引失效。 - 使用 OR 连接非索引列:OR 两侧只要有一列未建索引,就可能退化为全表扫描。
- 对索引列判断 NULL:
IS NULL或IS NOT NULL在某些场景下会影响索引使用。
需要说明的是,索引失效并非绝对,最终是否走索引取决于优化器的成本估算。排查时应结合EXPLAIN观察type、key和rows,不要只凭经验判断。
五、事务与锁高频题
5.1 什么是事务?ACID 特性分别是什么
事务是一组不可分割的数据库操作集合,要么全部成功提交,要么全部回滚。事务的 ACID 特性包括:
- 原子性(Atomicity):事务内的操作要么全部成功,要么全部失败,依靠 Undo Log 实现。
- 一致性(Consistency):事务执行前后,数据都处于合法的约束状态,由数据库约束、事务机制共同保证。
- 隔离性(Isolation):多个事务并发执行时互不干扰,依靠锁和 MVCC 实现。
- 持久性(Durability):事务一旦提交,修改会永久保存,依靠 Redo Log 实现。
5.2 MySQL 的事务隔离级别有哪些?分别解决什么问题
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交(Read Uncommitted) | 可能 | 可能 | 可能 |
| 读已提交(Read Committed) | 解决 | 可能 | 可能 |
| 可重复读(Repeatable Read) | 解决 | 解决 | 大部分解决 |
| 串行化(Serializable) | 解决 | 解决 | 解决 |
其中三类并发问题的含义是:脏读是读到其他事务未提交的数据;不可重复读是同一事务内两次读取同一行数据结果不同;幻读是同一事务内范围查询的结果行数发生变化。MySQL 默认隔离级别为可重复读,通过 MVCC 加间隙锁,在多数场景下也能避免幻读。
5.3 MVCC 是什么?如何实现
MVCC(多版本并发控制)通过保存数据的历史版本,让读操作不加锁也能读到一致的数据,从而提升并发性能。其实现依赖 InnoDB 隐藏列(如trx_id和roll_pointer)、Undo Log 版本链以及 ReadView 可见性判断。
普通 SELECT 是快照读,读取创建 ReadView 时可见的版本;SELECT ... FOR UPDATE和UPDATE等则是当前读,读取最新数据并加锁。理解这一区分,是解释隔离级别与并发控制的关键。
5.4 什么是死锁?如何避免和定位
死锁是指两个或多个事务相互持有对方需要的锁,又都在等待对方释放,从而陷入永久等待。InnoDB 会主动检测死锁并回滚其中一个事务,使其不真正卡死。
减少死锁的实践建议:
- 固定加锁顺序:多事务访问相同资源时保持一致的顺序。
- 缩小事务范围:减少事务持有锁的时间。
- 合理建立索引:避免因为无索引而锁住过多行。
定位方式可通过SHOW ENGINE INNODB STATUS查看最近一次死锁信息,或开启innodb_print_all_deadlocks把死锁日志输出到错误日志。
六、MySQL 架构与日志高频题
6.1 MySQL 的整体架构是怎样的
MySQL 主要分为 Server 层和存储引擎层。Server 层负责连接、解析、优化和执行,包括连接器、查询缓存、分析器、优化器和执行器;存储引擎层负责数据的存储和读取,常用 InnoDB,也支持 MyISAM、Memory 等,插件式设计使其可替换。
6.2 redo log、undo log 和 binlog 有什么区别
| 日志 | 层次 | 类型 | 主要作用 |
|---|---|---|---|
| redo log | InnoDB 引擎层 | 物理日志 | 崩溃恢复,保证持久性 |
| undo log | InnoDB 引擎层 | 逻辑日志 | 事务回滚与 MVCC |
| binlog | Server 层 | 逻辑日志 | 主从复制与数据恢复 |
redo log 采用 WAL(Write-Ahead Logging)机制,先写日志再写盘,保证事务提交后即使宕机也能恢复;binlog 则是 MySQL 自身用于复制和恢复的日志,二者共同构成两阶段提交的经典组合。
6.3 InnoDB 的 Buffer Pool 有什么作用
Buffer Pool 是 InnoDB 在内存中缓存数据页和索引页的区域,能显著减少磁盘 IO。它通过类似 LRU 的算法管理页淘汰,并结合 Change Buffer 优化二级索引的写入。合理设置innodb_buffer_pool_size,使其约为物理内存的 50%-70%,是常见的调优起点。
七、SQL 优化与慢查询高频题
7.1 如何定位慢查询
首先开启慢查询日志并设置阈值:slow_query_log=ON与long_query_time=1。然后借助mysqldumpslow或pt-query-digest分析日志,找到执行次数多、耗时高的 SQL,再结合 EXPLAIN 进一步分析。
7.2 EXPLAIN 中常见的字段怎么看
EXPLAIN 是 SQL 优化的核心工具,关键字段如下:
- type:访问类型,性能从好到差依次为
system、const、eq_ref、ref、range、index、ALL。 - key:实际使用的索引,为 NULL 说明可能未走索引。
- rows:预估扫描行数,值越大越需要关注。
- Extra:
Using index表示覆盖索引,Using filesort和Using temporary通常需要优化。
7.3 分库分表有哪些常见方案
当单表数据量过大时,可进行拆分:垂直拆分是按业务或字段拆分,水平拆分是按分片键把同一张表数据分散到多个库表。拆分后需要处理跨库 Join、分布式事务和全局唯一 ID 等问题,因此应先考虑索引优化、缓存和读写分离,确实无法支撑再分库分表。
八、Redis 高频题
8.1 Redis 常见数据类型及应用场景
常见类型包括:
- String:缓存、计数器、分布式锁。
- Hash:对象属性存储、购物车。
- List:消息队列、最新列表。
- Set:去重、共同好友、标签。
- ZSet:排行榜、延迟队列。
- Bitmap、HyperLogLog、GEO、Stream:分别用于签到统计、基数估算、地理位置和消息流。
8.2 RDB 和 AOF 持久化的区别
| 对比项 | RDB | AOF |
|---|---|---|
| 机制 | 定时生成内存快照 | 追加记录每次写命令 |
| 文件体积 | 较小,压缩快照 | 通常较大 |
| 恢复速度 | 快 | 需重放命令,较慢 |
| 数据完整性 | 可能丢失最近一次快照后的数据 | 取决于刷盘策略,丢失更少 |
生产环境通常两者同时开启,利用 RDB 快速恢复、AOF 保证完整性;Redis 也支持混合持久化,在 AOF 重写时写入 RDB 格式提高效率。
8.3 缓存穿透、缓存击穿和缓存雪崩
缓存穿透是查询一个数据库和缓存都不存在的数据,攻击者可借此绕过缓存,可用布隆过滤器和缓存空值应对。缓存击穿是某个热点 key 过期瞬间大量请求打向数据库,可通过互斥锁或逻辑过期解决。缓存雪崩是大量 key 同时过期或 Redis 整体不可用,可通过给过期时间增加随机值、使用高可用集群等方式缓解。
8.4 Redis 如何实现分布式锁
核心命令是SET key value NX EX seconds,利用 NX 保证互斥、EX 防止死锁。释放锁时必须校验持有者,并用 Lua 脚本保证「判断 + 删除」的原子性。生产环境常用 Redisson 的看门狗机制自动续期,避免业务超时导致锁提前释放。
九、总结与复习建议
数据库面试题看似零散,其实主线清晰:基础概念建立认知,SQL 语法体现动手能力,索引和事务是区分度最高的部分,MySQL 架构日志与 Redis 则考察工程实践深度。建议复习时做到三个「能」:
- 能画图:B+ 树、聚簇索引、MVCC、主从复制等都能手绘结构。
- 能写 SQL:JOIN、窗口函数、EXPLAIN 都能熟练书写。
- 能讲清为什么:不仅记住结论,还能说明背后的设计取舍和适用场景。
最后建议结合真实慢查询案例做一次完整优化复盘,从发现问题、分析执行计划到索引调优,形成自己的面试表达闭环。