 数据迁移方案文档)
以下是完整的GBase 8s CLOB → VARCHAR(32739) 数据迁移方案文档代码已根据实际运行验证修正可直接交付客户使用。GBase 8s CLOB 转 VARCHAR(32739) 数据迁移方案一、方案概述1.1 背景客户表t_da_attach_log的op_content字段为CLOB类型存在性能问题需迁移到新表t_da_attach_log_new将字段改为VARCHAR(32739)。数据量约800 万行。1.2 核心设计表格问题解决方案CLOB 全表扫描内存暴涨JDBC 服务器端游标setFetchSize()流式读取客户端只缓存一批数据UUID 主键分页慢不用分页。顺序游标遍历天然无 OFFSET 性能衰减问题逻辑日志压力大分批提交每COMMIT_EVERY行如 5000 行执行一次commit()CLOB 转 VARCHAR(32739)Java 层截断Clob.getSubString(1, 32739)不依赖数据库端函数LOB 资源泄漏连接串AUTO_FREE_LOB1;lobcache-1 代码显式clob.free()1.3 为什么不使用 ROWID / LIMIT 分页表格方案缺点结论ROWID范围扫描ROWID 非严格连续分片表/删除后产生空洞导致数据遗漏或重复❌ 不推荐ORDER BY id LIMIT n OFFSET mUUID 无序OFFSET 越大全表扫描越慢800万行后期极慢❌ 不推荐JDBC 游标流式读取无分页逻辑内存稳定速度均匀兼容所有表类型✅采用二、数据库准备在testdb库中执行以下 SQL创建原表和目标表sql-- -- 1. 创建原表含 CLOB -- DROP TABLE IF EXISTS t_da_attach_log; CREATE TABLE t_da_attach_log ( id VARCHAR(32) NOT NULL, archive_id VARCHAR(32), op_user_id VARCHAR(36), op_date DATETIME YEAR TO FRACTION(5), op_content CLOB, -- 原字段CLOB op_user_name VARCHAR(100), op_flag VARCHAR(100), opinion VARCHAR(500), log_type VARCHAR(10), node_name VARCHAR(50), PRIMARY KEY (id) ); -- -- 2. 创建目标表CLOB 改为 VARCHAR(32739) -- DROP TABLE IF EXISTS t_da_attach_log_new; CREATE TABLE t_da_attach_log_new ( id VARCHAR(32) NOT NULL, archive_id VARCHAR(32), op_user_id VARCHAR(36), op_date DATETIME YEAR TO FRACTION(5), op_content VARCHAR(32739), -- 新字段VARCHAR(32739) op_user_name VARCHAR(100), op_flag VARCHAR(100), opinion VARCHAR(500), log_type VARCHAR(10), node_name VARCHAR(50), PRIMARY KEY (id) );三、Java 代码将以下 3 个文件保存到同一目录如/root/clob_to_varchar/。3.1 DbConfig.java — 数据库配置java/** * 数据库连接配置常量类 * 根据实际环境修改以下参数 */ public class DbConfig { /** * GBase 8s JDBC URL * AUTO_FREE_LOB1 : 自动释放 LOB防止内存泄漏 * lobcache-1 : 将 LOB 缓存到内存 * ifx_lock_mode_wait60 : 锁等待 60 秒 */ public static final String URL jdbc:gbasedbt-sqli://192.168.0.11:9088/testdb: GBASEDBTSERVERgbase01; AUTO_FREE_LOB1; lobcache-1; ifx_lock_mode_wait60; /** 数据库用户名 */ public static final String USER gbasedbt; /** 数据库密码 */ public static final String PASSWORD GBase20260410; /** JDBC 驱动类名GBase 8s 兼容 Informix 驱动 */ public static final String DRIVER_CLASS com.gbasedbt.jdbc.Driver; // 迁移参数配置 /** * 游标每次从服务器读取的行数。 * 值越大网络往返越少但服务端游标占用资源越多。 * 800万数据量建议 5000 ~ 10000。 */ public static final int FETCH_SIZE 5000; /** * JDBC 批量插入的批次大小。 * 每积累 BATCH_SIZE 行执行一次 executeBatch()。 */ public static final int BATCH_SIZE 1000; /** * 每处理多少行执行一次 commit()。 * 控制逻辑日志增长避免单事务过大。 * 必须是 BATCH_SIZE 的整数倍。 */ public static final int COMMIT_EVERY 5000; /** * CLOB 转 VARCHAR 时的最大字符长度。 * GBase 8s VARCHAR 最大 32765这里取 32739 留余量。 */ public static final int CLOB_MAX_LENGTH 32739; private DbConfig() {} }3.2 TestDataGenerator.java — 测试数据生成器javaimport java.sql.*; import java.util.UUID; /** * 测试数据生成器 * 向原表 t_da_attach_log 插入测试数据用于本地验证迁移程序 * * 运行前请确保 * 1. 已执行建表 SQL * 2. 已放置 GBase 8s JDBC 驱动 jar 到 classpath */ public class TestDataGenerator { public static void main(String[] args) { // 要生成的测试数据条数本地验证建议 1万条压力测试可改大 final int TOTAL_ROWS 10000; try { Class.forName(DbConfig.DRIVER_CLASS); } catch (ClassNotFoundException e) { System.err.println(❌ 找不到 JDBC 驱动请将 gbasedbt jdbc jar 加入 classpath); e.printStackTrace(); return; } String insertSql INSERT INTO t_da_attach_log (id, archive_id, op_user_id, op_date, op_content, op_user_name, op_flag, opinion, log_type, node_name) VALUES (?, ?, ?, CURRENT YEAR TO FRACTION(5), ?, ?, ?, ?, ?, ?); try (Connection conn DriverManager.getConnection(DbConfig.URL, DbConfig.USER, DbConfig.PASSWORD)) { // 关闭自动提交手动控制事务 conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement(insertSql)) { long startTime System.currentTimeMillis(); for (int i 1; i TOTAL_ROWS; i) { // 生成 UUID 作为主键去横线32位 String id UUID.randomUUID().toString().replace(-, ); String archiveId UUID.randomUUID().toString().replace(-, ); String opUserId UUID.randomUUID().toString(); // 构造 CLOB 内容模拟不同长度的文本 String clobContent buildClobContent(i); ps.setString(1, id); ps.setString(2, archiveId); ps.setString(3, opUserId); ps.setString(4, clobContent); // CLOB 字段直接传 String驱动自动处理 ps.setString(5, 用户_ (i % 100)); ps.setString(6, i % 2 0 ? 新增档案 : 修改档案); ps.setString(7, i % 3 0 ? 同意 : ); ps.setString(8, i % 5 0 ? XZDA : BGDA); ps.setString(9, i % 7 0 ? 审批节点 : null); ps.addBatch(); // 每 1000 条提交一次避免事务过大 if (i % 1000 0) { ps.executeBatch(); conn.commit(); System.out.printf(⏳ 已插入 %d / %d 条耗时 %.1f 秒%n, i, TOTAL_ROWS, (System.currentTimeMillis() - startTime) / 1000.0); } } // 提交剩余批次 ps.executeBatch(); conn.commit(); long totalTime System.currentTimeMillis() - startTime; System.out.println(━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━); System.out.printf(✅ 测试数据生成完成共插入 %,d 条总耗时 %.2f 秒%n, TOTAL_ROWS, totalTime / 1000.0); } catch (SQLException e) { conn.rollback(); throw e; } } catch (SQLException e) { System.err.println(❌ 数据库操作失败: e.getMessage()); e.printStackTrace(); } } /** * 构造模拟 CLOB 内容 * 部分记录短文本部分记录长文本接近/超过 32K 边界验证截断逻辑 */ private static String buildClobContent(int index) { StringBuilder sb new StringBuilder(); sb.append(【操作记录第).append(index).append(条】); // 模拟不同长度 // 50% 短文本 (100~300 字符) // 30% 中等文本 (1000~6000 字符) // 20% 长文本 (30000~35000 字符会触发截断) int length; if (index % 10 5) { length 100 (index % 200); } else if (index % 10 8) { length 1000 (index % 5000); } else { length 30000 (index % 5000); } // 填充内容 String template 这是一段模拟的档案操作日志内容用于测试CLOB字段迁移到VARCHAR(32739)的兼容性。; while (sb.length() length) { sb.append(template); } return sb.toString(); } }3.3 ClobToVarcharMigration.java — 核心迁移程序javaimport java.sql.*; /** * GBase 8s CLOB → VARCHAR(32739) 数据迁移程序 * * 核心机制 * 1. 【流式读取】使用 Statement.setFetchSize() 启用服务器端游标 * 避免一次性将 800万行加载到客户端内存。 * 2. 【Java层截断】不在SQL层用 dbms_lob_substrGBase 8s默认无此Oracle函数 * 而是在Java层通过 JDBC Clob.getSubString() 读取并截断为 32K 内文本 * 兼容性最好零依赖不依赖任何数据库扩展包。 * 3. 【批量插入】使用 PreparedStatement.addBatch() 积累数据减少网络往返。 * 4. 【分批提交】每 COMMIT_EVERY 行执行 commit()控制逻辑日志大小。 * * 优势 * - 不依赖 LIMIT/OFFSET无分页越往后越慢的问题 * - 不依赖 ROWID兼容分片表和普通表 * - 不依赖 UUID 主键顺序 * - 内存占用稳定只缓存 FETCH_SIZE BATCH_SIZE 行 */ public class ClobToVarcharMigration { // 统计计数器 private long totalRead 0; // 已读取行数 private long totalInsert 0; // 已插入行数 private long totalCommit 0; // 已提交次数 private long totalTruncate 0; // CLOB 被截断的行数 public static void main(String[] args) { new ClobToVarcharMigration().run(); } public void run() { // 加载 JDBC 驱动 try { Class.forName(DbConfig.DRIVER_CLASS); System.out.println(✅ JDBC 驱动加载成功: DbConfig.DRIVER_CLASS); } catch (ClassNotFoundException e) { System.err.println(❌ 找不到 JDBC 驱动请将 gbasedbt jdbc jar 加入 classpath); e.printStackTrace(); return; } long startTime System.currentTimeMillis(); // // 源表查询 SQL直接读取 CLOB不在 SQL 层截断 // // 不使用 dbms_lob_substr()Oracle函数GBase 8s默认无此函数 // 截断逻辑放到 Java 层处理兼容性最好 String selectSql SELECT id, archive_id, op_user_id, op_date, op_content, // 直接读取 CLOB不在 SQL 层截断 op_user_name, op_flag, opinion, log_type, node_name FROM t_da_attach_log; // 目标表插入 SQL String insertSql INSERT INTO t_da_attach_log_new (id, archive_id, op_user_id, op_date, op_content, op_user_name, op_flag, opinion, log_type, node_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); try (Connection readConn DriverManager.getConnection(DbConfig.URL, DbConfig.USER, DbConfig.PASSWORD); Connection writeConn DriverManager.getConnection(DbConfig.URL, DbConfig.USER, DbConfig.PASSWORD)) { // 读取连接配置启用服务器端游标 // 关键setAutoCommit(false) TYPE_FORWARD_ONLY CONCUR_READ_ONLY // 配合 setFetchSize() 启用游标实现流式读取 readConn.setAutoCommit(false); writeConn.setAutoCommit(false); System.out.println( 数据库连接建立成功); System.out.printf( 配置参数: FETCH_SIZE%d, BATCH_SIZE%d, COMMIT_EVERY%d%n, DbConfig.FETCH_SIZE, DbConfig.BATCH_SIZE, DbConfig.COMMIT_EVERY); System.out.println(⏳ 开始迁移数据请耐心等待...); System.out.println(━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━); // 创建读取 Statement启用游标 try (Statement readStmt readConn.createStatement( ResultSet.TYPE_FORWARD_ONLY, // 只向前游标内存友好 ResultSet.CONCUR_READ_ONLY); // 只读模式 PreparedStatement insertPs writeConn.prepareStatement(insertSql)) { // 【核心】设置游标每次获取的行数启用服务器端游标 // 对于 Informix/GBase 8s 驱动fetchSize 0 时会使用游标而非全量缓存 readStmt.setFetchSize(DbConfig.FETCH_SIZE); // 执行查询 try (ResultSet rs readStmt.executeQuery(selectSql)) { int batchCounter 0; // 当前批次计数 int commitCounter 0; // 当前事务内行数计数 while (rs.next()) { totalRead; // 从源表读取数据 String id rs.getString(id); String archiveId rs.getString(archive_id); String opUserId rs.getString(op_user_id); Timestamp opDate rs.getTimestamp(op_date); // 【关键】通过 JDBC Clob 接口读取只取前 CLOB_MAX_LENGTH 个字符 // 配合连接串 AUTO_FREE_LOB1;lobcache-1读取后自动释放 LOB 资源 String opContent readClobTruncated(rs, op_content); String opUserName rs.getString(op_user_name); String opFlag rs.getString(op_flag); String opinion rs.getString(opinion); String logType rs.getString(log_type); String nodeName rs.getString(node_name); // 设置插入参数 insertPs.setString(1, id); insertPs.setString(2, archiveId); insertPs.setString(3, opUserId); insertPs.setTimestamp(4, opDate); insertPs.setString(5, opContent); // VARCHAR(32739) 字段 insertPs.setString(6, opUserName); insertPs.setString(7, opFlag); insertPs.setString(8, opinion); insertPs.setString(9, logType); insertPs.setString(10, nodeName); insertPs.addBatch(); batchCounter; commitCounter; // 批量执行插入 if (batchCounter DbConfig.BATCH_SIZE) { insertPs.executeBatch(); insertPs.clearBatch(); totalInsert batchCounter; batchCounter 0; } // 分批提交控制逻辑日志 if (commitCounter DbConfig.COMMIT_EVERY) { writeConn.commit(); totalCommit; commitCounter 0; // 打印进度 printProgress(startTime); } } // 处理剩余未提交的批次 if (batchCounter 0) { insertPs.executeBatch(); totalInsert batchCounter; } if (commitCounter 0) { writeConn.commit(); totalCommit; } } // ResultSet 自动关闭游标释放 } // Statement 自动关闭 // 迁移完成打印统计 long totalTime System.currentTimeMillis() - startTime; System.out.println(━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━); System.out.println(✅ 数据迁移完成); System.out.printf( 总读取行数: %,d%n, totalRead); System.out.printf( 总插入行数: %,d%n, totalInsert); System.out.printf( 总提交次数: %,d%n, totalCommit); System.out.printf( CLOB 截断行数: %,d%n, totalTruncate); System.out.printf(⏱️ 总耗时: %.2f 秒 (%.2f 分钟)%n, totalTime / 1000.0, totalTime / 60000.0); System.out.printf(⚡ 平均速度: %.1f 行/秒%n, totalRead * 1000.0 / totalTime); // 数据校验对比源表和目标表行数 verifyData(readConn); } catch (SQLException e) { System.err.println(❌ 迁移过程中发生错误: e.getMessage()); e.printStackTrace(); } } /** * 安全读取 CLOB 并截断 * * 逻辑 * 1. 通过 rs.getClob() 获取 Clob 对象不加载全部内容到内存 * 2. 获取 CLOB 总长度计算实际读取长度取 min(长度, CLOB_MAX_LENGTH) * 3. 使用 getSubString(1, readLength) 精确读取指定长度 * 4. 显式调用 clob.free() 释放资源双保险AUTO_FREE_LOB 手动释放 * * param rs ResultSet * param columnName CLOB 字段名 * return 截断后的 String最大 CLOB_MAX_LENGTH 字符null 如果原值为 null */ private String readClobTruncated(ResultSet rs, String columnName) throws SQLException { Clob clob null; try { // 获取 Clob 对象 clob rs.getClob(columnName); if (clob null) { return null; } // 获取 CLOB 总长度 long clobLength clob.length(); // 如果长度超过限制标记截断计数 if (clobLength DbConfig.CLOB_MAX_LENGTH) { totalTruncate; } // 计算实际读取长度取较小值 int readLength (int) Math.min(clobLength, DbConfig.CLOB_MAX_LENGTH); // 从第 1 个字符开始读取 readLength 个字符 // 注意getSubString 的索引从 1 开始符合 SQL 标准 String result clob.getSubString(1, readLength); return result; } finally { // 显式释放 CLOB 资源即使 AUTO_FREE_LOB1显式释放更保险 if (clob ! null) { try { clob.free(); } catch (SQLException e) { // 忽略释放异常不影响主流程 } } } } /** * 打印实时进度 */ private void printProgress(long startTime) { long elapsed System.currentTimeMillis() - startTime; double speed totalInsert * 1000.0 / elapsed; System.out.printf(⏳ 已处理 %,d 行 | 已提交 %d 次 | 已运行 %.1f 秒 | 速度 %.0f 行/秒%n, totalInsert, totalCommit, elapsed / 1000.0, speed); } /** * 数据校验对比源表和目标表的行数 */ private void verifyData(Connection conn) { System.out.println(━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━); System.out.println( 开始数据校验...); String countSrc SELECT COUNT(*) FROM t_da_attach_log; String countDst SELECT COUNT(*) FROM t_da_attach_log_new; try (Statement stmt conn.createStatement(); ResultSet rs1 stmt.executeQuery(countSrc)) { long srcCount 0, dstCount 0; if (rs1.next()) srcCount rs1.getLong(1); try (ResultSet rs2 stmt.executeQuery(countDst)) { if (rs2.next()) dstCount rs2.getLong(1); } System.out.printf( 源表行数: %,d%n, srcCount); System.out.printf( 目标表行数: %,d%n, dstCount); if (srcCount dstCount) { System.out.println(✅ 校验通过源表和目标表行数一致); } else { System.out.printf(⚠️ 校验警告行数不一致差额 %,d 行%n, srcCount - dstCount); } } catch (SQLException e) { System.err.println(⚠️ 校验过程出错: e.getMessage()); } } }四、编译与运行步骤4.1 准备环境将gbasedbtjdbc_3.5.1_3X3_6XFDA_b9a1d9.jar或对应版本的 JDBC 驱动放到代码同一目录。4.2 编译bashcd /root/clob_to_varchar # Linux/macOS 用冒号 : 分隔 classpath javac -cp .:gbasedbtjdbc_3.5.1_3X3_6XFDA_b9a1d9.jar DbConfig.java TestDataGenerator.java ClobToVarcharMigration.java # Windows 用分号 ; 分隔 # javac -cp .;gbasedbtjdbc_3.5.1_3X3_6XFDA_b9a1d9.jar DbConfig.java TestDataGenerator.java ClobToVarcharMigration.java4.3 生成测试数据bashjava -cp .:gbasedbtjdbc_3.5.1_3X3_6XFDA_b9a1d9.jar TestDataGenerator预期输出plain⏳ 已插入 1000 / 10000 条耗时 x.x 秒 ... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ✅ 测试数据生成完成共插入 10,000 条总耗时 xx.xx 秒4.4 执行迁移bashjava -cp .:gbasedbtjdbc_3.5.1_3X3_6XFDA_b9a1d9.jar ClobToVarcharMigration预期输出plain✅ JDBC 驱动加载成功: com.gbasedbt.jdbc.Driver 数据库连接建立成功 配置参数: FETCH_SIZE5000, BATCH_SIZE1000, COMMIT_EVERY5000 ⏳ 开始迁移数据请耐心等待... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ⏳ 已处理 5,000 行 | 已提交 1 次 | 已运行 x.x 秒 | 速度 xxxx 行/秒 ⏳ 已处理 10,000 行 | 已提交 2 次 | 已运行 x.x 秒 | 速度 xxxx 行/秒 ... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ✅ 数据迁移完成 总读取行数: 10,000 总插入行数: 10,000 总提交次数: 2 CLOB 截断行数: xxx ⏱️ 总耗时: x.xx 秒 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 开始数据校验... 源表行数: 10,000 目标表行数: 10,000 ✅ 校验通过源表和目标表行数一致五、800万数据量生产环境调参表格参数本地测试生产环境800万行说明FETCH_SIZE50005000 ~ 10000游标批次减少网络往返BATCH_SIZE10001000 ~ 2000批量插入批次COMMIT_EVERY50005000 ~ 10000控制逻辑日志避免单事务过大JVM 堆内存512M2G ~ 4G留足批量缓存空间运行时段任意业务低峰期迁移期间目标表大量写入避开高峰生产运行前务必做两件事预估 CLOB 截断影响先跑统计 SQL确认超长记录数sqldbaccess testdb - create function gbasedbt.dbms_lob_getlength (clob) returns integer external name $GBASEDBTDIR/extend/excompat.1.0/excompat.bld(dbms_lob_getlength) language c; SELECT COUNT(*) FROM t_da_attach_log WHERE dbms_lob_getlength (op_content) 32739;业务低峰期执行迁移期间目标表会有大量插入建议避开业务高峰。六、常见问题排查表格错误信息原因解决找不到或无法加载主类java -cp缺少当前目录.java -cp .:jar文件 类名找不到符号 DbConfig编译时缺少依赖javac -cp .:jar文件 *.java不能解析例行程序 (dbms_lob_substr)使用了 Oracle 函数已修复改用 Java 层Clob.getSubString()No suitable driver foundJDBC URL 错误或驱动未加载检查 URL 格式和Class.forName()OutOfMemoryErrorFETCH_SIZE 或 BATCH_SIZE 过大适当调小参数或增大 JVM 堆内存七、代码交付清单plain/root/clob_to_varchar/ ├── gbasedbtjdbc_3.5.1_3X3_6XFDA_b9a1d9.jar # JDBC 驱动客户提供 ├── DbConfig.java # 数据库配置 ├── TestDataGenerator.java # 测试数据生成器 ├── ClobToVarcharMigration.java # 核心迁移程序 └── README.md # 本文档使用流程修改DbConfig.java中的连接参数执行数据库建表 SQL编译javac -cp .:jar *.java生成测试数据java -cp .:jar TestDataGenerator执行迁移java -cp .:jar ClobToVarcharMigration验证数据程序自动输出源表/目标表行数对比