前几天有人丢给我一个三十多兆的 .dmp 文件说“你把里面的建表语句和初始化数据抠出来给我一份能直接跑的 .sql”。我用文本编辑器打开一看全是二进制乱码。这件事的根源在于达梦数据库DM8里“导出”这个词至少背了三种完全不同的含义而大部分人心里想的那一种——生成一份纯文本、别人拿到手用disql一跑就能建库建表灌数据的脚本——恰好是dexp不负责的那一种。这篇东西就是把这个模糊地带拆开讲。我会说清楚 DM8 里导出 SQL 文件的几条主流路径各自解决什么问题、图形客户端里那些勾选框到底在控制什么、用DBMS_METADATA批量捞 DDL 时哪里会被截断、数据行拼成INSERT时字符串和日期怎么转义、以及最要命的一步为什么你导出的脚本在自己机器上好好的换一台库就跑不通。适合已经能连上 DM8、手上有建表权限、需要把结构和数据交付出去或做版本管理的开发者纯新手也能跟因为关键步骤我都写了为什么。1. 先分清“导出 SQL 文件”在 DM8 里到底指哪件事1.1 二进制备份、纯 DDL、可执行数据脚本是三样东西在达梦里提出“导出一份 SQL”这个需求实际可能对应三种交付物它们的生成工具、内部格式和下游用途完全不同。分不清这一点后面所有操作都是在碰运气。产物形态主力工具能否用文本编辑器打开典型用途增量可控性.dmp二进制备份dexp/dimp不能整库或整模式的备份迁移高支持按表、按条件纯 DDL 脚本DBMS_METADATA、图形客户端能建库建表、结构纳入版本管理一般按对象粒度DDL DML 混合脚本图形客户端导出向导、自拼INSERT能交付演示数据、小体量库整体搬运视方案而定手工拼最灵活这三者的关系不是替代而是分工。你要是做生产库的例行备份dexp生成的.dmp是最稳的速度快、一致性好、支持并行你要是要把表结构提交到代码仓库做 review那必须是纯文本 DDL你要是给测试环境准备一份能一键起库的初始化脚本那就得是 DDL 加 DML 的混合体。我见过最常见的误区是拿.dmp当交付物发给外部对方环境版本稍有差异dimp直接报错然后两边开始互相怀疑对方的库有问题。二进制格式天生就把版本绑死了跨版本、跨平台、跨字符集的场景下纯文本脚本的容错空间要大得多。判断标准很简单这份东西的接收方能不能连上你的库能连走dexp不能连走文本脚本。1.2 dexp 的输出格式决定了它不是给人看的dexp是达梦的逻辑导出工具输出的是达梦私有的二进制文件只有配套的dimp能解析。它的设计目标是完整、快速、可恢复不是可读。你可以在导出时选择是否包含索引、约束、触发器、权限但没法让它吐出CREATE TABLE这样的文本。一个典型的整模式导出长这样dexp USERIDSYSDBA/YourPassword127.0.0.1:5236 \ DIRECTORY/data/export \ FILEapp_schema_20240115.dmp \ LOGapp_schema_20240115.log \ SCHEMASAPP \ ROWSY \ INDEXY \ CONSTRAINTSY \ TRIGGERSY \ GRANTSY几个参数值得单独说。SCHEMAS和OWNER都表示按模式导出前者可以一次跟多个模式名后者是早期写法ROWSY控制是否带数据只要结构的话设成NQUERY参数可以加过滤条件做条件导出比如只导最近三个月的数据这个在超大表上非常实用DIRECTORY指的是执行dexp那台机器上的本地路径不是数据库服务器上的路径——这一点很多人第一次用会搞错以为文件会落在服务器上。日志文件一定要看。dexp失败的时候不一定在终端里把错误打全但日志里会有完整的对象级错误信息比如某张表因为锁等待超时被跳过。我有一次导出一个两百多张表的模式终端里显示成功日志里却有四张表因为“对象不存在”被跳过——原因是那几张表属于另一个模式只是通过同义词访问。这种问题不看日志根本发现不了。1.3 四条路径怎么选按交付对象倒推与其记住所有工具不如从“这份脚本最终要给谁、在什么环境跑”倒推。下面这张表是我自己在不同场景下的实际选择可以直接对照。场景推荐路径理由只交付表结构接收方自建库DBMS_METADATAspool纯文本、可版本管理、批量可控结构 少量配置数据几十张表以内图形客户端导出向导勾选即出不用写脚本结构 大量数据百万行以上dexp出.dmp接收方dimp文本脚本在这个体量下会慢到不可接受需要精确控制导出内容、做数据脱敏自拼INSERT语句每一列都能改能过滤能替换表格里第四行是很多人忽略的。做数据脱敏或者构造测试数据时图形工具给不了你“把手机号中间四位打码再导出”这种能力只有自己拼 SQL 才能做到。代价是要处理类型转换和转义这部分我在第 4 节会展开。选型的核心判断点其实就两个数据量和接收方环境。数据量决定你能不能承受文本脚本的体积和导入耗时接收方环境决定你能不能直接用二进制格式。2. 图形客户端导出向导每个勾选框背后的含义2.1 对象范围的圈定比想象中更需要小心达梦的图形管理客户端里导出入口通常挂在对象树的右键菜单上可以对整个模式、单张表、或者一批选中的对象操作。看起来很简单但“选中什么”这件事本身就有歧义。如果你在模式节点上右键导出导出的范围通常是该模式下的全部对象包括表、视图、序列、存储过程、自定义函数、甚至同义词。这里有个坑视图和存储过程的 DDL 里可能引用其他模式的对象如果你的目标库没有对应的模式脚本在执行时会报“对象不存在”。稳妥的做法是导出前先查一遍依赖。-- 查当前模式下所有视图及其定义长度先看规模 SELECT VIEW_NAME, TEXT_LENGTH FROM ALL_VIEWS WHERE OWNER APP ORDER BY VIEW_NAME;对于表级导出我一般会先按体量排序把大表和配置表分开处理。配置表走文本脚本大表走dexp两条线并行最后在目标库合并。-- 按行数粗排找出体量大的表 SELECT TABLE_NAME, NUM_ROWS FROM ALL_TABLES WHERE OWNER APP ORDER BY NUM_ROWS DESC NULLS LAST;NUM_ROWS依赖统计信息如果库很久没收集过统计这个值可能是空的或者严重失真那就得用COUNT(*)实测。我在一次迁移里就因为信了统计信息把一张实际有八百万行、统计里显示两万行的表当小表处理结果文本脚本导出跑了四十分钟中途还因为临时空间不足挂了。2.2 导出选项里的三个决策点图形向导的选项页通常有一堆勾选框其中真正影响结果的只有三个是否包含数据、DDL 和 DML 是否分文件、以及对象类型的勾选范围。“包含数据”这一项决定输出是纯结构还是结构加数据。如果你的目的是做结构版本管理务必关掉它否则每次导出的文件里都混着成千上万行INSERTdiff 出来一片红根本没法 review。“是否分文件”这一项在小规模时无所谓但对象一多就体现出价值。分文件的好处是可以单独重跑某一类对象比如只重建索引坏处是文件数量爆炸几百张表就是几百个文件。我通常的做法是表结构、索引约束、序列、视图存储过程各出一个文件四到五个文件既能分批执行又好管理。对象类型的勾选范围最容易出事。默认往往全选但“全选”里包含了一些你未必想要的东西——比如表的存储参数表空间名、初始簇大小、增长方式。这些参数在你本地环境里指向的是某个具体表空间导到目标库时如果那个表空间不存在建表语句直接失败。要么在导出时关掉存储参数要么在目标库先建好同名表空间。一个实用技巧导出后先用文本搜索把STORAGE、TABLESPACE、USING INDEX TABLESPACE这些关键字过一遍看看有没有硬编码的环境信息。这一步花两分钟能省掉后面半小时的报错排查。2.3 产物结构和二次编辑的实际处理向导生成的脚本一般是有序的先是各种CREATE TABLE然后是主键、唯一约束、索引再是外键最后是数据。这个顺序是有讲究的因为外键依赖被引用表的存在索引建在数据加载之后比边插边维护快得多。但顺序正确不代表可以直接跑。实际拿到的脚本通常需要三轮加工。第一轮处理环境相关的部分。把表空间名换掉或者删掉把STORAGE子句里的具体数值改成目标库的合理值。如果源库的表建在小表空间上而目标库用的是自动扩展的大表空间这些参数留着反而是负担。第二轮处理字符和标识符。达梦默认大小写敏感不加双引号的标识符会被折叠成大写。如果源库建表时用了带引号的小写表名导出的脚本里也会带引号这部分必须原样保留一旦有人手工把引号删了表名就变了。这个坑我在第 5 节还会展开。第三轮是补上脚本缺失的部分。图形工具导出的内容通常不包含用户创建、权限授予、角色分配也不包含COMMENT ON注释语句取决于版本和勾选项。如果目标库需要完整的权限体系这些得自己补。-- 补齐注释先查出所有列注释再拼成 COMMENT ON 语句 SELECT COMMENT ON COLUMN || OWNER || . || TABLE_NAME || . || COLUMN_NAME || IS || REPLACE(COMMENTS, , ) || ; FROM ALL_COL_COMMENTS WHERE OWNER APP AND COMMENTS IS NOT NULL;这段查询本身就是个“生成语句的语句”跑出来的结果直接粘贴到脚本末尾就行。同类思路可以用于生成权限语句、生成索引重建语句非常省事。3. 用 DBMS_METADATA 把 DDL 批量捞出来3.1 单表 GET_DDL 的参数顺序与常见报错达梦提供了兼容的DBMS_METADATA系统包其中GET_DDL是最常用的一个能从数据字典里直接把对象的建语句取出来。基本调用形式是这样SELECT DBMS_METADATA.GET_DDL(TABLE, T_ORDER, APP) FROM DUAL;三个参数分别是对象类型、对象名、所属模式。顺序不能错而且这三个参数的大小写处理跟数据库的大小写敏感设置直接相关。在大小写敏感的实例里如果表是以大写形式存储的你传小写表名就会报“对象不存在”反过来如果表名本身建的时候带了双引号是小写的你就必须传小写。我一般先用数据字典确认存储形式再决定传什么SELECT OWNER, TABLE_NAME FROM ALL_TABLES WHERE UPPER(TABLE_NAME) T_ORDER;看到返回的是T_ORDER就传大写看到t_order就传小写别猜。这个查询看起来啰嗦但比反复试错快。对象类型的取值不止TABLE。索引是INDEX视图是VIEW序列是SEQUENCE约束是CONSTRAINT存储过程是PROCEDURE。不同小版本支持的类型范围不完全一致用之前可以先拿一个不存在的类型试一下看报错信息里有没有列出支持列表。3.2 CLOB 结果被截断的两级处理GET_DDL返回的是 CLOB 类型问题就出在这里。命令行工具默认的显示宽度很小一张有几十个字段、带完整存储参数的表返回的语句可能有几千个字符你在终端里看到的往往只有开头几行加一个省略标记。对付截断有两个层次的解法。轻量级的是调整disql的显示参数把行宽和长字段显示长度都调大SET LINESIZE 32767; SET LONG 999999; SELECT DBMS_METADATA.GET_DDL(TABLE, T_ORDER, APP) FROM DUAL;这些SET命令的具体可用性和取值范围跟版本有关如果执行报错就换成图形客户端来做。图形客户端里 CLOB 字段通常可以直接双击展开或者另存为文件不看截断问题。重量级的解法是绕开显示直接把结果写进文件。这时用spoolSPOOL /data/export/ddl_app.sql; SET HEADING OFF; SET FEEDBACK OFF; SET PAGESIZE 0; SELECT DBMS_METADATA.GET_DDL(TABLE, T_ORDER, APP) FROM DUAL; SPOOL OFF;SET HEADING OFF去掉列标题SET FEEDBACK OFF去掉“已选择 1 行”这类提示SET PAGESIZE 0去掉分页。这三个设置不做的话导出的文件里会混进一堆非 SQL 内容跑脚本时直接语法错误。我踩过的坑spool文件默认编码跟随客户端环境如果客户端是 UTF-8 而目标库实例是 GB18030中文注释导过去就是乱码。导出前先用SELECT SF_GET_UNICODE_FLAG();确认实例字符集返回值 0 是 GB180301 是 UTF-82 是 UTF-16然后让客户端编码跟它保持一致。3.3 按模式批量生成 DDL 的循环写法单表调用一次一次写几百张表显然不现实。达梦的disql支持一些脚本控制语句可以用游标循环把整个模式的表全捞出来。思路是先查数据字典拿到表名列表再对每个表名调用GET_DDL把结果拼接进一个 CLOB 变量里输出。如果不想写复杂的存储过程有个更土但更可靠的办法先用查询生成“调用GET_DDL的语句”再把生成结果当作脚本执行。-- 第一步生成一批 SELECT 语句 SELECT SELECT DBMS_METADATA.GET_DDL(TABLE, || TABLE_NAME || , || OWNER || ) FROM DUAL; FROM ALL_TABLES WHERE OWNER APP AND TABLE_NAME NOT LIKE TMP_% ORDER BY TABLE_NAME;跑出来的结果是一批SELECT语句把它们复制出来配合spool一起执行就能得到一个模式里所有表的 DDL。这种“先生成再执行”的两段式写法好处是完全可控——你可以在第一步的查询里加任意过滤条件排除临时表、排除分区子表、只导某几个前缀的表比图形工具灵活得多。第二步执行时记得把SET LONG调大否则每张表都截断导出来的脚本全是半截语句比没有更糟。3.4 spool 落盘之后的清洗工作spool出来的文件不能直接用通常要清洗三件事。一是去掉首尾的空白行和连接信息。disql启动时会打印版本号和连接提示这些内容会混进spool文件的开头如果SPOOL命令是在连接之后才执行的一般不会有这个问题但保险起见还是搜一下有没有非 SQL 的残留行。二是处理GET_DDL返回值末尾的分号问题。有些版本返回的 DDL 自带结尾分号有些没有。如果批量拼接后每个语句都少了分号整个文件就是一堆连在一起的语法错误。快速检查的办法是搜一下文件中;的数量和CREATE的数量是否大致匹配。三是补上分隔符。disql执行脚本时默认用分号作为语句结束符但如果 DDL 里包含存储过程或触发器这种内部有分号的块就必须用/作为块结束符。这类对象建议单独放一个文件用SET SQLBLANKLINES ON之类的设置配合避免解析错乱。4. 数据行怎么变成 INSERT自己拼才可控4.1 一个能覆盖大多数场景的拼接模板结构和数据分开处理之后数据部分的自由度就大了。核心思路是对每一列按类型做分支处理用CASE WHEN拼出合法的字面量。下面这个模板适用于数值、字符串、日期三类常见字段SELECT INSERT INTO APP.T_ORDER(ID,ORDER_NO,AMOUNT,STATUS,CREATE_TIME) VALUES( || CASE WHEN ID IS NULL THEN NULL ELSE TO_CHAR(ID) END || , || CASE WHEN ORDER_NO IS NULL THEN NULL ELSE || REPLACE(ORDER_NO, , ) || END || , || CASE WHEN AMOUNT IS NULL THEN NULL ELSE TO_CHAR(AMOUNT) END || , || CASE WHEN STATUS IS NULL THEN NULL ELSE || REPLACE(STATUS, , ) || END || , || CASE WHEN CREATE_TIME IS NULL THEN NULL ELSE TO_TIMESTAMP( || TO_CHAR(CREATE_TIME, YYYY-MM-DD HH24:MI:SS.FF6) || ,YYYY-MM-DD HH24:MI:SS.FF6) END || ); FROM APP.T_ORDER WHERE CREATE_TIME TO_TIMESTAMP(2024-01-01 00:00:00, YYYY-MM-DD HH24:MI:SS);几个必须说清楚的地方。REPLACE(ORDER_NO, , )这一串引号看起来吓人拆开看就明白了SQL 里表示一个单引号字符需要写四个连续单引号表示两个连续单引号需要写六个。所以第一个参数代表待替换的单引号第二个参数代表替换成的两个单引号。这是 SQL 标准里转义单引号的唯一办法字符串里出现OBrien这种名字时不处理就会导致整条语句语法错误。日期部分用TO_TIMESTAMP而不是直接写字符串是因为达梦对隐式类型转换的处理受会话参数影响显式转换能保证在任何环境下结果一致。FF6表示保留六位小数秒如果你的表只到秒精度去掉它就行。WHERE条件是这套方案的核心优势。你可以加任意过滤只导特定时间段、特定状态的数据甚至可以对敏感字段做替换——把PHONE换成138****0000导出的脚本天然就是脱敏的。4.2 分类型处理的细节清单不同数据类型各有各的脾气我把实际遇到过的整理成一张表照着填基本不会错。数据类型拼接写法注意点INT/BIGINTTO_CHAR(col)空值必须走IS NULL分支否则TO_CHAR(NULL)结果不确定DECIMAL/NUMBERTO_CHAR(col)大精度数字可能被转成科学计数法必要时指定格式掩码VARCHAR/CHAR单引号包裹 REPLACE转义这是唯一必须做转义的类型DATETO_DATE(...,YYYY-MM-DD)显式给格式别依赖会话默认TIMESTAMPTO_TIMESTAMP(...)注意小数秒位数CLOB不建议用拼接见下一小节BLOB不建议用拼接见下一小节自增列跳过该列见 4.4科学计数法这个坑特别隐蔽。DECIMAL(30,10)类型的字段用TO_CHAR转出来在某些情况下会变成1.2345678901E10这种形式而INSERT语句里写科学计数法是合法的只是精度会丢。安全做法是加格式掩码TO_CHAR(AMOUNT, FM99999999999999999990.0999999999)FM去掉前导空格整数部分按最大位数写 9小数部分按精度写 9这样转出来是定点格式不会有精度损失。4.3 大字段和二进制硬拼 SQL 是下策CLOB和BLOB这两类字段用字符串拼接的方式生成INSERT会遇到硬性限制SQL 语句里的字符串字面量长度受参数约束通常在一万字节上下超过就报错。一张表里只要有一个字段存了几万字的文本这套方案就废了。技术上可以做分段拼接——先用INSERT插入除大字段外的所有列再用多条UPDATE配合||逐段追加。但这会把脚本体积放大好几倍性能也差。现实的选择是绕开。含大字段的表走dexp导出或者用图形客户端单独处理这张表文本脚本只负责其余部分。如果接收方必须拿到纯文本脚本那就得接受“大字段内容被截断到某个长度”或者“大字段单独存成文本文件、脚本里只放文件名”这类妥协。二进制字段更麻烦。RAWTOHEX能把BLOB转成十六进制字符串再用HEXTORAW转回去理论上是闭环的CASE WHEN ATTACH IS NULL THEN NULL ELSE HEXTORAW( || RAWTOHEX(ATTACH) || ) END但同样受字面量长度限制只能处理小文件。而且十六进制编码会把体积翻倍一张存了几百张缩略图的表导出的脚本能到几个 G用文本编辑器打开就卡死。这类数据老老实实走二进制备份通道不要为难自己。4.4 自增列、序列和触发器的处理顺序自增列的处理是数据导入阶段最容易出问题的地方。达梦的自增列IDENTITY在插入时如果不指定该列会自动按种子和步长生成如果显式指定需要额外的开关控制。我的建议是导出时直接跳过自增列让目标库重新生成。这样做的代价是自增值跟源库对不上如果业务上依赖这个值的连续性就得手动把起始值调过去。-- 查当前自增值 SELECT IDENT_CURRENT(APP.T_ORDER); -- 导入完成后调整起始值 ALTER TABLE APP.T_ORDER ALTER COLUMN ID SET INCREMENT BY 10000;IDENT_CURRENT和相关的IDENT_SEED、IDENT_INCR函数在不同小版本上的行为可能有差异用之前先在测试库上验一遍别直接在生产上试。序列的处理相对简单但顺序不能错。序列必须先于使用它的表创建否则表的默认值引用不到序列会报错。如果序列的当前值有业务意义比如单号不能重复导出时要单独记录当前值导入后执行ALTER SEQUENCE ... RESTART WITH n把它推到正确位置。这一步很容易漏漏了的后果是新库产生的单号跟旧库冲突。触发器必须最后建。数据导入过程中如果触发器已经存在每一行插入都会触发一次业务逻辑轻则变慢重则数据被改得面目全非。正确顺序是建表 → 建约束索引 → 导数据 → 建触发器 → 建外键。5. 从“能导出”到“能导进去”之间隔着什么5.1 大小写敏感最容易翻车的一处达梦实例在初始化时可以设置是否大小写敏感这是个建库时定死、后期改不了的参数。敏感模式下不加双引号的标识符会被统一折叠成大写不敏感模式下则保留原样或统一折叠成小写。这个差异会直接决定你的脚本能不能跑。查当前实例的设置SELECT CASE_SENSITIVE();返回 1 表示敏感。如果源库是敏感的、目标库不敏感或者反过来同一个脚本在两个库上的行为就不一样。最典型的翻车场景是源库里有一张用双引号建的小写表t_user导出的 DDL 里带引号一切正常但有人在目标库手工执行时把引号删了表就变成了T_USER后续所有引用t_user的语句全部报对象不存在。我的处理原则是导出的脚本一个字都不要手工改标识符尤其是引号。要改就在源头上改重新生成一遍。5.2 字符集中文乱码的两个来源中文乱码通常来自两个地方得分开排查。一是实例字符集不一致。源库是 GB18030目标库是 UTF-8脚本文件本身是什么编码就变得很关键。如果脚本文件按 GB18030 存在 UTF-8 的库上执行中文注释和中文数据都会变乱码。解决办法是在导出时就把客户端编码对齐到目标库的字符集或者导出后统一转码。二是客户端工具的编码设置。disql和图形客户端的编码不一定跟实例一致中间多了一层转换。比较稳的做法是让导出和执行两端使用同一套工具、同一份编码配置。-- 确认实例字符集 SELECT SF_GET_UNICODE_FLAG();返回 0 是 GB180301 是 UTF-82 是 UTF-16。这个函数的返回值我在多个版本上验证过比较可靠。导出前查一次执行前在目标库再查一次两个值不一致就得走转码流程别指望自动兼容。5.3 表空间与存储子句能导出但导不进去的头号原因前面提过一次这里展开说。图形工具和GET_DDL导出的建表语句通常会把存储信息带上形如CREATE TABLE APP.T_ORDER ( ... ) STORAGE(ON APP_DATA, CLUSTERBTR);ON APP_DATA指定了表放在哪个表空间。目标库如果没有APP_DATA这个表空间这条语句直接失败。更麻烦的是有些语句里的表空间名藏在索引定义、分区定义里搜一遍TABLESPACE未必能全找出来。两个处理方向。一是目标库提前建好同名表空间参数按目标环境的实际情况配这样脚本一个字不用改。二是导出时就剥掉存储信息让表落到用户的默认表空间里。第二种更省事适合临时环境和测试库生产环境我倾向于第一种因为表空间规划本身就是部署的一部分不该让脚本自己决定。索引的表空间也要注意。索引和表放在不同表空间是常见做法导出时如果只处理了表的存储子句索引建的时候还是会去找不存在的表空间。5.4 约束、索引、外键的执行顺序一个能跑的脚本对象顺序必须满足依赖关系。我用的顺序是这样的创建用户和模式脚本里通常没有得手工补创建表空间如果目标库没有创建序列表创建所有表不带外键创建主键和唯一约束创建普通索引导入数据创建外键创建触发器授予权限、添加注释收集统计信息把外键放到数据导入之后是因为外键约束会在每行插入时做一次检查几百万行的表这么搞会慢到离谱。索引同理先建索引再插数据每次插入都要维护 B 树比插完再统一建索引慢一个数量级。这个顺序在手工整理脚本时容易搞乱尤其是图形工具一次导出全部对象、文件里的顺序未必符合上面的要求。我的做法是导出后按对象类型拆成多个文件按上面的顺序编号命名执行时按文件名排序跑一眼就能看出顺序对不对。6. 验证让脚本在空库里跑一遍6.1 回放验证的最小流程导出完成不等于交付完成。我给自己定的规矩是任何交付出去的脚本必须在干净环境里完整跑通至少一次。这个动作花不了多少时间但能拦下绝大部分低级错误。流程大致是这样。先起一个干净的实例或者建一个空模式字符集和大小写敏感设置尽量贴近目标环境——这两项是建库时定死的事后模拟不了所以必须一开始就对上。然后用disql按顺序执行拆分好的脚本文件。disql SYSDBA/YourPassword127.0.0.1:5236 \ -e START /data/export/01_tablespace.sql \ -e START /data/export/02_tables.sql \ -e START /data/export/03_constraints.sqldisql的START命令用来执行脚本文件也可以用-e直接跟语句。执行过程中把输出重定向到日志跑完搜一遍日志里的ERROR和错误关键字。达梦的报错信息有时候中英文混着出两个都搜。跑通之后再做一轮反向检查源库和目标库分别统计对象数量逐个比对。-- 表数量比对 SELECT COUNT(*) FROM ALL_TABLES WHERE OWNER APP; -- 索引数量比对 SELECT COUNT(*) FROM ALL_INDEXES WHERE TABLE_OWNER APP; -- 约束数量比对 SELECT CONSTRAINT_TYPE, COUNT(*) FROM ALL_CONSTRAINTS WHERE OWNER APP GROUP BY CONSTRAINT_TYPE;三类数量都对得上基本可以认为结构完整。数量对不上就按对象名做差集找出漏掉的。6.2 行数与抽样比对结构之外还要验数据。全表COUNT(*)比对是最直接的方式源库和目标库各跑一遍看差值。-- 源库 SELECT T_ORDER AS TBL, COUNT(*) AS CNT FROM APP.T_ORDER UNION ALL SELECT T_USER, COUNT(*) FROM APP.T_USER; -- 目标库执行同样的语句结果做比对表多的时候手工比对不现实可以把两边的结果导成文本用diff比。或者更省事一点在源库生成一段拼接好的统计 SQL目标库直接跑输出格式统一diff 起来干净。行数一致不代表内容一致还得抽样看。我一般挑三类表抽有中文内容的、有日期字段的、有金额字段的。这三类分别覆盖了字符集问题、时区或格式问题、精度问题。抽的时候直接SELECT *挑几行对比肉眼扫一遍比写复杂的校验脚本快。对精度要求高的场景可以做聚合校验比如对金额字段求和、对字符串字段算长度总和SELECT SUM(AMOUNT) AS TOTAL_AMT, SUM(LENGTH(ORDER_NO)) AS TOTAL_LEN, COUNT(DISTINCT STATUS) AS STATUS_KINDS FROM APP.T_ORDER;这三个值在两边都算一遍对上了说明数据和编码都没出大问题。这个方法不能发现行内错位这类问题但作为快速验证足够用。6.3 我自己的几条检查清单攒了几年我把每次交付前必查的项目固定成了几条脚本文件开头没有disql的版本信息、连接提示、空白行每条语句以分号结尾存储过程和触发器单独成文件并用块结束符搜索TABLESPACE、STORAGE、ON 确认没有硬编码的环境信息搜索小写的带引号标识符确认它们全部原样保留自增列的当前值记录在单独的文件里导入后有对应的调整语句序列的当前值同上中文内容和中文注释在目标库抽查过没有乱码目标库的对象数量、行数、聚合校验三项都通过这套清单看起来琐碎但每一条都对应着我真实踩过的坑。特别是第三条和第四条几乎每次换环境部署都会中一次。养成导出后先搜一遍关键字的习惯能挡掉大半问题。最后再分享一个小技巧如果你的导出脚本要反复用在不同环境把环境相关的部分抽成变量放在文件头比如表空间名、模式名、字符集转换方式下面所有语句引用变量。disql支持DEFINE和变量替换虽然不如专业迁移工具那么健全但应付多环境部署足够了。这样一份脚本维护一套比每个环境复制一份改一遍要可靠得多也不会出现改了 A 环境忘了改 B 环境的情况。