【完全ガイド】ORA-01489: result of string concatenation is too long の原因と解決方法|LISTAGG 4000 バイト・ON OVERFLOW TRUNCATE 徹底解説

【完全ガイド】ORA-01489: result of string concatenation is too long の原因と解決方法|LISTAGG 4000 バイト・ON OVERFLOW TRUNCATE 徹底解説

Oracle 開発者が集約クエリ実装時に頻繁に遭遇する典型的エラー:

SELECT department_id, 
       LISTAGG(last_name, '; ') WITHIN GROUP (ORDER BY last_name) AS emp_list
FROM employees
WHERE department_id = 50
GROUP BY department_id;
*
ERROR at line 2:
ORA-01489: result of string concatenation is too long

**「文字列連結の結果が長すぎる」**というシンプルなメッセージ。SQL 集約処理の実装で最頻出のサイズ系エラーです:

  • LISTAGG の結果が 4000 バイト超過
  • CSV / JSON エクスポートの失敗
  • レポートクエリの実行エラー
  • アプリ側で対処必須
  • 12.2+ で ON OVERFLOW TRUNCATE で優雅に対処可能

このエラーの本質は、Oracle の SQL レベル VARCHAR2 型のサイズ制限:

SQL VARCHAR2 デフォルト: 4000 バイト
LISTAGG 戻り値の型:    VARCHAR2
→ 集約結果が 4000 バイト超過
→ ORA-01489 発生

目次

SQL VARCHAR2 vs PL/SQL VARCHAR2 の重大な違い

日本語圏で混乱が多い重要な事実:

SQL VARCHAR2:
  デフォルト: 4000 バイト(20年以上不変)
  MAX_STRING_SIZE=EXTENDED: 32767 バイト(12c+)

PL/SQL VARCHAR2:
  常に 32767 バイト
  内部変数として利用

→ SQL 側で LISTAGG は 4000 バイト制限
→ PL/SQL 内で構築するなら 32767 バイト
→ CLOB を使えば 4GB まで

ORA-01489 は SQL レベルで発生、PL/SQL では ORA-06502(別エラー)。

姉妹エラーとの区別

文字列サイズ系エラー4種:

ORA-01489: result of string concatenation is too long
  → SQL レベル、VARCHAR2 4000 バイト超過(本記事)

ORA-06502: numeric or value error
  → PL/SQL レベル、VARCHAR2 32767 バイト超過等
  
ORA-12899: value too large for column
  → INSERT/UPDATE 時、テーブル列サイズ超過
  
ORA-01704: string literal too long
  → SQL リテラル、4000 バイト超過

現場で最も典型的なパターン:

  • LISTAGG 結果 > 4000 バイト(超典型、最頻出)
  • CONCAT / || による大量結合
  • CSV / TSV エクスポート(複数行結合)
  • JSON 生成(JSON_ARRAYAGG 未使用)
  • XML 生成
  • HTML レポート(複数行連結)
  • サブクエリでの結合
  • 動的 SQL 生成(IN 句の値列挙)
  • 監査ログ集約
  • Data Pump export(一部)

多くの日本語記事が「LISTAGG は避けよ」で終わりますが、実務では:

  • LISTAGG ON OVERFLOW TRUNCATE(12.2+ の決定版)
  • XMLAGG + GETCLOBVAL() による古典的回避
  • TO_CLOB(...) への変換
  • MAX_STRING_SIZE = EXTENDED による DB 全体拡張
  • JSON_ARRAYAGG(12c+)による CLOB 出力
  • PL/SQL でループ処理
  • SUBSTR による切り詰め
  • ON OVERFLOW TRUNCATE 'terminator' WITH/WITHOUT COUNT の細かい制御
  • Rails / Java / Python でのCLOB 受け取り

さらに、Oracle 12c Release 2(12.2)で登場した ON OVERFLOW TRUNCATE は革命的な改善:

-- 12.2+
SELECT LISTAGG(name, ', ' ON OVERFLOW TRUNCATE) 
       WITHIN GROUP (ORDER BY name) AS names
FROM emp;

-- 4000 バイト超えたら自動切り詰め + '...' 付加
-- エラーなし、優雅な対処

本記事では、ORA-01489: result of string concatenation is too long の完全な原因と解決方法を、リファレンスとして実用的に整理します。10大発生パターン、SQL vs PL/SQL の違い、6つの解決策、Rails/Java/Python 対応、実践シナリオ、FAQまで完全網羅。この1本で ORA-01489 を根本から解決できるようになります。


結論:ON OVERFLOW TRUNCATE か CLOB 化

時間がない方向けに、最速の対処を先に示します。

エラーメッセージの読み方

ORA-01489: result of string concatenation is too long
                    ↑
              連結結果が長すぎる

= SQL レベルで VARCHAR2 4000 バイト超過
  LISTAGG や || で発生
  12.2+ なら ON OVERFLOW TRUNCATE で対処
  それ以外は CLOB / XMLAGG で

最速の対処(Oracle バージョン別)

12.2 以降(推奨):

SELECT LISTAGG(col, ',' ON OVERFLOW TRUNCATE) 
       WITHIN GROUP (ORDER BY col) AS result
FROM tab;

11g R2 以前 or CLOB 必要:

SELECT RTRIM(XMLAGG(XMLELEMENT(E, col || ',') ORDER BY col).EXTRACT('//text()').GETCLOBVAL(), ',') AS result
FROM tab;

12c+(JSON 用途):

SELECT JSON_ARRAYAGG(col ORDER BY col RETURNING CLOB) AS result
FROM tab;

PL/SQL に持ち込み:

DECLARE
  v_result VARCHAR2(32767);
BEGIN
  FOR r IN (SELECT col FROM tab ORDER BY col) LOOP
    v_result := v_result || r.col || ',';
  END LOOP;
END;

6つの解決策

#手法使う場面
①ON OVERFLOW TRUNCATE12.2+、切り詰め OK
②XMLAGG + GETCLOBVAL11g R2 以前
③TO_CLOB 変換CLOB 全長必要
④MAX_STRING_SIZE=EXTENDED32767 まで拡張
⑤JSON_ARRAYAGGJSON 用途
⑥PL/SQL ループ複雑処理

文字列サイズ系エラー4種

エラー意味
ORA-01489SQL 連結 4000 超過
ORA-06502PL/SQL VARCHAR2
ORA-12899列サイズ超過
ORA-01704リテラル 4000 超過

詳細は以下で解説します。


Oracle VARCHAR2 サイズの仕組み

VARCHAR2 の階層

【SQL レベル】
デフォルト:
  VARCHAR2: 4000 バイト
  NVARCHAR2: 4000 バイト(キャラクタ数)
  RAW: 2000 バイト

拡張 (MAX_STRING_SIZE = EXTENDED、12c+):
  VARCHAR2: 32767 バイト
  NVARCHAR2: 32767 バイト
  RAW: 32767 バイト

【PL/SQL レベル】
常に 32767 バイト

【CLOB】
最大 4GB (128TB with BIGFILE)

MAX_STRING_SIZE パラメータ(12c+)

-- 現状確認
SHOW PARAMETER max_string_size;

-- STANDARD (デフォルト): 4000 バイト
-- EXTENDED: 32767 バイト

-- 変更(非常に注意深く)
-- 1. DB 停止
-- 2. STARTUP UPGRADE
-- 3. utl32k.sql 実行
-- 4. パラメータ変更
-- 5. 再起動

⚠️ MAX_STRING_SIZE の変更は一方通行(STANDARD → EXTENDED のみ、戻せない)。

LISTAGG の戻り値型

LISTAGG の戻り値:
  引数が VARCHAR2 なら VARCHAR2
  引数が NVARCHAR2 なら NVARCHAR2
  → SQL レベル、4000 バイト制限

【原因①】LISTAGG 結果 > 4000 バイト(最頻出)

症状(超典型)

SELECT department_id, 
       LISTAGG(employee_name, ', ') 
         WITHIN GROUP (ORDER BY employee_name) AS employees
FROM employees
GROUP BY department_id;
-- ORA-01489

大部門で 100+ 従業員を連結 → 4000 バイト超過。

解決A: ON OVERFLOW TRUNCATE(12.2+、推奨)

-- 基本形('...' で末尾表示)
SELECT LISTAGG(name, ', ' ON OVERFLOW TRUNCATE) 
       WITHIN GROUP (ORDER BY name) AS employees
FROM employees;

-- 出力例: "Alice, Bob, ..., ..., (3)"
-- 最後の (3) は省略件数

-- カスタム終了記号
SELECT LISTAGG(name, ', ' ON OVERFLOW TRUNCATE '~~~') 
       WITHIN GROUP (ORDER BY name)
FROM employees;

-- 件数表示なし
SELECT LISTAGG(name, ', ' ON OVERFLOW TRUNCATE WITHOUT COUNT) 
       WITHIN GROUP (ORDER BY name)
FROM employees;

-- 明示的エラー(デフォルト動作)
SELECT LISTAGG(name, ', ' ON OVERFLOW ERROR) 
       WITHIN GROUP (ORDER BY name)
FROM employees;

解決B: XMLAGG + GETCLOBVAL(全バージョン)

SELECT RTRIM(
  XMLAGG(XMLELEMENT(E, employee_name || ', ') 
    ORDER BY employee_name).EXTRACT('//text()').GETCLOBVAL(),
  ', '
) AS employees
FROM employees;
-- CLOB で返却、4GB まで

解決C: TO_CLOB 変換

-- 12c+
SELECT LISTAGG(TO_CLOB(name || ', '))
         WITHIN GROUP (ORDER BY name)
FROM employees;

【原因②】CONCAT / || による大量結合

シナリオ

SELECT MAX(col1 || col2 || col3 || col4 || col5) AS combined
FROM huge_table;
-- 各列が 1000 バイト → 5000 バイト → ORA-01489

解決

A. CLOB 変換:

SELECT MAX(TO_CLOB(col1) || col2 || col3 || col4 || col5) AS combined
FROM huge_table;
-- 最初を TO_CLOB で他も CLOB として扱う

B. SUBSTR で切り詰め:

SELECT MAX(SUBSTR(col1 || col2 || col3, 1, 4000)) AS combined
FROM huge_table;

【原因③】CSV / TSV エクスポート

シナリオ

-- CSV 出力用集約
SELECT LISTAGG(id || ',' || name || ',' || email || CHR(10)) 
       WITHIN GROUP (ORDER BY id) AS csv_data
FROM customers;
-- 大量レコードで超過

解決

A. ON OVERFLOW TRUNCATE:

SELECT LISTAGG(id || ',' || name || ',' || email || CHR(10),
               '' ON OVERFLOW TRUNCATE)
       WITHIN GROUP (ORDER BY id) AS csv_data
FROM customers;

B. PL/SQL + UTL_FILE:

DECLARE
  v_file UTL_FILE.FILE_TYPE;
BEGIN
  v_file := UTL_FILE.FOPEN('DATA_DIR', 'customers.csv', 'W', 32767);
  FOR r IN (SELECT id, name, email FROM customers ORDER BY id) LOOP
    UTL_FILE.PUT_LINE(v_file, r.id || ',' || r.name || ',' || r.email);
  END LOOP;
  UTL_FILE.FCLOSE(v_file);
END;
/

【原因④】JSON 生成

シナリオ

SELECT LISTAGG(
  '"' || name || '":' || salary || ',',
  ' '
) WITHIN GROUP (ORDER BY name) AS json_str
FROM emp;
-- 大量データで超過

解決: JSON_ARRAYAGG(12c+、推奨)

-- CLOB で返却
SELECT JSON_ARRAYAGG(
  JSON_OBJECT('name' VALUE name, 'salary' VALUE salary)
  ORDER BY name
  RETURNING CLOB
) AS json_result
FROM emp;

-- 結果は完全な JSON、サイズ制限なし

【原因⑤】XML 生成

シナリオ

SELECT LISTAGG(
  '<emp><id>' || id || '</id><name>' || name || '</name></emp>'
) WITHIN GROUP (ORDER BY id) AS xml_str
FROM emp;

解決: XMLAGG + GETCLOBVAL

SELECT XMLELEMENT("employees",
  XMLAGG(
    XMLELEMENT("emp",
      XMLFOREST(id, name)
    ) ORDER BY id
  )
).GETCLOBVAL() AS xml_result
FROM emp;

【原因⑥】HTML レポート

シナリオ

-- HTML テーブル生成
SELECT '<table>' || LISTAGG(
  '<tr><td>' || id || '</td><td>' || name || '</td></tr>'
) WITHIN GROUP (ORDER BY id) || '</table>' AS html
FROM emp;

解決

PL/SQL でループ:

DECLARE
  v_html CLOB;
BEGIN
  DBMS_LOB.CREATETEMPORARY(v_html, TRUE);
  DBMS_LOB.WRITEAPPEND(v_html, 7, '<table>');
  
  FOR r IN (SELECT id, name FROM emp ORDER BY id) LOOP
    DECLARE
      v_row VARCHAR2(4000);
    BEGIN
      v_row := '<tr><td>' || r.id || '</td><td>' || r.name || '</td></tr>';
      DBMS_LOB.WRITEAPPEND(v_html, LENGTH(v_row), v_row);
    END;
  END LOOP;
  
  DBMS_LOB.WRITEAPPEND(v_html, 8, '</table>');
  -- v_html を使用
END;
/

【原因⑦】サブクエリでの結合

シナリオ

SELECT 
  (SELECT LISTAGG(name, ',') FROM emp WHERE dept_id = d.id) AS emp_list
FROM dept d;
-- 大部門で超過

解決: ON OVERFLOW TRUNCATE

SELECT 
  (SELECT LISTAGG(name, ',' ON OVERFLOW TRUNCATE) 
     WITHIN GROUP (ORDER BY name) 
   FROM emp WHERE dept_id = d.id) AS emp_list
FROM dept d;

【原因⑧】動的 SQL 生成(IN 句)

シナリオ

DECLARE
  v_ids VARCHAR2(4000);
  v_sql VARCHAR2(32767);
BEGIN
  -- ID リスト取得
  SELECT LISTAGG(id, ',') WITHIN GROUP (ORDER BY id)
    INTO v_ids
    FROM large_table
    WHERE flag = 'Y';
  -- ORA-01489 or ORA-06502
END;
/

解決

A. PL/SQL で 32767 まで:

DECLARE
  v_ids VARCHAR2(32767);
BEGIN
  FOR r IN (SELECT id FROM large_table WHERE flag = 'Y') LOOP
    v_ids := v_ids || r.id || ',';
    IF LENGTH(v_ids) > 30000 THEN
      EXIT;   -- 制限
    END IF;
  END LOOP;
END;

B. Bind Variable / Array 使用(推奨):

-- IN 句を BULK COLLECT で
SELECT * FROM t WHERE id IN (SELECT column_value FROM TABLE(id_array));

【原因⑨】監査ログ集約

シナリオ

SELECT LISTAGG(action || '@' || TO_CHAR(action_time, 'YYYY-MM-DD HH24:MI:SS'), '; ')
       WITHIN GROUP (ORDER BY action_time)
FROM audit_log
WHERE user_id = 100
  AND action_time >= SYSDATE - 30;
-- 大量ログで超過

解決

-- ページング + CLOB
SELECT XMLAGG(XMLELEMENT(E, action || '@' || TO_CHAR(action_time)) 
              ORDER BY action_time).GETCLOBVAL()
FROM audit_log
WHERE user_id = 100 
  AND action_time >= SYSDATE - 30;

【原因⑩】Data Pump 関連

シナリオ

Data Pump export/import で内部エラー
特定オブジェクトの DDL が 4000 バイト超過

対処

Data Pump パラメータ調整 or オブジェクト分割。

Data Pump 関連は Oracle Data Pump 使い方の記事も参照してください。


診断ツール完全リファレンス

現状確認

-- MAX_STRING_SIZE 確認
SHOW PARAMETER max_string_size;

-- Oracle バージョン
SELECT * FROM v$version;

-- LISTAGG のサポート機能
-- 12.2+ で ON OVERFLOW TRUNCATE 使用可

集約サイズ事前予測

-- 予想バイト数
SELECT SUM(LENGTHB(name) + 2) AS estimated_bytes
FROM emp
WHERE dept_id = 50;
-- 4000 超えるなら要対策

実行結果の長さ確認

-- CLOB の場合
SELECT DBMS_LOB.GETLENGTH(result) FROM (
  SELECT XMLAGG(XMLELEMENT(E, name || ',')).GETCLOBVAL() AS result
  FROM emp
);

PL/SQL でのサイズ検証

DECLARE
  v_result VARCHAR2(32767);
BEGIN
  SELECT LISTAGG(name, ',' ON OVERFLOW TRUNCATE)
    WITHIN GROUP (ORDER BY name)
    INTO v_result
    FROM emp;
  DBMS_OUTPUT.PUT_LINE('Length: ' || LENGTH(v_result));
END;
/

6つの解決策 完全リファレンス

解決策① LISTAGG ON OVERFLOW TRUNCATE(12.2+)

-- 基本形
SELECT LISTAGG(name, ', ' ON OVERFLOW TRUNCATE) 
       WITHIN GROUP (ORDER BY name)
FROM emp;
-- 例: "Alice, Bob, Carol, ..., ..., (10)"

-- カスタム記号
SELECT LISTAGG(name, ', ' ON OVERFLOW TRUNCATE 'MORE') 
       WITHIN GROUP (ORDER BY name)
FROM emp;
-- 例: "Alice, Bob, Carol, MORE(10)"

-- 件数表示なし
SELECT LISTAGG(name, ', ' ON OVERFLOW TRUNCATE WITHOUT COUNT) 
       WITHIN GROUP (ORDER BY name)
FROM emp;
-- 例: "Alice, Bob, Carol, ..."

-- 明示的エラー(デフォルト)
SELECT LISTAGG(name, ', ' ON OVERFLOW ERROR) 
       WITHIN GROUP (ORDER BY name)
FROM emp;
-- 超過時 ORA-01489

解決策② XMLAGG + GETCLOBVAL

-- 全バージョン対応、CLOB で返却
SELECT RTRIM(
  XMLAGG(XMLELEMENT(E, name || ',') 
    ORDER BY name).EXTRACT('//text()').GETCLOBVAL(),
  ','
) AS result
FROM emp;

注意: XML エスケープが発生(<, >, & 等)。

XML エスケープ回避版:

SELECT DBMS_XMLGEN.CONVERT(
  RTRIM(
    XMLAGG(XMLELEMENT(E, name || ',') 
      ORDER BY name).EXTRACT('//text()').GETCLOBVAL(),
    ','
  ),
  1  -- Decode(アンエスケープ)
) AS result
FROM emp;

解決策③ TO_CLOB 変換

-- 12c+
SELECT LISTAGG(TO_CLOB(name), ',')
       WITHIN GROUP (ORDER BY name) AS result
FROM emp;
-- 戻り値が CLOB になり制限なし

解決策④ MAX_STRING_SIZE = EXTENDED(DB 全体)

-- 現状
SHOW PARAMETER max_string_size;
-- STANDARD

-- ⚠️ 変更手順(一方通行!)
-- 1. バックアップ取得
-- 2. SHUTDOWN
-- 3. STARTUP UPGRADE
-- 4. ALTER SYSTEM SET MAX_STRING_SIZE = EXTENDED SCOPE = SPFILE;
-- 5. @?/rdbms/admin/utl32k.sql
-- 6. SHUTDOWN
-- 7. STARTUP

-- 効果: SQL VARCHAR2 も 32767 まで拡張

解決策⑤ JSON_ARRAYAGG(12c+)

-- CLOB で返却
SELECT JSON_ARRAYAGG(
  JSON_OBJECT(
    'id' VALUE id,
    'name' VALUE name,
    'salary' VALUE salary
  )
  ORDER BY id
  RETURNING CLOB
) AS json_data
FROM emp;

-- 出力例:
-- [{"id":1,"name":"Alice","salary":5000},...]

解決策⑥ PL/SQL でループ処理

DECLARE
  v_result CLOB;
BEGIN
  DBMS_LOB.CREATETEMPORARY(v_result, TRUE);
  
  FOR r IN (SELECT name FROM emp ORDER BY name) LOOP
    DBMS_LOB.WRITEAPPEND(v_result, LENGTH(r.name || ','), r.name || ',');
  END LOOP;
  
  -- 末尾カンマ削除
  IF DBMS_LOB.GETLENGTH(v_result) > 0 THEN
    DBMS_LOB.TRIM(v_result, DBMS_LOB.GETLENGTH(v_result) - 1);
  END IF;
  
  -- v_result 使用
  DBMS_LOB.FREETEMPORARY(v_result);
END;
/

Rails / Java / Python 対応

Rails ActiveRecord

LISTAGG を CLOB で取得:

result = ActiveRecord::Base.connection.select_value(<<-SQL)
  SELECT LISTAGG(TO_CLOB(name), ',')
         WITHIN GROUP (ORDER BY name) AS result
  FROM employees
SQL
# result は文字列(Ruby でサイズ制限なし)

エラーハンドリング:

begin
  result = Employee.pluck(:name).join(',')
  # または SQL 直接
rescue ActiveRecord::StatementInvalid => e
  if e.message.include?("ORA-01489")
    Rails.logger.error "String concatenation too long"
    # ON OVERFLOW TRUNCATE 版で再試行
    result = ActiveRecord::Base.connection.select_value(<<-SQL)
      SELECT LISTAGG(name, ',' ON OVERFLOW TRUNCATE)
             WITHIN GROUP (ORDER BY name) FROM employees
    SQL
  end
end

Rails 8 系の詳細は Rails 8 アップグレードガイドの記事、find/find_by/where の記事も参照してください。

Java (JDBC)

CLOB を Reader で取得:

String sql = "SELECT XMLAGG(XMLELEMENT(E, name || ',') " +
             "ORDER BY name).GETCLOBVAL() AS result FROM emp";

try (Statement stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery(sql)) {
    while (rs.next()) {
        Clob clob = rs.getClob("result");
        try (Reader r = clob.getCharacterStream()) {
            StringBuilder sb = new StringBuilder();
            char[] buf = new char[8192];
            int len;
            while ((len = r.read(buf)) != -1) {
                sb.append(buf, 0, len);
            }
            String result = sb.toString();
        }
    }
}

Python (oracledb)

import oracledb

# CLOB を文字列で取得
cursor.execute("""
    SELECT LISTAGG(TO_CLOB(name), ',')
           WITHIN GROUP (ORDER BY name) AS result
    FROM employees
""")
result = cursor.fetchone()[0]
# CLOB オブジェクトの場合
if hasattr(result, 'read'):
    result = result.read()

実践シナリオ

シナリオ1:レポート SQL の 12.2+ 対応

-- Before(4000 バイトで失敗の可能性)
SELECT dept_id,
       LISTAGG(emp_name, ', ') WITHIN GROUP (ORDER BY emp_name) AS members
FROM emp
GROUP BY dept_id;

-- After(安全)
SELECT dept_id,
       LISTAGG(emp_name, ', ' ON OVERFLOW TRUNCATE) 
         WITHIN GROUP (ORDER BY emp_name) AS members
FROM emp
GROUP BY dept_id;

シナリオ2:CSV エクスポート

-- PL/SQL + UTL_FILE で大量データ出力
DECLARE
  v_file UTL_FILE.FILE_TYPE;
BEGIN
  v_file := UTL_FILE.FOPEN('CSV_DIR', 'emp_' || TO_CHAR(SYSDATE, 'YYYYMMDD') || '.csv', 'W', 32767);
  
  -- ヘッダー
  UTL_FILE.PUT_LINE(v_file, 'id,name,dept,salary');
  
  -- データ
  FOR r IN (SELECT id, name, dept_id, salary FROM emp ORDER BY id) LOOP
    UTL_FILE.PUT_LINE(v_file, 
      r.id || ',' || 
      '"' || r.name || '",' || 
      r.dept_id || ',' || 
      r.salary);
  END LOOP;
  
  UTL_FILE.FCLOSE(v_file);
END;
/

シナリオ3:JSON API レスポンス

-- JSON_ARRAYAGG で CLOB
SELECT JSON_ARRAYAGG(
  JSON_OBJECT(
    'id' VALUE emp_id,
    'name' VALUE emp_name,
    'dept' VALUE dept_name,
    'salary' VALUE salary
  )
  ORDER BY emp_id
  RETURNING CLOB
) AS api_response
FROM emp_view;

シナリオ4:11g R2 環境での回避

-- ON OVERFLOW TRUNCATE 使えない環境
SELECT RTRIM(
  DBMS_XMLGEN.CONVERT(
    XMLAGG(XMLELEMENT(E, emp_name || ', ') 
      ORDER BY emp_name).EXTRACT('//text()').GETCLOBVAL(),
    1
  ),
  ', '
) AS members
FROM emp;

シナリオ5:Rails でのレポート機能

class DepartmentReport
  def self.member_lists
    ActiveRecord::Base.connection.select_all(<<-SQL).to_a
      SELECT 
        dept_id,
        LISTAGG(emp_name, ', ' ON OVERFLOW TRUNCATE)
          WITHIN GROUP (ORDER BY emp_name) AS members
      FROM employees
      GROUP BY dept_id
    SQL
  end
end

Rails 8 系の詳細は Rails 8 アップグレードガイドの記事も参照してください。

シナリオ6:Java Spring レポート

@Query(value = """
    SELECT dept_id,
           LISTAGG(emp_name, ',' ON OVERFLOW TRUNCATE)
             WITHIN GROUP (ORDER BY emp_name) AS members
    FROM employees
    GROUP BY dept_id
    """, nativeQuery = true)
List<DeptMemberDto> findAllDeptMembers();

シナリオ7:Python バッチ処理

import oracledb

def export_dept_members():
    with oracledb.connect(user=user, password=pw, dsn=dsn) as conn:
        cursor = conn.cursor()
        cursor.execute("""
            SELECT dept_id,
                   LISTAGG(emp_name, ',' ON OVERFLOW TRUNCATE)
                     WITHIN GROUP (ORDER BY emp_name) AS members
            FROM employees
            GROUP BY dept_id
        """)
        for dept_id, members in cursor:
            print(f"Dept {dept_id}: {members}")

シナリオ8:Docker Oracle でのテスト

docker exec -it oracle-xe sqlplus scott/tiger <<EOF
CREATE TABLE t (id NUMBER, val VARCHAR2(200));
INSERT INTO t SELECT LEVEL, LPAD('X', 100, 'X')
FROM dual CONNECT BY LEVEL <= 100;
COMMIT;

-- 通常 LISTAGG → 超過
SELECT LISTAGG(val, ',') WITHIN GROUP (ORDER BY id) FROM t;
-- ORA-01489

-- ON OVERFLOW TRUNCATE
SELECT LISTAGG(val, ',' ON OVERFLOW TRUNCATE) WITHIN GROUP (ORDER BY id) FROM t;
-- 成功

-- XMLAGG
SELECT DBMS_LOB.GETLENGTH(
  XMLAGG(XMLELEMENT(E, val || ',')).EXTRACT('//text()').GETCLOBVAL()
) AS byte_len FROM t;
EOF

Docker 関連は docker daemon 接続エラーの記事、Docker no space left on device の記事も参照してください。

シナリオ9:MAX_STRING_SIZE 導入検討

-- 影響評価
-- 1. Extended データ型テーブル確認
SELECT COUNT(*) FROM dba_tab_columns 
WHERE data_type = 'VARCHAR2' AND data_length > 4000;

-- 2. アプリ影響評価(列サイズが変わるわけではない)
-- SQL 関数の戻り値のみ影響

-- 3. 実施(バックアップ後)
-- SHUTDOWN → STARTUP UPGRADE
-- ALTER SYSTEM SET MAX_STRING_SIZE = EXTENDED SCOPE = SPFILE;
-- @?/rdbms/admin/utl32k.sql
-- 再起動

シナリオ10:Autonomous DB

Autonomous DB では:
- MAX_STRING_SIZE = EXTENDED がデフォルト
- SQL VARCHAR2 も 32767 まで
- 4000 バイト制限緩和
- LISTAGG も 32767 まで

トラブルシューティング

ON OVERFLOW TRUNCATE 使えない

Oracle バージョン確認、12.2 以降必須。

XMLAGG で XML エスケープ

DBMS_XMLGEN.CONVERT or XMLCAST でデコード。

CLOB を Rails で受け取れない

oracle_enhanced adapter の設定確認、activerecord-oracle_enhanced-adapter バージョン。

JSON_ARRAYAGG が使えない

Oracle 12c+ 必須、それ以前は手動 JSON 構築。

PL/SQL でも 32767 超える

CLOB 変数使用:

v_result CLOB;
DBMS_LOB.CREATETEMPORARY(v_result, TRUE);

PostgreSQL からの移行

PG の STRING_AGG は制限なし、Oracle 移行時は要注意。


よくある質問(FAQ)

Q1. SQL VARCHAR2 と PL/SQL VARCHAR2

  • SQL: 4000 バイト(EXTENDED で 32767)
  • PL/SQL: 常に 32767 バイト

Q2. ON OVERFLOW TRUNCATE の版

Oracle 12c Release 2 (12.2)+ で使用可。

Q3. データ切り詰めのリスク

ON OVERFLOW TRUNCATE はデータ損失、要件次第。

Q4. XMLAGG の副作用

XML エスケープ発生、DBMS_XMLGEN.CONVERT で対処。

Q5. MAX_STRING_SIZE 変更

一方通行、STANDARD → EXTENDED のみ、慎重に。

Q6. JSON_ARRAYAGG 推奨場面

JSON 用途なら 12c+ で JSON_ARRAYAGG 一択。

Q7. Rails での対応

ON OVERFLOW TRUNCATE or CLOB 受け取り。

Q8. Java での対応

Clob.getCharacterStream() で読み取り。

Q9. Python での対応

oracledb の CLOB 対応、.read() メソッド。

Q10. パフォーマンス影響

XMLAGG は遅い、CLOB 生成コスト、ON OVERFLOW TRUNCATE 推奨。

Q11. Autonomous DB での挙動

MAX_STRING_SIZE = EXTENDED デフォルト、4000 制限緩和。

Q12. 予防策

  • 12.2+ なら ON OVERFLOW TRUNCATE デフォルト
  • CLOB 受け取り体制
  • サイズ事前予測
  • CSV は UTL_FILE 使用

参考リンク

Oracle 公式


まとめ

ORA-01489: result of string concatenation is too long の要点を再整理します。

エラーの本質

SQL レベルで文字列連結結果が VARCHAR2 4000 バイト超過
→ LISTAGG や || で発生
→ 12.2+ なら ON OVERFLOW TRUNCATE で優雅に対処
→ 全バージョンは XMLAGG + GETCLOBVAL

エラーメッセージの読み方

ORA-01489: result of string concatenation is too long
                    ↑
              連結結果が長すぎる

VARCHAR2 サイズ階層

レベルサイズ
SQL VARCHAR2 (default)4000 バイト
SQL VARCHAR2 (EXTENDED)32767 バイト
PL/SQL VARCHAR232767 バイト(常に)
CLOB4GB

文字列サイズ系エラー4種

エラー意味
ORA-01489SQL 連結 4000 超過
ORA-06502PL/SQL VARCHAR2
ORA-12899列サイズ超過
ORA-01704リテラル 4000 超過

10大原因

#原因対処
①LISTAGG > 4000ON OVERFLOW TRUNCATE
②|| 大量結合TO_CLOB
③CSV エクスポートUTL_FILE
④JSON 生成JSON_ARRAYAGG
⑤XML 生成XMLAGG
⑥HTML レポートPL/SQL + CLOB
⑦サブクエリON OVERFLOW TRUNCATE
⑧動的 SQLArray/Bind
⑨監査ログ集約XMLAGG
⑩Data Pumpパラメータ調整

6つの解決策

-- ① ON OVERFLOW TRUNCATE(12.2+、推奨)
SELECT LISTAGG(col, ',' ON OVERFLOW TRUNCATE) 
       WITHIN GROUP (ORDER BY col) FROM tab;

-- ② XMLAGG + GETCLOBVAL(全バージョン)
SELECT RTRIM(
  XMLAGG(XMLELEMENT(E, col || ',') ORDER BY col)
    .EXTRACT('//text()').GETCLOBVAL(), ','
) FROM tab;

-- ③ TO_CLOB 変換
SELECT LISTAGG(TO_CLOB(col), ',') 
       WITHIN GROUP (ORDER BY col) FROM tab;

-- ④ MAX_STRING_SIZE = EXTENDED
-- DB 全体で 32767 まで

-- ⑤ JSON_ARRAYAGG(12c+)
SELECT JSON_ARRAYAGG(col ORDER BY col RETURNING CLOB) 
FROM tab;

-- ⑥ PL/SQL でループ
DECLARE v_result CLOB;
BEGIN
  DBMS_LOB.CREATETEMPORARY(v_result, TRUE);
  FOR r IN (SELECT col FROM tab) LOOP
    DBMS_LOB.WRITEAPPEND(v_result, LENGTH(r.col), r.col);
  END LOOP;
END;

ON OVERFLOW TRUNCATE の詳細(12.2+)

-- 基本
LISTAGG(col, ',' ON OVERFLOW TRUNCATE)
-- 出力: "A,B,C,...,(N)" N = 省略件数

-- カスタム記号
LISTAGG(col, ',' ON OVERFLOW TRUNCATE '~~~')
-- 出力: "A,B,C,~~~,(N)"

-- 件数なし
LISTAGG(col, ',' ON OVERFLOW TRUNCATE WITHOUT COUNT)
-- 出力: "A,B,C,..."

-- 明示エラー(デフォルト)
LISTAGG(col, ',' ON OVERFLOW ERROR)
-- 超過時 ORA-01489

各言語での対応

Rails:  ON OVERFLOW TRUNCATE or CLOB 受け取り
Java:   Clob.getCharacterStream()
Python: oracledb で LOB.read()
共通:   バージョン確認、CLOB 対応強化

予防のポイント

1. Oracle 12.2+ なら ON OVERFLOW TRUNCATE 標準採用
2. 集約サイズを事前予測
3. JSON は JSON_ARRAYAGG (12c+)
4. XML は XMLAGG
5. CSV は UTL_FILE
6. Rails/Java/Python で CLOB 対応
7. MAX_STRING_SIZE=EXTENDED は影響評価後
8. Autonomous DB では気にせず使える
9. PostgreSQL 移行時は STRING_AGG 変換注意
10. リファクタリング機会に対応

これらの知識は、Oracle での SQL 開発・レポート機能・CSV/JSON エクスポート・API 開発・監査ログ集約・Rails / Java / Python アプリ運用・データ移行・CI/CD パイプライン・Autonomous DB 活用など、あらゆる場面で活用できます。本記事をブックマークしておけば、ORA-01489 に出会っても冷静に的確に対処できるようになります。


本記事は2026年6月時点の情報をもとに、Oracle Database 12c〜23ai での動作確認・公式ドキュメントに基づき作成しています。Oracle のバージョンにより挙動が異なる場合があるため、最新の情報は Oracle 公式ドキュメント(docs.oracle.com)もあわせてご確認ください。