Skip to main content

关系型数据库(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 INIS 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 UPDATEUPDATEDELETE)会锁住记录及其间隙,阻止其他事务插入,从而解决幻读。

  • 粒度:表锁(开销小、并发低)、行锁(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 优化思路

  1. EXPLAIN 分析执行计划,重点看 type(至少到 range,避免 ALL 全表扫描)、key(是否命中索引)、rowsExtra(避免 Using filesort/Using temporary)。
  2. 加合适的索引、用覆盖索引避免回表。
  3. 避免 SELECT *,只查需要的列。
  4. 大分页优化:LIMIT 100000, 10 很慢 → 用 WHERE id > ? LIMIT 10(游标分页)或延迟关联。
  5. 大表分库分表、读写分离。

Node.js 中的实践

  • 连接池:每次新建连接成本高,用连接池复用(mysql2createPool)。
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 BuilderPrisma(TS 友好、类型安全,新项目首选)、TypeORMSequelizeKnex。优点是开发效率高、防注入;缺点是复杂查询性能不如手写 SQL,要会看它生成的 SQL。

面试题:N+1 查询问题是什么? ORM 中查列表后又对每条记录单独查关联数据,导致 1 次主查询 + N 次关联查询。解决:用 JOINinclude/populate 预加载,或批量 IN 查询。