☰
MySQL四层能力学习路径:从SQL到索引事务与高可用
2026/11/2 12:49:27 网站建设 项目流程

带过几届新人之后我发现,绝大多数人学 MySQL 的学习路径是反的:先买一本厚书,从关系代数看到范式理论,再啃事务隔离级别和 MVCC,等合上书准备动手时,连一条带索引的三表关联都写不利索。这不是勤奋程度的问题,是顺序错了。数据库这门手艺本质上是"手感活"加"系统观"的结合体,手感来自大量写坏的 SQL 和跑慢的查询,系统观来自你在真实故障现场对日志、锁、执行计划的反复对照。所以下面这条学习路径不按教材章节排,而是按能力层级排:先能写对,再能看懂,然后是能调得动,最后是能扛得住。每一层我都给出明确的验收标准、时间预算和必须动手做的练习,你照着走,三个月左右能到"独立负责一个中等规模业务库"的水平,而不是停留在"能背出四种隔离级别"的表面。

1. 把"学 MySQL"拆成四层能力,比按章节顺序啃书管用

1.1 一条被反复验证的能力递进线:写得出、看得懂、调得动、扛得住

我给这条线起了四个很好记的名字。写得出,指的是给你一个业务需求,你能在十分钟内设计出合理的表结构,写出正确的增删改查,知道什么时候该用 JOIN 什么时候该拆成两次查询,知道一个字段该用DATETIME还是BIGINT存时间戳。看得懂,指的是随手一条慢 SQL 丢给你,你能用EXPLAIN说出它为什么慢、慢在哪一步、缺哪个索引。调得动,指的是并发一上来出现数据不一致、锁等待、死锁的时候,你能定位到具体是哪两条语句在互相等,而不是重启了事。扛得住,指的是这套库要备份、要扩容、要主从、要监控,出事之后能在可接受的时间内恢复。

这条线的价值在于它天然带着反馈。你学完"写得出"马上能验证——建个表跑一下就知道对不对;学完"看得懂"马上能验证——加个索引再EXPLAIN一次看type有没有从ALL变成ref。而按教材章节学的最大问题是反馈周期太长,你读了三百页还不知道自己会不会用,很容易在半路放弃。

我见过不少人卡在第二层和第三层之间的位置:能写 SQL,也知道索引重要,但看到执行计划里一堆字段就发懵,于是回头去读 B+ 树的论文,读完更懵。问题不在理论,在于他没有带着"这条 SQL 到底扫了多少行"的具体疑问去读。所以下面每一层我都会先给一个具体到能立刻上手的动作,再补它背后的原理。

1.2 每层的验收标准和时间预算

时间预算我按"每天投入两小时、周末各半天"的节奏估的,如果你是全职学习可以压缩到一半,但不要跳层。

能力层建议投入验收标准(做不到就别往下走)
写得出3 周能独立设计一个含 6 到 8 张表的业务库,写多表关联、分组统计、子查询不出错
看得懂4 周拿到慢 SQL 能用 EXPLAIN 判断扫描行数与索引使用情况,并给出改写方案
调得动3 周能复现一次死锁,从日志里读出冲突的两条语句和加锁顺序
扛得住3 周能搭一主一从、做过一次完整备份并在另一台实例上恢复成功

这张表里的验收标准是硬指标,尤其是最后两条。很多人学到"扛得住"之前就去找工作了,然后在第一次线上锁等待时手足无措。我个人的看法是:前三层决定你能不能干活,第四层决定你敢不敢值班。如果你短期内不需要值班,第四层可以放慢,但备份恢复这一项一定要自己动手做一遍,因为这是唯一一个"平时完全没感觉、出事时救命"的技能。

还有一点要提前说清楚:不要在这条路径上同时学两种数据库。有人一边学 MySQL 一边学 Redis 一边学 MongoDB,最后三个都是半吊子。先把 MySQL 这一条线走通,形成"我面对一个陌生数据库知道从哪几个维度切入"的方法论,再去学别的,效率会高得多。

2. 第一层"写得出":环境、建表与查询手感

2.1 装环境:三种方式的取舍,以及为什么我建议先用容器

装 MySQL 这件事本身不难,但新手最容易被"装在哪、怎么连"卡住半天,热情就消耗掉了。常见的三种方式:直接装在本机、用容器跑、用云上的托管实例。我的建议是先用容器把 8.0 跑起来,理由是版本干净、卸载干净、可以同时开两个实例做实验(这点在学到主从和锁的时候极其重要),而且不会污染本机环境。

docker run -d --name mysql-learn \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=Learn@1234 \ -e MYSQL_DATABASE=demo \ -v mysql-learn-data:/var/lib/mysql \ mysql:8.0 \ --character-set-server=utf8mb4 \ --collation-server=utf8mb4_0900_ai_ci

几个参数值得解释。-v挂一个具名卷是为了让容器删了数据还在,你不希望练到一半发现表全没了。--character-set-server=utf8mb4是必须显式指定的,虽然 8.0 的默认值已经是它,但显式写出来能提醒你这件事很重要。MYSQL_DATABASE=demo会帮你建好一个库,省掉第一次登录还要CREATE DATABASE的动作。

注意:容器里的 MySQL 默认只监听容器内部,端口映射是必须的;如果你连不上,先确认容器是否在运行(docker ps),再确认客户端连的是127.0.0.1而不是localhost,后者在某些系统上会走 socket 而不是 TCP。

装好之后,第一件事不是急着写 SQL,而是把客户端配置好。命令行客户端、图形化工具、IDE 插件三选一即可,别纠结。我个人的习惯是主力用命令行,因为你在排查线上问题时只有命令行可用,早点熟悉\G、source、tee这些用法有好处。

2.2 数据类型和字符集:第一天就该定下的规矩

新手建表最容易犯的错全在类型上,而这些错在数据量小的时候完全看不出来,等数据涨到几百万行才变成事故。我把最常踩的几条列出来,建议你从第一张表开始就按这个来。

金额不要用FLOAT或DOUBLE,用DECIMAL(18,2)或者用整数存最小单位(分)。浮点数在数据库里做累加会出现精度漂移,这是二进制表示法的固有问题,不是 MySQL 的 bug。时间不要用字符串存,用DATETIME或TIMESTAMP,前者范围大、不随时区变化,后者占空间小、会自动转时区——业务里我一般选DATETIME并约定全库统一存 UTC,展示层再转换。VARCHAR的长度不要拍脑袋写255,按实际业务留 20% 余量,因为索引长度和排序缓冲区都和它有关。

字符集这块,8.0 之后基本不用纠结了,统一utf8mb4。所谓 utf8mb4 就是每字符最多 4 字节的完整 UTF-8 实现,能存下所有常见字符,包括一些表情符号。如果你还见到utf8这个写法,在 MySQL 里它其实指的是每字符最多 3 字节的旧实现,是个历史遗留坑,遇到一律改成utf8mb4。

还有一个容易被忽略的点:主键用什么。自增BIGINT是最稳妥的选择,因为 InnoDB 的表本质上是按主键组织的一棵 B+ 树,主键递增意味着新数据总是插在树的最右侧,页分裂少、写入快。用 UUID 做主键则会导致随机插入,页分裂频繁,二级索引也因为主键太长而变得臃肿。这一点在测出效果之后你就再也不想用 UUID 当主键了。

2.3 六类必须练到不用查手册的语句

这一阶段的目标不是"知道有这种语法",而是"手放键盘上就能敲出来"。我建议你建一个练习库,比如一个简单的图书借阅系统:读者表、图书表、借阅记录表、分类表。就这四张表,足够你把下面六类语句练熟。

第一类是建表与改表,包括CREATE TABLE、ALTER TABLE ADD COLUMN、ALTER TABLE MODIFY COLUMN、加索引和加唯一约束。第二类是单表条件查询,重点是WHERE里AND、OR、IN、BETWEEN、LIKE的组合,以及NULL的处理——NULL参与任何比较都是未知,所以= NULL永远是假,必须用IS NULL。第三类是多表关联,INNER JOIN、LEFT JOIN的区别必须清楚:左连接保留左表所有行,右表没匹配上就补NULL,很多统计漏数据就是因为把左连接写成了内连接。

第四类是聚合与分组,GROUP BY配合COUNT、SUM、AVG,以及HAVING和WHERE的分工——WHERE在分组前过滤行,HAVING在分组后过滤组,写错位置会让结果完全不同。第五类是子查询和派生表,包括IN子查询、EXISTS子查询和FROM后面跟一个子查询。第六类是数据修改,特别是UPDATE一定要带WHERE,DELETE也是,以及在INSERT时用ON DUPLICATE KEY UPDATE做"存在则更新"。

-- 一个能覆盖多知识点的练习:统计每类图书的借阅次数与借阅人数 SELECT c.name AS category_name, COUNT(br.id) AS borrow_times, COUNT(DISTINCT br.reader_id) AS reader_cnt FROM category c LEFT JOIN book b ON b.category_id = c.id LEFT JOIN borrow_record br ON br.book_id = b.id AND br.created_at >= '2025-01-01' GROUP BY c.id, c.name HAVING borrow_times > 0 ORDER BY borrow_times DESC LIMIT 20;

这条 SQL 里藏了几个考点:用左连接保证没有借阅记录的分类也能出现(虽然被HAVING过滤了,但这是有意的筛选逻辑),条件br.created_at写在ON里而不是WHERE里,是为了不把左连接退化成内连接。这种细节靠看书很难形成条件反射,必须自己写错一次、看到结果不对、再改对,才会真正记住。

3. 第二层"看得懂":索引和执行计划

3.1 B+ 树到底解决了什么问题

很多人背过"B+ 树矮胖、适合磁盘",但说不清它到底比二叉树强在哪。换个角度想:数据库的数据在磁盘上,一次随机读要花的时间是内存的几万倍,所以索引结构的设计目标只有一个——把随机读的次数压到最少。二叉树每个节点只有两个叉,一百万条数据树高大约 20 层,意味着最多 20 次随机读;B+ 树每个节点能放几百甚至上千个键(一个 16KB 的页,主键 8 字节加 6 字节指针,大概能放一千多个),树高只有 3 到 4 层,随机读次数直接降到 3 到 4 次。

这就是"矮胖"的实际意义。理解了这一点,很多结论就顺理成章了:为什么主键要短(每个节点能放的键更多,树更矮)、为什么范围查询高效(叶子节点之间有双向链表,找到起点后顺序扫就行)、为什么二级索引要回表(叶子节点存的是主键值,不是整行数据,拿到主键还得回主键树再查一次)。

聚簇索引和二级索引的区别也在这里落地了。InnoDB 的主键索引叶子节点直接挂着整行数据,所以按主键查最快;二级索引叶子节点挂的是主键值,所以按二级索引查要么回表、要么靠覆盖索引避免回表。理解这一层,你在写SELECT *的时候就会下意识想一下:这一行是不是可以只取我需要的列,让索引直接覆盖掉。

3.2 EXPLAIN 只看五个字段就够用

EXPLAIN输出十几个字段,新手一看就晕。实际上日常排查九成的问题只看五个就够:type、key、rows、filtered、Extra。

type是访问类型,从好到坏大致是system>const>eq_ref>ref>range>index>ALL。看到ALL就是全表扫描,数据量大的表上基本等于事故;看到index是全索引扫描,比全表稍好但也不理想;range是范围扫描,正常;ref、eq_ref是走索引等值匹配,很好。key是实际用上的索引名,如果这一列是NULL而possible_keys有值,说明有索引但没用上,这就是你要找的问题。rows是优化器估算的扫描行数,注意是估算,统计信息不准时会偏差很大,但它和实际行数的量级对比仍然有参考价值。filtered是扫描后剩下行的百分比,和rows相乘能估算出参与后续连接的行数。Extra里最关键的是三个:Using index表示覆盖索引,非常好;Using filesort表示需要额外排序,数据量大时会很慢;Using temporary表示用了临时表,常见于GROUP BY和DISTINCT。

EXPLAIN FORMAT=JSON SELECT id, title FROM book WHERE author_id = 100 AND status = 1 ORDER BY created_at DESC;

8.0 里我更推荐用EXPLAIN FORMAT=JSON或者EXPLAIN ANALYZE。后者会真正执行语句并给出每一步的实际耗时和实际行数,对定位"估算 100 行实际扫了 10 万行"这类问题特别有效。代价是它会真的跑一遍,别在写操作上用。

3.3 索引失效的六种典型场景与验证手法

索引失效这件事,光记结论没用,必须自己构造数据验证一遍。我把最常见的六种列出来,每一种你都可以在我上面那个图书库上复现。

第一种是在索引列上做函数运算或表达式计算,比如WHERE DATE(created_at) = '2025-01-01',改成范围写法created_at >= '2025-01-01' AND created_at < '2025-01-02'就能走索引。第二种是隐式类型转换,字段是VARCHAR你却传了个数字,WHERE phone = 13800000000,MySQL 会把字段转成数字比较,索引直接失效,必须加引号。第三种是前导模糊匹配,LIKE '%关键词'无法利用索引的有序性,LIKE '关键词%'可以。第四种是OR连接的两边有一边没索引,整体就会退化成全表扫描。第五种是联合索引不满足最左前缀,跳过了第一列直接用第二列。第六种是对索引列做!=、NOT IN、IS NOT NULL这类否定判断,当匹配行占比很高时优化器会主动放弃索引——注意这是优化器的理性选择,不是失效。

验证手法很固定:先EXPLAIN看key和type,改写法,再EXPLAIN对比。同一个查询改前改后两次执行计划放在一起看,比读十篇文章印象都深。我一般会要求新人在练习库里把六种场景各造一组数据,写成一个小笔记,这个过程大概花两小时,但之后遇到问题能立刻反应出方向。

3.4 联合索引的最左前缀,用一张表讲透

联合索引(a, b, c)的排序规则是:先按 a 排,a 相同再按 b 排,b 相同再按 c 排。这个"字典序"的理解方式比背规则有用得多。所以WHERE a = 1 AND b = 2能用上,WHERE b = 2用不上,WHERE a = 1 AND c = 3只能用到 a 那一部分。

有一个常被误解的点是范围查询之后的部分。WHERE a = 1 AND b > 2 AND c = 3,在(a, b, c)上,a 和 b 能用上索引,c 用不上(8.0 有索引下推,c 会在存储引擎层做过滤,减少回表,但不参与定位)。所以联合索引的列顺序要把等值条件放前面、范围条件放后面。

还有个实战技巧:ORDER BY也能用索引来避免排序。如果查询是WHERE a = 1 ORDER BY b,那么(a, b)这个联合索引既完成了过滤又完成了排序,Extra里就不会出现Using filesort。这个技巧在分页查询里特别值钱,因为LIMIT 100000, 20这种深分页如果还要排序,性能会非常难看。

4. 第三层"调得动":事务、锁和并发下的行为

4.1 三种日志各管一段,别混着记

redo log、undo log、binlog 这三个东西,很多人背了定义但说不清它们各自解决什么问题。用一句话概括:redo log 保证崩溃后已提交的数据不丢,undo log 保证回滚和 MVCC 有历史版本可读,binlog 保证数据可以复制到别的实例和用于按时间点恢复。

redo log 是 InnoDB 层的物理日志,记录"某个数据页的某个位置改成了什么",采用循环写,空间固定,写满之后会触发检查点把脏页刷盘。undo log 也是 InnoDB 层的,但它是逻辑日志,记录的是"反向操作",比如插入对应删除、更新对应改回旧值,它既服务于事务回滚,也服务于 MVCC——你读到旧版本,就是顺着 undo 链往前找出来的。binlog 是 Server 层的,记录的是逻辑上的变更语句或行变更,采用追加写,主要用于复制和时间点恢复。

三者之间靠两阶段提交保持一致:写 redo log 时先标prepare,再写 binlog,最后把 redo log 标commit。这样即使在这中间崩溃,恢复时也能通过比对 binlog 是否完整来决定这个事务是提交还是回滚,不会出现主库和从库数据不一致。这个机制理解之后,你就能明白为什么sync_binlog和innodb_flush_log_at_trx_commit这两个参数决定了"数据安全性"和"写入性能"的取舍——都设成 1 是最安全的,每次提交都刷盘,性能会掉;设成 0 或 2 快,但崩溃可能丢最后若干个事务。

4.2 MVCC:同一条记录为什么两个会话看到的不一样

开两个命令行窗口,都连同一个库。窗口 A 开事务,查一次某行数据;窗口 B 更新这行并提交;窗口 A 再查一次,会发现看到的还是旧值。这个现象就是 MVCC,也是面试最爱问的点,但真正理解它需要动手做一遍。

原理上说,每行数据有两个隐藏列,一个记录最后修改它的事务 ID,一个记录指向 undo log 的回滚指针。每个事务开始时生成一个 Read View,里面记录了"当前活跃的、还没提交的事务 ID 列表"。读取时顺着 undo 链往前找,找到第一个"对当前事务可见"的版本。判断规则大致是:如果这个版本的事务 ID 小于 Read View 里的最小活跃 ID,说明它早就提交了,可见;如果大于等于最大 ID,说明是之后才开始的,不可见;如果在中间,就看它是否在活跃列表里,在就不可见。

关键差别在于 Read View 的生成时机。REPEATABLE READ是在事务里第一次读的时候生成一次,之后整个事务复用,所以叫"可重复读";READ COMMITTED是每次读都重新生成,所以能看到别人新提交的数据。这就是为什么同样的操作在两个隔离级别下结果不同。这里还有个常被忽略的细节:REPEATABLE READ下的快照读不加锁,但UPDATE、DELETE、SELECT ... FOR UPDATE是当前读,读的是最新版本,所以你可能会遇到"查出来的和改的不是同一行"的困惑,本质是快照读和当前读混用了。

4.3 锁的实测:一行 SQL 锁了多大范围

InnoDB 的锁按粒度分行锁和表锁,行锁又分记录锁、间隙锁、临键锁。临键锁是记录锁加间隙锁的组合,锁住的是"左开右闭"的区间。REPEATABLE READ下,走索引的等值查询命中记录时是记录锁,没命中时会锁间隙;范围查询默认加临键锁。

这段理论不看实验是很难有体感的。你可以这么做:建一张表,插入 id 为 1、5、10 三条记录,开一个事务执行SELECT * FROM t WHERE id = 7 FOR UPDATE,然后在另一个窗口试插 id 为 6 和 8 的行,会发现插不进去——因为 7 不存在,锁的是 (5, 10] 这个间隙。再把条件换成id > 5,看它锁的范围怎么变。做完这一组实验,你对"间隙锁"的理解会比看十遍文档都牢固。

顺便说一个实用结论:尽量让加锁的语句走唯一索引或主键。走唯一索引等值命中时只需要记录锁,不产生间隙锁,并发度最高;走普通索引或者没索引,锁的范围会大很多,如果是全表扫描的UPDATE,会锁住扫描到的每一行,等同于把整张表锁死,这是线上事故的高发原因。

4.4 死锁排查的实际操作

死锁是两个事务各持有一把锁、又都在等对方释放,形成环路。MySQL 有死锁检测机制,发现后会主动回滚代价小的那个事务,报Deadlock found when trying to get lock。业务代码里如果没处理好这个错误,用户看到的就是一个莫名其妙的失败。

排查靠SHOW ENGINE INNODB STATUS,输出里有一个LATEST DETECTED DEADLOCK段落,会列出两个事务各自执行了什么 SQL、持有哪些锁、在等哪把锁。真正难的不是读日志,是复现。想稳定复现,可以在会话里临时打开innodb_print_all_deadlocks = ON,把所有死锁打到错误日志里,比只留最后一次更全。

复现的经典场景是两个事务以相反顺序更新同一批行:

-- 会话 A BEGIN; UPDATE account SET balance = balance - 100 WHERE id = 1; UPDATE account SET balance = balance + 100 WHERE id = 2; -- 会话 B(几乎同时) BEGIN; UPDATE account SET balance = balance + 100 WHERE id = 2; UPDATE account SET balance = balance - 100 WHERE id = 1;

两边同时执行到第二条时就会互等。解决方式不是加重试就完了,而是统一加锁顺序:所有涉及多行的更新,都按主键升序执行。转账场景里可以先SELECT ... ORDER BY id FOR UPDATE把两行都锁住再更新,环就被打破了。重试只能是兜底,不能是方案。

5. 第四层"扛得住":复制、备份与容量

5.1 主从复制先跑通再谈原理

主从复制的原理说起来三句话:主库把变更写进 binlog,从库的 IO 线程把 binlog 拉过来写进本地的 relay log,从库的 SQL 线程重放 relay log。但只有自己搭一遍,你才会知道中间有多少坑。

用容器搭最方便,起两个实例,端口分别映射到 3307 和 3308,各自配置server_id不同、log_bin打开、binlog_format = ROW。然后在从库上执行CHANGE REPLICATION SOURCE TO指定主库地址和起始位点(或者用 GTID 模式省掉位点),再START REPLICA,最后SHOW REPLICA STATUS看Replica_IO_Running和Replica_SQL_Running是否都是Yes。

常见的失败原因就那几个:账号没有复制权限、server_id冲突、主库位点填错、从库有写入了主库没有的数据导致主键冲突。我建议第一次搭的时候故意制造一次"从库写入导致同步中断",然后自己用SET GLOBAL sql_replica_skip_counter = 1跳过一条事务修好它——这个操作在生产上是禁忌,但你必须知道它为什么禁忌。

binlog_format为什么推荐ROW?因为STATEMENT格式记录的是原始 SQL,遇到NOW()、RAND()、自增这类不确定函数时,从库重放的结果可能和主库不一致;ROW记录的是每行数据的实际变化,是确定的。代价是日志量大,一个批量更新可能产生大量 binlog,这个取舍要清楚。

5.2 备份不做恢复演练等于没备份

这句话我说了很多遍。备份文件躺在那里,你没恢复过,就不知道它是不是完整、是不是能在一个干净实例上跑起来、需要多长时间。真出事的时候你只有一次机会。

逻辑备份用mysqldump,关键是加--single-transaction,这样在 InnoDB 上会开一个一致性快照,不需要锁表,业务可以继续写。加--routines导出存储过程和函数,加--triggers导出触发器,加--set-gtid-purged=OFF避免导入时产生 GTID 冲突。

mysqldump -h127.0.0.1 -uroot -p \ --single-transaction \ --routines --triggers \ --set-gtid-purged=OFF \ --databases demo \ > demo_$(date +%Y%m%d).sql

物理备份适合数据量大的场景,社区里常用的是直接拷贝数据文件的方式配合专门的备份工具,或者用云上的自动备份。不管哪种,恢复演练的步骤要写下来:新建一个空实例、导入备份、核对关键表的行数和最大 ID、跑几条业务查询确认数据可用、记录整个过程的耗时。这个耗时就等于你的 RTO,也就是真出事时业务要停多久,这个数字你应该提前知道,而不是在现场估算。

注意:mysqldump默认不加--single-transaction时会锁表,在业务库上直接跑会导致写入阻塞。这个参数不是可选项。

5.3 慢查询日志和三个必看指标

慢查询日志是性能排查的入口。开启方式是设slow_query_log = ON、long_query_time = 1(单位秒,超过这个时间的会被记录)、log_queries_not_using_indexes = ON(这个开关谨慎用,在没索引的小表多的库上会产生大量噪音)。日志是文本格式,可以用mysqldumpslow做初步聚合,更细的分析一般用第三方工具做统计排序。

分析慢日志的顺序我总结成三步:先按总耗时排序而不是按单次耗时,因为一条执行 10 毫秒但每秒跑一千次的 SQL,危害远大于一条执行 3 秒但一天跑一次的报表 SQL;再看扫描行数与返回行数的比值,比值大说明索引不够精准;最后看是否出现同一类 SQL 反复执行,那通常意味着业务代码里有循环查库,应该改成批量查询。

除了慢日志,日常监控三个指标就够了:连接数(Threads_connected接近max_connections时会拒绝新连接,这是最常见的"数据库挂了"的表象)、InnoDB 缓冲池命中率(低于 95% 说明内存不够或者有大量全表扫描)、以及锁等待时间。前两个用SHOW GLOBAL STATUS就能看到,第三个在 8.0 里查performance_schema.data_lock_waits更直观。

6. 用一个能上线的小项目把四层串起来

6.1 选题与表结构:从需求到字段

光练单点技能容易忘,必须有一个贯穿全线的项目。选题不用大,一个"内容发布加评论"的小系统就够,因为它的读写比例、关联关系、统计需求都很典型。

表结构至少包含用户表、内容表、评论表、标签表和内容标签关联表。设计时把这几个问题想清楚:内容正文用TEXT还是拆成单独的表(大字段会影响行溢出和查询效率,正文不常查的话可以拆);评论要不要支持层级(支持的话需要parent_id和路径字段);标签和内容是多对多,必须有中间表;每张表都要有created_at、updated_at,并且updated_at设成ON UPDATE CURRENT_TIMESTAMP省得业务层漏写。

索引在设计阶段就要一起定。内容列表页按发布时间倒序分页,那(status, created_at)是必备的联合索引;评论按内容 ID 查并按时间排,(content_id, created_at)必备;标签反查内容需要(tag_id, content_id)。这些索引是跟着查询走的,先想清楚有哪些查询,再定索引,这个顺序不能反。

6.2 造数据、压测、定位、优化的一条龙

表建好之后,用存储过程或者脚本造个五十万到一百万行数据。不要用几百行数据做性能测试,优化器在数据量小的时候会做出完全不同的选择,你看到的执行计划没有参考价值。

造完数据之后,写几条典型查询:列表分页、按标签筛选、统计某内容下的评论数、按关键词搜索标题。先用EXPLAIN看每条的执行计划,把有ALL和Using filesort的记下来。然后针对性地加索引或改写 SQL,再EXPLAIN对比。最后用一个压测工具跑一遍,记录 QPS 和平均延迟,改完再跑一遍对比。

这个过程里你会遇到很多"预期之外"的情况,比如加了索引反而不走、深分页依然很慢、COUNT(*)在百万行上要好几秒。这些都是真实的收益点,比看教程有价值得多。深分页的经典解法是改成"记住上一页最后一个 ID"的游标分页:WHERE id < last_id ORDER BY id DESC LIMIT 20,从 O(offset) 变成 O(1)。统计类的则可以考虑维护计数表或者用缓存。

6.3 项目复盘的五个问题

做完之后,用五个问题自检:这张表为什么用这个主键?这条查询为什么走这个索引而不是那个?这个事务的隔离级别选对了吗?这次备份能不能恢复?如果 QPS 翻十倍,先动哪里?

能把这五个问题答清楚,说明四层能力都到位了。答不清楚的地方,就是你下一轮要补的地方。我把这套自检当成一个习惯保留了下来,每次接新项目都会在文档里写一遍,写不出来的地方往往就是埋雷的地方。

7. 这条路径上最容易走偏的几处,以及我的绕行经验

7.1 只背八股不写 SQL

面试题里的"四种隔离级别""三大日志"确实重要,但把它们背下来和会用是两回事。判断自己是不是掉进这个坑很简单:给你一个从没见过的业务需求,你能不能在十分钟内把表结构和查询写出来?如果不能,说明手感没建立,得回去补第一层。我的建议是每天至少写十条 SQL,可以是业务库里的真实查询,也可以是练习库里的自造场景,重点是保持手感。

7.2 上来就调参

网上流传着一堆"MySQL 优化配置",什么缓冲池设多大、连接数设多少,照着抄一遍感觉很专业。但在你不清楚业务读写比例、数据量级、机器规格的情况下,这些参数抄过来大概率是错的。innodb_buffer_pool_size确实应该设成物理内存的 50% 到 70%,但如果你只有 2G 内存的机器,设成 70% 之后系统本身就没多少内存了,反而会触发交换。参数是在你理解了指标之后才调的东西,不是起点。

7.3 过早钻源码

有人学到索引就去读 InnoDB 的源码,读到一半放弃,前面的东西也忘了。源码适合在你已经能解决大部分实际问题、需要突破瓶颈的时候读,而不是初学阶段。初学阶段最好的"源码"是你的执行计划和错误日志,它们直接用业务语言告诉你发生了什么。

7.4 十二周的具体节奏建议

我把上面的内容排成一张周表,方便你对照执行。

周次主线任务周末必做
第 1 周环境搭建、数据类型、建表规范设计并建好练习库四张表
第 2 周单表查询、多表关联、聚合分组写出十条统计类 SQL 并核对结果
第 3 周子查询、数据修改、事务基本用法完成一个小型的增删改查闭环
第 4 周B+ 树与索引原理、EXPLAIN 字段对上周的 SQL 逐条看执行计划
第 5 周联合索引、覆盖索引、索引失效复现六种索引失效场景
第 6 周SQL 改写、深分页、排序优化优化三条慢 SQL 并记录对比数据
第 7 周redo/undo/binlog、两阶段提交画出三种日志的写入时序
第 8 周MVCC、隔离级别、快照读与当前读两个窗口复现不可重复读与幻读
第 9 周锁的类型、间隙锁、死锁稳定复现一次死锁并读日志
第 10 周主从复制原理与搭建搭好一主一从并验证同步
第 11 周备份恢复、慢查询分析、监控指标完整做一次备份与恢复演练
第 12 周综合项目:造数、压测、优化写一份优化前后的对比报告

这张表的用法是"完成即前进",某一周没做完不要往后跳,因为后面每一层都依赖前面的手感。如果时间紧张,可以把第 10 到 11 周的内容压缩,但第 4 到 6 周绝对不能省,那是从"会写"到"会调"的分水岭。

最后分享一个我个人一直在用的习惯:每次遇到一个不理解的数据库行为,不管是慢查询、锁等待还是数据不一致,都先在本地复现一遍,把过程写成一段不超过五百字的小结,记下现象、原因、验证方法和结论。写了三十条之后你会发现,MySQL 的很多"玄学"问题其实只有那么几类根因,剩下的都是同一批原因在不同场景下的变体。这个小结的积累速度,比读任何一本书都快。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询