dbms_lob.substr 的迁移脚本得到 "character string buffer too small"

Migration script with dbms_lob.substr gets "character string buffer too small"

我在将数据从旧的 table 迁移到新的脚本时遇到问题。旧 table 中的其中一列是 CLOB,但在新 table 中它是 VARCHAR2。我尝试使用下面的代码插入那些。但是我遇到了错误的麻烦:

ORA-06502: PL/SQL: numeric or value error: character string buffer too small.

DECLARE
    CURSOR CUR IS
        SELECT T.*
          FROM ACTIVITY_EVENT T
         WHERE T.POST_TEXT IS NOT NULL;
    R CUR%ROWTYPE;
BEGIN
 FOR R IN CUR
    LOOP
        INSERT INTO STREAM_TEXT
            WITH STR AS
             (SELECT T.*
                FROM STREAM T
               WHERE T.OLD_ID = R.ID)
            SELECT SEQ$STREAM_TEXT.NEXTVAL AS ID,
                   DBMS_LOB.SUBSTR(T.POST_TEXT, 4000, 1) AS TEXT,
                   T.DT AS DT,
                   'READY' AS STATE,
                   STR.ID AS STREAM_ID
              FROM ACTIVITY_EVENT T
              LEFT JOIN STR
                ON STR.OLD_ID = T.ID
             WHERE T.POST_TEXT IS NOT NULL
               AND STR.OLD_ID = T.ID;
    END LOOP;
END;

我先在没有循环的情况下编写了这段代码,但遇到了同样的问题,所以我尝试创建一个循环。但是结果是一样的

这个简单的查询失败并出现同样的错误:

SELECT T.ID, DBMS_LOB.SUBSTR(T.POST_TEXT, 4000, 1)
FROM ACTIVITY_EVENT T
WHERE T.POST_TEXT IS NOT NULL

如果您的源列实际上是 NCLOB 而不是 CLOB,您将收到此错误。这没关系:

create table t42 (id number, dt date, post_text clob);
insert into t42 (id, dt, post_text) values (1, sysdate, dbms_random.string('p', 4000));
select id, dbms_lob.substr(post_text, 4000, 1) from t42;

但是这个错误,只是将 CLOB 更改为 NCLOB,长度超过 2000:

create table t42 (id number, dt date, post_text nclob);
insert into t42 (id, dt, post_text) values (1, sysdate, dbms_random.string('p', 4000));
select id, dbms_lob.substr(post_text, 4000, 1) from t42;

SQL Error: ORA-06502: PL/SQL: numeric or value error: character string buffer too small
ORA-06512: at line 1

这是用AL32UTF8和AL16UTF16作为数据库和国家字符集。

因此,如果您的来源 table 是 NCLOB,您只能提取前 2000 个字符以放入您的 stream_text table.

如果源列是 CLOB 并且前 4000 个字符包含任何多字节字符,您也会看到这一点。 dbms_log.substr(x, 4000, 1) 始终获取 CLOB 的前 4000 个字符,这可能超过 4000 个字节 - 这是 SQL 上下文中 VARCHAR2 值的最大大小,即使它被声明为 varchar2(4000 char), 因为它仍然不能超过 4000 字节的限制。

如果你想得到最多 4000 个字符,那么你可以通过一个 PL/SQL VARCHAR2 变量来实现,另外 substrb() 调用:

DECLARE
    CURSOR CUR IS
        SELECT SEQ$STREAM_TEXT.NEXTVAL AS ID,
               T.POST_TEXT,
               T.DT AS DT,
               'READY' AS STATE,
               S.ID AS STREAM_ID
          FROM ACTIVITY_EVENT T
          LEFT JOIN STREAM S
            ON S.OLD_ID = T.ID
         WHERE T.POST_TEXT IS NOT NULL;

    TMP_TEXT VARCHAR2(4000);
BEGIN
    FOR R IN CUR
    LOOP
        TMP_TEXT := SUBSTRB(DBMS_LOB.SUBSTR(R.POST_TEXT, 4000, 1), 1, 4000);
        INSERT INTO STREAM_TEXT (ID, TEXT, DT, STATE, STREAM_ID)
        VALUES (R.ID, TMP_TEXT, R.DT, R.STATE, R.STREAM_ID);
    END LOOP;
END;
/

substrb(..., 1, 4000) 部分在 SQL 中也不起作用,因为内部表达式仍然太大,但它在 PL/SQL 中起作用。您将获得前 4000 个字符的前 4000 个字节。 (尽管如果第 4000 个字节是多字节字符的中途,您可能仍然会遇到问题)。

我猜到了目标 table 中的列名,所以显然要使用真实的列名。如果您有大量数据,则进行批量插入会更好;提取到集合中并使用 FORALL 批量插入而不是逐行插入;像这样的东西可以作为起点:

DECLARE
    TYPE TMP_REC_TYPE IS RECORD (
      ID STREAM_TEXT.ID%TYPE,
      POST_TEXT ACTIVITY_EVENT.POST_TEXT%TYPE,
      TEXT STREAM_TEXT.TEXT%TYPE,
      DT STREAM_TEXT.DT%TYPE,
      STATE STREAM_TEXT.STATE%TYPE,
      STREAM_ID STREAM_TEXT.STREAM_ID%TYPE
    );
    TYPE TMP_REC_TAB_TYPE IS TABLE OF TMP_REC_TYPE;
    TMP_REC_TAB TMP_REC_TAB_TYPE;
    RC SYS_REFCURSOR;
BEGIN
    OPEN RC FOR
        SELECT SEQ$STREAM_TEXT.NEXTVAL AS ID,
               T.POST_TEXT,
               NULL AS TEXT,
               T.DT AS DT,
               'READY' AS STATE,
               S.ID AS STREAM_ID
          FROM ACTIVITY_EVENT T
          LEFT JOIN STREAM S
            ON S.OLD_ID = T.ID
         WHERE T.POST_TEXT IS NOT NULL;
    LOOP
        FETCH RC BULK COLLECT INTO TMP_REC_TAB LIMIT 100;
        FOR I IN 1..TMP_REC_TAB.COUNT LOOP -- populate text field
            TMP_REC_TAB(I).TEXT := SUBSTRB(
              DBMS_LOB.SUBSTR(TMP_REC_TAB(I).POST_TEXT, 4000, 1), 1, 4000);
        END LOOP;
        FORALL I IN 1..TMP_REC_TAB.COUNT -- bulk insert
            INSERT INTO STREAM_TEXT (ID, TEXT, DT, STATE, STREAM_ID)
            VALUES (TMP_REC_TAB(I).ID, TMP_REC_TAB(I).TEXT, TMP_REC_TAB(I).DT,
                TMP_REC_TAB(I).STATE, TMP_REC_TAB(I).STREAM_ID);
        EXIT WHEN RC%NOTFOUND;
    END LOOP;
END;
/