【Oracle】ORA-01000: 最大オープン・カーソル数を超えました の原因と解決方法完全ガイド|PL/SQL・JDBC・診断SQLまで解説

【SQL】JOIN完全ガイド|INNER・LEFT・RIGHT・FULL・CROSS・自己結合・ON vs USING・3テーブル結合まで解説 Oracle

ORA-01000: 最大オープン・カーソル数を超えました(英語: maximum open cursors exceeded)は、1つのデータベース・セッションがオープンできるカーソルの上限数に達したときに発生するエラーです。PL/SQLでカーソルを閉じ忘れた場合だけでなく、JavaやPythonなどアプリケーション側でSQL文やResultSetを適切にクローズしないままにしておくと発生します。一度発生すると同一セッション内での以降のSQL実行がすべて失敗するため、影響範囲が広く早急な対応が必要です。

この記事でわかること

  • ORA-01000 の発生メカニズムとオープン・カーソルの概念
  • OPEN_CURSORS パラメータの確認・変更方法
  • PL/SQL でのカーソル漏れパターンと対処法
  • Java(JDBC)でのリソースリークパターンと対処法
  • V$OPEN_CURSOR を使ったリアルタイム診断SQL
  • 根本解決のためのコーディングベストプラクティス
スポンサーリンク

ORA-01000 の発生メカニズム

オープン・カーソルとは

Oracleではすべての SQL 実行にカーソルが使われます。SELECT 文はもちろん、INSERTUPDATEDELETE にも内部的にカーソルが割り当てられます。カーソルには「オープン(実行中・結果取得中)」と「クローズ(使用済み・解放済み)」の状態があり、クローズされていないカーソルの数がセッション単位の上限に達するとORA-01000が発生します。

オープン・カーソルには大きく2種類あります。

種類 説明
明示的カーソル PL/SQLでCURSOR宣言してOPEN/FETCH/CLOSEする CURSOR c IS SELECT ...
暗黙的カーソル SQL文実行時にOracleが自動でオープンするカーソル アプリから実行するSELECT/DML全般

OPEN_CURSORS パラメータ

セッションあたりのオープン・カーソル上限は OPEN_CURSORS 初期化パラメータで制御されています。デフォルト値は300ですが、接続セッション数やアプリ特性に応じて調整が必要です。

OPEN_CURSORS の現在値を確認する
-- 現在の設定値を確認
SELECT name, value
FROM v$parameter
WHERE name = 'open_cursors';

-- セッションごとのオープン・カーソル数を確認
SELECT ses.username, ses.sid, ses.serial#, sta.value AS open_cursors
FROM v$sesstat sta
JOIN v$statname stn ON sta.statistic# = stn.statistic#
JOIN v$session ses ON sta.sid = ses.sid
WHERE stn.name = 'opened cursors current'
  AND ses.username IS NOT NULL
ORDER BY sta.value DESC;
OPEN_CURSORS は1セッションあたりの上限
OPEN_CURSORS はデータベース全体ではなく1セッションあたりのオープン・カーソル数の上限です。接続数が多い環境でむやみに増やすと全体のSGAメモリを圧迫する可能性があります。根本原因を修正せずに値を上げるだけでは、時間をかけて再び上限に達するだけです。

ORA-01000 の診断方法

V$OPEN_CURSOR でリークを特定する

どのセッション・どのSQLがカーソルを大量に保持しているかは V$OPEN_CURSOR ビューで確認できます。

V$OPEN_CURSOR ── カーソルが多いセッションと SQL を特定する
-- セッション別のオープン・カーソル数TOP10
SELECT
    oc.sid,
    s.username,
    s.program,
    COUNT(*) AS open_cursor_count
FROM v$open_cursor oc
JOIN v$session s ON oc.sid = s.sid
WHERE s.username IS NOT NULL
GROUP BY oc.sid, s.username, s.program
ORDER BY open_cursor_count DESC
FETCH FIRST 10 ROWS ONLY;
特定セッションで保持中のカーソルと SQL を表示する
-- SID=123 のセッションが保持しているカーソルと SQL テキスト
SELECT
    oc.sid,
    oc.cursor#,
    oc.sql_text
FROM v$open_cursor oc
WHERE oc.sid = 123
ORDER BY oc.cursor#;

同一の SQL テキストが大量に並んでいる場合は、ループ内でカーソルを閉じずに繰り返し開いているケースが多いです。SQL テキストが毎回異なる(リテラル値が埋め込まれている)場合は、バインド変数を使わずに動的SQLを生成しているケースを疑います。

カーソル数の増加傾向を監視する

時系列でオープン・カーソル数の増加を監視する
-- 10秒おきに実行してカーソル数が増え続けていないか確認
SELECT
    TO_CHAR(SYSDATE, 'HH24:MI:SS') AS check_time,
    ses.sid,
    ses.username,
    sta.value AS open_cursors
FROM v$sesstat sta
JOIN v$statname stn ON sta.statistic# = stn.statistic#
JOIN v$session ses ON sta.sid = ses.sid
WHERE stn.name = 'opened cursors current'
  AND ses.username IS NOT NULL
  AND sta.value > 10
ORDER BY sta.value DESC;

原因と対処法:PL/SQL のカーソル漏れ

明示的カーソルを CLOSE し忘れる

最も典型的なパターンです。OPEN したカーソルを例外処理のパスで CLOSE せずに終了するケースが特に見落とされがちです。

NG: 例外発生時にカーソルが CLOSE されない
DECLARE
    CURSOR c_emp IS SELECT employee_id FROM employees;
    v_id employees.employee_id%TYPE;
BEGIN
    OPEN c_emp;
    LOOP
        FETCH c_emp INTO v_id;
        EXIT WHEN c_emp%NOTFOUND;
        -- ここで例外が発生すると CLOSE が実行されない
        process_employee(v_id);
    END LOOP;
    CLOSE c_emp;  -- 例外発生時はスキップされる
END;
OK: EXCEPTION ブロックで確実に CLOSE する
DECLARE
    CURSOR c_emp IS SELECT employee_id FROM employees;
    v_id employees.employee_id%TYPE;
BEGIN
    OPEN c_emp;
    BEGIN
        LOOP
            FETCH c_emp INTO v_id;
            EXIT WHEN c_emp%NOTFOUND;
            process_employee(v_id);
        END LOOP;
    EXCEPTION
        WHEN OTHERS THEN
            -- 例外が発生しても必ず CLOSE する
            IF c_emp%ISOPEN THEN
                CLOSE c_emp;
            END IF;
            RAISE;
    END;
    CLOSE c_emp;
END;

明示的カーソルの実装方法と OPEN/FETCH/CLOSE のパターンについてはOracle 明示的カーソル(Explicit CURSOR)完全ガイドを参照してください。

ループ内で繰り返しカーソルをオープンする

NG: ループ内でカーソルを繰り返しオープン(閉じない)
DECLARE
    CURSOR c_dept IS SELECT dept_id FROM departments;
    CURSOR c_emp(p_dept_id NUMBER) IS
        SELECT employee_id FROM employees WHERE dept_id = p_dept_id;
    v_dept departments.dept_id%TYPE;
    v_emp  employees.employee_id%TYPE;
BEGIN
    OPEN c_dept;
    LOOP
        FETCH c_dept INTO v_dept;
        EXIT WHEN c_dept%NOTFOUND;
        -- c_emp を OPEN するが CLOSE していない
        OPEN c_emp(v_dept);
        FETCH c_emp INTO v_emp;
        -- 処理が途中で終わっても c_emp がオープンのまま残る
    END LOOP;
    CLOSE c_dept;
END;
OK: ループ内でも確実に CLOSE する
DECLARE
    CURSOR c_dept IS SELECT dept_id FROM departments;
    CURSOR c_emp(p_dept_id NUMBER) IS
        SELECT employee_id FROM employees WHERE dept_id = p_dept_id;
    v_dept departments.dept_id%TYPE;
    v_emp  employees.employee_id%TYPE;
BEGIN
    OPEN c_dept;
    LOOP
        FETCH c_dept INTO v_dept;
        EXIT WHEN c_dept%NOTFOUND;
        OPEN c_emp(v_dept);
        LOOP
            FETCH c_emp INTO v_emp;
            EXIT WHEN c_emp%NOTFOUND;
            process_employee(v_emp);
        END LOOP;
        CLOSE c_emp;  -- ループの外でかつ内側ループの後に確実にCLOSE
    END LOOP;
    CLOSE c_dept;
END;

最善策:カーソル FOR ループで自動クローズ

カーソル FOR ループを使うと、OPEN・FETCH・CLOSE をすべて Oracle が自動的に行います。例外が発生した場合もカーソルは自動的に閉じられるため、最も安全な実装方法です。

カーソル FOR ループ ── OPEN/CLOSE を書かなくてよい
-- 明示的カーソルで FOR ループを使う場合
BEGIN
    FOR r IN (SELECT employee_id, first_name FROM employees WHERE dept_id = 10)
    LOOP
        -- カーソルのOPEN/FETCH/CLOSEはOracleが自動処理
        DBMS_OUTPUT.PUT_LINE(r.employee_id || ': ' || r.first_name);
    END LOOP;
    -- ループ終了(または例外発生)時に自動的にCLOSEされる
END;
BULK COLLECT ── 大量データはまとめて取得してカーソル保持時間を短くする
DECLARE
    TYPE t_emp_tbl IS TABLE OF employees%ROWTYPE;
    l_emps t_emp_tbl;
BEGIN
    -- LIMIT 1000 で1000件ずつ取得し、カーソルを長時間保持しない
    SELECT * BULK COLLECT INTO l_emps
    FROM employees
    WHERE dept_id = 10;

    FOR i IN 1..l_emps.COUNT LOOP
        process_employee(l_emps(i).employee_id);
    END LOOP;
END;

カーソル FOR ループの使い方やパフォーマンス比較についてはPL/SQL カーソル FOR ループと明示的カーソルの使い分けも参考にしてください。

原因と対処法:動的 SQL のカーソル漏れ

OPEN … FOR で開いた REF CURSOR(SYS_REFCURSOR)もカーソル数にカウントされます。ループ内で繰り返し OPEN した場合、対応する CLOSE がないとカーソルが蓄積します。

NG: SYS_REFCURSOR を CLOSE せずに再 OPEN
DECLARE
    l_cur SYS_REFCURSOR;
    v_val VARCHAR2(100);
BEGIN
    FOR i IN 1..500 LOOP
        -- 毎回 OPEN しているが CLOSE していない
        OPEN l_cur FOR 'SELECT name FROM items WHERE id = ' || i;
        FETCH l_cur INTO v_val;
        -- CLOSE l_cur; ← これが抜けている
    END LOOP;
    -- 500個のカーソルが開いたまま → ORA-01000
END;
OK: SYS_REFCURSOR を使った後は確実に CLOSE する
DECLARE
    l_cur SYS_REFCURSOR;
    v_val VARCHAR2(100);
BEGIN
    FOR i IN 1..500 LOOP
        OPEN l_cur FOR
            SELECT name FROM items WHERE id = :id
            USING i;  -- バインド変数を使う
        FETCH l_cur INTO v_val;
        CLOSE l_cur;  -- 毎回確実に CLOSE
    END LOOP;
END;

原因と対処法:Java(JDBC)のリソースリーク

Javaアプリケーションから JDBC でOracleに接続する場合も、PreparedStatementResultSetclose() せずにループすると、各実行がオープン・カーソルとして残り続けます。接続プールを使っている場合、接続が再利用されても古いカーソルがそのまま残るため、気づかないうちに蓄積することがあります。

NG: PreparedStatement を close() しないパターン
// BAD: ループのたびに PreparedStatement を作成するが close しない
public void processBatch(Connection conn, List<Integer> ids) throws SQLException {
    for (int id : ids) {
        PreparedStatement ps = conn.prepareStatement(
            "SELECT name FROM items WHERE id = ?"
        );
        ps.setInt(1, id);
        ResultSet rs = ps.executeQuery();
        // rs も ps も close していない → カーソルが蓄積
        while (rs.next()) {
            process(rs.getString(1));
        }
    }
    // ids が 300 件を超えると ORA-01000
}
OK: try-with-resources で確実にリソースを解放する(Java 7+)
// GOOD: try-with-resources により例外発生時も確実に close される
public void processBatch(Connection conn, List<Integer> ids) throws SQLException {
    // PreparedStatement はループの外で1度だけ作成するのがベスト
    try (PreparedStatement ps = conn.prepareStatement(
            "SELECT name FROM items WHERE id = ?")) {
        for (int id : ids) {
            ps.setInt(1, id);
            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) {
                    process(rs.getString(1));
                }
            } // ResultSet は try-with-resources で自動 close
        }
    } // PreparedStatement も自動 close
}
OK: 接続プール環境でのベストプラクティス
// 接続プール(HikariCP/DBCP等)を使う場合も同様
// Connection.close() は物理切断ではなくプールへの返却だが、
// 未 close の PreparedStatement はカーソルとして残るため必ず close すること

public void safeBatchInsert(DataSource ds, List<Item> items) throws SQLException {
    try (Connection conn = ds.getConnection();
         PreparedStatement ps = conn.prepareStatement(
             "INSERT INTO items (id, name) VALUES (?, ?)")) {
        for (Item item : items) {
            ps.setInt(1, item.getId());
            ps.setString(2, item.getName());
            ps.addBatch();
        }
        ps.executeBatch();
        conn.commit();
    } // Connection も PreparedStatement も自動 close
}

原因と対処法:Python(cx_Oracle / python-oracledb)のリソースリーク

Pythonで cx_Oracle または python-oracledb を使う場合も同様で、cursor.close() を忘れるとカーソルが残ります。

Python での安全なカーソル管理(with 文)
import oracledb  # または cx_Oracle

conn = oracledb.connect(user="hr", password="pw", dsn="localhost/orcl")

# with 文を使うと例外発生時も確実にカーソルが close される
with conn.cursor() as cursor:
    cursor.execute("SELECT employee_id, first_name FROM employees WHERE dept_id = :1", [10])
    for row in cursor:
        print(row)

# with ブロックを抜けると cursor.close() が自動実行される
conn.close()

OPEN_CURSORS パラメータを変更する(暫定対応)

根本的な対処を行う前の暫定対応として、OPEN_CURSORS の値を増やすことができます。ただしこれはリークを隠蔽するだけで、最終的には再発します。あくまで時間稼ぎの対応です。

OPEN_CURSORS を変更する
-- 現在の値を確認
SHOW PARAMETER open_cursors;

-- 動的に変更(再起動不要。ただし再起動すると spfile の値に戻る)
ALTER SYSTEM SET open_cursors = 1000;

-- spfile に永続化(再起動後も有効)
ALTER SYSTEM SET open_cursors = 1000 SCOPE = BOTH;

-- セッション単位で変更することはできない(システム全体パラメータ)
-- ALTER SESSION SET open_cursors = 1000; → エラーになる
OPEN_CURSORS の上限目安
一般的には 300〜1000 の範囲が多く使われます。数千以上に設定している場合は、コードに深刻なカーソルリークがある可能性が高いです。増やすことで一時的にエラーは収まりますが、メモリ使用量が増加することに注意してください。

SESSION_CACHED_CURSORS との違い

SESSION_CACHED_CURSORS は、過去に実行したカーソルをキャッシュして再利用する仕組みのパラメータです。OPEN_CURSORS とは別のパラメータで、カーソルキャッシュは「クローズ済みのカーソルを再利用可能な状態で保持する数」を指します。ORA-01000 とは直接関係しません。

パラメータ 役割 ORA-01000との関係
OPEN_CURSORS 1セッションが同時にオープンできるカーソル数の上限 直接関係あり(上限を超えるとエラー)
SESSION_CACHED_CURSORS クローズ済みカーソルをキャッシュして再利用する数 直接関係なし

例外処理とカーソルの安全な組み合わせ

PL/SQL でカーソルを使う処理に例外処理を組み合わせる場合、%ISOPEN 属性で状態を確認してから CLOSE するのが確実です。

例外処理ブロックで %ISOPEN を使ったカーソル解放
DECLARE
    CURSOR c_orders IS
        SELECT order_id, amount FROM orders WHERE status = 'PENDING';
    v_order orders%ROWTYPE;
BEGIN
    OPEN c_orders;
    LOOP
        FETCH c_orders INTO v_order.order_id, v_order.amount;
        EXIT WHEN c_orders%NOTFOUND;
        process_order(v_order.order_id, v_order.amount);
    END LOOP;
    CLOSE c_orders;

EXCEPTION
    WHEN OTHERS THEN
        -- カーソルがオープン中であれば必ず CLOSE する
        IF c_orders%ISOPEN THEN
            CLOSE c_orders;
        END IF;
        -- エラー情報を記録してから再 RAISE
        INSERT INTO error_log (error_msg, created_at)
        VALUES (SQLERRM, SYSDATE);
        RAISE;
END;

PL/SQLの例外処理の詳細についてはOracle PL/SQL 例外処理(Exception Handling)完全ガイドを参照してください。

ORA-01000 の発生パターンと対処法まとめ

発生パターン 原因 対処法
PL/SQL 明示的カーソル OPEN後にCLOSEしないまま例外が発生 EXCEPTION ブロックでIF %ISOPEN THEN CLOSE
PL/SQL ネストループ 内側のカーソルをCLOSEせずに次反復 内側ループ後に確実にCLOSEする
SYS_REFCURSOR OPEN FOR後にCLOSEしない FETCH後に毎回CLOSE、またはFOR LOOPを使う
Java JDBC PreparedStatement/ResultSetをcloseしない try-with-resourcesで確実にclose
Python cx_Oracle cursor.close()を呼ばない with文でカーソルを管理する
接続プール 返却前にカーソルが残ったまま 使い終わったStatementは必ずclose

ORA-01000 を予防する設計指針

  • PL/SQL はカーソル FOR ループを優先する:OPEN/CLOSE を自動管理してくれるため、閉じ忘れが原理的に発生しない
  • 明示的カーソルを使う場合は必ず EXCEPTION ブロックでも CLOSE するIF %ISOPEN THEN CLOSE END IF をセットで書く習慣をつける
  • Java は try-with-resources を使う:Java 7 以降では PreparedStatementResultSet を try-with-resources でラップするのが標準
  • ループ内で PreparedStatement を再生成しない:ループ外で一度だけ準備し、バインド変数で値を切り替えて再利用する
  • V$OPEN_CURSOR を定期監視する:本番環境では監視ジョブで上限の 80% を超えたらアラートを出す仕組みを作る
  • バインド変数を使う:バインド変数を使うと同一SQLがカーソルキャッシュで共有されるため、カーソルの消費効率が上がる

まとめ

ORA-01000 は「オープンしたカーソルを閉じ忘れる」という単純なリソース管理の問題ですが、例外処理パスでのCLOSE漏れや、JDBCのリソースリークなど気づきにくい形で蓄積するケースが多いです。

  • V$OPEN_CURSOR でカーソルを大量に保持しているセッションとSQLを特定し、原因コードを探す
  • PL/SQLではカーソル FOR ループを使うと OPEN/CLOSE が自動管理される
  • 明示的カーソルは EXCEPTION ブロックでも %ISOPEN チェック後に CLOSE する
  • JDBC(Java)では try-with-resources を使って PreparedStatement/ResultSet を確実にclose
  • OPEN_CURSORS を増やすのはあくまで暫定対応。根本原因のコード修正を必ず行う
  • 関連エラーとして ORA-01555(スナップショットが古すぎます)も、長時間のカーソル保持が引き金になることがある