关系型数据库(MySQL)
全栈面试里,数据库(尤其是 MySQL 索引、事务、隔离级别)几乎必问,重要程度不亚于 JS。
索引
面试题:为什么用 B+ 树做索引,而不是 B 树 / 红黑树 / 哈希?
- 红黑树/二叉树:树高随数据量增长很快,磁盘 IO 次数多(每下探一层就是一次磁盘 IO)。
- 哈希索引:等值查询 O(1) 很快,但不支持范围查询、排序、最左前缀匹配。
- B 树:每个节点都存数据,单节点能存的 key 变少,树更高。
- B+ 树:非叶子节点只存索引(不存数据),单页能存更多 key → 树更矮(通常 3 层就能存千万级数据,最多 3 次 IO);叶子节点用双向链表串联,天然支持范围查询和排序。
关键概念
- 聚簇索引(主键索引):InnoDB 中叶子节点直接存整行数据。一张表只有一个。
- 二级索引(非主键索引):叶子节点存的是主键值,查完还要拿主键回主键索引再查一次 → 回表。
- 覆盖索引:查询的列都在索引里,无需回表(
SELECT id, name FROM t WHERE name=?,name上有索引且只查这两列)。是 SQL 优化的重要手段。 - 联合索引与最左前缀原则:
(a, b, c)索引,只有从最左列开始连续匹配才生效。WHERE a=1 AND b=2走索引;WHERE b=2不走。 - 索引下推(ICP):MySQL 5.6+,在存储引擎层用索引中已有的列先过滤,减少回表次数。
面试题:哪些情况会导致索引失效?
- 对索引列做运算或函数:
WHERE YEAR(create_time) = 2024- 类型隐式转换:
phone是 varchar 却WHERE phone = 138...(数字)- 模糊查询以
%开头:LIKE '%abc'- 联合索引未遵循最左前缀
OR连接的条件中有列没索引- 使用
!=、NOT IN、IS NOT NULL可能导致优化器放弃索引(视情况)
事务与隔离级别
面试题:ACID 是什么?
- A 原子性(Atomicity):事务内操作要么全成功要么全回滚。由
undo log实现。- C 一致性(Consistency):事务前后数据完整性约束不被破坏(其他三个特性的最终目的)。
- I 隔离性(Isolation):并发事务互不干扰。由锁 + MVCC 实现。
- D 持久性(Durability):提交后数据永久保存。由
redo log实现。
并发会引发三类问题,隔离级别就是用来权衡「隔离性 vs 性能」的:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 Read Uncommitted | 可能 | 可能 | 可能 |
| 读已提交 Read Committed(Oracle 默认) | 解决 | 可能 | 可能 |
| 可重复读 Repeatable Read(MySQL 默认) | 解决 | 解决 | 基本解决 |
| 串行化 Serializable | 解决 | 解决 | 解决 |
- 脏读:读到了别的事务未提交的数据。
- 不可重复读:同一事务内两次读同一行,结果不同(别的事务 update 并提交了)。
- 幻读:同一事务内两次范围查询,行数变了(别的事务 insert 了)。
面试题:MySQL 的可重复读是怎么实现的?为什么能基本解决幻读?
- MVCC(多版本并发控制):通过
undo log版本链 + ReadView 实现「快照读」,普通SELECT读的是事务开始时的一致性快照,避免不可重复读。- 间隙锁 Next-Key Lock:当前读(
SELECT ... FOR UPDATE、UPDATE、DELETE)会锁住记录及其间隙,阻止其他事务插入,从而解决幻读。
锁
- 粒度:表锁(开销小、并发低)、行锁(InnoDB 支持,开销大、并发高)。
- 行锁类型:记录锁(Record Lock)、间隙锁(Gap Lock)、临键锁(Next-Key Lock = 记录锁 + 间隙锁)。
- 共享锁 S / 排他锁 X:读锁可共享,写锁互斥。
- 乐观锁 vs 悲观锁:
- 悲观锁:
SELECT ... FOR UPDATE,假设一定会冲突,先加锁。 - 乐观锁:加
version字段,更新时WHERE version = ?,冲突则重试。适合读多写少。
- 悲观锁:
- 死锁:两个事务互相等待对方持有的锁。InnoDB 会检测并回滚代价小的事务。
redo log / undo log / binlog
| 日志 | 作用 | 层级 |
|---|---|---|
| redo log | 崩溃恢复(持久性),物理日志,循环写 | InnoDB 引擎层 |
| undo log | 回滚(原子性)+ MVCC 版本链 | InnoDB 引擎层 |
| binlog | 主从复制、数据恢复,逻辑日志,追加写 | MySQL Server 层 |
两阶段提交:更新时先写 redo log(prepare)→ 写 binlog → 提交 redo log(commit),保证两个日志的一致性。
SQL 优化思路
EXPLAIN分析执行计划,重点看type(至少到range,避免ALL全表扫描)、key(是否命中索引)、rows、Extra(避免Using filesort/Using temporary)。- 加合适的索引、用覆盖索引避免回表。
- 避免
SELECT *,只查需要的列。 - 大分页优化:
LIMIT 100000, 10很慢 → 用WHERE id > ? LIMIT 10(游标分页)或延迟关联。 - 大表分库分表、读写分离。
Node.js 中的实践
- 连接池:每次新建连接成本高,用连接池复用(
mysql2的createPool)。
const mysql = require("mysql2/promise");
const pool = mysql.createPool({
host: "localhost",
user: "root",
database: "test",
waitForConnections: true,
connectionLimit: 10, // 最大连接数
queueLimit: 0,
});
// 用参数化查询防止 SQL 注入(不要手动拼接字符串)
const [rows] = await pool.query("SELECT * FROM user WHERE id = ?", [userId]);
- ORM / Query Builder:
Prisma(TS 友好、类型安全,新项目首选)、TypeORM、Sequelize、Knex。优点是开发效率高、防注入;缺点是复杂查询性能不如手写 SQL,要会看它生成的 SQL。
面试题:N+1 查询问题是什么? ORM 中查列表后又对每条记录单独查关联数据,导致 1 次主查询 + N 次关联查询。解决:用
JOIN、include/populate预加载,或批量IN查询。