OracleのPROCESSESとOPEN_CURSORS、増やす前に何を見る? ORA-01000・ORA-00020の原因をV$RESOURCE_LIMITとV$OPEN_CURSORで切り分ける(19c)

OracleのPROCESSESとOPEN_CURSORS、増やす前に何を見る?

こんにちは、おうどんです🍜

新人さん「ORA-01000 が出ました! OPEN_CURSORS を 300 から 3000 に増やしておきますね」

おうどん「10倍!?」

新人さん「ついでに PROCESSES も 2000 にしておきます。大は小を兼ねるので」

おうどん「……それ、再起動もついてくるやつだよ」

(会話は説明用の架空の場面です)

気持ちは、すごく分かるんです。 エラーに「maximum」と書いてある。上限に当たったなら、上限を上げればいい。

……でも、ちょっと待って。

うどん屋で、お客さんが丼を返してくれなくて、棚の丼が足りなくなったとします。 そこで丼を10倍買い足すのは、正解でしょうか。

店長「丼、3000個届きました!」

常連さん「じゃあ、3000杯ぶん置いていきますね」

……返さない人がいる限り、棚はいつか空になる。

今回は、Oracle の PROCESSES と OPEN_CURSORS を「増やす前に」何を見ればいいかを、ローカルの Oracle 19c で ORA-01000 をわざと起こしながら確かめていきます。

最初に、初心者向けのチェックリストです。

  • ORA-01000 は「1つのセッション」が同時に開けるカーソルの上限。まず、閉め忘れを疑う
  • 増やす前に、V$RESOURCE_LIMIT の MAX_UTILIZATION(起動してからの最大使用数)を見る
  • PROCESSES は ALTER SYSTEM で今すぐは変えられない。変えるには再起動が要る
目次

3つの上限は、数えているものが違う

店で言うと、PROCESSES は「働く人の数」、SESSIONS は「席の数」、OPEN_CURSORS は「1人のお客さんが同時に手元に置ける丼の数」です。 同じ「上限」でも、数えている相手がまったく違うんです。

まず用語です。

  • セッション:DB に接続して、SQL を流せる状態の1本の会話。ログインすると1つできる
  • プロセス:OS の上で動いている Oracle の働き手。接続を受け持つものと、裏方の仕事をするものがいる
  • カーソル:1つの SQL を実行するために、セッションが手元に開いておく作業台(Oracle の言葉では、プライベート SQL 領域への取っ手)

公式の Oracle Database 19c リファレンスをもとに並べると、こうなります。

パラメータ何の上限か既定値ALTER SYSTEM で変えられるか
OPEN_CURSORS1つのセッションが同時に開けるカーソルの数50変えられる(PDB の中でも可)
PROCESSESOracle に同時に接続できる OS のユーザープロセスの数。バックグラウンドプロセスなども見込む導出値(アラートログに出るコア数などで決まる)変えられない(Modifiable が No)
SESSIONSシステムで作れるセッションの数導出値 (1.5 × PROCESSES) + 22PDB の中で、その PDB の値だけ変えられる。非 CDB と CDB$ROOT では変えられない

それぞれの出典は PROCESSES・SESSIONS のページです。

店長「OPEN_CURSORS って、店全体の丼の数じゃないの?」

Oracle「1人あたりです。お客さん全員で300個ではありません」

……全員で300個だと思っていたら、店に丼が15万個ある計算になる。

ここ、いちばんの勘違いポイントです。 OPEN_CURSORS はセッションごとの上限なので、ORA-01000 は「ある1つのセッションが、丼を抱えすぎた」という意味です。DB 全体が忙しい、という意味ではありません。

今回の検証環境(Oracle Database 19c 19.3、シングルインスタンス・CDB 構成)で、今の値を見てみます。

select name, value, isdefault from v$parameter
 where name in ('processes','sessions','open_cursors','session_cached_cursors') order by name;
NAME                     VALUE      ISDEFAULT
------------------------ ---------- ---------
open_cursors             300        FALSE
processes                320        FALSE
session_cached_cursors   50         TRUE
sessions                 504        TRUE

4 rows selected.

(CDB$ROOT に sqlplus / as sysdba で接続して実行した結果です。列幅を整える SQL*Plus の COL 設定は省略しています)

OPEN_CURSORS は 300、PROCESSES は 320。 SESSIONS は自分で設定していない(ISDEFAULT が TRUE)ので導出値です。

新人さん「1.5 × 320 + 22 = 502。……504 なんですけど」

おうどん「2席、どこから来たんだろうね」

……計算式どおりにいかない席が2つ。今回の検証環境では 504 で、式の 502 とは2ずれていました。理由は確かめられていないので、ここでは「導出値は実際に v$parameter で確かめる」とだけ覚えておきましょう。

PROCESSES は「お客さんの数」ではない

今回の検証環境では、接続していた sqlplus は2つだけなのに、プロセスは60個使われていました。 PROCESSES の 320 は、320人のお客さんが入れるという意味ではないんです。

V$RESOURCE_LIMIT で、今と最大の使用数を見ます。

select resource_name, current_utilization, max_utilization, initial_allocation, limit_value
  from v$resource_limit where resource_name in ('processes','sessions') order by resource_name;
RESOURCE_NAM CURRENT_UTILIZATION MAX_UTILIZATION INITIAL_AL LIMIT_VALU
------------ ------------------- --------------- ---------- ----------
processes                     60              82        320        320
sessions                      71             110        504        504

2 rows selected.

(同じ環境で、sqlplus を2つ接続した状態で実行した結果です)

V$RESOURCE_LIMIT のリファレンスによると、列の意味はこうです。

  • CURRENT_UTILIZATION:今使っている数
  • MAX_UTILIZATION:インスタンスを起動してからの最大使用数
  • LIMIT_VALUE:上限。ここを超えるとエラー

じゃあ、60個の内訳は?

select pname, program from v$process where background is null order by pname;
PNAME  PROGRAM
------ ----------------------------
D000   ORACLE.EXE (D000)
P000   ORACLE.EXE (P000)
P001   ORACLE.EXE (P001)
P002   ORACLE.EXE (P002)
P003   ORACLE.EXE (P003)
P004   ORACLE.EXE (P004)
P005   ORACLE.EXE (P005)
P006   ORACLE.EXE (P006)
P007   ORACLE.EXE (P007)
S000   ORACLE.EXE (S000)
       PSEUDO
       ORACLE.EXE (SHAD)
       ORACLE.EXE (SHAD)

13 rows selected.

(同じ環境で実行した結果です)

V$PROCESS のリファレンスでは、BACKGROUND 列は「SYSTEM のバックグラウンドプロセスなら 1、フォアグラウンドか SYSTEM 以外のバックグラウンドプロセスなら NULL」です。今回は BACKGROUND = '1' が47個で、それ以外がこの13個でした。

さらに バックグラウンドプロセスの一覧の冒頭には、V$PROCESS で PNAME が NULL でないものがバックグラウンドプロセスと定義されています。同じページによると、

  • Pnnn:パラレル実行(1つの SQL を複数の働き手で分けて処理する)の作業プロセス
  • Dnnn:共有サーバー構成で、クライアントとの通信を受け持つディスパッチャー
  • Snnn:共有サーバー構成で、リクエストを処理する共有サーバー

つまり60個のうち、PNAME があるのは57個。PNAME のない行は3つだけで、そのうち2つが今回の sqlplus の接続を受け持っていたものと考えられます(ORACLE.EXE (SHAD) が2つ。PSEUDO は今回は深追いしていません)。

店長「320人雇える店で、60人働いてる。お客さんは?」

Oracle「2人です」

店長「……厨房、混みすぎじゃない?」

……仕込み、洗い場、出前の電話番。お客さんの前に出てこない人のほうが、ずっと多い。

PROCESSES のリファレンスにも、この値にはロック、ジョブキュー、パラレル実行などのバックグラウンドプロセスを見込んでおくことと書かれています。 だから、「同時接続は100人だから PROCESSES は100」と決めると、厨房スタッフの分が足りなくなります。

新人さん「じゃあ、厨房の47人にも整理券を配っておきます」

……整理券を配る係も、たぶんプロセスに数えられる。

じゃあ、PROCESSES はめちゃくちゃ大きくしておけば安心?

PROCESSES を変えると、SESSIONS(と TRANSACTIONS)の既定値もそれに合わせて変わる、と PROCESSES のリファレンスに書かれています。1つ変えると連鎖するので、増やす前に「本当に足りないのか」を MAX_UTILIZATION で確かめるのが先です。今回の環境なら、最大でも 82 / 320 でした。

ORA-01000 を起こしてみる:丼を返さないと、299杯目で満席

カーソルを開きっぱなしにすると、OPEN_CURSORS の 300 に届いたところで ORA-01000 になります。 今回は PDB の中で、わざと閉め忘れるプログラムを動かしました。

DBMS_SQL は、SQL を文字列で組み立てて、カーソルを自分で開け閉めする PL/SQL のパッケージです。閉め忘れを再現しやすいので使います。

alter session set container=pdb19;
set serveroutput on tab off
exec dbms_application_info.set_module('leaky_app', null)
declare
  type t_ids is table of integer;
  ids t_ids := t_ids();
  n   pls_integer := 0;
  err varchar2(200);
begin
  dbms_session.sleep(0.01);  -- load the package before the leak
  begin
    for i in 1..1000 loop
      ids.extend;
      ids(ids.count) := dbms_sql.open_cursor;
      dbms_sql.parse(ids(ids.count), 'select ' || i || ' from dual', dbms_sql.native);
      n := i;
    end loop;
  exception
    when others then
      err := sqlerrm;
      dbms_session.sleep(20);  -- keep the leaked cursors open while another session looks
  end;
  for k in 1..ids.count loop
    if ids(k) is not null and dbms_sql.is_open(ids(k)) then
      dbms_sql.close_cursor(ids(k));
    end if;
  end loop;
  dbms_output.put_line('opened before error: ' || n);
  dbms_output.put_line('error: ' || err);
end;
/
select 'after close' as msg from dual;
exit

やっていることは3つです。

  1. 1000個のカーソルを、閉めずに開き続ける
  2. エラーになったら、20秒そのまま待つ(その間に別のセッションから覗くため)
  3. 最後に全部閉めて、何個開けたかを表示する
Session altered.


PL/SQL procedure successfully completed.

opened before error: 299
error: ORA-01000: maximum open cursors exceeded

PL/SQL procedure successfully completed.


MSG
-----------
after close

(今回の検証環境の PDB19 で、sqlplus -S -L / as sysdba @leak.sql として実行した結果です。テスト専用のプログラムなので、本番の DB では動かさないでください)

299個開けたところで、300個目が ORA-01000 になりました。 上限は 300 なのに、なぜ 299 なのか。

この PL/SQL のブロック自身も、実行中は1つのカーソルを使っているからです。別のセッションから見たこのセッションの数は、あとの章で出てきますが、ちょうど 300 でした。

常連さん「丼を299個、テーブルに積みました!」

店長「1個は、あなたが今食べてるやつね」

常連さん「300個目をください」

店長「満席です。……あなた1人で」

……1人で店のルールの上限まで食べる人、はじめて見た。

そして最後の after close の SELECT は、全部閉めたあとなので普通に動いています。 ORA-01000 は DB が壊れたのではなく、そのセッションが抱えている丼が多すぎるだけなんです。

ORA-01000 のエラーヘルプの「原因」にも、アプリケーションがカーソルをきちんと閉じていない、必要以上に長く開いている、または本当に OPEN_CURSORS より多くのカーソルが要る、と並んでいます。「処置」の最初は、アプリケーションのコードを確認して、新しく開く前に閉じられるカーソルを閉じること。OPEN_CURSORS を増やすのは、本当に同時にたくさん要る場合です。

新人さん「じゃあ、エラーヘルプの順番どおり、まずコードを見ます」

おうどん「3000 に増やすのは?」

新人さん「……二番目にします」

……順番が入れ替わっただけでも、大きな前進。

🔰 ここまで読めば今日から困らない

ここまでで、最低限の判断はできます。

  • ORA-01000:1つのセッションのカーソルの数が OPEN_CURSORS に届いた。まず閉め忘れを疑う
  • PROCESSES:お客さんの数ではない。バックグラウンドプロセスも数に入る
  • 増やす前に、V$RESOURCE_LIMIT の MAX_UTILIZATION で、実際どこまで使ったかを見る

いちばん短い確認 SQL はこれです。

select resource_name, current_utilization, max_utilization, initial_allocation, limit_value
  from v$resource_limit where resource_name in ('processes','sessions') order by resource_name;

(前の章で実行したものと同じ SQL です)

新人さん「1本で済むなら、毎朝の朝礼で読み上げます」

……朝礼で V$RESOURCE_LIMIT を唱える現場、ちょっと見てみたい。

新人さん「MAX_UTILIZATION が 82 で、上限 320。……増やさなくていいですね」

おうどん「うん。席より先に、丼の返却口を見よう」

……店の拡張工事の前に、返却口の掃除。

ここから先は中級者向け。 どのセッションが丼を抱えているかを、別のセッションから探す方法と、完成版の確認 SQL を見ていきます。

別のセッションから犯人を探す:opened cursors current

どのセッションが OPEN_CURSORS に近いかは、統計値 opened cursors current で分かります。 前の章の leak.sql が20秒待っている間に、別の sqlplus から CDB$ROOT で調べました。

select s.con_id, s.sid, s.username, s.program, st.value as opened_current
  from v$sesstat st
  join v$statname n on n.statistic# = st.statistic#
  join v$session s on s.sid = st.sid
 where n.name = 'opened cursors current'
 order by st.value desc
 fetch first 5 rows only;
    CON_ID        SID USERNAME   PROGRAM                      OPENED_CURRENT
---------- ---------- ---------- ---------------------------- --------------
         3        403 SYS        sqlplus.exe                             300
         0          7            ORACLE.EXE (MMON)                        42
         0        258            ORACLE.EXE (W000)                        10
         0        404            ORACLE.EXE (M003)                        10
         0        149            ORACLE.EXE (M002)                         8

5 rows selected.

(今回の検証環境で、leak.sql の実行中に別のセッションから実行した結果です。観察用スクリプト observe.sql の一部を抜き出しています)

統計値の説明では、opened cursors current は「今開いているカーソルの総数」です。 CON_ID 3(PDB19)の SID 403 が、ちょうど 300。上限ぴったりです。

店長「テーブルの上の丼、数えてきて」

店員「403番テーブル、300個です」

店長「……ほかのテーブルは?」

店員「多くて42個です。MMON さんの席です」

……ちなみに MMON は、バックグラウンドプロセスの一覧によると、管理用のいろいろな仕事をする厨房の人です。お客さんではありません。

次に、そのセッションが何の SQL のカーソルを抱えているかを見ます。 SQL の中の数字を N に置き換えて数えると、同じ形の SQL がまとまります。

select regexp_replace(oc.sql_text, '[0-9]+', 'N') as sql_pattern, count(*) as cnt
  from v$open_cursor oc join v$session s on s.saddr = oc.saddr
 where s.module = 'leaky_app'
 group by regexp_replace(oc.sql_text, '[0-9]+', 'N')
 order by cnt desc
 fetch first 5 rows only;
SQL_PATTERN                                     CNT
---------------------------------------- ----------
select N from dual                              299
declare          pdb_name varcharN(N);            1
        begin

select metadata from kopm$  where name='          1
DB_FDO'

BEGIN DBMS_OUTPUT.ENABLE(NULL); END;              1
select decode(upper(failover_method), NU          1
LL, N , 'BASIC', N,


5 rows selected.

(同じタイミングで、PDB19 に切り替えて実行した結果です。observe.sql の一部です)

select N from dual が 299個。犯人は一目瞭然です。

V$OPEN_CURSOR のリファレンスによると、SQL_TEXT は SQL の先頭60文字です。実際のアプリでは、where id = 1、where id = 2 のように値を直接 SQL に埋め込んでいると、同じ形の SQL が大量に並びます。数字を N に置き換えるのは、その束を見つけるための工夫です。

新人さん「select N from dual さん、299人もいる」

おうどん「全員、同じ人が書いた伝票だね」

……指名手配の似顔絵が、全部同じ顔。

実際のアプリでは、module で絞れないのでは?

そうなんです。今回は検証用に dbms_application_info.set_module で目印を付けましたが、実際は1つ目の SQL で SID を見つけて、where oc.sid = <SID> で絞ります。完成版の SQL は、いちばん多いセッションを自動で選ぶ形にしました(後の章)。

閉めれば1000回でも足りる。ただし V$OPEN_CURSOR は多めに見える

同じ1000回でも、使い終わったカーソルを閉めれば、上限 300 のまま ORA-01000 は出ません。 そして、閉めたあとでも V$OPEN_CURSOR には行が残る、という落とし穴があります。

alter session set container=pdb19;
set serveroutput on lines 200 pages 100 tab off
declare
  c integer;
begin
  for i in 1..1000 loop
    c := dbms_sql.open_cursor;
    dbms_sql.parse(c, 'select ' || i || ' from dual', dbms_sql.native);
    dbms_sql.close_cursor(c);
  end loop;
  dbms_output.put_line('opened and closed: 1000');
end;
/
select 1 from dual;
select 1 from dual;
select 1 from dual;
select 1 from dual;
col name for a32
select n.name, m.value
  from v$mystat m join v$statname n on n.statistic# = m.statistic#
 where n.name in ('opened cursors current', 'opened cursors cumulative', 'session cursor cache count')
 order by n.name;
col cursor_type for a34
select cursor_type, count(*) as cnt
  from v$open_cursor
 where sid = sys_context('userenv', 'sid')
 group by cursor_type order by cnt desc;
exit
Session altered.

opened and closed: 1000

PL/SQL procedure successfully completed.


         1
----------
         1


         1
----------
         1


         1
----------
         1


         1
----------
         1


NAME                                  VALUE
-------------------------------- ----------
opened cursors cumulative              1016
opened cursors current                    4
session cursor cache count               50


CURSOR_TYPE                               CNT
---------------------------------- ----------
SESSION CURSOR CACHED                      50
OPEN                                        4

(今回の検証環境の PDB19 で実行した結果です)

1000個開けて、1000個閉めた。opened cursors current は 4 です。 上限は 300 のまま。丼を返してくれれば、300個の棚で1000杯出せるわけです。

常連さん「食べたら返す。食べたら返す。……1000杯いけました」

店長「最初からそうして」

……ルールを守っただけで、拍手が起きる店。

ここからが落とし穴です。 同じセッションの V$OPEN_CURSOR には、OPEN の 4行のほかに、SESSION CURSOR CACHED が 50行あります。合計54行。

V$OPEN_CURSOR のリファレンスには、このビューは各セッションが「開いて解析した、またはキャッシュした」カーソルを表示する、と書かれています。キャッシュ分も行として出てくるんです。

SESSION_CACHED_CURSORS のリファレンスによると、同じ SQL の解析を繰り返すと、そのカーソルはセッションのカーソルキャッシュに移されます。既定値は 50。今回の session cursor cache count も 50 で、キャッシュの行もちょうど 50 でした。

前の章の leak.sql のときも、V$OPEN_CURSOR を種類別に数えると、こうなっていました。

select oc.cursor_type, count(*) as cnt
  from v$open_cursor oc join v$session s on s.saddr = oc.saddr
 where s.module = 'leaky_app'
 group by oc.cursor_type order by cnt desc;
CURSOR_TYPE                               CNT
---------------------------------- ----------
OPEN-RECURSIVE                            299
SESSION CURSOR CACHED                       8
OPEN                                        1

3 rows selected.

(leak.sql の実行中に、別のセッションから PDB19 で実行した結果です。observe.sql の一部です)

合計 308行。でも opened cursors current は 300 で、エラーもちょうど 300 で起きています。 今回の結果は、キャッシュの行は OPEN_CURSORS の 300 に数えられていないことと合っています。

店員「棚の予備の丼まで数えたら、308個ありました!」

店長「その8個は、常連さん用に洗って伏せてあるやつ」

……数え方を間違えると、上限を超えた丼を持っている人が出てきてしまう。

だから、ORA-01000 の調査で数えるのは opened cursors current。V$OPEN_CURSOR は、何の SQL かを見るために使い、数えるなら cursor_type like 'OPEN%' で絞ります。

じゃあ、DBMS_SQL の 299個が OPEN-RECURSIVE なのはなぜ?

今回の検証では、PL/SQL の中から DBMS_SQL で開いたカーソルが OPEN-RECURSIVE として出ました。リファレンスの説明は「開いている再帰カーソル」の一行だけなので、どういう基準で RECURSIVE になるかの細かい話は断定しません。調べるときは OPEN% でまとめて見ておけば取りこぼしません。

完成版:増やす前の確認 SQL

ここまでの確認を、1本にまとめました。CDB$ROOT(または非 CDB)で、DBA 権限のあるユーザーで実行します。

  • 1:PROCESSES と SESSIONS の今・最大・上限、最大が上限の何%か
  • 2:プロセスの内訳(SYSTEM のバックグラウンド・それ以外のバックグラウンド・PNAME なし)
  • 3:OPEN_CURSORS にいちばん近いセッション(PDB で OPEN_CURSORS を変えていれば、その値と比べるつもりの作り。この場合は未検証)
  • 4:いちばん多いセッションが開いている SQL(キャッシュは除く)
-- check_before_raise.sql : run as a privileged user in CDB$ROOT (or a non-CDB)
set lines 200 pages 100 feedback off tab off
col resource_name for a10
col limit_value   for a11
col kind          for a34
col username      for a10
col program       for a24
col sql_pattern   for a40
col cursor_type   for a16

prompt == 1. processes / sessions : now, peak since startup, limit ==
select resource_name,
       current_utilization as now_used,
       max_utilization     as peak,
       limit_value,
       round(max_utilization / to_number(limit_value) * 100, 1) as peak_pct
  from v$resource_limit
 where resource_name in ('processes', 'sessions')
 order by resource_name;

prompt == 2. what is using PROCESSES ==
select case
         when background = '1'  then 'SYSTEM background'
         when pname is not null then 'other background: ' || regexp_replace(pname, '[0-9]+$')
         else 'no PNAME (session server etc.)'
       end as kind,
       count(*) as cnt
  from v$process
 group by case
         when background = '1'  then 'SYSTEM background'
         when pname is not null then 'other background: ' || regexp_replace(pname, '[0-9]+$')
         else 'no PNAME (session server etc.)'
       end
 order by cnt desc;

prompt == 3. sessions closest to OPEN_CURSORS ==
select s.con_id, s.sid, s.username, s.program,
       st.value as opened_current,
       to_number(p.value) as open_cursors,
       round(st.value / to_number(p.value) * 100, 1) as pct
  from v$sesstat st
  join v$statname n on n.statistic# = st.statistic#
  join v$session  s on s.sid = st.sid
  join v$system_parameter p on p.name = 'open_cursors' and p.con_id in (0, 1, s.con_id)
 where n.name = 'opened cursors current'
   and p.con_id = (select max(p2.con_id) from v$system_parameter p2
                    where p2.name = 'open_cursors' and p2.con_id in (0, 1, s.con_id))
 order by st.value desc
 fetch first 5 rows only;

prompt == 4. what the top session keeps open (cached cursors excluded) ==
select regexp_replace(oc.sql_text, '[0-9]+', 'N') as sql_pattern,
       oc.cursor_type,
       count(*) as cnt
  from v$open_cursor oc
 where oc.sid = (select sid from (
                   select st.sid
                     from v$sesstat st join v$statname n on n.statistic# = st.statistic#
                    where n.name = 'opened cursors current'
                    order by st.value desc)
                  where rownum = 1)
   and oc.cursor_type like 'OPEN%'
 group by regexp_replace(oc.sql_text, '[0-9]+', 'N'), oc.cursor_type
 order by cnt desc
 fetch first 5 rows only;
exit

leak.sql が閉め忘れたまま待っている間に実行した結果です。

== 1. processes / sessions : now, peak since startup, limit ==

RESOURCE_N   NOW_USED       PEAK LIMIT_VALUE   PEAK_PCT
---------- ---------- ---------- ----------- ----------
processes          60         82        320        25.6
sessions           71        110        504        21.8
== 2. what is using PROCESSES ==

KIND                                      CNT
---------------------------------- ----------
SYSTEM background                          47
other background: P                         8
no PNAME (session server etc.)              3
other background: S                         1
other background: D                         1
== 3. sessions closest to OPEN_CURSORS ==

    CON_ID        SID USERNAME   PROGRAM                  OPENED_CURRENT OPEN_CURSORS        PCT
---------- ---------- ---------- ------------------------ -------------- ------------ ----------
         3        403 SYS        sqlplus.exe                         300          300        100
         0          7            ORACLE.EXE (MMON)                    42          300         14
         0        258            ORACLE.EXE (W000)                    10          300        3.3
         0        404            ORACLE.EXE (M003)                    10          300        3.3
         0        149            ORACLE.EXE (M002)                     8          300        2.7
== 4. what the top session keeps open (cached cursors excluded) ==

SQL_PATTERN                              CURSOR_TYPE             CNT
---------------------------------------- ---------------- ----------
select N from dual                       OPEN-RECURSIVE          299
declare   type t_ids is table of integer OPEN                      1
;   ids t_ids := t_i

(今回の検証環境で、この SQL を check.sql という名前で保存し、sqlplus -S -L / as sysdba @check.sql として実行した結果です。今回の PDB19 は OPEN_CURSORS を個別に設定していないので、3 の OPEN_CURSORS はどの行も 300 です。PDB で個別に設定した場合の動きは試していません)

読み方はこうです。

  • 1 の PEAK_PCT が低い(今回は 25.6%)なら、PROCESSES を増やす理由はまだない
  • 3 の PCT が 100 のセッションがあれば、そのセッションが ORA-01000 の当事者
  • 4 で同じ形の SQL が何百も並んでいたら、そこが閉め忘れの場所

新人さん「4番で select N from dual が299。つまり……」

おうどん「つまり?」

新人さん「select N from dual を書いた人に、お茶を出しに行きます」

……説教ではなく、お茶から入るのは大人の対応。でも、コードは直してもらおう。

なぜ 3 は v$parameter ではなく v$system_parameter?

PDB ごとの OPEN_CURSORS を CDB$ROOT から比べたいからです。V$SYSTEM_PARAMETER はインスタンス全体の値で、CON_ID 列で PDB ごとの値を区別できます。v$parameter は、調べている自分のセッションに効いている値です。

新人さん「この SQL、名前が長いので『丼チェック.sql』にしていいですか」

おうどん「中身が分かるなら、何でもいいよ」

……半年後、誰かがこのファイルを開いて「なぜ丼」と首をかしげる未来が見える。

切り分けと、本当に増やすときの手順

エラー番号ごとに、まず見る場所をまとめました。

エラー意味まず見るもの増やすならどれ
ORA-010001つのセッションのカーソルが OPEN_CURSORS に届いた完成版 SQL の 3 と 4OPEN_CURSORS(ALTER SYSTEM で変更可)
ORA-00020プロセスの数が PROCESSES に届いた完成版 SQL の 1 と 2、接続プールの最大数PROCESSES(再起動が必要)
ORA-00018セッションの数が上限に届いた。PDB では、その PDB の SESSIONS を超えたときV$RESOURCE_LIMIT の sessions、PDB の SESSIONSSESSIONS(PDB の中でだけ ALTER SYSTEM 可)

ORA-00020 と ORA-00018 は、今回は再現していません。意味は ORA-00020 のエラーヘルプ・ORA-00018 のエラーヘルプと、SESSIONS のリファレンスにある PDB の説明で確認したものです。ORA-00020 の処置には、既存の接続を切るか、PROCESSES を増やしてインスタンスを再起動する、とあります。

新人さん「ORA-00020 が出たら、とりあえず再起動すれば直りますか?」

おうどん「接続が全部切れるから、一瞬はね」

……満員電車を、一度全員降ろして解決したことにする方式。次の駅でまた満員になります。

閉め忘れがなく、本当に足りないと分かったときの変更手順です。どちらも今回は実行していない例です(今回の検証環境では、PROCESSES は変えない決まりにしています)。

OPEN_CURSORS を増やす場合:

-- 確認(変更前の値を記録する)
select name, value, isdefault, ismodified from v$parameter where name = 'open_cursors';
select name, value from v$spparameter where name = 'open_cursors';

-- 変更:CDB$ROOT で、SPFILE 起動の場合
alter system set open_cursors = 500 scope = both;

-- 変更後の確認
select name, value from v$system_parameter where name = 'open_cursors';
select name, value from v$spparameter where name = 'open_cursors';

-- 戻す(変更前の値に)
alter system set open_cursors = 300 scope = both;
  • 前提:SPFILE で起動していること(PFILE で起動しているなら、PFILE 側も書き換える)
  • 影響範囲:OPEN_CURSORS を個別に設定していない PDB を含め、インスタンス全体。PDB の中で実行すれば、その PDB の値になる
  • 反映時期:ALTER SYSTEM で変えられるパラメータ(ISSYS_MODIFIABLE が IMMEDIATE)。再起動のいらない反映のされ方は、初期化パラメータの再起動要否の記事で実際に確かめています
  • 注意:OPEN_CURSORS のリファレンスには、セッションが実際にその数まで開かないなら、必要より大きくしても追加のオーバーヘッドはない、とあります。つまり増やすこと自体は怖くない。ただし閉め忘れは直らないので、増やした数に届くまでの時間が延びるだけです

PROCESSES を増やす場合:

-- 確認
select resource_name, current_utilization, max_utilization, limit_value
  from v$resource_limit where resource_name in ('processes', 'sessions');
select name, value, isdefault from v$parameter where name in ('processes', 'sessions');

-- 変更:SPFILE に書くだけ。今のインスタンスには効かない
alter system set processes = 400 scope = spfile;

-- 再起動(業務の停止時間をとって行う)
shutdown immediate
startup

-- 再起動後の確認
select name, value, isdefault from v$parameter where name in ('processes', 'sessions');

-- 戻す:元の値を SPFILE に書いて、もう一度再起動
alter system set processes = 320 scope = spfile;
  • 前提:SPFILE で起動していること。PROCESSES は Modifiable が No なので、SCOPE=SPFILE で書いて再起動する
  • 影響範囲:インスタンス全体。SESSIONS を自分で設定していなければ、SESSIONS の既定値も変わる(PROCESSES のリファレンスに、SESSIONS と TRANSACTIONS の既定値はこのパラメータから導出されるので、見直すように、と書かれています)
  • 反映時期:次の起動から。それまでは v$parameter には古い値のまま
  • 注意:再起動前に、上がらなかったときの戻し方(元の値、PFILE の控え)を用意しておく

新人さん「PROCESSES を 2000 にする案は……」

おうどん「MAX_UTILIZATION が 82 だったね」

新人さん「……320 のまま、返却口を掃除してきます」

……冒頭の丼10倍計画、静かに撤回。

ここから先は上級者向け。読み飛ばしてもOKです。 満席のセッションで何が起きるか、PDB での見え方を見ていきます。

満席のセッションでは、調べるための1文もエラーになる

カーソルが上限に届いたセッションでは、Oracle が内部で流す SQL(再帰 SQL)までカーソルを取れなくなります。今回の検証中に、それが分かる失敗が起きました。

leak.sql を最初に作ったとき、dbms_session.sleep(0.01); の行(パッケージを先に読み込ませる行)がありませんでした。ORA-01000 のあと、例外の処理の中で初めて DBMS_SESSION を呼んだら、こうなりました。

declare
*
ERROR at line 1:
ORA-00604: error occurred at recursive SQL level 1
ORA-01000: maximum open cursors exceeded
ORA-06508: PL/SQL: could not find program unit being called: "SYS.DBMS_SESSION"
ORA-06512: at line 15
ORA-01000: maximum open cursors exceeded
ORA-06512: at "SYS.DBMS_SQL", line 1098
ORA-06512: at line 6

(今回の検証中に、最初の版の leak.sql を実行したときの出力から、PL/SQL ブロックのエラー部分を抜き出したものです。最初の版は、上の leak.sql と違って、先に読み込ませる行も、最後にカーソルを閉じる部分もなく、例外の処理の中で件数を表示してから DBMS_SESSION で待つ形でした)

上から読むと、

  • ORA-00604:再帰 SQL(レベル1)でエラー
  • ORA-01000:カーソルの上限
  • ORA-06508:DBMS_SESSION が見つからない

DBMS_SESSION は、もちろん存在します。 今回の結果からは、まだ読み込まれていなかった DBMS_SESSION を使うために Oracle が内部で SQL を流そうとして、その SQL のカーソルが取れなかった、と読めます。パッケージを先に1回呼んでおいたら、この失敗は起きなくなりました。

常連さん「店員さん、メニュー表を見せてください」

店長「メニュー表を持っていくための丼が……ない」

常連さん「メニュー表、丼で運ぶんですか」

……この店、何でも丼で運ぶらしい。

もう1つ、今回の検証では、閉め忘れたカーソルを残したまま SQLPlus で次の SELECT を流すと、SELECT の結果は表示されたあとに ORA-01000 が出ました。同じ状態でも SET SERVEROUTPUT OFF にしたあとの SELECT では出ませんでした。SERVEROUTPUT が ON のとき、SQLPlus が文のあとに出力を取りに行く処理でカーソルが1つ要ったように見えますが、SQL*Plus の内部の動きは確かめていないので推測です。

ここから言える実務の教訓は1つです。

ORA-01000 の調査は、そのセッションの中ではなく、別のセッションから行う。 満席のセッションの中では、調べるための SQL 自体が失敗することがあります。完成版 SQL も、別のセッションから流す前提で作っています。

新人さん「満席のテーブルで、ほかのテーブルの人に調べてもらう、ってことですね」

おうどん「そう。自分のテーブルの上は、もう丼で埋まってるから」

……事件現場の外から捜査する、の DB 版。

CDB と PDB で、見え方が変わる

V$RESOURCE_LIMIT は、今回の検証環境では PDB の中で見ると0行でした。

select resource_name, current_utilization, max_utilization, initial_allocation, limit_value, con_id
  from v$resource_limit where resource_name in ('processes','sessions') order by resource_name;
no rows selected

(今回の検証環境で、alter session set container=pdb19; のあとに実行した結果です。observe.sql の一部です)

同じ SQL(CON_ID 列なし)を CDB$ROOT で流すと、前の章のように processes と sessions の2行が出ます。 PROCESSES は「Modifiable in a PDB」が No で、インスタンス全体で1つの値です。PDB の中から PROCESSES の使用状況を見ようとしても見えないので、PROCESSES と SESSIONS の全体の使用状況は CDB$ROOT で見る、と覚えておくと迷いません。

PDB の担当者「うちの PDB のプロセス数を見たいんですけど」

Oracle「この建物の従業員名簿は、本社にしかありません」

……テナントさんは、ビル全体の社員数を知らない。

PDB ごとに違うのは、ここです(どれもリファレンスで確認した内容で、今回は PDB の値を変えていません)。

パラメータPDB での扱い
OPEN_CURSORSModifiable in a PDB が Yes。PDB の中で ALTER SYSTEM すれば、その PDB の値になる
SESSIONSPDB の中で、その PDB の値だけ変えられる。既定は CDB$ROOT の値。CDB の値より大きくはできない。PDB が上限を超えて使おうとすると ORA-00018
PROCESSESPDB では変えられない。インスタンス全体で1つ

SESSIONS のリファレンスには、もう1つ細かい違いがあります。CDB$ROOT の SESSIONS には再帰セッションのぶん約10%の余裕を見込むよう書かれていますが、PDB の SESSIONS は再帰セッションを数えないので、その調整は要らない、とあります。

新人さん「PDB ごとに SESSIONS を絞れるなら、うるさい PDB だけ席を減らせますね」

おうどん「言い方」

……「混雑しやすい PDB」くらいにしておこう。席数の上限で、ほかの PDB の席を守るのは、ちゃんとした使い方です。

まとめ:上限を上げる前に、丼を返してもらう

冒頭の新人さんは、OPEN_CURSORS を10倍、PROCESSES を2000にしようとしていました。

  • OPEN_CURSORS は1つのセッションの上限。ORA-01000 は、まず閉め忘れを疑う(今回は 299個 + ブロック自身で 300 に届いた)
  • 同じ1000回でも、閉めれば上限 300 のままで足りた
  • 数えるのは opened cursors current。V$OPEN_CURSOR はキャッシュの行も出るので多く見える
  • PROCESSES はお客さんの数ではない。今回は2接続で60プロセス、うち57個がバックグラウンド
  • 増やす前に、V$RESOURCE_LIMIT の MAX_UTILIZATION を見る。PROCESSES は再起動が要り、SESSIONS の既定値も連動する
  • 満席のセッションの中では調査の SQL も失敗しうる。別のセッションから調べる

常連さん「丼、返しにきました。299個」

店長「ありがとう。……次からは、食べたらすぐね」

新人さん「丼の10倍発注、キャンセルしておきました」

……やっと、丼の数と、お客さんの食べ方が噛み合った。

新人さん「浮いた予算で、返却口に『ありがとう』って書いておきます」

……close_cursor の1行に、心が宿った。

上限は「足りない」ときに上げるもの。「返ってこない」ときに上げても、行列の先頭が少し遠くなるだけです。

まずは、完成版の確認 SQL を、ORA-01000 が出ていない平和な日に1回流してみてください。 平和な日の数字を知っておくと、事件の日の数字が、どれだけおかしいかが分かります🍜

参考資料

確認日:2026-10-10。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

おうどん|癒しと創作をたのしむ雑食クリエイター
写経アプリやクレイセラピー、優しい和風デザインがすき。
ZARDと刀剣と文字に癒されて、今はアプリ作ってます。

コメント

コメントする

目次