导出的本质是「把结构和数据写成一段能重跑的 SQL」,导入就是把它跑一遍。 CSV 更小,但它不带结构;SQL 脚本更大,但它能直接跑。 导入一定要包在事务里:一千次提交就是一千次等磁盘。
前面三篇都在库里操作,这一篇讲「把库搬走」。 它是整章最实用的一篇:备份、迁移、给同事一份测试数据,全靠它。 第 21 章讲备份与恢复时会再深入一层,这一篇是那章的手工版。 和 3.2 节的关系:删库之前先导出,导出的就是这一篇的东西。
一、导出成 SQL 脚本
一份完整的 SQL 导出脚本,结构固定是三段。
| 段 | 长什么样 | 作用 |
|---|---|---|
| 删表 | DROP TABLE IF EXISTS 表; | 让脚本能重复跑 |
| 建表 | CREATE TABLE 表 (...); | 把结构带过去 |
| 插数据 | INSERT INTO 表 VALUES (...); | 一行一条,或者一条多行 |
第一段是最容易被忽略、又最重要的一段。
没有它,脚本在已经有表的库上跑会直接撞「表已存在」。
mysqldump 默认就带这一段。
它救过很多人(重跑一次就恢复),也坑过很多人(把线上表覆盖了)。
所以导出的脚本拿到手,第一件事是看开头三行。
二、导出成 CSV
CSV 只装数据,不装结构、不装类型、不装主键。 换来的是体积小、能被 Excel 打开、能被几乎所有工具读。
| 对比 | SQL 脚本 | CSV |
|---|---|---|
| 带不带结构 | 带 | 不带 |
| 能不能直接跑 | 能 | 不能,得先建表 |
| 体积 | 大(每行都带 INSERT 关键字) | 小 |
| 给人看 | 费劲 | Excel 一开就懂 |
| 字符集风险 | 低(脚本里能声明) | 高(Excel 默认编码不一定对) |
最后一行是个真坑:Excel 存 CSV 默认可能是 GBK,导进 utf8mb4 的库就是一堆乱码。 导之前问清楚编码,比导完再修数据便宜得多。
三、导入:一定要包在事务里
导入慢,九成不是数据库慢,是「提交太多次」。
每条 INSERT 单独提交,就要等磁盘确认一次。一千条就是一千次。
包在一个事务里,只在最后确认一次。
Demo 里这个差距是几百倍。
倍数取决于磁盘,但方向永远一样:提交次数越少越快。
所以 mysqldump 生成脚本时会带上 LOCK TABLES 和一段大事务。
不是它啰嗦,是为了这个。
四、导完怎么验:三个数
只看「导完了没报错」是不够的。 要验的是「导进去的和原来的一样」。
| 验什么 | 怎么验 | 能发现什么问题 |
|---|---|---|
| 行数 | SELECT COUNT(*) | 漏了几行、多了几行 |
| 金额或数量合计 | SELECT SUM(列) | 值被截断、类型转换出错 |
| 内容摘要 | 把所有行拼起来算哈希 | 列错位、字符被替换 |
为什么三个都要?因为各抓一类问题。 行数对了但值错了,SUM 能抓到;SUM 对了但顺序或字符错了,摘要能抓到。
五、动手 Demo:导出是拼文本,导入是重放这些文本
这一课真导一次、真导回去,把三笔账算清楚:多大、多久、对不对。 六段:导出 SQL、导出 CSV、导回去、事务对比、三个校验数、真机上还要问什么。
import io, hashlib, sqlite3, time
# 一、导出成 SQL 脚本
ddl = c.execute("SELECT sql FROM sqlite_master "
"WHERE name = 'tb_goods'").fetchone()[0]
buf = io.StringIO()
buf.write("DROP TABLE IF EXISTS tb_goods;\n")
buf.write(ddl + ";\n")
for r in c.execute("SELECT * FROM tb_goods ORDER BY id"):
buf.write("INSERT INTO tb_goods VALUES (%r, %r, %r, %r);\n" % r)
script = buf.getvalue()
# 二、导出成 CSV(不带结构)
cb = io.StringIO()
for r in c.execute("SELECT * FROM tb_goods ORDER BY id"):
cb.write(",".join("%s" % v for v in r) + "\n")
csv = cb.getvalue()
# 三、导回去:包在一个事务里
c2 = sqlite3.connect("imp.db")
c2.executescript("BEGIN;\n" + script + "COMMIT;")
# 五、三个校验数
def digest(cc):
h = hashlib.sha256()
for r in cc.execute("SELECT * FROM tb_goods ORDER BY id"):
h.update(("|".join("%s" % v for v in r)).encode("utf-8"))
return h.hexdigest()[:16]
print(digest(c), digest(c2))
实跑输出:
--------------------------------------------------------------------
第十三课:导出是拼文本,导入是重放这些文本
--------------------------------------------------------------------
**3.5 节讲导出和导入,这是整章最实用的一节。**
**导出的本质是「把数据和结构写成一段能重跑的 SQL」,导入就是把它跑一遍。**
**这一课真导一次、真导回去,把三笔账算清楚:多大、多久、对不对。**
**源表 2000 行。**
一、导出成 SQL 脚本
--------------------------------------------------------------------
**脚本 121559 字节,第一行是删表,第二行是建表,后面每行一条 INSERT。**
脚本开头长这样:
DROP TABLE IF EXISTS tb_goods;
CREATE TABLE tb_goods (id INTEGER PRIMARY KEY, name TEXT NOT NULL,…
INSERT INTO tb_goods VALUES (1, '商品0001', 11.0, 1);
二、导出成 CSV
--------------------------------------------------------------------
格式 字节数 带不带表结构 能不能直接跑
----------------------------------------------------
SQL 脚本 121559 带 能,复制粘贴就行
CSV 49433 不带 不能,得先建表再导入
**CSV 小得多,因为它不写列名和 INSERT 关键字。**
**代价是:拿到 CSV 的人不知道表结构、不知道类型、不知道主键是什么。**
三、导回去:重放脚本
--------------------------------------------------------------------
**重放完成:2000 行,耗时 0.022 秒。**
**注意脚本外面多包了一层 `BEGIN` 和 `COMMIT`,这一层是有讲究的。**
四、为什么要包在事务里:1000 行两种导入
--------------------------------------------------------------------
写法 耗时 差在哪
-------------------------------------------------------
每条 INSERT 自动提交 11.842 秒 每条都要等磁盘确认一次
全部包在一个事务里 0.021 秒 最后只确认一次
**同一份数据、同一台机器,差的是一个 `BEGIN`:571 倍。**
**倍数照实报,但它取决于磁盘——机械盘上差得更多,固态盘上差得少些。**
**能确定的是方向:一千次提交就是一千次等磁盘。**
五、对不对:算三个数
--------------------------------------------------------------------
校验项 源库 导入后的库
--------------------------------------------
行数 2000 2000
金额合计 519000.00 519000.00
内容摘要 9c20c45a09d88036 9c20c45a09d88036
**两个摘要一样,才算真导对了。**
**只看行数是不够的——列错位也可能行数一样。**
**导出之后一定要验这三个数,这是把「以为导好了」变成「确认导好了」的唯一办法。**
六、真机上还得多问三个问题
--------------------------------------------------------------------
问题 为什么重要
-------------------------------------------------------------------------
字符集是什么 导出的文件按什么编码写的,导进去按什么读的
要不要带 DROP TABLE 不带,导第二次会撞主键;带了,会覆盖现有数据
大表怎么导 一次导出几百 G,得用分块或者物理备份,不能用这个办法
**`mysqldump` 默认带 `DROP TABLE`,这事救过很多人,也坑过很多人。**
第一段的三行是导出的骨架。
注意 %r 这个格式化——它给字符串自动加单引号、给数字不加,正好对上 SQL 的字面量规则。
真写导出工具时不能这么偷懒(要处理引号里的引号、换行符),得用专门的转义。
第二段的 CSV 只有一半大小,因为不写列名和 INSERT 关键字。
代价是它单独存在时没用——拿到 CSV 的人不知道主键是什么、类型是什么。
第三段包了 BEGIN 和 COMMIT。
没包的时候,同一份数据导了十几秒;包了之后零点零几秒。
这个差距不是数据库慢,是「每行等一次磁盘」。
第五段那两个摘要一样,才算真导对了。
只看行数是自欺欺人:列错位也可能行数一样。
30 秒实验
- 把 Demo 里那句
BEGIN;删掉再跑一遍,看两种写法的耗时。差的次数就是行数。 - 把
DROP TABLE IF EXISTS那行删掉,然后在同一个库上跑两次脚本。第二次会撞「表已存在」。 这就是mysqldump为什么要默认带这一行的原因。 - 想一想:如果表有 50 G,这个办法还能用吗? 不能。内存里拼不下 50 G 的字符串,真机上要用分块导出、或者直接物理备份(拷数据文件)。
常见误区
- 「导完了没报错,就是导对了。」 没报错只说明语句都执行了,不说明数据一样。 行数、合计、摘要,三个至少对两个。
- 「CSV 小,所以导出用 CSV 更好。」 CSV 不带结构,拿到的人没法直接恢复。 给别人看用 CSV,给自己备份用 SQL 脚本。
- 「导入慢是因为数据库性能不行。」 九成是因为每条都提交了一次。 包进一个事务,同一个库、同一台机器,能快几百倍。
自测三问(点开对答案)
1. 一份完整的 SQL 导出脚本有哪三段? —— **删表、建表、插数据。** **第一段的 `DROP TABLE IF EXISTS` 是为了让脚本能重复跑。** 2. 为什么导入要包在事务里? —— **因为每条 INSERT 单独提交都要等磁盘确认一次。** **包起来只在最后确认一次,快几百倍。** 3. 导完为什么不能只看行数? —— **因为列错位、字符被替换,行数都可能一样。** **再加一个合计和一个内容摘要,才能覆盖这两类问题。**下一篇讲字符集:一个字到底占几个字节、utf8 和 utf8mb4 差在哪、
以及为什么排序规则能让登录名不分大小写。
- ☐ (本篇完,下一篇 3.6 设置编码格式)