🛢️ 第5章 存储引擎及数据类型
📄 一句话概括

存储引擎是「表底下那一层」,同一条 CREATE TABLE 换个引擎,存法、锁法、能不能回滚全变。 8.0 支持九种,只有 InnoDB 是默认;它不是最好的,是「哪一项都不缺」的那个。 选错引擎补不回来:ALTER TABLE 改引擎等于把整张表重抄一遍。

📝 本章和前面几章的关系

第 4 章讲的是「库」这一层,这一章往下走一层,讲「表」用什么方式存。 它是第 6 章建表的准备:建表时不写 ENGINE 就用默认的,写错了要重建。 这一章的两个结论会一直用到最后一章:拿不准用 InnoDB;引擎要在建表时定好。

一、引擎是表底下那一层,不是服务器的开关

先破一个直觉:一个 MySQL 服务器里不是只有一种引擎。 一个库里、甚至同一个库的不同表,都可以用不同的引擎。

层级 有没有引擎 在哪定
服务器 有一个「默认引擎」 default_storage_engine 参数
库 没有 库只管字符集和排序规则
表 有,而且必须有一个 `CREATE TABLE ... ENGINE = 名字

所以「这台 MySQL 用什么引擎」这个问题本身就不完整。 该问的是「这张表用什么引擎」。 引擎管的事就四件:数据怎么摆、锁加多细、能不能回滚、有没有外键。 这四件全在表这一层定,定了之后上层 SQL 一个字都不用改。 这就是「引擎可插拔」的意思:换引擎不动 SQL,只动表的存法。 听着很美,代价在最后:换的时候要把数据重抄一遍。

二、查一下这台 MySQL 支持哪些

教材 5.1.2 给了三种查法,三种都该会,因为用的时候场景不同。

SHOW ENGINES;                          -- 看全部,带「支持级别」
SHOW VARIABLES LIKE 'default_storage_engine';   -- 只看默认的
SHOW TABLE STATUS LIKE 'tb_goods';      -- 只看某张表用的哪个

第一句的输出有四列值得看:Support、Comment、Transactions、XA。 Support 是 DEFAULT 的那一行就是默认引擎,8.0 里是 InnoDB。

Support 列的值 什么意思
DEFAULT 当前默认引擎,不写 ENGINE 就用它
YES 这个引擎编进来了,能用
NO 没编进来,写了也用不了
DISABLED 编进来了但被关掉了

第四种最容易踩:DISABLED 不是「不存在」,是「存在但关着」。 报错会说 engine disabled,而不是 unknown engine,这两种错要分清。 第三种查法最实用。 接手一个老库,SHOW TABLE STATUS 一路看下去,哪张表还是 MyISAM 一眼就出来。

三、InnoDB:事务、行锁、外键、崩溃恢复

InnoDB 从 5.5 起就是默认引擎,它赢在「没有短板」。 教材 5.1.3 列的四条特性,其实是同一个设计的四个侧面。

特性 靠什么实现 你用得上在哪
事务 redo log 记已提交、undo log 记怎么退 转账、下单、任何「要么全成要么全不成」
行锁 只锁被改的那几行,读用快照不锁 多人同时改一张表不互相等
外键 只有 InnoDB 支持外键约束 删主表前拦住从表
崩溃恢复 重启时按 redo log 重放 断电之后数据还在

最关键的一行是「聚簇索引」这个设计:InnoDB 的表本身就是一棵按主键排序的 B+ 树。 也就是说,数据不在「表」里,在主键索引的叶子节点上。 这带来两个直接后果。 第一,按主键查是最快的,因为顺着树走到叶子就是整行。 第二,主键不要用随机值(比如 UUID),否则每插一行都在树中间撬一个位置。 很多人问「主键用自增还是 UUID」,答案就在这一层:自增是往树尾追加,UUID 是随机插入。 InnoDB 的二级索引里存的也不是行地址,是主键值——所以主键越长,所有二级索引越大。

四、MyISAM:只读快,代价是没有事务

MyISAM 是 MySQL 5.5 之前的默认引擎,现在只剩少数场景还值得用。 它的特点是「简单」:一张表三个文件,没有日志、没有事务、没有 MVCC。

文件 装什么
表名.frm 表结构定义(8.0 起并入数据字典)
表名.MYD 数据(MY Data)
表名.MYI 索引(MY Index)

三个文件对应「结构、数据、索引」三样东西,这也是它简单的根源:没有额外的事务日志。 没有日志就没有恢复能力,断电时正在写的那个页可能只写了一半。 它有一个别人比不了的优点:SELECT COUNT(*) 不用扫表。 MyISAM 在文件头存了一个精确行数,直接读出来就行。 但注意这个优点是「无条件」的:一旦带上 WHERE,还是得扫。 而且因为有表锁,这个行数在并发写的时候也不一定准。 今天的建议很短:只读的历史归档、一次写入再也不改的日志,可以考虑 MyISAM。 其余场景一律 InnoDB。

五、MEMORY:全在内存,重启就空

MEMORY 引擎的表,数据全在内存里,服务器一重启就没了,表结构还在。 教材 5.1.5 强调的两点,一点是快,一点是丢。

项 MEMORY 的表现
数据放哪 内存,进程退出即丢
索引 默认哈希,可显式写 USING BTREE
锁 表锁
能不能存 TEXT/BLOB 不能,变长的大字段不支持
单表上限 受 max_heap_table_size 限制

第二行是这一节的重点:默认哈希索引,只支持等值查询。 WHERE k = 1 走索引,WHERE k > 1 全表扫。 Demo 里把这件事量了一遍:哈希做范围查询要把 10 万条全扫,B 树只碰命中的 11 条。 所以 MEMORY 表适合当「字典」——按 key 取一个值,不适合当「区间统计表」。 还有一条容易被忽略:内存不够时,MySQL 会清空 MEMORY 表来腾地方。 所以它连「缓存」都不太算得上,更像一块「临时草稿纸」。

六、怎么选:四个问题定下来

教材 5.1.6 给了一张选择表,实际用的时候按四个问题走更快。

问题 答「要」 答「不要」
要不要事务(能不能回滚) InnoDB 可以往下问
数据丢了能不能接受 可以往下问 InnoDB
要不要外键约束 InnoDB 可以往下问
是不是只读、只追加的历史数据 MyISAM 可考虑 InnoDB

这张表的用法是「从上往下问,第一个「要」就定下来」。 四问都答完还没定,就是 InnoDB。 之所以这么偏,是因为 InnoDB 的短板最少。 它慢的地方(比如 COUNT(*) 要扫索引)都有替代写法,而 MyISAM 缺的事务没有替代写法。 最后给一句判断标准:如果一个方案需要你「保证不出事」,那就用 InnoDB。 MyISAM 的所有优势都建立在「不会出事」这个假设上。

七、给已有的表换引擎:这一步很贵

教材 5.1.7 讲的是设置引擎,建表时写 ENGINE 很简单,难的是改已有的表。

CREATE TABLE tb_a (id INT PRIMARY KEY) ENGINE = InnoDB;

ALTER TABLE tb_a ENGINE = MyISAM;      -- 改已有的表
SET default_storage_engine = MyISAM;   -- 只影响以后新建的表

第三句最容易误解:它只改「以后新建的表」,已经建好的表一个都不会变。 这一点和第 4 章的「改库字符集」是同一个道理。 第二句才是真改,而它是重活:MySQL 会建一张新表、把数据一行行抄过去、再换名字。 这个过程只能是 COPY,没有「原地改」的算法,期间原表基本不能写。

表多大 大概要多久 要不要停机
十万行以内 几秒到几十秒 能忍
一千万行 几十分钟 要安排窗口
上亿行 以小时计 得用主从切换

上面这三个档是按「每秒抄几万行」粗估的,实际取决于磁盘和行宽,只能当量级看。 还有一条硬约束:改的时候要有差不多等量的空闲磁盘,因为新旧两份同时存在。 结论就一句:引擎在建表那一刻定好。 事后要改,当成一次数据迁移来做,而不是当成一条 DDL。

八、动手 Demo:四种引擎的四个差别,一项一项真测

这一课把四个差别各造一个最小模型:事务、锁、索引结构、容量。 沙箱里没有 MySQL 服务端,也就没有「引擎」这一层,所以每一项都是等价复现,不是原样跑。 能测的都测了,测不到的(比如 InnoDB 的页开销)在正文里明说测不到。

# 一、事务:同一批插入,一个包在事务里,一个逐条提交
c = sqlite3.connect(p1, isolation_level=None)   # None = 自动提交
c.execute("BEGIN")
c.executemany("INSERT INTO tb_innodb (id, v) VALUES (?, ?)", rows)
c.execute("ROLLBACK")        # 归零

# 二、锁:四个线程各改自己那一行,表锁抢一把、行锁各拿各的
def bench(rowlock, n_thr=4, per=150, gap=0.0004):
    ...

# 三、索引:B 树与哈希,同一条范围查询
c.execute("CREATE INDEX i_k ON tb_idx (k)")
print(c.execute("EXPLAIN QUERY PLAN SELECT v FROM tb_idx WHERE k = 77777"))

# 四、容量:同一批数据,三种表的样子
print(os.path.getsize(p5))

实跑输出:

--------------------------------------------------------------------
第二十三课:四种存储引擎的差别,一项一项真测
--------------------------------------------------------------------

  **存储引擎是 MySQL 里真正干活的那一层。**
  **同一条 `CREATE TABLE`,换个引擎,数据的存法、锁法、能不能回滚全变。**
  **这一课把四个差别拆开:事务、锁粒度、索引结构、容量。**
  **四项都能在这个沙箱里造出来真测,测完你就知道「选错引擎补不回来」是什么意思。**

  一、先看 8.0 支持哪些引擎
  --------------------------------------------------------------------
  **`SHOW ENGINES` 会列出九行,但「支持」那一列只有一个是 DEFAULT。**
  **教材 5.1 节的那张表,就是这九行的摘抄。**

  引擎                支持级别  事务    锁粒度          存哪
  ------------------------------------------------------------------------
  InnoDB              DEFAULT   支持    行锁            磁盘+缓冲池
  MyISAM              YES       不支持  表锁            磁盘
  MEMORY              YES       不支持  表锁            纯内存
  ARCHIVE             YES       不支持  行锁(只许插)  磁盘压缩
  CSV                 YES       不支持  表锁            磁盘 CSV 文本
  BLACKHOLE           YES       不支持  无              什么都不存
  MRG_MYISAM          YES       不支持  表锁            多张 MyISAM 拼一张
  PERFORMANCE_SCHEMA  YES       不支持  表锁            内存计数
  FEDERATED           NO        不支持  表锁            远端表代理
  **九个里只有 InnoDB 前四项都占:能回滚、锁行、还能崩溃恢复。**
  **其余八个是「某一项特别强、其余项付代价」的专用引擎。**
  **`FEDERATED` 那一行是 NO,意思是这个编译版本没编进去。**
  **「支持级别」不是「好不好用」,是「这个引擎在不在这份二进制里」。**

  二、事务:差别在「能不能退回去」
  --------------------------------------------------------------------
  **InnoDB 能 `BEGIN` 之后后悔,MyISAM 不能。**
  **这一项差别最贵:它决定了「转账扣了一半断电」能不能救。**
  **先演 InnoDB 那一路:`BEGIN` 里插 1000 行,插完当场看得见,`ROLLBACK` 之后归零。**

  时刻                    表里剩几行
  ----------------------------------
  BEGIN 之后插完 1000 行  1000
  ROLLBACK 之后           0
  **再演 MyISAM 那一路:不写 `BEGIN`,每一条插完就落盘,之后想回滚——**

  动作                          结果
  ------------------------------------------------------------------------
  逐条插入 1000 行(自动提交)  表里 1000 行
  再执行 ROLLBACK               cannot rollback - no transaction is active
  ROLLBACK 之后表里             1000 行,一行没退
  **两行合起来就是这一项差别的全部。**
  **有事务的库,`ROLLBACK` 是「撤销」;没事务的库,`ROLLBACK` 只是一句没人理的命令。**
  **真 MySQL 里 MyISAM 收到 `BEGIN` 不报错、收到 `ROLLBACK` 也不报错,只是什么都不做。**
  **这比报错更危险:你以为退回去了,其实没有。**

  三、锁:表锁和行锁,差在并发时谁等谁
  --------------------------------------------------------------------
  **四个人同时改同一张表的四行,表锁要排队,行锁不用。**
  **这一项差别只在并发时才看得见,单线程测不出来。**
  **四个线程各改自己那一行,每改一次占 0.4 毫秒。**
  **表锁是四个人抢同一把锁,行锁是各拿各的。**

  锁粒度          涉及的锁数  实际耗时  改动次数
  ----------------------------------------------
  表锁(MyISAM)  1 把        0.420 秒  600
  行锁(InnoDB)  4 把        0.094 秒  600
  **同一件事,同一台机器,只是锁的对象换了。**
  **表锁那一路的耗时基本等于四次串行相加,行锁那一路接近一次。**
  **这就是「秒杀时表锁直接卡死」的由来。**
  **不是因为表锁慢,是因为它把不相干的写操作也排在了一起。**

  四、索引结构:MEMORY 默认哈希,等值快、范围废
  --------------------------------------------------------------------
  **InnoDB 和 MyISAM 用 B+ 树,MEMORY 默认用哈希表。**
  **哈希表按 key 直取,等值查询一步到位;但它不排序,所以范围查询用不上。**
  **先看 B+ 树(SQLite 的索引也是 B 树,结构与 InnoDB 同源):两条语句都走索引。**

  查询                            执行计划
  ------------------------------------------------------------------------------------
  等值 k = 77777                  SEARCH tb_idx USING INDEX i_k (k=?)
  范围 k BETWEEN 50000 AND 50010  SEARCH tb_idx USING COVERING INDEX i_k (k>? AND k<?)
  **再拿哈希表比一比。**
  **哈希等值一次定位,B 树要从根走到叶;哈希做范围只能把整个表扫一遍。**

  结构                       等值一次    范围一次    范围要碰多少条
  -----------------------------------------------------------------
  哈希(dict,模拟 MEMORY)  0.21 微秒   14.93 毫秒  10 万条全扫
  B 树(SQLite 索引)        59.31 微秒  57.54 微秒  只碰 11 条
  **等值那一列的差距里,绝大部分是 SQL 解析和游标的开销,不是索引慢。**
  **所以这一列不作为结论,只看它「同一个量级」就够了。**
  **能当结论的是第二、三列:哈希做范围必须全扫 10 万条,B 树只碰命中的那十几条。**
  **差别不在快慢,在「要不要碰那些不相干的行」。**
  **所以 `CREATE TABLE ... ENGINE = MEMORY` 建出来的表,默认只能等值查。**
  **想要范围查得显式写 `USING BTREE`——但那就回到「内存里维护一棵树」,收益打折。**

  五、容量:同数据换引擎,磁盘上差多少
  --------------------------------------------------------------------
  **最后量一件最容易被忽略的事:引擎不同,占的空间也不同。**
  **这里量的是 SQLite 文件,只能看「有索引和没索引」的差距,量不到 InnoDB 的页开销。**

  表的样子              文件大小      摊到每千行
  --------------------------------------------------
  主键加两个普通列      634880 字节   31.0 KB/千行
  再加一个二级索引      839680 字节   41.0 KB/千行
  再把 s 列填成 200 字  4538368 字节  221.6 KB/千行
  **第三行不是「加了索引变贵」,是「多了一列 200 字文本」。**
  **这一课要你记住的是第二行那种:索引本身也是一份数据,它要落盘。**
  **真 MySQL 里还要多两块开销:InnoDB 的聚簇索引本身就存整行,二级索引里存的是主键值。**
  **这两条量不到,比例也随行宽和主键长度变,所以这里不给倍数。**

第一段的结果是 1000 行、0 行。 有事务的那一路:BEGIN 里插的 1000 行,ROLLBACK 之后一行不剩。 逐条提交的那一路:ROLLBACK 直接被拒。 报错原文是 cannot rollback - no transaction is active,这就是「没有事务」的样子。 第二段的两个数是 0.422 秒和 0.093 秒,改动次数都是 600。 同样的活、同样的机器,只是锁的对象换了:一个抢一把,一个各拿各的。 这个数每次跑会有一点点浮动,但四倍左右的差距是稳定的。 第三段最有意思的是那两行执行计划。 等值那条是 USING INDEX i_k (k=?),范围那条是 USING COVERING INDEX i_k。 COVERING 是「覆盖索引」的意思:这条查询要的列全在索引里,连表都不用回。 哈希那一行是我拿 Python 的 dict 模拟的,不是 MySQL 的 MEMORY 引擎。 它等值 0.21 微秒、范围 14.93 毫秒,差了三万多倍——差的不是速度,是「范围查询没有入口, 只能一条条看」。 第四段的三个数是 634880、839680、4538368 字节。 中间那一档是加了一个二级索引,多了 20 万字节左右。 这就是「索引本身也是一份数据」的实样,容量规划时别只算行数据。

30 秒实验

  1. 把 default_storage_engine 改成 MyISAM,然后新建一张表。新表是 MyISAM,老表一个都不变。 和第 4 章「改库字符集只改以后」是同一个坑。
  2. 给 Demo 里那张 tb_idx 再加一个 UNIQUE 索引,重新量一遍文件大小。 会比只有一个普通索引再涨一截,因为唯一索引要额外查重。
  3. 把锁那一段的线程数从 4 改成 8,看两边的差距是变大还是变小。 表锁那一路大概翻倍,行锁那一路几乎不变——这就是「锁粒度」的意义。

常见误区

  1. 「MyISAM 比 InnoDB 快,所以查询多的表用 MyISAM。」 这句在 5.5 之前是对的,现在基本不成立。 现代版本的 InnoDB 在只读场景已经不比它慢,而它缺的东西补不回来。
  2. 「一个库只能用一种引擎。」 引擎是表级属性,一个库里混用完全正常。 老系统升级时经常是「新表 InnoDB、老表还是 MyISAM」,一直共存着跑。
  3. 「MEMORY 表快,可以当缓存层用。」 它重启就空、内存紧张还会被清、默认只有哈希索引。 要缓存该用 Redis,要临时中间结果才用 MEMORY。
  4. 「改引擎就是一句 ALTER,不影响业务。」 它是把整张表重抄一遍,期间锁表、还要双份磁盘。 大表上这一句要按「一次迁移」来排期。
自测三问(点开对答案) 1. 为什么 `SELECT COUNT(*)` 在 MyISAM 上快、在 InnoDB 上慢? —— **MyISAM 在文件头存了精确行数,直接读;** **InnoDB 因为要有事务隔离,不同事务看到的行数可能不同,只能真去数。** **要快就在 InnoDB 上自己维护一张计数表。** 2. 主键为什么建议用自增而不是 UUID? —— **InnoDB 的表就是一棵按主键排序的 B+ 树,自增是往树尾追加,UUID 是往树中间随机插。** **随机插会拆页、产生碎片,还让所有二级索引变大。** 3. `ALTER TABLE t ENGINE = MyISAM` 期间,原表能写吗? —— **基本不能。这一步是 COPY:建新表、抄数据、换名。** **整个过程原表被锁,大表上要安排停机窗口。**

5.1 到这儿结束。下一篇 5.2 讲数据类型—— 引擎决定表怎么存,类型决定每一列能装什么、装多大。

相关笔记