GBase 8s CLOB → VARCHAR(32739) 数据迁移方案文档

以下是完整的 GBase 8s CLOB → VARCHAR(32739) 数据迁移方案文档,代码已根据实际运行验证修正,可直接交付客户使用。


GBase 8s CLOB 转 VARCHAR(32739) 数据迁移方案

一、方案概述

1.1 背景

客户表 t_da_attach_logop_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_LOB=1;lobcache=-1 + 代码显式 clob.free()

1.3 为什么不使用 ROWID / LIMIT 分页?

表格

方案 缺点 结论
ROWID 范围扫描 ROWID 非严格连续,分片表/删除后产生空洞,导致数据遗漏或重复 ❌ 不推荐
ORDER BY id LIMIT n OFFSET m UUID 无序,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_LOB=1    : 自动释放 LOB,防止内存泄漏
     * lobcache=-1        : 将 LOB 缓存到内存
     * ifx_lock_mode_wait=60 : 锁等待 60 秒
     */
    public static final String URL = 
        "jdbc:gbasedbt-sqli://192.168.0.11:9088/testdb:" +
        "GBASEDBTSERVER=gbase01;" +
        "AUTO_FREE_LOB=1;" +
        "lobcache=-1;" +
        "ifx_lock_mode_wait=60";
    
    /** 数据库用户名 */
    public static final String USER = "gbasedbt";
    
    /** 数据库密码 */
    public static final String PASSWORD = "GBase@20260410";
    
    /** 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 --- 测试数据生成器

java

复制代码
import 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 --- 核心迁移程序

java

复制代码
import java.sql.*;

/**
 * GBase 8s CLOB → VARCHAR(32739) 数据迁移程序
 * 
 * 核心机制:
 * 1. 【流式读取】使用 Statement.setFetchSize() 启用服务器端游标,
 *    避免一次性将 800万行加载到客户端内存。
 * 2. 【Java层截断】不在SQL层用 dbms_lob_substr(GBase 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_LOB=1;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_LOB=1,显式释放更保险)
            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 编译

bash

复制代码
cd /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.java

4.3 生成测试数据

bash

复制代码
java -cp ".:gbasedbtjdbc_3.5.1_3X3_6XFDA_b9a1d9.jar" TestDataGenerator

预期输出:

plain

复制代码
⏳ 已插入 1000 / 10000 条,耗时 x.x 秒
...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 测试数据生成完成!共插入 10,000 条,总耗时 xx.xx 秒

4.4 执行迁移

bash

复制代码
java -cp ".:gbasedbtjdbc_3.5.1_3X3_6XFDA_b9a1d9.jar" ClobToVarcharMigration

预期输出:

plain

复制代码
✅ JDBC 驱动加载成功: com.gbasedbt.jdbc.Driver
🔌 数据库连接建立成功
📋 配置参数: FETCH_SIZE=5000, BATCH_SIZE=1000, COMMIT_EVERY=5000
⏳ 开始迁移数据,请耐心等待...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⏳ 已处理 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_SIZE 5000 5000 ~ 10000 游标批次,减少网络往返
BATCH_SIZE 1000 1000 ~ 2000 批量插入批次
COMMIT_EVERY 5000 5000 ~ 10000 控制逻辑日志,避免单事务过大
JVM 堆内存 512M 2G ~ 4G 留足批量缓存空间
运行时段 任意 业务低峰期 迁移期间目标表大量写入,避开高峰

生产运行前务必做两件事:

  1. 预估 CLOB 截断影响:先跑统计 SQL,确认超长记录数:

    sql

    复制代码
    dbaccess 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;
  2. 业务低峰期执行:迁移期间目标表会有大量插入,建议避开业务高峰。


六、常见问题排查

表格

错误信息 原因 解决
找不到或无法加载主类 java -cp 缺少当前目录 . java -cp ".:jar文件" 类名
找不到符号 DbConfig 编译时缺少依赖 javac -cp ".:jar文件" *.java
不能解析例行程序 (dbms_lob_substr) 使用了 Oracle 函数 已修复:改用 Java 层 Clob.getSubString()
No suitable driver found JDBC URL 错误或驱动未加载 检查 URL 格式和 Class.forName()
OutOfMemoryError FETCH_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                                  # 本文档

使用流程:

  1. 修改 DbConfig.java 中的连接参数

  2. 执行数据库建表 SQL

  3. 编译:javac -cp ".:jar" *.java

  4. 生成测试数据:java -cp ".:jar" TestDataGenerator

  5. 执行迁移:java -cp ".:jar" ClobToVarcharMigration

  6. 验证数据:程序自动输出源表/目标表行数对比

相关推荐
秋田君1 小时前
QT_QColorDialog颜色对话框
java·数据库·qt
涛声依旧393161 小时前
运维项目:InnoAI SQL 助手
运维·数据库·sql
万悉科技1 小时前
时尚品牌GEO技术选型指南:LLM原生优化与AI记忆构建全解析
数据库·人工智能·搜索引擎
lb29172 小时前
在window上安装orcale数据库流程(快速,简洁)
数据库
IT小盘2 小时前
09-使用Redis保存大模型多轮对话上下文
数据库·redis·缓存
秋田君2 小时前
QT_QT布局常用类QSplitter窗口分割类与QDockWidget窗口停靠类
开发语言·数据库·qt
小果因子实验室2 小时前
策略研究--qmt实现新股自动申购卖出策略
数据库
Darling噜啦啦2 小时前
从零实战Milvus向量数据库:用RAG打造你的AI日记助手
数据库
淼澄研学3 小时前
PyTorch深度学习实战:5个核心方法从0到1构建神经网络
前端·数据库·python