🦏 第17章 数据备份
📄 一句话概括

MySQL 只备几张表:mysqldump -u 用户 -p 库 表1 表2 > 文件.sql。 库内快照用 create table 新表 like 旧表 加 insert … select。 create table 新表 as select * from 旧表 有个大坑:它只带数据,不带任何约束—— 主键、唯一键、非空、check 全没了,而且你看不出来。 本沙箱里跨库靠 attach;备份一张表除了数据和建表语句,还有索引和触发器要一起拷。

📝 本章和前面几章的关系

上一篇备份整库,这一篇只备份一张表——实际工作里「只备一张表」比「备整库」更常见: 改一张表之前先存个底,改坏了拿回来。 这一篇有个很值得记住的坑: create table … as select 丢约束,而且丢了不报错。 这种「静默降级」是最难发现的一类问题。

一、MySQL 里怎么只备份一张表

mysqldump -u root -p mydb students classes > two_tables.sql—— 库名后面直接跟表名,就是只导这几张。 库内快速存个底: create table students_bak as select * from students; 一句话搞定,但有个大坑,下一节就讲。 可靠的做法是两步:先用 create table 新表 like 旧表 把结构复制过来(索引和约束都在, 外键除外),再 insert … select 灌数据。 like 这一句是 MySQL 特有的,很方便。

二、这里的第一种写法:attach 一个目标库再拷

attach database '源库文件' as src 之后, 就能用 src.表名 访问它。 于是「拷一张表到另一个库」就是一句 create table … as select。 Demo 里五行都拷过来了,看起来没问题。 但看它建出来的表结构:id INT、name TEXT、 height REAL、class_id INT——INTEGER 变成了 INT,primary key 那句不见了。 pragma table_info 更直接:最后一列 pk 全是 0。 也就是说:主键丢了。 为什么? create table … as select 只根据「结果集的列」推断类型,不复制任何约束。 主键、唯一键、非空、默认值、check、外键,一个都不带过来。 这是 SQL 标准里的行为,不是某个数据库的 bug。 后果很实在:这张备份表可以插重复的主键、可以插空值, 拿它当「原表」用会静默出错。 更麻烦的是「它看起来像一张正常的表」,你不会发现少了什么。

三、正确做法:先建表,再灌数据

手写一遍 create table,再 insert into … select * from。 Demo 里这样做之后 pk 那一列是 1 了,主键在。 代价是要手抄结构,累一点但结果对。 更好的办法是从原表把建表语句取出来直接用: select sql from sqlite_master where name = 'students', 把表名改掉、执行一遍。 这样主键、唯一键、check、默认值全都原样带过来,一个不漏。 MySQL 那边就是 create table 新表 like 旧表, 一句话顶这一整套。 不过它不复制外键—— 要连外键一起,得自己补 alter table … add constraint。

四、跨库拷表:attach 是那把钥匙

attach database '另一个文件' as 别名 之后, 用 别名.表名 就能访问。 这是本沙箱里唯一「跨库操作」的办法—— 没有 mysqldump,也没有 select … into outfile。 连 sqlite_master 都能带库名访问: select name from src.sqlite_master where type = 'table', 「那个库里有什么表」一句话就查出来了。 迁移数据时非常有用。 两个细节要记住:同时最多挂 10 个库(编译时可以调大); 事务是跨库的—— 一个事务里改了两个挂上来的库,rollback 会一起撤。 这一点和第 15 章讲的事务是打通的。

五、备份一张表,别忘了表之外的东西

一张表不只是「列 + 行」, 它还有:索引、触发器、check 约束、默认值、注释。 只把数据拷过去,这些东西都没了。 所以「备份一张表」完整地说要拷三样:建表语句(sqlite_master 里的 sql)、 索引语句(type = 'index' 那几行)、触发器语句(type = 'trigger' 那几行)。 少拷一样,恢复出来的表就是「残缺」的。 Demo 里查 tbl_name = 'students' 只列出一个 table—— 因为那张表上没建索引、没建触发器。 如果建过,这里会多出几行,那几行也要一起备份。 「备份」这件事,粒度永远比想象中细。 整库备份没这个问题(iterdump 会把索引一起导出来);只备一张表才有。 这也算是「图省事」的代价。

六、动手 Demo:用 attach 拷一张表,看主键怎么丢的、怎么补回来

Demo 五段:挂上源库、用 create table as select 拷一份并查结构、手写建表再灌一次、 把原表的建表语句取出来、最后看「表之外」还有什么要备份。 第二段那个 pk 全是 0 的表格,是这一篇的核心证据——数据一行不少,结构已经废了。

import sqlite3
import _q46 as Q
c = Q.con()

# 1) 另一条连接挂上源库和备份库
b = sqlite3.connect("bak.db")
b.execute("ATTACH DATABASE 'school.db' AS src")

# 2) 一句 create table as select:数据齐了,结构丢了一半
Q.run(b, "CREATE TABLE s_bad AS SELECT * FROM src.students")
Q.show(b, "PRAGMA table_info(s_bad)")     # pk 一列全是 0

# 3) 正确做法:先照原样建表,再 insert select
Q.run(b, "CREATE TABLE s_good (id INTEGER PRIMARY KEY, name TEXT, "
         "height REAL, class_id INTEGER)")
Q.run(b, "INSERT INTO s_good SELECT * FROM src.students")
Q.show(b, "PRAGMA table_info(s_good)")     # pk 是 1

# 4) 更好:把原表的建表语句取出来直接用
Q.show(b, "SELECT sql FROM src.sqlite_master WHERE name = 'students'")

# 5) 表之外的东西:索引、触发器也要备份
Q.show(b, "SELECT type, name FROM src.sqlite_master "
          "WHERE tbl_name = 'students' ORDER BY type")

实跑输出:

--------------------------------------------------------------------
第八十课:表的备份与还原
--------------------------------------------------------------------

  MySQL 里怎么只备份一张表
  MySQL 原码(本沙箱不执行):
  | # 只备份指定的两张表
  | mysqldump -u root -p mydb students classes > two_tables.sql
  | 
  | # 库内直接拷一份(快,但只在本库)
  | CREATE TABLE students_bak AS SELECT * FROM students;
  | 
  | # 或者先建表再灌数据(能保住约束)
  | CREATE TABLE students_bak LIKE students;
  | INSERT INTO students_bak SELECT * FROM students;
  **`create table 新表 as select * from 旧表` 是最省事的一句**,
  **但它有个大坑——下一篇就是讲这个。**
  **可靠的做法是两步**:**先用 `create table 新表 like 旧表` 把结构复制过来**(索引和约束都在,
  外键除外),**再 `insert … select` 灌数据。**

  这里的第一种写法:attach 一个目标库再拷
  SQL> CREATE TABLE s_bad AS SELECT * FROM src.students
  完成
  SQL> SELECT COUNT(*) AS 行数 FROM s_bad
  行数
  -----
  5
  (1 行)
  **五行都拷过来了,看起来没问题。** **但看看它建出来的表结构:**
    CREATE TABLE s_bad(
      id INT,
      name TEXT,
      height REAL,
      class_id INT
    )
  SQL> PRAGMA table_info(s_bad)
  cid     name         type     notnull     dflt_value     pk
  --------------------------------------------------------------------
  0       id           INT      0           NULL           0
  1       name         TEXT     0           NULL           0
  2       height       REAL     0           NULL           0
  3       class_id     INT      0           NULL           0
  (4 行)
  **注意两处**:**`INTEGER` 变成了 `INT`——原来「`id INTEGER PRIMARY KEY`」那一句,只剩下了类型**;
  **`pragma table_info` 最后一列 `pk` 全是 0。** **也就是说:主键丢了。**
  **为什么?** **因为 `create table … as select` 只根据「结果集的列」推断类型,不复制任何约束。**
  **主键、唯一键、非空、默认值、`check`、外键,一个都不带过来。**
  **这是 SQL 标准里的行为,不是 SQLite 的 bug。**
  **后果很实在**:
  **这张备份表可以插重复的主键、可以插空值,拿它当「原表」用会静默出错。**
  **更麻烦的是——它看起来像一张正常的表,你不会发现少了什么。**

  正确做法:先建表,再灌数据
  SQL> CREATE TABLE s_good (id INTEGER PRIMARY KEY, name TEXT, height REAL, class_id INTEGER)
  完成
  SQL> INSERT INTO s_good SELECT * FROM src.students
  影响 5 行
  SQL> SELECT COUNT(*) AS 行数 FROM s_good
  行数
  -----
  5
  (1 行)
  SQL> PRAGMA table_info(s_good)
  cid     name         type        notnull     dflt_value     pk
  -----------------------------------------------------------------------
  0       id           INTEGER     0           NULL           1
  1       name         TEXT        0           NULL           0
  2       height       REAL        0           NULL           0
  3       class_id     INTEGER     0           NULL           0
  (4 行)
  **这次 `pk` 那一列是 1 了**,主键在。
  **代价是要手写一遍 `create table`**——**累一点,但结构是对的。**
  **更好的办法是从原表把建表语句取出来直接用**:
  **`select sql from sqlite_master where name = 'students'`**,
  **把表名改掉、执行一遍。**
  **这样主键、唯一键、`check`、默认值全都原样带过来,一个不漏,也不用自己抄。**
  **MySQL 那边就是 `create table 新表 like 旧表`**,**一句话顶这一整套。**

  跨库拷表:attach 是那把钥匙
  **`attach database '另一个文件' as 别名`**,
  **之后就能用 `别名.表名` 访问它。** **这是本沙箱里唯一「跨库操作」的办法**——
  **没有 `mysqldump`,也没有 `select … into outfile`。**
  SQL> SELECT name FROM src.sqlite_master WHERE type = 'table'
  name
  ---------
  classes
  students
  (2 行)
  **注意 `src.sqlite_master` 这个写法**:**`sqlite_master` 也能带库名访问**,
  **所以「那个库里有什么表」一句话就查出来了。** **迁移数据时非常有用。**
  **`attach` 还有两个细节**:**同时最多挂 10 个库**(编译时可以调大);
  **事务是跨库的**——
  **一个事务里改了两个挂上来的库,`rollback` 会一起撤。**
  **这一点和第 15 章讲的「事务」是打通的。**

  备份一张表之后,别忘了补上「表之外」的东西
  **一张表不只是「列 + 行」**,
  **它还有:索引、触发器、`check` 约束、默认值、注释。** **只把数据拷过去,这些东西都没了。**
  **所以「备份一张表」完整地说要拷三样**:**建表语句**(`sqlite_master` 里的 `sql`)、
  **索引语句**(`type = 'index'` 那几行)、**触发器语句**(`type = 'trigger'` 那几行)。
  **少拷一样,恢复出来的表就是「残缺」的。**
  SQL> SELECT type, name FROM sqlite_master WHERE tbl_name = 'students' ORDER BY type
  type      name
  -------------------
  table     students
  (1 行)
  **这里只列出一个 `table`**——
  **因为 `students` 上没建索引、没建触发器。** **如果建过,这里会多出几行**,
  **那几行也要一起备份。** **「备份」这件事,粒度永远比想象中细。**

第一段 s_bad 拷过来 5 行,但建表语句变成 id INT,pk 那一列全是 0。 第二段手写建表后 s_good 的 pk 是 1,主键回来了。 第三段能直接取出原表的建表语句,这就是最省事又最可靠的做法。 第五段 tbl_name = 'students' 只有一行 table—— 这张表上没索引、没触发器,所以没有别的要拷。

30 秒实验

  1. 给 students 建一个索引和一个触发器,再跑第五段,会多出几行?
  2. create table as select 拷过去的表能插重复主键吗?试一下。
  3. 为什么 create table 新表 like 旧表 不复制外键?这会造成什么问题?

常见误区

  1. 用 create table as select 当「备份」。 约束全丢,而表面上看不出来,最危险。
  2. 只备数据不备建表语句。 恢复出来的表类型都变了(INTEGER 变 INT),主键也没了。
  3. 忘了索引和触发器。 表能查但慢、自动逻辑也没了,备份要做到「表之外」那几行。
自测三问(点开对答案) 1. `create table 新表 as select * from 旧表` 会丢什么? —— 所有约束:主键、唯一键、非空、默认值、check、外键。它只按结果集推断列类型,而且不会报错。 2. 本沙箱里跨库拷表靠什么? —— `attach database '文件' as 别名`,然后用 `别名.表名` 访问;连 `sqlite_master` 也能带库名查。 3. 备份一张表除了数据还要拷什么? —— 建表语句、索引语句、触发器语句三样。少一样恢复出来就是残缺的。

第 17 章完了: 备份的理由、整库备份、单表备份,三件都过了。 下一篇进第 18 章——其他内容:系统库、分页、表的设计。

相关笔记