🛢️ 第3章 使用MySQL图形化管理工具
📄 一句话概括

导出的本质是「把结构和数据写成一段能重跑的 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 秒实验

  1. 把 Demo 里那句 BEGIN; 删掉再跑一遍,看两种写法的耗时。差的次数就是行数。
  2. 把 DROP TABLE IF EXISTS 那行删掉,然后在同一个库上跑两次脚本。第二次会撞「表已存在」。 这就是 mysqldump 为什么要默认带这一行的原因。
  3. 想一想:如果表有 50 G,这个办法还能用吗? 不能。内存里拼不下 50 G 的字符串,真机上要用分块导出、或者直接物理备份(拷数据文件)。

常见误区

  1. 「导完了没报错,就是导对了。」 没报错只说明语句都执行了,不说明数据一样。 行数、合计、摘要,三个至少对两个。
  2. 「CSV 小,所以导出用 CSV 更好。」 CSV 不带结构,拿到的人没法直接恢复。 给别人看用 CSV,给自己备份用 SQL 脚本。
  3. 「导入慢是因为数据库性能不行。」 九成是因为每条都提交了一次。 包进一个事务,同一个库、同一台机器,能快几百倍。
自测三问(点开对答案) 1. 一份完整的 SQL 导出脚本有哪三段? —— **删表、建表、插数据。** **第一段的 `DROP TABLE IF EXISTS` 是为了让脚本能重复跑。** 2. 为什么导入要包在事务里? —— **因为每条 INSERT 单独提交都要等磁盘确认一次。** **包起来只在最后确认一次,快几百倍。** 3. 导完为什么不能只看行数? —— **因为列错位、字符被替换,行数都可能一样。** **再加一个合计和一个内容摘要,才能覆盖这两类问题。**

下一篇讲字符集:一个字到底占几个字节、utf8 和 utf8mb4 差在哪、 以及为什么排序规则能让登录名不分大小写。

相关笔记