数据泵(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 秒实验
- 把
dump()改成每次只写 100 行就 flush,再量一次内存峰值。 看峰值是不是只跟批次大小有关、跟数据总量无关。 - 在
dump()里加上time.sleep(0),再跑一次。 看耗时变化多少——这说明「循环开销」在这一跑里占了大头。 - 如果有 Oracle 环境,
SELECT * FROM DBA_DIRECTORIES看一眼已有的目录对象。 注意DIRECTORY_PATH是服务器路径,不是你的电脑路径。
常见误区
- 「目录对象就是写个路径字符串」。 它是数据库对象,要先
CREATE DIRECTORY、再GRANT, 而且路径是服务器的。 - 「expdp 一定比 exp 快」。 它的优势是「不经过客户端」, 在客户端内存和网络不是瓶颈的场景下,速度差别不大。
- 「导出过程中多出来的
EXP_XXX表是垃圾,删掉它」。 那是主表,跑完自己会删。 手动删会让作业失败。 - 「全库导出最省事,什么时候都用它」。 它会带上系统对象, 导到别的环境往往用不上还容易撞名。日常交数据用模式导出更干净。
自测三问(点开对答案)
1. 数据泵和老的 exp 最根本的区别是什么? —— **数据泵跑在服务端,数据不经过客户端;exp 是客户端程序,数据要在网络上走两趟。** 2. 为什么必须先建目录对象,不能直接在命令里写路径? —— **安全。** 允许直接写路径等于给了文件系统写权限; **目录对象把「能写哪儿」限定成一份白名单。** 3. 四种导出模式的参数分别是什么? —— **`FULL=Y`、`SCHEMAS=`、`TABLES=`、`TABLESPACE=`。**最常用的是模式和表两种。 4. 导出过程中多了一张 `EXP_XXX` 表,是什么? —— **主表(master table),记录这次导出的对象清单和行数。**跑完会自动删除; **异常中断时可能残留,要 `DROP TABLE ... PURGE` 清掉。**下一篇讲 EXPDP 的具体参数。这一篇说清了「怎么流」,下一篇说「导什么、导多少、开几路」。 四个参数决定导出内容:CONTENT、QUERY、EXCLUDE、PARALLEL。
- ☐ (本篇完,下一篇 17.2 EXPDP 导出数据)
相关笔记
- 使用 RMAN 工具备份
- 备份与恢复概述
- 表空间与数据文件的关系
-
- 🐬 MySQL必知必会/00-索引
- 🏺 Oracle从入门到精通 · 索引