MySQL 面试总结

4 阅读12分钟

MySQL 的逻辑架构

可以把 MySQL 想象成一家餐厅:

  1. 连接器 (Connectors): 迎宾员。负责跟客户端建立连接、获取权限。
  2. 解析器 (Parser): 报单员。分析你的 SQL 语法对不对,识别出表名、字段名。
  3. 优化器 (Optimizer): 厨师长(核心)。决定用哪个索引,按什么顺序连表。它不一定选最快的,但会选它认为代价最小的。
  4. 执行器 (Executor): 服务员。调用存储引擎的接口,把数据取出来返回给你。
  5. 存储引擎 (Storage Engines): 仓库管理员。MySQL 最牛的地方是插件式存储引擎,最常用的是 InnoDB

索引

没有索引,必须全表扫描(Full Table Scan);有了索引,可以通过索引寻址(Index Seek),速度极快。

数据库中的索引

在 MySQL 中,索引其实是一堆独立的数据结构(通常是 B+ 树)。它把表里某个字段(比如 nameid)拿出来,排好序,单独存一个文件。

索引的核心就是“排序”。因为排好了序,数据库就可以用二分查找法。

索引的代价

  • 占空间: 索引也是要存硬盘的。索引太多,数据库文件会变得非常臃肿。

  • 慢了“写”操作: 当你往表里 INSERT(新增)或 UPDATE(修改)一条数据时,MySQL 不仅要改原始数据,还要重新维护和排序那一堆索引

同步维护索引树,会带来一定的性能开销。

何时使用索引

我们通常只在经常出现在 WHERE 条件、ORDER BY 排序或 JOIN 连接的字段上建立索引。

经常被查询排序,且更新频率低字段长度要小的字段需要索引。

何时不适用索引

  • 数据量太小: 几十行数据,全表扫描比读索引文件还快。
  • 经常更新的字段: 每次改数据都要重新调整 B+ 树,开销很大。
  • 区分度低的字段: 比如“性别”。B+ 树里一半是男一半是女,这目录跟没写一样。

常见的几种索引

  • 主键索引 (Primary Key): 书的正文页码,唯一的,不能重复,不能为空。
  • 唯一索引 (Unique Index): 比如身份证号,不能重复。
  • 普通索引 (Normal Index): 最基本的,为了加速查询。
  • 复合索引 (Composite Index): 比如你按“省份 + 城市”一起找。注意: 这里有个“最左匹配原则”,即你查“省份”能用到索引,但只查“城市”用不到这个索引(就像目录是先按省排再按市排,你直接跳过省找市就乱了)。

DRF 中 定义索引

单字段索引

  • db_index=True
  • unique=True:也会自动创建唯一索引
  • models.ForeignKey:外键字段自动加上索引

复核索引

经常同时根据两个字段查询,你需要复合索引

class Article(models.Model):
    category = models.CharField(max_length=50)
    is_published = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        indexes = [
            # 复合索引:加速同时过滤 category 和 is_published 的查询
            models.Index(fields=['category', 'is_published'], name='cat_pub_idx'),
        ]

B+树

  • B 树很“矮胖”: 一个节点可以存上百个索引。100 万个数据,B 树可能只需要 3 层。这意味着你只需要读 3 次硬盘就能找到数据。
  • B+ 树的非叶子节点只存索引,不存真实数据,这样每一层能装下的索引更多,树更矮。
  • B+ 树的所有叶子节点是连在一起的(双向链表)。如果你查 WHERE age > 20,它找到 20 后,顺着链表往后一拉就全出来了,不需要再回过头去爬树。

从上面的特点可以得知:

  • 最左匹配原则: 如果你建了复合索引 (name, age),B+ 树是先按 name 排序,再在 name 一样的情况下按 age 排序。如果你查询时跳过 name 只查 age,这棵树就没法用了,因为 age 在全局看是乱序的。
  • 范围查询: 为什么 >< 可以用索引,但 LIKE '%keyword'(前缀模糊查询)不能用?因为前缀模糊会导致你没法在有序的树里做二分定位,只能苦逼地全表扫描。

日志

在 MySQL 中,日志不只是为了记录错误,它们是数据恢复、主从复制以及性能优化的核心。

MySQL 的四大日志

为了显得专业,你明天可以顺带提一下这几个日志,证明你对数据库底层有全局观:

  • 错误日志 (Error Log): 记录 MySQL 启动、运行或停止时出现的问题。打不开数据库先看它。
  • 查询日志 (General Query Log): 记录建立的客户端连接和执行的所有 SQL。一般不开启,因为记录每一条 SQL 太耗性能。
  • 二进制日志 (Binlog): 极其重要! 记录所有修改数据的语句。它是做数据恢复主从复制(读写分离) 的基础。
  • 慢查询日志 (Slow Query Log): 优化的起点。

慢查询日志

慢查询日志就像一个“监控摄像头”,它专门盯着那些执行时间超过你设定阈值(比如 1 秒)的 SQL 语句。

  • 发现问题: 开启慢查询日志。MySQL 会把所有跑得慢的 SQL 记录在一个文件里。
  • 分析问题: 你打开日志,看到某条 SELECT ... JOIN ... 跑了 3 秒,扫描了 100 万行数据,却只返回了 10 行。
  • 解决问题: 针对这条具体的 SQL,使用 EXPLAIN 命令看它为什么慢(比如没走索引),然后去加索引或改写 SQL。

配置慢查询日志

  • slow_query_log:开关(ON/OFF)。
  • long_query_time:阈值(比如设为 1,表示执行超过 1 秒的就记录)。
  • log_queries_not_using_indexes:进阶配置。如果设为 ON,即使 SQL 跑得很快,但只要没用索引,也会被记录下来。这对早期优化非常有帮助!

拿到日志后怎么看

可视化工具:比如 Navicat 或阿里云的数据库控制台,都有现成的图表。

一个接口响应很慢,作为后端开发会怎么排查?

  • 先看是不是网络或业务逻辑代码的问题。
  • 如果确定是数据库慢,我会查看 MySQL 的慢查询日志 (Slow Query Log),找出具体的慢 SQL。
  • 拿到 SQL 后,我会观察它的执行时间以及扫描的行数 (Rows_examined)。
  • 会使用 EXPLAIN 命令查看执行计划,重点关注 type(连接类型)和 key(是否命中索引)这两个字段。
  • 通常通过增加合适的索引、优化复合索引,能解决大部分问题。

EXPLAIN 命令

EXPLAIN 命令 是后端开发在优化数据库时最核心、最常用的工具。

EXPLAIN 是一个命令前缀。你只需要在任何 SELECT 语句前面加上这个词,MySQL 就会告诉你:它是打算如何执行这条查询的

type(连接类型)

  • system / const:查主键或唯一索引,只有一行数据。最快。
  • eq_ref / ref:Join 查询或普通索引查询。非常快。
  • range:范围查询(比如 age > 20)。只要有索引,也可以接受。
  • index:扫描了整棵索引树(虽然比全表快,但也慢)。
  • ALL:全表扫描。必须优化!

key(实际使用的索引)

如果这个字段是 NULL,说明没走索引 或者 索引失效了。

rows(预估扫描行数)

数值越小越好。它是评估 SQL 性能最直观的指标。

读写分离

横向扩展的架构思维。

  • 主库 (Master / Primary): 专门负责“写”操作(INSERTUPDATEDELETE)。
  • 从库 (Slave / Replica): 专门负责“读”操作(SELECT)。

在绝大多数互联网应用(包括 AI 应用)中,读的次数远多于写的次数。多搞几台“只读”的从库,把压力分担出去。

核心原理

  • 主库记录: 当你在主库写下一条数据时,主库会把这个动作记录到 Binlog 里。
  • 从库复制: 从库会一直盯着主库的 Binlog,一旦发现有更新,就赶紧把日志拉过来。
  • 重做日志: 从库把拉过来的日志存进自己的 Relay Log(中继日志) ,然后按照日志里的指令,在自己的数据库里把这个操作再做一遍。

面试避坑点: :主从同步通常是异步的。这意味着主库写完后,从库可能要等个几十毫秒甚至几秒才能同步过去。这就会导致一个问题:主从延迟

主从延迟怎么办

  • 二次查询: 如果从库查不到,强制去主库查一次(慎用)。
  • 关键业务走主库: 比如订单支付、个人中心修改等敏感操作,直接在代码里指定查主库。
  • 缓存同步: 写入主库的同时,顺便把数据塞进 Redis 缓存,读的时候先看 Redis。

DRF中的主从分离

在 Django 中实现读写分离,通常是在 settings.py 里配置多个 DATABASES,然后编写一个 Router 类。在这个类里重写 db_for_readdb_for_write 方法:

  • 如果是查询操作,返回 slave 数据库;
  • 如果是写入操作,返回 default(主库)数据库。”

定义多个数据库

# settings.py

DATABASES = {
    'default': { # 主库,负责写
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'my_db_master',
        'USER': 'admin',
        'PASSWORD': 'password',
        'HOST': '192.168.1.10', 
    },
    'replica': { # 从库,负责读
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'my_db_master', # 注意:数据库名通常是一样的
        'USER': 'admin',
        'PASSWORD': 'password',
        'HOST': '192.168.1.11', # 不同的 IP
    }
}

编写数据库路由 (Router)

告诉 Django:什么时候用 default,什么时候用 replica。

# my_project/router.py

class MySQLRouter:
    def db_for_read(self, model, **hints):
        """所有的读取操作都指向从库"""
        return 'replica'

    def db_for_write(self, model, **hints):
        """所有的写入操作都指向主库"""
        return 'default'

    def allow_relation(self, obj1, obj2, **hints):
        """允许跨库关联(因为数据其实是一样的)"""
        return True

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        """只允许在主库进行数据表结构迁移"""
        return db == 'default'

激活路由

在 settings.py 中注册这个路由:

# settings.py

DATABASE_ROUTERS = ['my_project.router.MySQLRouter']

手动强制切换

# 强制从主库读取最新数据,不走从库
latest_data = Article.objects.using('default').get(id=1)

分库分表

垂直拆分 (Vertical Splitting) —— “按功能拆”

把一个表里的字段拆到不同的表里,或者把不同的表拆到不同的数据库。

  • 例子: 你的 User 表原本有 50 个字段(包括用户名、密码、头像、个人简介、联系方式、家庭住址)。
  • 怎么拆:
    1. 基础表user_base(id, username, password)—— 经常查,体积小。
    2. 详情表user_extra(id, intro, address)—— 不常用,字段长(比如 TextField)。
  • 作用: 减少单行数据的大小,让内存能缓存更多行,提升查询速度。

水平拆分 (Horizontal Splitting) —— “按行数拆”

把同一个表的数据,按某种规则散落到不同的库或表里。这是真正的“大招”。

  • 例子: 你的 Order(订单)表有 1 亿行。
  • 怎么拆: 准备 10 个订单表(order_0order_9)。
  • 规则(Shard Key): 按照 user_id % 10 来存。
    • 用户 ID 为 123 的订单,永远存到 order_3
    • 用户 ID 为 456 的订单,永远存到 order_6
  • 作用: 把 1 亿行的数据压力,平均分摊到 10 个小表里,每个表只有 1000 万行,恢复了索引性能。

数据一致性

无论发生什么,数据库里的数据逻辑必须是正确且合法的。MySQL 的 InnoDB 引擎之所以成为行业标配,就是因为它完美实现了 ACID 特性.

InnoDB 与一致性的关系(核心:ACID)

MySQL 的 InnoDB 引擎之所以成为行业标配,就是因为它完美实现了 ACID 特性,而 C (Consistency - 一致性) 是 ACID 的最终目的。其他三个字母都是为了保障一致性而存在的手段。

① A (Atomicity - 原子性):靠 Undo Log 实现

  • 原理: 把事务看成一个不可分割的原子。
  • InnoDB 的做法: 记录 Undo Log(回滚日志) 。在你改数据之前,先把旧数据存起来。如果中间出错了,InnoDB 就按着 Undo Log 把数据改回去。

② I (Isolation - 隔离性):靠 MVCC 和 锁 实现

  • 原理: 当两个人在同时改数据时,互不干扰。
  • InnoDB 的做法: 使用 MVCC(多版本并发控制) 。就像给数据拍快照,你读你的旧版本,我改我的新版本,互不冲突,防止“脏读”。

③ D (Durability - 持久性):靠 Redo Log 实现

  • 原理: 只要事务提交了,哪怕这时候电脑爆炸了,数据也得在磁盘里。
  • InnoDB 的做法: Redo Log(重做日志) 。MySQL 不是立刻把数据写入磁盘(太慢),而是先写一份日志。哪怕断电,重启后也能根据日志把没写进磁盘的数据“重做”一遍。

强一致性 vs 最终一致性(面试进阶点)

你在聊读写分离和分库分表时,面试官可能会追问这个。

  • 强一致性(Strong Consistency): 写入后,立刻就能读到最新的。单机数据库(InnoDB)能做到。
  • 最终一致性(Eventual Consistency): 写入后,允许有几秒的延迟(主从同步延迟),但最终数据会一致。读写分离架构下,通常是最终一致性。

总结

数据一致性是指数据库从一个合法状态转变到另一个合法状态,不违反业务规则和完整性约束。

在 InnoDB 中,一致性是通过 ACID 模型来保障的:

  • 通过 Undo Log 保证原子性,支持事务回滚;
  • 通过 Redo Log 保证持久性,确保断电不丢数据(WAL 技术);
  • 通过 锁机制MVCC 保证隔离性,防止并发下的数据混乱。

另外,在分布式或读写分离场景下,我们要权衡强一致性和性能。通常我们会接受最终一致性,并利用 Redis 或强制路由主库来解决关键业务的实时性问题。