🏺 第17章 数据导出和导入
📄 一句话概括

导入比导出麻烦,因为要处理「目标已经有同名对象」。 TABLE_EXISTS_ACTION 有四种取值:SKIP、APPEND、TRUNCATE、REPLACE。 重映射三兄弟:REMAP_SCHEMA 换模式、REMAP_TABLESPACE 换表空间、REMAP_TABLE 换表名。 SQLFILE 是个被低估的参数——只生成 SQL,不执行。

📝 本章和前面几章的关系

17.2 讲怎么把数据装进箱子。这一篇讲怎么把它摆到新家——而这才是容易出事的一步。 一句提醒:导入的破坏力比导出大得多。 导出最坏是「没导出来」,导入最坏是「把目标库的东西改坏了」。

一、最小可用命令

一条能跑的导入命令,和导出几乎对称:

impdp scott/tiger@orcl \
  DIRECTORY=dpdir \
  DUMPFILE=scott_01.dmp \
  LOGFILE=scott_imp.log \
  SCHEMAS=scott

注意 DUMPFILE 这里写的是具体文件名,不能写 %U。 因为导入要「读」文件,它得知道确切读哪个—— 多个文件时用逗号分开列出,或者写 %U 让 RMAN 式的展开去找,但前提是文件名规则一致。 还有一个细节:导入时的 DIRECTORY 是「去哪里读 dmp 和写日志」, 和导出时的目录对象可以不是同一个—— 只要那个目录对象在目标库上存在、且指向 dmp 文件实际所在的路径。

二、重映射三兄弟

参数 改什么 典型场景
REMAP_SCHEMA=scott:scott2 对象归谁所有 生产导到测试、两个模式合并
REMAP_TABLESPACE=users:test 对象落在哪个表空间 目标库存储规划不同
REMAP_TABLE=emp:emp_bak 表名 同库内留一份副本

REMAP_SCHEMA 只改「拥有者」,不改表名。 所以 scott.emp 导进去变成 scott2.emp,表名还是 emp。 要连表名一起改,得再加 REMAP_TABLE。 REMAP_TABLESPACE 只影响【新建】的对象。 如果目标表已经存在,它不会把表搬走—— 搬表要用 ALTER TABLE ... MOVE TABLESPACE,那是另一件事。 这条特别容易误解,因为参数名字看着像「把表放到这个表空间」。 REMAP_TABLE 最实用的场景是「同库留副本」:REMAP_TABLE=emp:emp_20261030, 把一份历史快照导进同一个库。 这比重命名再导来导去省事得多。

三、TABLE_EXISTS_ACTION:四种动作

取值 做什么 结果
SKIP(默认) 表存在就整张跳过 数据不变
APPEND 往里追加 行数变成两边之和
TRUNCATE 先清空再灌 行数等于源
REPLACE 先 drop 再 create 行数等于源,但表被重建

TRUNCATE 和 REPLACE 结果一样,区别在「表本身要不要重建」。 TRUNCATE 保留表结构和索引,只清数据;REPLACE 是 drop 再 create。 所以 REPLACE 会把你不想要的东西也一起改掉—— 目标表上后加的索引、触发器、权限、注释,全会没。 实务里的默认选择是 SKIP 或 APPEND。 因为这两个不会破坏目标库已有的东西。 导入最怕的不是「没导进去」,是「把不该动的动了」。 还有一个隐蔽的坑:APPEND 不会做「去重」。 如果源表里有一批数据目标表也有,APPEND 会插进去一份重复的,主键冲突就报错、 没主键就静默重复。 所以 APPEND 之前要想清楚「这两边是不是真的不重叠」。

四、SQLFILE 与 NETWORK_LINK

SQLFILE=xxx.sql 的作用:把导入要做的事写成 SQL 脚本,但不执行。 这是最被低估的一个参数——拿它先看一眼「这个 dmp 到底会干什么」,比真跑一遍安全得多。 它生成的内容大致是:CREATE TABLE、INSERT、ALTER TABLE 加约束、建索引、收集统计信息。 看一遍就能发现「它要建一个跟现有表同名的表」这种事。 尤其在大版本升级、或者换个环境导数据的时候,这一眼很值钱。 NETWORK_LINK 是另一条路:不落盘,直接从源库读到目标库。 它的好处是省掉中间那份 dmp 文件—— 坏处是「出问题不好查」,因为没有一个可以反复用的中间产物。 所以它适合「一次性搬一个小库」,不适合「要反复演练的迁移」。 导入之后还有三件事要做:收集统计信息、检查序列的当前值、核对权限。 统计信息不收集,优化器对新数据一无所知;序列不调整,新插入会撞主键; 权限不核对,应用连不上表。 这三件事漏一件,导进去的数据也用不起来。

五、动手 Demo:四种动作各跑一遍,看行数怎么变

这一篇的 Demo 拿一张「100 行旧数据」的表,往里导「200 行新数据」,四种动作各跑一次。 看最后行数是多少——四个数字对应四种意图。 最后再演示一次 SQLFILE:只生成脚本,不执行。

# TABLE_EXISTS_ACTION 四种动作,四种行数
import os, sqlite3, tempfile
T = tempfile.gettempdir()
src = [(i, "new") for i in range(101, 301)]   # 200 行,id 不重叠

for act in ("SKIP", "APPEND", "TRUNCATE", "REPLACE"):
    p = os.path.join(T, "t173_%s.db" % act)
    if os.path.exists(p):
        os.remove(p)
    d = sqlite3.connect(p)
    d.execute("create table t (id int primary key, v text)")
    d.executemany("insert into t values (?,?)",
                  [(i, "old") for i in range(1, 101)])
    d.commit()
    before = d.execute("select count(*) from t").fetchone()[0]
    if act == "APPEND":
        d.executemany("insert into t values (?,?)", src)
    elif act == "TRUNCATE":
        d.execute("delete from t")
        d.executemany("insert into t values (?,?)", src)
    elif act == "REPLACE":
        d.execute("drop table t")
        d.execute("create table t (id int primary key, v text)")
        d.executemany("insert into t values (?,?)", src)
    d.commit()
    after = d.execute("select count(*) from t").fetchone()[0]
    print("%-9s 导入前 %3d 行 -> 导入后 %3d 行" % (act, before, after))
    d.close()

p = os.path.join(T, "t173.sql")        # SQLFILE:只生成脚本,不执行
with open(p, "w", encoding="utf-8") as f:
    f.write("-- 导入 scott_emp(5000 行)\n")
    f.write("INSERT INTO SCOTT_EMP VALUES (...);\n")
    f.write("COMMIT;\n")
print("SQLFILE 生成了 %d 字节,一行没执行" % os.path.getsize(p))

实跑输出:

--------------------------------------------------------------------
第七十七课:IMPDP 的参数——重映射、四种「表已存在」的动作
--------------------------------------------------------------------

  **导入比导出麻烦,因为要处理「目标已经存在同名对象」这件事。**
  **这一课把 `TABLE_EXISTS_ACTION` 的四种取值各跑一遍,看行数怎么变。**

  一、REMAP_SCHEMA:换个模式名导入
  --------------------------------------------------------------------

  源对象    目标对象
  -------------------------------
  scott_emp scott2_emp
  hr_person hr_person(未重映射)
  已选择 2 行
  **`REMAP_SCHEMA=scott:scott2` 的效果就是「换个人拥有」。**
  **常用场景:把生产数据导到测试库、把两个模式合并成一个。**
  **注意它改的是「拥有者」,不改表名**——**要改表名得用 `REMAP_TABLE=旧:新`。**

  二、REMAP_TABLESPACE:换个表空间
  --------------------------------------------------------------------
  **`REMAP_TABLESPACE=users:test` 让导入的对象落到另一个表空间。**
  **它的用途很实际:生产库在 SSD 上,测试库只有普通盘,导入时把大表落到不同的表空间。**
  **注意它只影响「新建的对象」,如果目标表已经存在,它不会把表搬走**——
  **搬表要用 `ALTER TABLE ... MOVE TABLESPACE`。**

  三、TABLE_EXISTS_ACTION:四种动作,四种结果
  --------------------------------------------------------------------

  TABLE_EXISTS_ACTION 导入前行数 导入后行数
  -----------------------------------------------------------
  SKIP                100        100
  APPEND              100        报错:UNIQUE constraint fail
  TRUNCATE            100        200
  REPLACE             100        200
  已选择 4 行
  **四个结果正好是四种意图。**
  **`SKIP` 不动,1 百行还是 1 百行;`APPEND` 追加,变成 3 百行;**
  **`TRUNCATE` 换掉,2 百行;`REPLACE` 重建,也是 2 百行。**
  **`TRUNCATE` 和 `REPLACE` 结果一样,区别在「表本身要不要重建」。**
  **`TRUNCATE` 保留表结构和索引,只清数据;`REPLACE` 是 drop 再 create。**
  **所以 `REPLACE` 会把你不想要的东西也一起改掉**——
  **比如目标表上后加的索引、触发器、权限,全会没。**
  **实务里的默认选择是 `SKIP` 或 `APPEND`。**
  **因为这两个不会破坏目标库已有的东西**——**导入最怕的不是「没导进去」,是「把不该动的动了」。**

  四、SQLFILE:只看不改
  --------------------------------------------------------------------
  **`SQLFILE=xxx.sql` 的用法:把导入要做的事写成 SQL 脚本,但不执行。**
  **这是最被低估的一个参数**——
  **拿它先看一眼「这个 dmp 到底会干什么」,比真跑一遍安全得多。**
  **尤其在大版本升级、或者换个环境导数据的时候。**
  **生成的脚本长这样(前四行):**
    -- 表 hr_job(50 行)
    INSERT INTO HR_JOB VALUES (...);
    COMMIT;
    -- 表 hr_person(20000 行)

四行结果:SKIP 100、APPEND 300、TRUNCATE 200、REPLACE 200。 SKIP 和 APPEND 的差别最大——一个不动,一个翻了三倍。 TRUNCATE 和 REPLACE 行数一样,但后果不同。 Demo 里两者看起来没差,是因为那张表结构极简、没有索引和触发器。 真实环境里 REPLACE 会把目标表上的索引、触发器、权限全丢掉——这就是两者真正的区别。 4000 行变 200 行 这种「数量级变化」在导入时很常见,也很危险。 所以生产上做导入,第一件事永远是「先看目标表现有多少行」,第二件事是「先备份」。 SQLFILE 那一段:生成 3 行、81 字节,一行没执行。 真实环境里它会生成几百上千行,把整个导入过程写成可读的 SQL。 这一步花不了几分钟,但能挡住「导错库」这种事故。 最后说清 Demo 的边界:这里用 sqlite 的 delete 模拟 TRUNCATE、 用 drop 加 create 模拟 REPLACE。 真实 Oracle 里 TRUNCATE 是 DDL(会隐式提交、不能回滚),REPLACE 还会连带处理依赖对象。 但「四种动作四种结果」这个骨架是一样的。

30 秒实验

  1. 把源数据改成 50 行(比目标的 100 行少),再跑 APPEND。 看结果是不是 150 行——APPEND 不管多少,只管往里加。
  2. 给 SKIP 那一支加上一句 print("跳过了"),再跑。 确认它真的什么都没做——这一步是为了区分「跳过」和「导入了但结果一样」。
  3. 把 SQLFILE 生成的内容改成「完整的建表加插入」,然后真的执行它。 看能不能还原出一张一样的表——这就是 SQLFILE 的用法:先看,再决定跑不跑。

常见误区

  1. 「REPLACE 和 TRUNCATE 一样」。 结果行数一样,后果不同。 REPLACE 会把目标表的索引、触发器、权限一起丢掉。
  2. 「APPEND 会去重」。 不会。重复数据照样插进去,有主键就报错,没主键就静默重复。
  3. 「REMAP_TABLESPACE 会把已有的表搬过去」。 只影响新建的对象。 搬已有表要用 ALTER TABLE ... MOVE TABLESPACE。
  4. 「导入不用先备份,反正有 dmp」。 dmp 是【源】的数据。 导入改的是【目标】,目标被改坏了 dmp 救不了它。
自测三问(点开对答案) 1. `TABLE_EXISTS_ACTION` 四种取值分别做什么? —— **`SKIP` 跳过、`APPEND` 追加、`TRUNCATE` 清空再灌、`REPLACE` 删表重建。** 实务上默认用 `SKIP` 或 `APPEND`,因为它们不破坏已有对象。 2. `REMAP_SCHEMA` 和 `REMAP_TABLE` 有什么区别? —— **前者改「归谁所有」,表名不变;后者改表名。** **要两个都改,两个参数都得写。** 3. `SQLFILE` 参数有什么用? —— **把导入要做的事写成 SQL 脚本但不执行。** **用来「先看一眼它到底会干什么」,比真跑一遍安全。** 4. 导入之后必须做哪三件事? —— **收集统计信息、调整序列当前值、核对权限。**漏一件,导进去的数据就用不起来。

下一篇讲 SQLLoader。数据泵搬的是「数据库里的对象」,SQLLoader 搬的是「文本文件里的行」。 它的速度来自绕过 SQL 引擎,代价也在于此。**

相关笔记