最近有个朋友跟我聊起 MySQL 学习,他问我:每天刷 SQL 题基本都能答上来,但一到面试问原理就心里没底,尤其是存储引擎、索引、触发器这几块,总感觉会写但讲不透。我特别理解这种感觉,因为我自己学到 DAY09 这个阶段时,正好卡在同样的位置上——光知道CREATE INDEX能加速、CREATE TRIGGER能自动执行,但完全说不清它们内部是怎么配合的。
DAY09 我把这三块放在一起啃,是有原因的。存储引擎决定了数据怎么存、怎么锁、怎么恢复,是 MySQL 的“地基”;索引决定了数据怎么被快速找出来,是“查得快”的关键;触发器则是把一部分业务逻辑下沉到数据库里,让它在特定事件发生时自动响应。这三者表面上看是三个独立知识点,实际上环环相扣:索引的结构取决于存储引擎的实现方式,触发器又必须依附于某张具体的数据表,而表最终由存储引擎来管理。学完这一轮我最大的感受是,零散地记命令和系统地把它们串成一条线,效果完全不一样。
这篇文章就把我 DAY09 的学习过程整理下来,重点是把我踩过的坑、想通的逻辑和可以直接抄走的代码示例都放进来。如果你是正在学 MySQL 的开发者,或者是准备面试想系统过一遍存储引擎、索引、触发器的人,这篇笔记应该能帮你省不少时间。
1. 存储引擎入门:为什么 InnoDB 成了默认选项
存储引擎这个词第一次听会觉得有点抽象,说白了它决定了 MySQL 在底层怎么组织数据、怎么处理并发、怎么保证数据不丢。MySQL 从 5.5.5 版本开始把 InnoDB 设为默认引擎,这一选就是十几年,不是没有理由的。
1.1 InnoDB 和 MyISAM 的正面 PK
我在学习时最先做的就是对比 InnoDB 和 MyISAM,因为早期接触 MySQL 的人一定绕过不这两个高频主角。直接把关键差异列出来,可能比任何解释都直观。
| 对比维度 | InnoDB | MyISAM |
|---|---|---|
| 事务支持 | 支持 ACID 事务 | 不支持 |
| 锁粒度 | 行级锁 | 表级锁 |
| 外键约束 | 支持 | 不支持 |
| 聚簇索引 | 是,主键直接决定物理存储顺序 | 否,数据和索引分开存放 |
| 全文索引 | MySQL 5.6 前不支持,5.6 后支持 | 原生支持 |
| 崩溃恢复 | 通过 redo log 实现崩溃恢复 | 无类似机制,崩溃后易丢数据 |
| 适用场景 | 高并发 OLTP、交易系统 | 只读查询、数据仓库、日志类表 |
学习的时候我特意给自己的练习库建了一张 MyISAM 表,又建了一张 InnoDB 表,然后开启两个事务去改同一条记录。结果 InnoDB 这边第二个事务会等锁,而 MyISAM 那边直接整张表锁住,影响范围明显大得多。行锁和表锁的差距,只有实际触发过才知道什么叫“并发瓶颈”。
1.2 InnoDB 三大看家本领:事务、行锁、崩溃恢复
InnoDB 能成为默认引擎,核心是它同时握住了三个对业务系统至关重要的能力。
第一个是事务。你可以把事务理解成“要么全做,要么全不做”的一组 SQL。转账就是个最经典的场景,扣钱和加钱必须同时成功或同时失败,不能扣了钱对方却没收到。InnoDB 通过 undo log 记录回滚前的数据状态,支持 ROLLBACK,也通过锁机制保证并发事务之间的隔离。MySQL 八股文里常问的 ACID,就是在说:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。这四个特性不是天生就有的,而是 InnoDB 用日志和锁机制一起堆出来的。
第二个是行级锁。InnoDB 的锁针对的是索引记录,不是整张表。这意味着在高并发场景下,两个事务同时修改同一张表里的不同记录时,彼此不会互相阻塞。这一点在订单系统、库存系统这种写频繁的场景里尤其重要,MyISAM 的表锁在这种场景下基本会被打爆。
第三个是崩溃恢复。数据库不可能永远不宕机,关键是宕机后怎么处理。InnoDB 的 redo log 好比记账草稿——每次提交事务时先把变更顺序写入 redo log,数据文件可以慢慢刷盘,但日志必须先落盘。如果服务器突然断电,重启后 InnoDB 会靠 redo log 把还没刷进数据文件的变更重新应用一遍,保证已经提交的事务不丢。这也是 InnoDB 和 MyISAM 在“数据可靠性”上最大的差别。
1.3 其他引擎在什么场合下有用
InnoDB 很强,不代表其他引擎就该被遗忘。学习这些冷门引擎不是凑数,而是为了遇到特定场景时能知道还有别的选择。
- MyISAM:现在业务系统里已经很少用了,但如果你维护的是纯只读的统计报表表、历史归档表,不在乎事务,读的性能依然不错。
- Memory:数据全部存在内存里,读写极快,但 MySQL 重启后数据全部清空。适合放会话信息、临时中间结果这类丢失了也不心疼的数据。
- Archive:只支持 INSERT 和 SELECT,不支持 UPDATE、DELETE,数据会被高比例压缩。适合存日志流水这种只增不改的冷数据。
- CSV:直接把数据存成 CSV 文件,方便外部程序处理,但没有任何索引能力,只适合做数据交换。
学习时我犯过一个认知错误:以为换存储引擎是一件很随意的事。实际上ALTER TABLE ... ENGINE=InnoDB这条命令在数据量大的线上表上执行时会锁表,重建索引的代价非常高。真实项目里应该在建表时就根据业务定好引擎,而不是上线后再去改。这一点希望大家不用像我一样踩过才知道。
2. 索引:从 B+ 树结构看懂索引为什么快
索引是很多人学 MySQL 时最熟悉的陌生人。平时知道给 where 条件后的字段加索引,但问到为什么索引能快、B+ 树和 B 树有什么区别、回表是什么,常常就说不清楚了。
我从一个最简单的问题开始:一张有 1000 万行数据的表,不加索引时查一条记录要遍历整张表,最多可能要比较 1000 万次;加上主键索引之后,走的是一棵 B+ 树,从根节点走到叶子节点大约只需要 3 到 4 次磁盘 IO。这个差距不是“快了一点点”,而是天壤之别。
2.1 B+ 树为什么比 B 树更适合 MySQL
教材上会说 B 树和 B+ 树都是多路平衡查找树,但实际数据库中 B+ 树完胜。两者最大的差异在于:B 树每个节点都会存数据,而 B+ 树只有叶子节点存数据,非叶子节点只存储索引键和指向子节点的指针。
这个差异带来了三个直接影响:
第一,B+ 树的非叶子节点不存数据,所以一个节点能容纳的索引键数量就更多,树的层数更矮。经典的 InnoDB 页大小是 16KB,假设主键是 BIGINT 类型,一个非叶子节点大概能放下上千个索引键。3 层 B+ 树就能存储上千万条数据。树越矮,磁盘 IO 次数越少,查询自然越快。
第二,B+ 树的叶子节点通过链表串联,非常适合范围查询。你在 SQL 里写WHERE id > 100 AND id < 500时,找到 100 之后沿着叶子节点的链表顺序往下读就行。B 树要做中序遍历,代价高得多。
第三,B+ 树的数据存得集中,磁盘预读能发挥作用。数据库底层读取磁盘时通常按页读取,一页里包含多条相邻记录,顺序扫描效率比 B 树那种分散存储好得多。
哈希索引也顺便提一句。哈希索引结构上是键值对,等值匹配(WHERE id = 100)的速度其实可以做到 O(1),比 B+ 树还快。但它完全无法支持范围查询和排序,所以 MySQL 默认不采用,只在 Memory 引擎里可以看到真正意义上的哈希索引,InnoDB 里的自适应哈希索引也只是缓存层的优化手段。
2.2 聚簇索引、二级索引与回表
InnoDB 中,索引和数据是放在一棵 B+ 树里的,这个树就是聚簇索引(clustered index)。你可以把它理解为“索引即数据,数据即索引”。每个 InnoDB 表只能有一个聚簇索引,主键就是它的检索入口;如果你建表时没定义主键,InnoDB 会找第一个非空的唯一索引,再找不到就生成一个隐藏的 rowid 作为主键。
二级索引(也叫辅助索引或非聚簇索引)则是另外单独建立的 B+ 树,它的叶子节点不存储完整的行数据,而是存储主键值。这也是为什么我学到这里时,突然明白了“尽量使用主键查询”这句话的真正含义——用二级索引查询时,InnoDB 先在二级索引树中找到符合条件的主键值,然后再到聚簇索引中按主键去取整行数据。这个过程叫回表(table lookup)。
回表不是必然发生的。如果查询的字段恰好全部包含在二级索引里,比如索引是(username, age),你查的是SELECT username, age FROM user WHERE username = '张三',那么二级索引的叶子节点上已经有这两个字段了,不需要回表,这种情况称为覆盖索引(covering index)。覆盖索引是日常优化查询的重要手段,能省一次聚簇索引的查找,在高频查询上收益特别明显。
2.3 联合索引的最左前缀原则
联合索引是另一个容易懵的地方。给(a, b, c)建联合索引,等于同时建了(a)、(a, b)、(a, b, c)三个索引,这就是所谓的最左前缀原则。我最初一直以为联合索引是把三个字段都建索引,直到实际验证才发现理解错了。
举个例子:
CREATE INDEX idx_abc ON t_order (user_id, status, create_time);这个联合索引生效的查询组合有:
WHERE user_id = 100WHERE user_id = 100 AND status = 1WHERE user_id = 100 AND status = 1 AND create_time > '2024-01-01'
不生效的组合有:
WHERE status = 1WHERE create_time > '2024-01-01'WHERE status = 1 AND create_time > '2024-01-01'
原因在于 B+ 树的索引键是按顺序排序的:先按 user_id 排,相同 user_id 再按 status 排,再相同再按 create_time 排。跳过了前导字段,后面的字段在索引里根本无法确定比较范围。为了验证,我建了一张 20 万行的测试表,分别用带status不带user_id的查询看执行计划,果然 type 变成了 ALL,走了全表扫描。
你在业务设计里经常遇到一个误区:为了“覆盖所有查询条件”,把常用字段都塞进索引。实际上字段越多,索引占用的空间越大,写入时维护索引的代价也越高。合理的做法是分析常用的最左前缀组合,而不是把每个查询都揣一遍。
3. 索引建了还是慢:8 个典型的索引失效场景
这是 DAY09 里我最想分享的部分。因为单单“会建索引”不算会,能在实际业务里判断出“为什么没走索引”才是真本事。我总结了 8 个真实踩过或排查过的场景,每个都值得盯一眼。
3.1 隐式类型转换和函数操作
隐式类型转换是我踩过次数最多的坑。想象一下phone字段在表里是 varchar 类型,但代码里传参时写成了整数类型:
SELECT * FROM user WHERE phone = 13800138000;MySQL 会自动把字符串类型的 phone 转成数字去比较,等于对索引列执行了类型转换,索引直接失效。验证方法也简单,用EXPLAIN看执行计划的 type 列,如果从 ref 变成 ALL,就是没走索引。解决办法是让传入参数保持和字段类型一致,电话这类身份证号、手机号建议统一用字符串存。
函数操作同理。WHERE DATE(create_time) = '2024-01-01'这样写,相当于对索引列做了函数计算,索引键的顺序被打乱,MySQL 只能全表扫描。正确写法是改成范围查询:
SELECT * FROM t_order WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02';这样 create_time 本身没有被包裹在函数里,才能正常走索引。
3.2 LIKE 模糊查询和 OR 条件
LIKE的关键在于通配符的位置。WHERE name LIKE '%张%'因为%在最前面,无法从索引树头部开始定位,索引失效;WHERE name LIKE '张%'则是可行的,因为 MySQL 能在 B+ 树里根据前缀定位。如果业务真的需要“包含”式搜索,建议上全文索引或用搜索引擎,而不是靠 SQL 硬憋。
OR 条件也是一个高频杀手。比如:
SELECT * FROM t_order WHERE user_id = 100 OR status = 1;即使 user_id 和 status 分别建了单列索引,MySQL 也可能选择全表扫描,因为 OR 只要一个条件成立就得返回行,导致优化器很难直接利用 B+ 树的结构做快速定位。实践中可以把 OR 改写成 UNION ALL,或者评估是否真的需要这种查询。
3.3 其他几个容易忽略的失效场景
剩下的几个场景,我用一张表快速记录,大家可以直接拿来当排查清单:
| 场景 | 原因分析 | 建议 |
|---|---|---|
对索引列使用!=或<> | InnoDB B+树无法从“不等于”中快速定位区间 | 改为>和<的联合条件 |
IS NOT NULL或IS NULL使用不当 | 索引列大量为 NULL 时优化器判断走索引不划算 | 给字段设置 NOT NULL 默认值 |
| 联合索引未遵循最左前缀 | 缺少前导字段,后续字段无序可比 | 调整查询条件顺序或修改索引字段顺序 |
使用OR连接非索引列 | 无法对所有分支都高效利用索引 | 改用 UNION ALL |
| 对索引列做表达式运算 | 表达式打乱索引键顺序 | 将表达式移到等号右侧 |
| 字符串列查询时数字与字符串类型混用 | 隐式类型转换 | 参数类型与字段类型保持一致 |
排查方法统一推荐:跑一句EXPLAIN看key和rows两列。key为 NULL 就是没用索引,rows估算扫描行数远超预期往往也是索引没生效的信号。在开发环境用真实数据量测试,别拿几十条数据的小表做判断,数据量太小的时候优化器确实会认为全表扫更快,这是正常现象,不代表索引没用。
4. 触发器:把业务逻辑塞进数据库的权衡
如果说索引是帮 MySQL“读得快”,那触发器就是让 MySQL“自动干活”。它的意思很简单:在某个表上发生 INSERT、UPDATE、DELETE 事件时,自动执行你预先定义好的 SQL 语句。听起来像灵丹妙药,但用起来牵扯不少权衡问题。
4.1 触发器语法与 NEW / OLD 的使用
MySQL 创建触发器的基本语法:
CREATE TRIGGER trigger_name {BEFORE | AFTER} {INSERT | UPDATE | DELETE} ON table_name FOR EACH ROW BEGIN -- 在这里写触发后要执行的逻辑 END;时间点有两类:BEFORE 表示在操作前执行,AFTER 表示在操作后执行。事件类型对应三种操作。FOR EACH ROW表示行级触发,即每一行受影响时都会执行一次。
触发器里最核心的两个关键词是NEW和OLD:
NEW代表新插入或更新后的那一行数据OLD代表更新或删除前的那一行数据
INSERT 只有 NEW,DELETE 只有 OLD,UPDATE 同时有 NEW 和 OLD。比如我们想记录订单金额变更前后的差异,可以这样写:
CREATE TRIGGER trg_order_amount_log AFTER UPDATE ON t_order FOR EACH ROW BEGIN INSERT INTO t_order_amount_log(order_id, old_amount, new_amount, change_time) VALUES (NEW.id, OLD.amount, NEW.amount, NOW()); END;这样每次t_order表的 amount 被更新后,都会自动在日志表里插入一条记录。你不用改业务代码,不用提心吊胆地担心漏记,触发器自己就会完成。
4.2 三个经典的触发器实战场景
第一个场景是审计日志。金融、电商系统里任何核心表的数据变更都要留痕。用触发器做审计日志的好处是逻辑集中,无论你从哪里发起 UPDATE,都会被记录下来,不会因为某个接口忘记写日志而漏掉。
第二个场景是级联更新。比如商品表和商品销量统计表,业务上下单后要更新统计表的销量。如果每次都在下单逻辑里手动更新,容易忘,尤其是从多个入口下单时。用触发器在t_order表 INSERT 后统一更新统计表,逻辑就集中在数据库一侧。
第三个场景是数据完整性校验。比如工单系统规定,完结状态的工单不能被再次修改。用 BEFORE UPDATE 触发器直接抛出一个错误:
CREATE TRIGGER trg_prevent_modify_finished_order BEFORE UPDATE ON t_work_order FOR EACH ROW BEGIN IF OLD.status = 'FINISHED' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '已完结工单不允许修改'; END IF; END;这样哪怕有人绕过服务端直接连数据库改数据,也会被数据库层面拦住。这种控制力是只在应用层做校验无法达到的。
4.3 触发器的坑,每一个我都踩过
触发器看着方便,但用不好就是给自己埋雷。我总结几条个人体会最深的。
第一,隐形逻辑会造成“惊喜”。尤其是团队协作时,应用开发者往往不知道表上有触发器,改了数据后莫名多出日志、多改了别的表,排查时懵很久。这个问题的解决方案是:触发器命名规范、代码评审时把触发器清单列出来、在数据库设计文档里注明。
第二,处理性能问题。触发器默认在同一个事务里执行,如果触发逻辑写得很重,等于每次操作都要额外承担一份开销。在高并发写入场景下,触发器里的每一次 UPDATE 或 INSERT 都会拉长主操作的事务时间,隐患相当明显。我见过有人把复杂的汇总计算写进触发器,结果导入一批数据时慢到无法接受,最后改成定时任务才解决。
第三,递归触发风险。A 表的触发器去更新 B 表,B 表的触发器又回来更新 A 表,如果设置不当,可能形成无限循环。虽然可以通过sql_mode和触发机制控制,但排查起来非常痛苦。我个人的建议是尽量让触发器单向执行,不要反向更新回源表。
第四,调试困难。普通 SQL 可以直接执行看结果,触发器是“事件驱动”,难以单步调试。当触发器逻辑出错,往往到数据出问题时才能发现。实话说,触发器只在逻辑简单、变化不频繁的场景下才真正划算;一旦逻辑变复杂,可以优先考虑放到服务端代码里实现。
5. 学习日记实战:订单、库存和审计日志的一次完整设计
前几节分别拆开了看,这里我把存储引擎、索引、触发器三块知识拼到一个完整的业务场景里。用模拟的订单系统来当例子,你会看到它们是怎么配合的。
5.1 建表与索引设计
假设我们有两张核心表:商品表t_product和订单表t_order。商品表存储当前库存,订单表记录每一笔下单明细。
CREATE TABLE t_product ( product_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '商品ID', product_name VARCHAR(64) NOT NULL COMMENT '商品名称', stock_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '剩余库存', version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (product_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表'; CREATE TABLE t_order ( order_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单ID', order_no VARCHAR(32) NOT NULL COMMENT '订单编号', product_id BIGINT NOT NULL COMMENT '商品ID', quantity INT UNSIGNED NOT NULL COMMENT '购买数量', amount DECIMAL(10,2) NOT NULL COMMENT '订单金额', user_id BIGINT NOT NULL COMMENT '用户ID', status TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态: 0已创建 1已支付 2已取消', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (order_id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_status (user_id, status), KEY idx_product_id (product_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';这里几个细节是想清楚了的:订单号用唯一索引uk_order_no保证不重复;高频查询是“查某用户某状态下的订单”,所以建联合索引idx_user_status;订单表只用 InnoDB,因为下单是典型的 OLTP 写操作,事务和行锁缺一不可。t_product表里留了一个 version 字段,是为了在应用层做乐观锁,防止并发下单时库存被超卖。
5.2 用触发器做库存扣减
在真实系统里,下单时扣减库存通常放在一个数据库事务里完成。但为了演示触发器的作用,我们可以设计一个更直接的场景:当订单表插入一条新记录时,自动扣减商品表里对应商品的库存。
CREATE TRIGGER trg_order_after_insert AFTER INSERT ON t_order FOR EACH ROW BEGIN UPDATE t_product SET stock_count = stock_count - NEW.quantity WHERE product_id = NEW.product_id; END;这个触发器在每次订单生成后自动扣库存。逻辑简单,用来演示很有代表性。但必须指出,真实电商系统往往不会只靠触发器扣库存,因为还要考虑库存不足回滚、分布式场景下的可靠性等因素。触发器的价值在于“单库内的强一致”,一旦跨服务、跨数据库,就要引入分布式事务方案了。
5.3 用触发器做审计日志
继续完善这个场景,订单表修改时留审计日志。我们建一张订单操作日志表,并且加一个 AFTER UPDATE 触发器,记录订单状态变化。
CREATE TABLE t_order_log ( log_id BIGINT NOT NULL AUTO_INCREMENT, order_id BIGINT NOT NULL, from_status TINYINT NOT NULL, to_status TINYINT NOT NULL, operate_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (log_id), KEY idx_order_id (order_id) ) ENGINE=InnoDB COMMENT='订单状态变更日志'; CREATE TRIGGER trg_order_status_log AFTER UPDATE ON t_order FOR EACH ROW BEGIN IF OLD.status != NEW.status THEN INSERT INTO t_order_log(order_id, from_status, to_status) VALUES (NEW.order_id, OLD.status, NEW.status); END IF; END;这里加了一个IF OLD.status != NEW.status的判断,避免订单金额等其他字段被更新时也写一条无意义的日志。这样的细节,正是实际开发中优化触发器性能的关键思路——能从源头减少无效数据写入。
5.4 这一套设计里值得反复琢磨的点
整个设计看起来顺理成章,但有个问题值得琢磨:触发器和索引竟然也有关联。订单表里建立了idx_product_id,所以在订单表插入时 MySQL 需要维护这个二级索引树;触发器更新库存时,t_product表的主键索引也要跟着更新。索引越多,写入时的维护成本越高,这正好呼应了前面说的“不要堆索引”。一个表里 3 到 5 个索引往往是性价比最高的范围,过多反而拖慢写入。
另外,触发器执行失败时,默认会导致主操作回滚。也就是说trg_order_after_insert里库存更新失败,INSERT INTO t_order也会失败。这在不经意间保证了订单和库存的一致性,属于触发器的隐藏性质,理解这一点对排查线上问题非常有帮助。
6. 面试题速查:存储引擎、索引、触发器怎么答
DAY09 学习清单里,面试对应试是绕不开的。我把今天学的三块内容整理成一份速查表,方便后续复习时一眼看清答题骨架。
| 常见面试题 | 答题要点 |
|---|---|
| InnoDB 和 MyISAM 的区别 | 事务、行锁 vs 表锁、外键、聚簇索引、崩溃恢复能力 |
| 为什么 InnoDB 使用 B+ 树而不是 B 树或哈希 | B+ 树非叶子节点不存数据、层数矮、磁盘 IO 少、叶子节点链表支持范围查询 |
| 什么是聚簇索引,什么是二级索引 | 聚簇索引叶子节点存完整行,二级索引叶子节点存主键 |
| 什么是回表 | 使用二级索引查到主键后,再到聚簇索引取整行 |
| 覆盖索引是什么 | 查询字段全部包含在索引中,不需要回表 |
| 联合索引最左前缀原则 | B+ 树按索引字段从左到右排序,跳过前导字段无法高效定位 |
| 哪些情况会导致索引失效 | 模糊匹配前缀%、隐式类型转换、函数操作、OR 连接非索引列、!=等 |
| 触发器有哪些使用场景 | 审计日志、级联更新、数据完整性校验、防止非法操作 |
| 触发器的缺点 | 隐形逻辑、性能损耗、调试困难、可能递归触发 |
| 存储过程、触发器、事件三者区别 | 存储过程手动调用,触发器自动触发,事件按时间调度 |
对于前端的同学可能没接触过数据库管理,我想强调一句:面试时不需要把每个机制都背到源码级别,但必须要能画出一条逻辑链。比如面试官问索引为什么快,你能答出“数据量大的时候遍历太慢,B+ 树把查找次数压缩到树高级别的磁盘 IO,所以快”,这就够用了。难点恰恰在于把知识连接起来,而不是背孤立的知识点。
7. DAY09 的最后一公里:把这套知识真正用起来
到这里,存储引擎、索引、触发器的基础知识基本过了一遍。但“学完”和“会用”之间还有一条不小的沟。我把 DAY09 实操过程中觉得最有价值的验证方法分享出来。
动手建一张至少 100 万行的测试表,分别试下全表扫描、主键查询、二级索引查询、联合索引查询,每次都用EXPLAIN看执行计划。真实数据量下的体验,远远比背几条规则来得扎实。比如你会看到在没有索引时EXPLAIN的rows直接等于表里的总行数,而加了索引后下降到几十行,直观感受到为什么一切优化都要围绕索引展开。
触发器一定要亲手写一个带SIGNAL报错的 BEFORE UPDATE 例子,体会下“数据库拒绝请求”是什么感觉。然后用一个日志表验证 AFTER INSERT 的自动写入。我的经验是:代码写得再熟,没有实际执行一遍,还是很难理解 NEW 和 OLD 在不同事件里到底保留了哪一侧的数据。
这一天的学习从“背概念”开始,到“连成一条线”结束。我现在再看一张建表语句时,脑子里会自动浮现出存储引擎决定行级锁粒度、主键索引决定数据物理排序、联合索引影响高频查询路径、触发器帮我在数据库层兜底的数据流画面。提升学习效率最实在的方式,不是多看几篇博客,而是让知识点之间形成互动——希望这份 DAY09 学习笔记也能帮你建立起同样的一根线。