面试题精讲:数据库

开篇:数据库是后端面试的「敲门砖」

Python 后端开发(无论是 Flask/Django 写 Web 服务、还是爬虫接口对接存储)都完全离不开数据库,据不完全统计,硬核数据库面试题能占到后端面经的 30%-40%。

这篇整理了 7 个最常考、写代码时也最容易踩坑的核心知识点,带点实战视角,不堆干巴巴的定义:


1. 数据库事务 (Transaction)

面试必背 + 实战避坑双项选择

核心就是背 ACID 四特性,但别只会念,要加一句「在 Python 中用 ORM(比如 Django ORM、SQLAlchemy)开启事务更安全,避免手动漏提交/回滚」。

四特性拆解(人话版)

特性解释实战举栗
A (Atomicity) 原子性事务是「不可拆分的包裹」,要么里面所有 SQL 全执行,要么全像没发生过一样 回滚用银行卡转账:A减100→B加100,任何一步失败(比如B卡异常),A的钱必须回到账上
C (Consistency) 一致性事务执行前后,数据逻辑得合理(不是技术状态,是业务/约束状态)还是转账:A和B的总余额必须不变
I (Isolation) 隔离性多个并发事务同时操作同一条数据时,要互不干扰(后面会讲 MVCC 就是解决这个的 InnoDB 核心)假设A正在转100给B,这时候C查A的余额,要么看到转前的,要么看到转后的,不能看到「转了一半没提交」的临时值
D (Durability) 持久性一旦事务显式提交(不是临时保存那种),数据就会永久存在磁盘里,重启数据库、断电都不会丢

2. 数据库索引与 B+ Tree

灵魂拷问:为什么 MySQL InnoDB 首选 B+ Tree?

二叉树/B-Tree/哈希索引都不行,核心原因围绕「数据库是存磁盘的,磁盘IO是性能杀手」展开。

先踩踩其他索引的坑

  1. 二叉搜索树/BST:极端情况(比如按顺序存1-10000)会退化成链表,查询时间复杂度O(n)
  2. 红黑树:虽然是平衡的,但高度太高(存10000条数据要14层左右),磁盘IO次数多
  3. B-Tree:内部节点也存数据,每个节点能装的索引少,树还是不够矮胖;范围查询要跨层遍历,慢
  4. 哈希索引:只适合等值查询(WHERE id=1),范围、排序、模糊查询全跪

B+ Tree 的三大杀招(对应 MySQL 高频考点)

// 简化版 B+ Tree 结构(3阶)
          [ 5, 9 ]          → 内部节点(只存索引,不存数据!)
        /   |    \
   [1,3] → [5,7,9] → [10,12]  → 叶子节点(有序存全量索引 + 行数据指针/主键值)
                             ↖双向链表↗
  1. 磁盘IO代价极低:内部节点全是空壳索引,一个磁盘页(InnoDB 默认 16KB)能装成百上千个索引,树矮胖到只有3-5层,1次查询最多5次磁盘IO
  2. 查询效率绝对稳定:所有数据都在叶子节点,从根到任意叶子的路径长度完全一致
  3. 范围/排序查询吊打一切:叶子节点用双向链表串起来了,直接扫链表就行,不用跳回根节点重查

聚集索引 vs 非聚集索引(Python 开发常搞混回表!)

# 伪代码模拟 SQLAlchemy 定义
class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)  # 自动生成聚集索引(InnoDB 必须有)
    name = Column(String(20), index=True)   # 普通非聚集索引
    age = Column(Integer)

# 1. 走聚集索引(主键查询,最快)
# SELECT * FROM users WHERE id=1;
# → 直接在聚集索引的叶子节点找到行数据,1步到位

# 2. 走非聚集索引(回表查询,稍慢)
# SELECT * FROM users WHERE name="张三";
# → 先在 name 索引的叶子节点找到主键值 1
# → 再拿着主键值 1 去聚集索引查行数据,2步到位

3. Redis 核心原理

灵魂拷问三连:单线程为什么快?数据结构用什么?怎么持久化不丢数据?

Redis 是 Python 后端缓存三剑客(Redis/Memcached/本地缓存)的绝对老大,面试必考!

单线程为什么能吊打多线程的 Memcached?

别再说「Redis 完全是单线程」了——Redis 6.0+ 后台任务(持久化、清理过期键)是多线程的,但命令处理的核心(内存操作+IO多路复用)还是单线程,这是它快的根本:

  1. 纯内存操作:读写都是在内存里搞,比磁盘IO快10万倍以上
  2. IO 多路复用(epoll/kqueue):一个线程就能监听成千上万个客户端连接,不用每个连接开一个线程(避免了上下文切换和锁竞争)
  3. 命令处理的核心无锁:单线程操作内存,根本不需要加锁解锁,效率飙升

高频数据结构(别只会说 String!)

Redis 命令对应底层结构Python 开发常用场景
SET/GET简单动态字符串(SDS)存 Token、缓存热门文章、分布式锁
HSET/HGET/HGETALL压缩列表/哈希表存用户信息、购物车
LPUSH/RPUSH/LPOP/RPOP压缩列表/双向链表消息队列、实时排行榜(仅限短列表)
SADD/SISMEMBER/SINTER整数集合/哈希表去重、共同关注、抽奖用户池
ZADD/ZRANGE/ZREVRANGE压缩列表/跳表实时热榜、延迟队列

持久化策略(RDB vs AOF vs 混合,主流选混合!)

纯内存操作有个致命缺点:断电/重启数据全丢,所以必须持久化到磁盘:

  1. RDB(快照):定时(比如 save 900 1)把内存里的所有数据全量压缩成二进制文件存到磁盘
    • ✅ 恢复极快(适合备份/迁移)
    • ❌ 容易丢数据(最多丢 save 间隔内的所有数据)
  2. AOF(追加日志):把每个写命令逐行追加到文本文件里(类似 MySQL 的 Binlog)
    • ✅ 数据安全性极高(配置 appendfsync always 几乎不丢)
    • ❌ 文件越来越大、恢复越来越慢
  3. 混合持久化(Redis 4.0+,Python 后端默认建议开):RDB 头(全量) + AOF 尾(增量),既有 RDB 恢复快的优点,又有 AOF 数据安全的优点

4. 悲观锁 vs 乐观锁

场景为王:读多写少选乐观,写多读少选悲观!

Python 开发经常会遇到「多个人同时改同一条库存」的问题,这时候就要加锁了。

悲观锁(先锁后用,稳但慢)

-- MySQL InnoDB 语法,Python ORM 也有对应的方法
BEGIN;
-- 先给要改的库存行加排他锁(FOR UPDATE)
SELECT stock FROM products WHERE id=1 FOR UPDATE;
-- 业务逻辑:减库存
UPDATE products SET stock=stock-1 WHERE id=1;
COMMIT;
  • ✅ 绝对不会出现超卖
  • ❌ 锁的时间长,并发性能差(只有拿到锁的人能操作)
  • 🎯 适用场景:抢票、秒杀(写多读少、并发竞争极其激烈)

乐观锁(假设没人抢,提交时再检查,快但有冲突)

-- 常用的版本号(Version)机制
BEGIN;
-- 先查库存和当前版本号
SELECT stock, version FROM products WHERE id=1;
-- 业务逻辑:假设查出来的 version=5,减1
UPDATE products SET stock=stock-1, version=version+1 WHERE id=1 AND version=5;
-- 检查受影响的行数:如果是0,说明版本号不对,冲突了,Python 里可以重试几次
COMMIT;
  • ✅ 不加锁,并发性能好
  • ❌ 冲突率高的时候需要频繁重试,反而更慢
  • 🎯 适用场景:修改用户信息、点赞(读多写少、冲突概率低)

5. MVCC (多版本并发控制)

InnoDB 的秘密武器:解决「读-写」冲突的非阻塞读!

如果只有锁,「读-写」操作也会互相阻塞,并发性能会很差,MVCC 就是为了解决这个问题的。

人话版原理

InnoDB 给每行数据加了两个隐式列(不需要手动建):

  1. 创建时间戳:记录创建/修改这行数据的事务 ID
  2. 删除时间戳:记录删除这行数据的事务 ID(如果没删除就是 NULL)

同时用 Undo Log(回滚日志) 存了数据的历史版本链表

两种读操作(Python 开发要注意!)

  1. 快照读(普通 SELECT,99% 的情况用的是这个):不加锁,读的是 Undo Log 里的历史版本,不会阻塞写操作,也不会被写操作阻塞
    SELECT * FROM users WHERE id=1; -- 快照读
  2. 当前读(INSERT/UPDATE/DELETE,或者加了锁的 SELECT):读的是最新版本,需要加锁
    SELECT * FROM users WHERE id=1 FOR UPDATE; -- 排他锁的当前读
    SELECT * FROM users WHERE id=1 LOCK IN SHARE MODE; -- 共享锁的当前读

6. 存储引擎:MyISAM vs InnoDB

Python 后端直接无脑选 InnoDB!除非…

MySQL 5.5 之后默认就是 InnoDB 了,但 MyISAM 偶尔还会在老项目里看到,背清楚对比表就行。

特性MyISAMInnoDB
事务支持❌ 不支持必须支持
锁颗粒度表锁(写性能差,读性能还行)行锁(并发高)
外键约束❌ 不支持支持
崩溃恢复❌ 很差(容易丢数据)强(利用 Redo Log+Undo Log)
count(*) 速度✅ 极快(内置了一个计数器)❌ 慢(需要全表/索引扫描)

7. Elasticsearch (ES) 搜索深度解析

当 MySQL 的模糊搜索慢到爆炸时,试试 ES!

ES 是基于 Lucene 的分布式全文检索服务器,Python 开发常用它做电商商品搜索、日志分析、实时监控。

核心杀招:倒排索引 (Inverted Index)

MySQL 的模糊搜索(LIKE "%手机%")需要全表扫描,ES 靠倒排索引把搜索速度提升了几个数量级:

// 简化版正排索引 vs 倒排索引
【正排索引】:文档ID → 内容(单词列表)
1 → ["苹果", "手机", "新款"]
2 → ["华为", "手机", "Mate60"]
3 → ["苹果", "平板", "Pro"]

【倒排索引】:单词 → 文档ID列表(有序)
苹果 → [1, 3]
手机 → [1, 2]
新款 → [1]
华为 → [2]
Mate60 → [2]
平板 → [3]
Pro → [3]

比如搜索「苹果手机」,直接查倒排索引里的「苹果」和「手机」的文档ID列表,取交集([1])就行,1秒都不到。


🚀 后续避坑建议

这一章跳了 Redis 和 ES,没讲太细。在 Python 后端面试中,Redis 的缓存雪崩、穿透、击穿是「必考题中的必考题」,我可以单独给你写一篇「Redis 三兄弟避坑指南」,附 Python 代码示例,要不要?