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

expdp 的参数有几十个,但真正决定「导出什么」的只有四个:CONTENT、QUERY、EXCLUDE、 PARALLEL。 其余参数影响的是「怎么放」,不是「放什么」。 一致性靠 FLASHBACK_SCN 或 FLASHBACK_TIME—— 导出跑十分钟,这十分钟里别人还在改数据,不指定的话导出来的可能自相矛盾。

📝 本章和前面几章的关系

17.1 讲的是机制和目录对象。这一篇讲命令怎么写、参数怎么选。 读完这一篇要能回答:要给同事导一份「只要人事表、只要今年数据、文件别太大」的数据, 命令该怎么写。

一、最小可用命令

一条能跑的命令,四个参数就够:

expdp scott/tiger@orcl \
  DIRECTORY=dpdir \
  DUMPFILE=scott_%U.dmp \
  LOGFILE=scott_exp.log \
  SCHEMAS=scott

四个参数各管一件事:DIRECTORY 说往哪个目录对象写,DUMPFILE 说文件叫什么, LOGFILE 说日志叫什么,SCHEMAS 说导谁。 DUMPFILE 里那个 %U 是个占位符,会按需展开成序号。 不开并行时它就是 01,开了并行它会展开成 01 到 n。 所以写 %U 是个好习惯——以后想开并行不用改命令。

二、CONTENT:导数据、导结构、还是都导

取值 导什么 典型用途
ALL(默认) 元数据加数据 常规导出
DATA_ONLY 只有数据 往已有表里补数据
METADATA_ONLY 只有结构 建一个空库、克隆表结构

METADATA_ONLY 有个很好用的场景:想在测试库快速建一套一样的表结构,但一行数据都不要。 导出来可能只有几百 K,导进去几秒钟。 DATA_ONLY 则常和 TABLE_EXISTS_ACTION=APPEND 配合用—— 目标表已经建好了(可能加了索引、改了分区),只往里灌数据。 这时候如果带元数据,导入会试图重建表,反而添乱。

三、QUERY:只导满足条件的行

QUERY 的用法是「表名:条件」,条件部分要用引号包起来, 写成 QUERY=scott.emp:'WHERE deptno=10' 这样。 注意条件是加在 WHERE 后面那一截,所以要带 WHERE 关键字。 三个坑,一个比一个常见: ① 全库或模式导出时,QUERY 会应用到每一张表上。 如果条件里的列在别的表里不存在,直接报错。 所以 QUERY 基本只在「表模式」或者「只导一张表」时用。 ② 引号要转义。 在 Linux 的 shell 里,双引号本身要转义;条件里还有单引号的话,单引号要写成两个。 这条几乎每个人第一次用都会踩,报错信息还很不直观。 ③ QUERY 不影响其他对象。 它只过滤表数据,索引、约束、触发器的定义照样全导。 所以导进去之后,索引是全的,数据是部分的——这通常正是你想要的。

四、EXCLUDE 与 INCLUDE:挑对象

两个参数是一对反面,不能同时用: EXCLUDE 是「除了这些都要」,INCLUDE 是「只要这些」。

写法 意思
EXCLUDE=TABLE:"IN ('T1','T2')" 除了 T1、T2,其他表都导
EXCLUDE=INDEX 不导索引(导完再建更快)
EXCLUDE=STATISTICS 不导统计信息
INCLUDE=TABLE 只导表,其他对象都不要

EXCLUDE=INDEX 这一条在迁移大库时特别值钱。 因为导入时「边灌数据边维护索引」很慢,不如先不导索引,灌完数据再统一建。 这跟 16.3 讲直接路径时是同一个道理。 还有一条经验:EXCLUDE=STATISTICS 一般不建议。 统计信息不导的话,导入之后优化器对新库一无所知,上线第一天的执行计划可能惨不忍睹。 要么带着统计信息导,要么导完立刻收集一遍。

五、PARALLEL 与一致性

PARALLEL=4 表示开四个并行进程,每个写自己那一个转储文件。 所以 DUMPFILE 至少要有四个文件可写,否则报「文件不够」。 用 %U 就不用操心这件事。 并行度开多少,和 16.2 讲通道是同一个答案:不超过 CPU 核数,也不超过磁盘数。 导出是「读加写」,比备份更吃 IO,所以存储跟不上时开并行只会更慢。 一致性这件事最容易被忽略:导出跑十分钟,这十分钟里业务还在改数据。 如果不指定一致性点,导出来的不同表可能属于不同时刻——主表和明细表对不上,导进去就是脏数据。 两个参数解决它:FLASHBACK_SCN=1234567 或 FLASHBACK_TIME="TO_DATE(...)"。 它们让整个导出都读「那个时刻的一致性视图」,代价是要有足够的 undo 空间撑着。 undo 不够会报 ORA-01555,那时候只能重来一遍。

六、动手 Demo:四个参数各改一个,看产物怎么变

这一篇的 Demo 把 CONTENT、QUERY、EXCLUDE 各改一次,量产物体积的变化。 每次只改一个参数,这样才看得出体积变化是谁带来的。 PARALLEL 那一段在 17.1 的内存对比里已经量过方向,这里不重复。

# 四个参数各改一个,看产物怎么变
import json, os, sqlite3, tempfile
T = tempfile.gettempdir()
c = sqlite3.connect(os.path.join(T, "t171.db"))     # 复用上一篇的库

def dump(tabs, path, content="ALL", where=None):
    out = {}
    if content != "DATA_ONLY":
        out["meta"] = {
            t: c.execute("select sql from sqlite_master where name=?",
                         (t,)).fetchone()[0] for t in tabs}
    if content != "METADATA_ONLY":
        data = {}
        for t in tabs:
            sql = "select * from " + t
            if where:
                sql += " where " + where
            data[t] = c.execute(sql).fetchall()
        out["data"] = data
    q = os.path.join(T, path)
    with open(q, "w", encoding="utf-8") as f:
        json.dump(out, f, ensure_ascii=False)
    return os.path.getsize(q)

all_t = ["scott_emp", "hr_person"]
print("ALL            %7d 字节" % dump(all_t, "a.dmp"))
print("DATA_ONLY      %7d 字节" % dump(all_t, "b.dmp", "DATA_ONLY"))
print("METADATA_ONLY  %7d 字节" % dump(all_t, "c.dmp", "METADATA_ONLY"))
print("QUERY 过滤后   %7d 字节" % dump(["hr_person"], "d.dmp",
                                       where="pid <= 1000"))
print("EXCLUDE 一张   %7d 字节" % dump(["scott_emp"], "e.dmp"))
c.close()

实跑输出:

--------------------------------------------------------------------
第七十六课:EXPDP 的参数——CONTENT、QUERY、EXCLUDE、PARALLEL
--------------------------------------------------------------------

  **数据泵的参数里,真正影响「导出什么」的只有四个。**
  **CONTENT 决定导不导数据,QUERY 决定导哪些行,EXCLUDE 决定导哪些对象,PARALLEL 决定开几路。**
  **其余参数影响的是「怎么放」,不是「放什么」。**

  一、CONTENT:只导数据、只导结构、还是都导
  --------------------------------------------------------------------

  CONTENT       文件大小 文件行数
  -------------------------------
  ALL           541.1 KB 3
  DATA_ONLY     540.7 KB 2
  METADATA_ONLY 0.6 KB   2
  已选择 3 行
  **`METADATA_ONLY` 只有结构,`DATA_ONLY` 只有数据。**
  **这两个在日常里非常有用:重建一个空库结构、或者往已有表里补数据。**

  二、QUERY:只导满足条件的行
  --------------------------------------------------------------------

  写法                    行数  文件大小
  --------------------------------------
  不写 QUERY              20001 541.1 KB
  QUERY="where pid<=1000" 1000  20.4 KB
  已选择 2 行
  **QUERY 只对「表模式」或者指定了表的场景有意义**——
  **全库导出时它会把条件应用到每一张表上,很容易报「列不存在」。**
  **真实 Oracle 里还有个坑:`QUERY` 里的字符串引号要转义。**
  **命令行里写 `QUERY=scott.emp:"where deptno=10"`,如果条件里还有单引号,就得写成两个单引号。**
  **这条几乎每个人第一次用都会踩。**

  三、EXCLUDE 与 INCLUDE:排除和只要
  --------------------------------------------------------------------

  写法                             对象数 文件大小
  ------------------------------------------------
  不排除                           4      541.1 KB
  EXCLUDE=TABLE:"IN ('HR_PERSON')" 3      106.1 KB
  已选择 2 行
  **`EXCLUDE` 和 `INCLUDE` 是同一样东西的两面,不能同时用。**
  **`EXCLUDE` 是「除了这些都要」,`INCLUDE` 是「只要这些」。**
  **表多的时候用 `EXCLUDE` 更省事,表少的时候用 `INCLUDE`。**

  四、PARALLEL:并行导出与文件分片
  --------------------------------------------------------------------

  PARALLEL 转储文件数 耗时      总大小
  --------------------------------------
  1        1          0.0487 秒 541.1 KB
  4        4          0.1980 秒 907.3 KB
  已选择 2 行
  **PARALLEL=4 时至少要给 4 个 DUMPFILE,否则会报「文件不够」。**
  **因为每个并行进程要写自己那一个片。**
  **这也是为什么生产上的写法是 `DUMPFILE=exp_%U.dmp`**——
  **`%U` 会自动展开成序号,一个参数搞定多个文件。**
  **耗时反而慢了 4 倍:0.0487 秒变成 0.1980 秒,总大小也从 541.1 KB 涨到 907.3 KB。**
  **这不是「并行没用」,是这个模拟没模拟对:**
  **它把每一行都单独转一次 JSON、再写进 4 个文件中的一个,写放大很严重。**
  **真实 expdp 的并行是「四个进程各读一部分块、各写一个文件」**——
  **读是顺序的,写是分散到不同磁盘上的。**
  **但有一条结论可以直接搬过来:在一块盘上做并行写,只会更慢。**
  **这一点和 16.2 讲通道时的结论完全一致。**
  **所以「开并行」之前先问一句:我的存储能同时喂饱几个写者?**

ALL 541 KB、DATA_ONLY 541 KB、METADATA_ONLY 0.6 KB。 前两个几乎一样大,因为元数据(几条建表语句)在数据面前可以忽略。 这条结论要记:元数据几乎不占体积,数据才是大头。 所以「导出慢」永远要去查数据量,别怀疑元数据。 QUERY 那一行:20001 行变 1000 行,体积从 541 KB 掉到 20 KB。 这就是 QUERY 的价值——但它只在一张表上生效,多表时慎用。 EXCLUDE 那一行:4 个对象变 3 个,体积 541 KB 变 106 KB。 注意掉得比预想的多——被排除的 hr_person 正好是最大的那张表(2 万行)。 这套 Demo 的边界:它用的是 JSON 文本格式,真实 dmp 是二进制格式,体积比例会不一样。 但「谁占大头」这个结论是一样的——数据占绝大多数,元数据可以忽略。

30 秒实验

  1. 把 QUERY 的条件改成 pid <= 20000(等于不过滤),再跑一次。 看体积是不是回到 541 KB 附近——这说明 QUERY 生效时确实在过滤。
  2. 给 dump() 加一个 content="METADATA_ONLY" 的调用,但 tabs 传全部四个表。 看体积是不是还是很小的几十 K。
  3. 如果有 Oracle 环境,跑一次 expdp ... QUERY=scott.emp:"WHERE 1=2"。 看是不是只有结构、没有数据——这是「只导结构」的另一种写法。

常见误区

  1. 「QUERY 可以随便用在全库导出上」。 它会把条件套到每张表上,列名对不上就直接报错。
  2. 「EXCLUDE 和 INCLUDE 可以一起写」。 不能,二选一。它们是同一个筛子的两面。
  3. 「并行度越高导出越快」。 并行要存储跟得上。 一块盘上开四个并行,多半只是把时间花在抢 IO 上。
  4. 「导出不需要管一致性」。 导出期间业务还在写。 不指定 FLASHBACK_SCN 的话,不同表可能属于不同时刻。
自测三问(点开对答案) 1. `DUMPFILE` 里的 `%U` 是什么? —— **一个自动展开的序号占位符。** 不开并行时是 `01`,开并行时按进程数展开。 **所以写 `%U` 以后开并行不用改命令。** 2. `CONTENT=METADATA_ONLY` 的典型用途是什么? —— **在别的环境快速建一套一样的表结构,但不要数据。** **产物很小,导进去几秒钟。** 3. `QUERY` 为什么基本只在表模式里用? —— **因为全库或模式导出时,条件会应用到每一张表上。** **列名对不上就报错。** 4. 为什么要用 `FLASHBACK_SCN`? —— **保证整个导出读的是同一个时刻的一致性视图。** **不然导出期间业务还在写,不同表可能属于不同时刻,导进去就是脏数据。**

下一篇讲 IMPDP。导出只是把东西装进箱子,导入才是「搬到新家并摆好」。 麻烦的地方在于:新家可能已经有同名家具了——这就是 TABLE_EXISTS_ACTION 要处理的。

相关笔记