mysql < 备份文件 能不能还原,取决于脚本里有没有建库语句。
回放 SQL 时表的顺序靠表名排,有外键的库会因此失败。
「还原成功」不算数,行数、合计、integrity_check 三样都对上才算数。
这一篇讲恢复。教材 18.2 有两小节:用 mysql 命令还原、直接复制到数据库目录。
恢复比备份更容易出问题,因为它有「看起来成功了但其实没成」这种状态。
所以这一篇的重心在后半段:还原完怎么验。
和第 14~17 章有个呼应:那些「库内代码」(过程、触发器、事件)默认不在备份里,
恢复时自然也补不回来。
一、mysql < 备份文件:教材的写法
mysql -u root -p < shop.sql
mysql -u root -p shop < two_tables.sql
SOURCE /backup/shop.sql;
第一行能还原,是因为脚本里带了 CREATE DATABASE 和 USE(也就是当初用了 --databases)。
第二行要求你先 CREATE DATABASE 再 USE,否则表会建到当前库里——而且不报错。
「不报错」这三个字是这一节最要紧的地方。
表建成功了、数据也插进去了,一切看起来正常,只是库不对。
要到几天后有人问「shop 里怎么没数据」,才发现灌到 mysql 库里去了。
第三行是客户端里的写法,效果一样,只是不用退出 mysql。
它和 < 重定向的区别在于:SOURCE 是客户端命令,< 是 shell 重定向。
所以脚本里用 <,交互时用 SOURCE。
二、回放 SQL:顺序靠表名
逻辑备份的回放就是「按顺序执行脚本里的每一条语句」。
没外键的库,顺序无所谓;有外键的库,顺序就是生死。
演示里建了一张子表 z_child 引用父表 z_parent,dump 出来是这样的顺序:
CREATE TABLE z_child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES z_parent(id));
CREATE TABLE z_parent (id INTEGER PRIMARY KEY);
子表在前、父表在后——因为 iterdump 按表名字母序排,z_child 恰好在 z_parent 前面。
外键开着回放,直接报 no such table: main.z_parent。
换句话说:这个脚本「有没有问题」取决于表名,不取决于依赖关系。
改个名字(比如 a_child)就成功了,再改回来又失败——这种不确定性最难查。
那为什么第一次试的时候成功?因为外键默认是关的。
关着的时候这条 REFERENCES 只是一句注释,建表顺序就无所谓了。
所以「还原成功」这件事,得先问一句「外键开着吗」。
mysqldump 在这一点上比 iterdump 讲究:它会先 SET FOREIGN_KEY_CHECKS=0,还原完再打开。
两边都用「临时关掉检查」这一招,只是 MySQL 是显式写的,SQLite 是默认关的。
显式写的好处是「你知道自己关了什么、也知道什么时候该打开」。
三、拷文件还原:版本必须对得上
物理备份还原就是把文件拷回去。它比回放 SQL 快得多,代价是「引擎版本必须一致」。 这不是约定,是写死在文件头里的:SQLite 的库文件前 16 个字节是固定魔数,第 18、 19 字节是「读写这个文件需要的格式版本」。
| 位置 | 内容 | 这次读到的 |
|---|---|---|
| 前 15 字节 | 魔数 | SQLite format 3 |
| 第 18 字节 | 读格式版本 | 1 |
| 第 19 字节 | 写格式版本 | 1 |
这两个字节就是物理备份的「版本声明」。引擎打开文件时会读它,高了就拒绝。 所以「拿新版本的库文件喂旧版本的引擎」必然失败,跟文件有没有坏没关系。 MySQL 也一样:8.0 的数据目录给 5.7,起来就报错,日志里一堆「表空间版本不对」。 这类错误看着吓人,其实原因很单纯:文件格式不兼容。 知道这一点有个实际用处:拿到一份来路不明的备份文件,先看版本号, 再看数据——顺序反了会白折腾半天。
四、还原之后必须验:三件事
「还原成功」这句话本身没有信息量。要验的是「还原出来的东西对不对」。 三样东西一起看:表数、行数、一个聚合值。
| 检查项 | 原库 | 还原后 |
|---|---|---|
| 表数 | 2 | 2 |
tb_g 行数 |
3 | 3 |
| 金额合计 | 12330.0 | 12330.0 |
三行全对,这一份备份才算真的能用。
只验行数不够:行数对、内容错的备份是存在的。
最常见的一种是「字段顺序错了」——CSV 导入时列对不上,name 灌进了 price,行数一条不少,
内容全乱。
加一行「合计」这种聚合校验,能挡住大部分「看着对其实不对」。
再加一条 PRAGMA integrity_check(MySQL 是 CHECK TABLE),它查的是页结构。
它和业务校验的分工很明确:一个查「文件有没有坏」,一个查「内容对不对」。
两个都跑,覆盖面才够。
工程上的做法是把这三件事写进还原脚本,跑完自动打印。
让「验」变成流程的一部分,而不是「记得的话就看一下」——靠人记的事情,迟早会漏。
五、动手 Demo:回放 SQL、外键顺序、文件版本、还原校验
这一段四件事:先把 dump 回放一遍看行数对不对;
再建一个有外键的库,看 iterdump 的顺序会不会让回放失败;然后读一下库文件头的版本字节;
最后把「还原后该验什么」跑成一张表。
import sqlite3
# ① 正常回放
src = sqlite3.connect(u"shop.db", isolation_level=None)
dump = u"\n".join(src.iterdump())
dst = sqlite3.connect(u"restore.db", isolation_level=None)
dst.executescript(dump)
print(dst.execute(u"SELECT count(*) FROM tb_g").fetchone()) # (3,)
# ② 有外键时:顺序按表名排,可能先建子表
fk = sqlite3.connect(u"fk.db", isolation_level=None)
fk.executescript(u"""
CREATE TABLE z_child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES z_parent(id));
CREATE TABLE z_parent (id INTEGER PRIMARY KEY);
INSERT INTO z_parent VALUES (1);
INSERT INTO z_child VALUES (1, 1);
""")
d2 = u"\n".join(fk.iterdump())
r2 = sqlite3.connect(u"fk_restore.db", isolation_level=None)
r2.execute(u"PRAGMA foreign_keys = ON") # 默认是关的
try:
r2.executescript(d2)
except sqlite3.Error as e:
print(e) # no such table: main.z_parent
# ③ 物理备份的版本声明藏在文件头
with open(u"shop.db", u"rb") as f:
head = f.read(20)
print(head[:15].decode(), head[18], head[19]) # SQLite format 3 1 1
# ④ 还原后必须验的三件事
print(dst.execute(u"SELECT count(*) FROM sqlite_master WHERE type='table'").fetchone())
print(dst.execute(u"SELECT count(*) FROM tb_g").fetchone())
print(dst.execute(u"SELECT sum(price*stock) FROM tb_g").fetchone())
print(dst.execute(u"PRAGMA integrity_check").fetchone())
实跑输出:
--------------------------------------------------------------------
数据恢复:还原之后必须验
--------------------------------------------------------------------
mysql < 备份文件:教材的写法
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| mysql -u root -p < shop.sql
| mysql -u root -p shop < two_tables.sql
| SOURCE /backup/shop.sql;
沙箱里没有 mysql 客户端,这三行是教材原码。
**第一行能还原,是因为脚本里带了 `CREATE DATABASE` 和 `USE`。**
**第二行要求你先 `CREATE DATABASE` 再 `USE`,否则表会建到当前库里。**
**这个区别就是 18.1 里那个 `--databases` 参数的分量。**
iterdump 还原:能还原,但顺序靠表名
--------------------------------------------------------------------
回放 -> 成功
表:tb_c、tb_g,tb_g 3 行
**这次成功。但成功不代表脚本是对的——换一个有外键的库再试。**
**建一张子表 `z_child` 引用父表 `z_parent`,外键打开,再 dump 再回放:**
dump 里的顺序:
CREATE TABLE z_child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES z_parent(id));
CREATE TABLE z_parent (id INTEGER PRIMARY KEY);
外键开着回放 -> no such table: main.z_parent
**报的是 `no such table: main.z_parent`——子表先建,父表还没建。**
**`iterdump` 是按表名字母序排的,`z_child` 在 `z_parent` 前面,纯属巧合。**
**换句话说:这个脚本「有没有问题」取决于表名,不取决于依赖关系。**
**那为什么刚才第一次试的时候成功?因为外键默认是关的。**
**关着的时候这条 `REFERENCES` 只是一句注释,建表顺序就无所谓了。**
**所以「还原成功」这件事,得先问一句「外键开着吗」。**
**`mysqldump` 在这一点上比 `iterdump` 讲究:它会先 `SET FOREIGN_KEY_CHECKS=0`,还原完再打开。**
**两边都用「临时关掉检查」这一招,只是 MySQL 是显式写的,SQLite 是默认关的。**
拷文件还原:能还原,但版本必须对得上
--------------------------------------------------------------------
**物理备份还原就是把文件拷回去。它比回放 SQL 快得多,代价是「引擎版本必须一致」。**
**SQLite 的库文件前 16 个字节是固定的魔数,第 18、19 字节是「写这个文件需要的格式版本」:**
魔数 SQLite format 3
读格式版本 1,写格式版本 1
**这两个字节就是物理备份的「版本声明」。引擎打开文件时会读它,高了就拒绝。**
**所以「拿新版本的库文件喂旧版本的引擎」必然失败,跟文件有没有坏没关系。**
**MySQL 也一样:8.0 的数据目录给 5.7,起来就报错,日志里一堆「表空间版本不对」。**
还原之后必须验:三件事
--------------------------------------------------------------------
检查项 原库 还原后
---------------------------
表数 2 2
tb_g 行数 3 3
金额合计 12330.0 12330.0
`PRAGMA integrity_check` -> ok
**三行数字全对,这一份备份才算真的能用。**
**只验行数不够:行数对、内容错的备份是存在的(比如按错误的字段顺序灌回去)。**
**加一行「合计」这种聚合校验,能挡住大部分「看着对其实不对」。**
**`integrity_check` 查的是页结构,不是业务内容。两个一起用,覆盖面才够。**
第②步那个报错是整段最有价值的一行:no such table: main.z_parent。
它看起来像「表丢了」,其实是「建的顺序不对」。
报错信息指向表,不指向顺序——这就是它难查的原因。
而它更难查的地方在于:外键默认关着。
同一份脚本,关着外键能跑通、开着就跑不通,换台机器、换个客户端,结果可能不一样。
第③步那三个值(SQLite format 3、1、1)是物理备份的「身份证」。
以后拿到一个打不开的库文件,先读这三个值,比看日志快。
第④步四行输出,前三行是业务校验、最后一行是结构校验。
注意 integrity_check 返回的是 ok 这个字符串,不是数字——它不告诉你「有几处坏」,
只告诉你「有没有坏」。
30 秒实验
- 把
z_child改名成a_child,不设外键时回放一次、设外键时回放一次,两种情况各记一个结果。 - 把
iterdump的输出按「先父表后子表」手工重排,再回放一次,确认外键开着也能成功。 - 故意把
restore.db里的tb_g删掉两行,再跑一遍那四条校验,看哪一条会先暴露问题。
常见误区
- 以为还原脚本报「成功」就万事大吉。表可能建到了错误的库里,也可能只还原了一半, 这两种都不报错。
- 以为
REFERENCES写在外键开着和关着是一样的。关着的时候它只是一句注释, 建表顺序完全无所谓;开着就会校验。 - 以为物理备份的文件能跨版本用。文件头里有格式版本号,高了旧引擎直接拒绝。
自测三问(点开对答案)
1. `mysql shop < two_tables.sql` 和 `mysql < shop.sql` 差在哪? —— 后者脚本里带 `CREATE DATABASE` 和 `USE`,能自己定位到正确的库; 前者要求你先建库、先 `USE`,否则表会建到当前库里,而且不报错。 2. 回放 SQL 时为什么可能因为外键失败? —— 因为脚本里表的顺序是「按名字排」而不是「按依赖排」。 子表排在父表前面时,建子表就会因为父表不存在而失败。 3. 还原之后至少要验哪三样? —— 表数、行数、一个聚合值(比如金额合计)。再加一条 `integrity_check` 查页结构。 只验行数不够,因为「行数对、内容错」是真实存在的。备份和恢复都讲完了。下一页讲迁移:同一套动作,但要求不一样。
- ☐ (本篇完,下一篇 18.3 数据库迁移)