迁移就是「备份加恢复」,但多了两个约束:不能丢东西、不能改行为。
同版本拷文件最快,但要过四道关:版本、字符集、停服务、存储引擎。
跨库跨版本只能走文本,而且 mysqldump 默认不导「库内代码」,得加参数。
这一篇讲迁移。教材 18.3 分两小节:相同版本之间的迁移、不同数据库之间的迁移。 它和备份恢复用的是同一套动作,但目标不一样:备份是「留一份以防万一」, 迁移是「搬过去继续用」。 目标不一样,验收标准就不一样——迁移完了要能跑,不只是「数据在」。 这一篇和第 14~17 章关系很紧:那些库内代码(过程、触发器、事件)默认不跟着走。
一、同一版本:拷文件最快,但要过四道关
教材给的办法就是拷数据目录。这条路最快,但它有几个隐含条件,少一个就出问题:
| 条件 | 不满足会怎样 |
|---|---|
| 版本一致 | 引擎拒绝打开,或读出错乱数据 |
| 字符集配置一致 | 中文变问号,或索引比较结果不对 |
| 停服务 | 拷到一半的数据页,还原后库损坏 |
| 存储引擎一致 | InnoDB 文件给 MyISAM 用,直接打不开 |
四条里最难查的是第二条:字符集。它不报错,只是把数据悄悄改了。 所以跨库迁移的正确做法是「用文本走一遍」,让字符集在文本层面对齐。 前两条是「硬失败」——打不开、报错,一眼就知道有问题。 后两条里,「停服务」是「可能失败」,「字符集」是「一定出问题但一定不报错」。 这个分类挺有用:迁移完先看有没有报错(能挡住前两条),再抽查几条中文(挡第三条), 最后比对一下行数合计(挡第四条)。 每一步都有明确的观测对象,不用瞎猜。
二、跨库跨版本:走 dump 文本
逻辑备份的好处在这里体现:文本是通用的,谁都读得懂。 代价是慢,而且有些东西文本表达不了。
mysqldump -u root -p --routines --triggers --events shop > shop.sql
这三个参数对应第 14~17 章讲的东西:存储过程、触发器、事件。
不加它们,mysqldump 只导表和数据的定义,那些「库内代码」一个都不带走。
换句话说:前四章辛苦建的那些逻辑,默认是不跟着备份走的。
备份脚本里漏了它们,还原出来的库是「表都在、逻辑没了」——而且不会报错。
这件事的教训可以推广:凡是「默认不包含」的东西,都要有一张清单。
视图、过程、触发器、事件、用户权限、字符集设置——每一样都要单独确认。
「导出成功」不等于「搬全了」。
三、改名:导出、改文本、灌回去
跨库迁移经常要顺带改名:shop 改成 shop_v2,或者给表加个前缀。
最土也最可靠的办法是:导出文本、改文本、灌回去。
| 步骤 | 结果 |
|---|---|
| 原库导出的文本 | tb_c、tb_g |
| 字符串替换后 | tb_c_v2、tb_g_v2 |
| 灌进新库 | 两张表都在,行数一致 |
注意这一步用的是「字符串替换」,所以它既可能多改、也可能少改。
多改:如果某条数据里正好有 tb_g 这三个字母,那条数据就被改了。
少改:如果表名出现在一个没预料到的地方(比如触发器体里的字符串),就漏了。
所以改完必须做两件事:grep 一遍确认没有残留,再比对行数和合计。
这两步很土,但它们能挡住 90% 的「改名改出事故」。
做得更讲究一点:只替换「行首的 CREATE TABLE/INSERT INTO/ALTER TABLE 后面那个名字」,
而不是全文替换。
正则写对了,误伤就少很多;写不对,就老老实实用「改完对一遍」。
四、字符集与排序规则:迁移里最安静的坑
SQLite 的编码在 PRAGMA encoding 里,建库时定,之后不能改;
排序规则靠 COLLATE,默认 BINARY(按字节比),可以声明 NOCASE。
| 比较写法 | 结果 |
|---|---|
a = 'APPLE'(NOCASE 列) |
2 行 |
b = 'APPLE'(默认列) |
0 行 |
同一张表、同样的值,两列结果不一样——差别只在建表时有没有写 COLLATE NOCASE。
这个属性是「跟着列走」的,迁移时如果只搬了数据没搬定义,行为就变了。
MySQL 那边对应的是 CHARSET 和 COLLATION。
utf8mb4_general_ci 里那个 ci,就是「不区分大小写」。
跨库迁移时最常见的翻车是:源库 utf8mb4、目标库 latin1。
数据进去变成问号,而且不报错。
所以迁移脚本的第一句应该是「查两边字符集」,不是「开始灌数据」。
再补一条更隐蔽的:utf8 和 utf8mb4 在 MySQL 里不是一回事。
utf8 最多三个字节,存不下 emoji 和一些生僻字;utf8mb4 是四个字节。
从 utf8 迁到 utf8mb4 通常没问题,反过来会截断——报错还是静默,取决于严格模式。
五、动手 Demo:拷文件四道关、加参数导库内代码、改文本改名、排序规则
这一段四件事:先列清「同版本迁移」的四个条件;再看 mysqldump 那三个参数为什么不能省;
然后用「导出、替换表名、灌回去」真做一次改名;最后量一下 COLLATE NOCASE 对比较结果的影响。
import sqlite3
# ① 改名:导出、改文本、灌回去
src = sqlite3.connect(u"shop.db", isolation_level=None)
dump = u"\n".join(src.iterdump())
new = dump.replace(u"tb_g", u"tb_g_v2").replace(u"tb_c", u"tb_c_v2")
dst = sqlite3.connect(u"shop_v2.db", isolation_level=None)
dst.executescript(new)
print([r[0] for r in dst.execute(
u"SELECT name FROM sqlite_master WHERE type='table'")])
# ② 字符集与排序规则
c = sqlite3.connect(u"cs.db", isolation_level=None)
c.executescript(u"""
CREATE TABLE t (a TEXT COLLATE NOCASE, b TEXT);
INSERT INTO t VALUES ('Apple','Apple'), ('apple','apple');
""")
print(c.execute(u"PRAGMA encoding").fetchone()) # ('UTF-8',)
print(c.execute(u"SELECT count(*) FROM t WHERE a='APPLE'").fetchone())
print(c.execute(u"SELECT count(*) FROM t WHERE b='APPLE'").fetchone())
实跑输出:
--------------------------------------------------------------------
数据库迁移:改名字、跨版本、跨引擎
--------------------------------------------------------------------
同一版本:拷文件最快
--------------------------------------------------------------------
**教材 18.3 讲「相同版本的 MySQL 之间迁移」,给的办法就是拷数据目录。**
**这条路最快,但它有几个隐含条件,少一个就出问题:**
条件 不满足会怎样
---------------------------------------------------
版本一致 引擎拒绝打开,或读出错乱数据
字符集配置一致 中文变问号,或索引比较结果不对
停服务 拷到一半的数据页,还原后库损坏
存储引擎一致 InnoDB 文件给 MyISAM 用,直接打不开
**四条里最难查的是第二条:字符集。它不报错,只是把数据悄悄改了。**
**所以跨库迁移的正确做法是「用文本走一遍」,让字符集在文本层面对齐。**
跨库/跨版本:走 dump 文本
--------------------------------------------------------------------
**逻辑备份的好处在这里体现:文本是通用的,谁都读得懂。**
**代价是慢,而且有些东西文本表达不了。**
**视图、触发器、存储过程、事件,`mysqldump` 默认都不导,要加参数。**
[教材原码 · 本沙箱不执行]
| mysqldump -u root -p --routines --triggers --events shop > shop.sql
教材没提这三个参数,但不加就等于「只搬了表」。
**这三个参数对应 14~17 章讲的东西:存储过程、触发器、事件。**
**换句话说,前四章辛苦建的那些「库内代码」,默认是不跟着备份走的。**
**备份脚本里漏了它们,还原出来的库「表都在、逻辑没了」。**
改名字:dump 出来改文本,再灌回去
--------------------------------------------------------------------
**跨库迁移经常要顺带改名:`shop` 改成 `shop_v2`,或者给表加个前缀。**
**最土也最可靠的办法是:导出文本、改文本、灌回去。**
替换表名后回放 -> 成功
新库里的表:tb_c_v2、tb_g_v2
新库 tb_g_v2 行数:3(原库 3)
**注意这一步用的是「字符串替换」,所以它既可能多改、也可能少改。**
**多改:如果某条数据里正好有 `tb_g` 三个字母,那条数据就被改了。**
**少改:如果表名出现在一个没预料到的地方(比如触发器体里的字符串),就漏了。**
**所以改完必须做两件事:`grep` 一遍确认没有残留,再比对行数和合计。**
字符集与排序规则:迁移里最安静的坑
--------------------------------------------------------------------
**SQLite 的编码在 `PRAGMA encoding` 里,建库时定,之后不能改。**
**排序规则靠 `COLLATE`,默认 `BINARY`(按字节比),可以声明 `NOCASE`。**
`PRAGMA encoding` -> UTF-8
比较写法 结果
--------------------------------
`a = 'APPLE'`(NOCASE 列) 2 行
`b = 'APPLE'`(默认列) 0 行
**同一张表、同样的值,两列结果不一样——差别只在建表时有没有写 `COLLATE NOCASE`。**
**这个属性是「跟着列走」的,迁移时如果只搬了数据没搬定义,行为就变了。**
**MySQL 那边对应的是 `CHARSET` 和 `COLLATION`。**
**`utf8mb4_general_ci` 里那个 `ci`,就是「不区分大小写」。**
**跨库迁移时最常见的翻车是:源库 `utf8mb4`、目标库 `latin1`。**
**数据进去变成问号,而且不报错。**
**所以迁移脚本的第一句应该是「查两边字符集」,不是「开始灌数据」。**
第①步的两行输出是新库里的表名:tb_c_v2、tb_g_v2,行数也一致。
但这一步能成功,靠的是「原来的表名足够特别」——tb_g 这三个字母不太可能出现在数据里。
换成 id、name 这种常见词,全文替换立刻出事。
第②步三个数要连起来读:编码是 UTF-8,NOCASE 列匹配 2 行,默认列匹配 0 行。
同样的两条数据、同样的查询,只因为建表时写法不同,结果差了一倍。
这就是「排序规则跟着列走」的具体后果:迁移时只搬数据不搬定义,行为就变了。
而它不会报错,只是「查出来的东西不一样了」——这种 bug 能活很久。
30 秒实验
- 把
replace的目标从tb_g改成g,看会发生什么(先想一遍再跑)。 - 在
cs.db里加一条'APPLE',再跑那两条查询,看NOCASE列的匹配数变成几。 - 把 dump 文本另存一份,用
grep -c tb_g数一下出现几次,和「表数加插入行数」对一下。
常见误区
- 以为同版本拷文件就能直接跑。还要看字符集配置、存储引擎、有没有停服务, 四条缺一条都可能出问题。
- 以为
mysqldump会把过程、触发器、事件一起导。不加--routines --triggers --events, 只搬表和数据。 - 以为全文替换表名是安全的。数据里出现同样的字符串就被误伤;定义里没预料到的地方又会漏。
自测三问(点开对答案)
1. 同版本迁移拷文件,四个条件里哪个最危险?为什么? —— 字符集。 另外三个要么直接报错、要么能看出来,字符集不报错,只是把中文变成问号、把比较结果改掉。 所以要靠「抽查中文」和「比对合计」来发现。 2. `--routines --triggers --events` 这三个参数为什么不能省? —— 因为 `mysqldump` 默认只导表结构和数据,库内代码不带走。 省了这三个参数,还原出来的库「表都在、逻辑没了」,而且不报错。 3. `COLLATE NOCASE` 会影响什么? —— 影响字符串比较和排序。 同一批数据,声明了 `NOCASE` 的列查 `'APPLE'` 能匹配到 `'Apple'`,没声明的匹配不到。 这个属性跟着列走,迁移时不能只搬数据。迁移讲完了。下一篇是第 18 章最后一篇:表的导出和导入——文本桥的具体用法。
- ☐ (本篇完,下一篇 18.4 表的导出和导入)