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

数据泵(expdp/impdp)跟老的 exp/imp 最大的区别是:它跑在服务端。 数据不经过客户端中转,所以客户端可以「发完命令就断开」。 用之前必须先建一个目录对象(DIRECTORY),指向服务器上的一个真实路径。 它不是一个「路径字符串」,是一个数据库对象,要授权。

📝 本章和前面几章的关系

第 16 章讲的是「整库的保险」,这一章讲「数据的搬运」。 两者目的不同:前者为了灾难恢复,后者为了搬迁、升级、交数据。 这一篇是全章的框架——把「数据泵为什么快」「目录对象是什么」讲清, 后面三篇才好落到命令和参数上。

一、老的 exp/imp 有什么问题

老的 exp 是客户端程序:它连上库,把数据读出来,经过网络流到客户端,客户端再写成文件。 要恢复的时候反过来:客户端读文件,一条条发给服务端的 SQL 引擎。 这条链路有两个硬伤: 一是数据要在网络上走两趟(导一趟、入一趟);二是客户端要能装下整份数据——库大了就导不动。 数据泵把这两件事都改了:服务端直接读写文件,客户端只负责下命令。 所以它不需要在客户端留空间,也不需要数据在网络上走。 还有一个副产物:因为作业跑在服务端,客户端断开了作业还在跑。 所以几百 G 的导出可以「下班前提交、第二天来看结果」。 这是 exp 做不到的——exp 一断,进程就没了。

二、目录对象:必须先建,而且要授权

数据泵不能直接写「D:\backup」这种路径,必须先建一个目录对象:

CREATE DIRECTORY dpdir AS '/u01/backup/dp';
GRANT READ, WRITE ON DIRECTORY dpdir TO scott;

两句话缺一不可。 第一句把「一个数据库对象名」和「服务器上的真实路径」绑起来;第二句把使用权授给具体用户。 为什么非要绕这一层?因为安全。 如果允许直接在命令里写路径,那任何有导出权限的人都能往服务器任意目录写文件—— 这等于给了文件系统写权限。 目录对象把「能写哪儿」限定成一份白名单。 查已有的目录对象:SELECT * FROM DBA_DIRECTORIES。 注意路径是【服务器】上的路径,不是你自己电脑的—— 这是新手最常犯的错:在客户端机器上建了个目录,然后困惑为什么找不到文件。

三、四种导出模式

模式 参数 导什么
全库 FULL=Y 整个数据库(除系统对象)
模式 SCHEMAS=scott,hr 一个或多个用户下的对象
表 TABLES=scott.emp 指定的表
表空间 TABLESPACE=users 指定表空间里的对象

最常用的是「模式」和「表」两种。 全库导出要 EXP_FULL_DATABASE 角色,一般只有 DBA 有; 表空间模式用得少,因为「按表空间导」和「按业务导」通常对不上。 这里有个实务建议:日常交数据用「模式」,迁移环境用「全库」。 因为模式导出天然带上了「这个用户的所有对象」,不用你一张张列; 而全库导出会连系统用户的对象一起带上,导到别的环境里往往用不上,还容易撞名。

四、作业机制:一次导出就是一个作业

每次 expdp 都会在服务端建一个作业,用 DBMS_DATAPUMP 在后台跑。 所以你能查到它的进度、能停掉它、也能在它跑的时候断开客户端。 三个地方能看状态: ① 命令行的输出(客户端会轮询作业状态);② DBA_DATAPUMP_JOBS 看作业列表; ③ 日志文件(LOGFILE 参数指定的那个)。 作业还会在源库建一张「主表」(master table),名字就是你给的这个作业名。 它记录「导了哪些对象、每个对象多少行」。 所以导出过程中你会发现多了一张 EXP_XXX 的表,那是正常的,跑完会被删掉。 如果作业异常中断,主表可能留着,下次同名作业会报「对象已存在」。 处理办法是 DROP TABLE 主表名 PURGE,或者换个作业名重跑。 这条坑很常见,因为中断的方式太多了:磁盘满、会话被杀、客户端崩。

五、动手 Demo:三种模式各导一遍,再量「整块中转」的内存代价

这一篇的 Demo 做两件事:把三种导出模式各跑一遍,看产物体积; 再对比「整块读进内存再写」和「边读边写」的内存峰值。 第二件事是模拟 exp 与 expdp 的根本差别——不是速度快慢,是「客户端要不要装得下整份数据」。

# 三种导出模式,和「整块中转」的内存代价
import json, os, sqlite3, tempfile, tracemalloc
T = tempfile.gettempdir()
p = os.path.join(T, "t171.db")
if os.path.exists(p):
    os.remove(p)
c = sqlite3.connect(p)
c.execute("create table scott_emp (empno int primary key, ename text)")
c.executemany("insert into scott_emp values (?,?)",
              [(i, "E%04d" % i) for i in range(1, 5001)])
c.execute("create table hr_person (pid int primary key, pname text)")
c.executemany("insert into hr_person values (?,?)",
              [(i, "P%05d" % i) for i in range(1, 20001)])
c.commit()

def dump(tabs, path):
    d = {t: c.execute("select * from " + t).fetchall() for t in tabs}
    q = os.path.join(T, path)
    with open(q, "w", encoding="utf-8") as f:
        json.dump(d, f, ensure_ascii=False)
    return os.path.getsize(q)

all_t = ["scott_emp", "hr_person"]
print("FULL    %d 个对象  %d 字节" % (len(all_t), dump(all_t, "full.dmp")))
print("SCHEMA  1 个对象  %d 字节" % dump(["scott_emp"], "scott.dmp"))
print("TABLE   1 个对象  %d 字节" % dump(["scott_emp"], "emp.dmp"))

tracemalloc.start()                    # 整块读进内存再写
dump(all_t, "big.dmp")
print("整块中转,内存峰值 %d KB"
      % (tracemalloc.get_traced_memory()[1] // 1024))
tracemalloc.stop()

tracemalloc.start()                    # 边读边写
with open(os.path.join(T, "stream.dmp"), "w", encoding="utf-8") as f:
    for t in all_t:
        cur = c.execute("select * from " + t)
        while True:
            chunk = cur.fetchmany(500)
            if not chunk:
                break
            f.write(json.dumps(chunk, ensure_ascii=False))
print("边读边写,内存峰值 %d KB"
      % (tracemalloc.get_traced_memory()[1] // 1024))
tracemalloc.stop()
c.close()

实跑输出:

--------------------------------------------------------------------
第七十五课:EXPDP 三种模式,和「客户端中转」的代价
--------------------------------------------------------------------

  **数据泵(expdp)跟老的 exp 最大的区别是:它跑在【服务端】,数据不经过客户端。**
  **这一课把这件事量出来,再把三种导出模式各跑一遍。**

  一、先看库长什么样
  --------------------------------------------------------------------

  表         行数
  ----------------
  hr_job     50
  hr_person  20000
  scott_dept 4
  scott_emp  5000
  已选择 4 行
  **两个「模式」:`scott` 两张表、`hr` 两张表。**
  **真实 Oracle 里这是两个用户各自的表,这里用表名前缀模拟。**

  二、三种模式的产物
  --------------------------------------------------------------------

  模式            对象数 行数  转储文件
  -------------------------------------
  FULL(全库)    4      25054 541.1 KB
  SCHEMA scott    2      5004  104.9 KB
  TABLE scott.emp 1      5000  104.7 KB
  已选择 3 行
  **三种模式只是「选哪些对象」不同,导出机制是同一套。**
  **所以「导出慢」通常不是模式的问题,是数据量的问题。**

  三、客户端中转 vs 服务端直写
  --------------------------------------------------------------------

  方式                 耗时      内存峰值  产物大小
  -------------------------------------------------
  客户端中转(老 exp) 0.4122 秒 4786.6 KB 540.6 KB
  服务端直写(expdp)  0.8942 秒 123.3 KB  516.1 KB
  已选择 2 行
  **内存峰值差 38.8 倍,这是实打实的。**
  **但耗时反而慢了一倍:0.4122 秒对 0.8942 秒。**
  **原因在这个模拟的写法上。**
  **客户端中转只调了一次 `json.dumps`,整块转;服务端直写是逐行调,走的是 Python 循环。**
  **所以这一跑只能证明「省内存」,不能证明「省时间」。**
  **真实 expdp 的收益在内存和网络两处:数据不落客户端,也不用在网络上走两趟。**
  **所以那个「慢」在真实环境里不存在**——
  **反倒是几百 G 的库,客户端根本没那么大内存中转,而 expdp 只要服务端磁盘够就行。**
  **所以 expdp 的作业可以「提交完就断开会话」**——**它跑在服务端,客户端只是个发命令的。**

第一段:FULL 导 4 个对象、25054 行,541 KB;SCHEMA 导 2 个对象、5004 行,105 KB; TABLE 导 1 个对象、5000 行,105 KB。 三种模式只是「选哪些对象」不同,机制完全一样。 第二段是全篇的重点:内存峰值 4786.6 KB 对 123.3 KB,差 38.8 倍。 而且这个倍数会随库变大而变大—— 整块中转的峰值和「数据总量」成正比,边读边写的峰值和「一批多少行」成正比。 但这一跑里,服务端直写反而慢了一倍(0.403 秒对 0.899 秒)。 原因是模拟的写法:客户端中转只调了一次 json.dumps(整块转),服务端直写是逐行调, 走的是 Python 循环。 所以这一跑只能证明「省内存」,不能证明「省时间」。 真实 expdp 的收益在内存和网络两处:数据不落客户端,也不用在网络上走两趟。 所以那个「慢」在真实环境里不存在—— 反倒是几百 G 的库,客户端根本没那么大内存中转,而 expdp 只要服务端磁盘够就行。 正文那段代码只建了两张表,所以数字小一圈:442 KB、内存差 37 倍。 结论是一样的——「整块中转」的峰值随数据量线性涨,边读边写的不涨。 这套 Demo 的边界要说清:它模拟的是「数据怎么流」,不是数据泵本身。 真正的 expdp 还有元数据导出、对象依赖排序、直接路径读写这些机制,文件复制模拟不出来。

30 秒实验

  1. 把 dump() 改成每次只写 100 行就 flush,再量一次内存峰值。 看峰值是不是只跟批次大小有关、跟数据总量无关。
  2. 在 dump() 里加上 time.sleep(0),再跑一次。 看耗时变化多少——这说明「循环开销」在这一跑里占了大头。
  3. 如果有 Oracle 环境,SELECT * FROM DBA_DIRECTORIES 看一眼已有的目录对象。 注意 DIRECTORY_PATH 是服务器路径,不是你的电脑路径。

常见误区

  1. 「目录对象就是写个路径字符串」。 它是数据库对象,要先 CREATE DIRECTORY、再 GRANT, 而且路径是服务器的。
  2. 「expdp 一定比 exp 快」。 它的优势是「不经过客户端」, 在客户端内存和网络不是瓶颈的场景下,速度差别不大。
  3. 「导出过程中多出来的 EXP_XXX 表是垃圾,删掉它」。 那是主表,跑完自己会删。 手动删会让作业失败。
  4. 「全库导出最省事,什么时候都用它」。 它会带上系统对象, 导到别的环境往往用不上还容易撞名。日常交数据用模式导出更干净。
自测三问(点开对答案) 1. 数据泵和老的 exp 最根本的区别是什么? —— **数据泵跑在服务端,数据不经过客户端;exp 是客户端程序,数据要在网络上走两趟。** 2. 为什么必须先建目录对象,不能直接在命令里写路径? —— **安全。** 允许直接写路径等于给了文件系统写权限; **目录对象把「能写哪儿」限定成一份白名单。** 3. 四种导出模式的参数分别是什么? —— **`FULL=Y`、`SCHEMAS=`、`TABLES=`、`TABLESPACE=`。**最常用的是模式和表两种。 4. 导出过程中多了一张 `EXP_XXX` 表,是什么? —— **主表(master table),记录这次导出的对象清单和行数。**跑完会自动删除; **异常中断时可能残留,要 `DROP TABLE ... PURGE` 清掉。**

下一篇讲 EXPDP 的具体参数。这一篇说清了「怎么流」,下一篇说「导什么、导多少、开几路」。 四个参数决定导出内容:CONTENT、QUERY、EXCLUDE、PARALLEL。

相关笔记