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 文はもちろん、INSERT・UPDATE・DELETE にも内部的にカーソルが割り当てられます。カーソルには「オープン(実行中・結果取得中)」と「クローズ(使用済み・解放済み)」の状態があり、クローズされていないカーソルの数がセッション単位の上限に達するとORA-01000が発生します。
オープン・カーソルには大きく2種類あります。
| 種類 | 説明 | 例 |
|---|---|---|
| 明示的カーソル | PL/SQLでCURSOR宣言してOPEN/FETCH/CLOSEする | CURSOR c IS SELECT ... |
| 暗黙的カーソル | SQL文実行時にOracleが自動でオープンするカーソル | アプリから実行するSELECT/DML全般 |
OPEN_CURSORS パラメータ
セッションあたりのオープン・カーソル上限は OPEN_CURSORS 初期化パラメータで制御されています。デフォルト値は300ですが、接続セッション数やアプリ特性に応じて調整が必要です。
-- 現在の設定値を確認 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セッションあたりのオープン・カーソル数の上限です。接続数が多い環境でむやみに増やすと全体のSGAメモリを圧迫する可能性があります。根本原因を修正せずに値を上げるだけでは、時間をかけて再び上限に達するだけです。
ORA-01000 の診断方法
V$OPEN_CURSOR でリークを特定する
どのセッション・どのSQLがカーソルを大量に保持しているかは V$OPEN_CURSOR ビューで確認できます。
-- セッション別のオープン・カーソル数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;
-- 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 せずに終了するケースが特に見落とされがちです。
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;
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)完全ガイドを参照してください。
ループ内で繰り返しカーソルをオープンする
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;
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 ループを使う場合
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;
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 がないとカーソルが蓄積します。
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;
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に接続する場合も、PreparedStatement や ResultSet を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
}
// 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
}
// 接続プール(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() を忘れるとカーソルが残ります。
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 の値を増やすことができます。ただしこれはリークを隠蔽するだけで、最終的には再発します。あくまで時間稼ぎの対応です。
-- 現在の値を確認 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; → エラーになる
一般的には 300〜1000 の範囲が多く使われます。数千以上に設定している場合は、コードに深刻なカーソルリークがある可能性が高いです。増やすことで一時的にエラーは収まりますが、メモリ使用量が増加することに注意してください。
SESSION_CACHED_CURSORS との違い
SESSION_CACHED_CURSORS は、過去に実行したカーソルをキャッシュして再利用する仕組みのパラメータです。OPEN_CURSORS とは別のパラメータで、カーソルキャッシュは「クローズ済みのカーソルを再利用可能な状態で保持する数」を指します。ORA-01000 とは直接関係しません。
| パラメータ | 役割 | ORA-01000との関係 |
|---|---|---|
OPEN_CURSORS |
1セッションが同時にオープンできるカーソル数の上限 | 直接関係あり(上限を超えるとエラー) |
SESSION_CACHED_CURSORS |
クローズ済みカーソルをキャッシュして再利用する数 | 直接関係なし |
例外処理とカーソルの安全な組み合わせ
PL/SQL でカーソルを使う処理に例外処理を組み合わせる場合、%ISOPEN 属性で状態を確認してから CLOSE するのが確実です。
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 以降では
PreparedStatementとResultSetを 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(スナップショットが古すぎます)も、長時間のカーソル保持が引き金になることがある
