Oracle Database 運用・トラブル解決ガイド|アーキテクチャの仕組みから障害対応・性能劣化の切り分けまで
目次
Oracle Databaseの運用で起きるトラブルは、症状としては「遅い」「止まった」「つながらない」の三つくらいしかありません。しかし原因は、SGAの構成からREDO/UNDOの管理、統計情報の鮮度、ストレージの空き容量まで幅広く散らばっています。現場で困るのは、症状から原因までの距離が遠いことです。「アプリが遅い」という報告だけが来て、そこから何を見に行けばいいのか分からない。この距離を縮めるには、Oracleが内部で何をしているかを大雑把にでも押さえておく必要があります。
このページは、Oracle運用で遭遇するトラブルを症状別に整理し、それぞれについて「なぜそうなるのか」と「どこを見て判断するか」をまとめた入口です。個別のテーマを深掘りしたい場合は、該当する詳細記事へ進んでください。
🗺️ Oracle運用でつまずくのはどこか(このガイドの全体像)
「遅い」「止まった」「入らない」の3系統で考える
障害報告は曖昧な言葉で届きます。「システムが動かない」と言われたとき、まずこの三系統に振り分けるだけで調査範囲が大きく絞れます。
「入らない」は接続できない状態です。リスナー、ネットワーク、認証、セッション数上限のいずれかで、DB本体が正常でも起きます。逆に、DBが停止していれば当然ここに現れます。切り分けが比較的機械的にできる系統で、tnspingとリスナーログでかなりのところまで分かります。
「止まった」はセッションがハングしている状態です。接続はできるがSQLが返ってこない。ロック待ち、領域不足でのハング、ライブラリキャッシュ競合、アーカイブログ領域が満杯でREDOが切り替われない状態などが該当します。この系統の特徴は、待機イベントを見れば「何を待っているか」が必ず分かることです。Oracleのセッションは、何もしていないのではなく必ず何かを待っています👀
「遅い」は処理が進むけれど時間がかかる状態です。実行計画の変動、統計情報の劣化、I/Oのボトルネック、CPU飽和、同時実行数の増加。この系統がいちばん厄介で、「遅い」の定義が人によって違い、しかも複数要因が重なります。
この三系統は排他ではありません。たとえばアーカイブログ領域の枯渇は、最初「一部の更新セッションが止まった」として現れ、放置すると「新規接続もできない」に発展します。ただ、初動で振り分けておくと、見るべきビューが決まります。
障害対応で最初に集めるべき情報(時刻・範囲・変更履歴)
💡 調査を始める前に、この三点を確定させてください。順番を守るだけで無駄な調査が減ります。
時刻。「いつから」が分からないと、AWRの比較対象も決まりません。ユーザーの体感ではなく、アプリログのタイムスタンプやバッチの開始時刻で押さえます。「今朝から」ではなく「9月26日 02:15以降」まで絞ります。
範囲。全ユーザーか特定ユーザーか、全SQLか特定SQLか、特定スキーマか。これはインスタンス全体の問題と個別SQLの問題を分ける決定的な情報です。全体が遅いならリソースかシステムイベント、特定SQLだけなら実行計画か統計情報を疑います。
変更履歴。直前に何を変えたか。パッチ適用、パラメータ変更、統計情報の再取得、アプリのリリース、データ量の急増、インデックスの追加削除。Oracleの性能劣化は、たいてい何かの変更が引き金です。「何も変えていない」と言われても、統計情報の自動収集ジョブは毎晩走っています。
現状のスナップショットは、調査中に状況が変わってしまう前に取っておきます。
-- 現在アクティブなセッションと待機イベント(最初に撮る)
SELECT s.sid, s.serial#, s.username, s.status,
s.event, s.wait_class, s.seconds_in_wait,
s.sql_id, s.blocking_session, s.machine, s.program
FROM v$session s
WHERE s.type = 'USER'
AND s.status = 'ACTIVE'
ORDER BY s.wait_class, s.seconds_in_wait DESC;
-- インスタンス全体でどのクラスに時間を使っているか
SELECT wait_class, total_waits, time_waited/100 AS time_waited_sec
FROM v$system_wait_class
ORDER BY time_waited DESC;
v$sessionのblocking_sessionが埋まっていれば、それはロック待ち系統です。wait_classがUser I/Oに偏っていればI/O、Concurrencyならラッチやバッファ競合、Configurationなら領域やREDO関連の設定が疑わしいという具合に、最初の当たりが付きます🔍
このページの使い方と個別記事への進み方
以降の章は、アーキテクチャの要点 → 症状別の切り分け → 予防と学習という順で並んでいます。すでに障害が発生中なら、該当する症状の章から読んでください。落ち着いたあとで、アーキテクチャの章を読み返すと、なぜその確認手順だったのかが繋がります。
各章では要点と確認用のSQL・コマンドを示します。より詳しい手順や、エンジンをまたいだ話題(Oracleから他DBへの移行など)は、該当箇所で個別記事へリンクしています。
🏗️ トラブルの原因が読めるようになるアーキテクチャの要点
インスタンスとデータベース:SGA・バックグラウンドプロセスの役割分担
Oracleでは「インスタンス」と「データベース」が明確に別物です。インスタンスはメモリ(SGA)とバックグラウンドプロセスの集合、データベースはディスク上のファイル群(データファイル、制御ファイル、REDOログファイル)。この区別が曖昧だと、「起動しない」トラブルの切り分けができません。インスタンスの起動には初期化パラメータだけあればよく、データベースのマウントには制御ファイルが、オープンにはデータファイルとREDOログが必要です。段階ごとに必要なものが違うので、どこで落ちたかが原因を教えてくれます。
SGAの主要コンポーネントと、それが枯渇したときに何が起きるかを押さえます。
バッファキャッシュはデータブロックのキャッシュです。ここにヒットしなければ物理読み込みが発生し、db file sequential read/db file scattered readといった待機イベントとして現れます。小さすぎると物理I/Oが増えますが、単純に大きくすれば速くなるわけではありません。非効率なフルスキャンで大量のブロックを流し込めば、キャッシュを汚すだけです。「バッファキャッシュヒット率が低いから増やす」という短絡は、SQLの問題を隠すだけになりがちです。
共有プールには、SQLの解析結果(ライブラリキャッシュ)とデータディクショナリのキャッシュが置かれます。ここが不足するとORA-04031が出ます。バインド変数を使わないSQLが大量に投げ込まれると、似たようなSQLで共有プールが埋まり、ハードパースが増え、library cache: mutex Xやlatch: shared poolといった競合が発生します。ORA-04031は「メモリを増やせ」で片付く場合もありますが、根本原因はリテラルSQLの乱発であることが多いです。
REDOログバッファからは、コミット時にログライタ(LGWR)がREDOログファイルへ書き出します。log file sync待機が長い場合、LGWRの書き込み先ストレージが遅いか、コミットが細かすぎるかです。1行ごとにコミットするループ処理は、この待機を跳ね上げます。
バックグラウンドプロセスでは、この四つの役割を理解しておくと障害時の見通しが立ちます。
- DBWn(データベースライタ)は、バッファキャッシュの変更済みブロックをデータファイルへ書き出す。遅延しても即座にデータは失われない(REDOがあるため)
- LGWR(ログライタ)は、REDOログバッファをREDOログファイルへ書き出す。コミットの応答性はこれに依存する
- CKPT(チェックポイント)は、チェックポイント情報を制御ファイルとデータファイルヘッダに記録する。リカバリの開始位置を決める
- ARCn(アーカイバ)は、REDOログをアーカイブログとして保存する。アーカイブ先が満杯になるとここが止まり、REDOログの循環が止まり、DB全体が更新できなくなる
最後のARCnの連鎖が、Oracle運用で最も起こりやすい「突然DBが止まった」の典型です💥
REDOとUNDOは何を守っているのか(コミットとロールバックの実像)
REDOとUNDOは名前が似ていますが、守っているものが正反対です。ここを混同したままだと、ORA-01555もリカバリ手順も理解できません。
REDOは「変更をやり直せるようにする」記録です。データブロックを変更するとき、Oracleは変更後の状態を再現できる情報をREDOログに書きます。コミット時に保証されるのは「REDOがディスクに書かれたこと」だけで、データファイルへの書き込みは後回しです。これがOracleのコミットが速い理由です。逆に言えば、インスタンスがクラッシュした時点でデータファイルには未完了の変更が混在しており、起動時のインスタンスリカバリでREDOを適用して整合状態に戻します(ロールフォワード)。
UNDOは「変更を取り消せるようにする」記録です。変更前の値をUNDO表領域に保存します。用途は三つあります。
- ロールバック(明示的な取り消し、およびリカバリ時の未コミットトランザクションの巻き戻し)
- 読み取り一貫性(後述)
- フラッシュバック問い合わせ
重要なのは、UNDOがコミット後も一定期間保持されることです。ロールバックのためだけなら、コミットした瞬間に捨ててよいはずです。捨てないのは、他のセッションが「過去の時点のデータ」を読むために必要だからです。この保持期間の設計が、次項の読み取り一貫性とORA-01555に直結します。
-- UNDOの使用状況と保持期間の実績値
SELECT to_char(begin_time,'YYYY-MM-DD HH24:MI') AS begin_time,
undoblks, maxquerylen, tuned_undoretention,
ssolderrcnt -- ORA-01555 の発生回数
FROM v$undostat
ORDER BY begin_time DESC
FETCH FIRST 24 ROWS ONLY;
💡 maxquerylen(その期間の最長クエリ秒数)とtuned_undoretention(Oracleが自動調整した保持秒数)を比べてください。最長クエリが保持期間を超えているなら、ORA-01555の予備軍です。
読み取り一貫性とMVCC:Oracleが「待たない」理由
Oracleの大きな特徴は、読み取りが書き込みをブロックせず、書き込みが読み取りをブロックしないことです。SELECTはロックを取りません。これはUNDOを使った多版同時実行制御(MVCC)で実現されています。
仕組みはこうです。クエリ開始時点のSCN(システム変更番号)が記録されます。ブロックを読むとき、そのブロックがクエリ開始SCNより後に変更されていれば、Oracleはそのままでは読みません。UNDOを辿って、クエリ開始時点の姿を再構成した「一貫性読み取りブロック」を作り、それを読みます。だから長時間のSELECTでも、途中で他のセッションがコミットした変更が混ざることがありません。
この仕組みから、運用上の帰結が三つ出てきます。
ORA-01555が起きる理由。UNDOが上書きされてしまうと、過去の姿を再構成できません。「スナップショットが古すぎます」はこのエラーです。長時間クエリとUNDOの回転速度の綱引きです。
SELECTが遅くなる隠れた原因。更新が激しいテーブルを長時間スキャンすると、一貫性読み取りブロックの再構成コストがかかります。consistent gets - examinationや、統計上のdata blocks consistent reads - undo records appliedが増えます。「同じSQLなのに、バッチが動いている時間帯だけ遅い」ときはこれを疑います。
ロック待ちは更新同士だけで起きること。行ロック待ち(enq: TX - row lock contention)が発生するのは、同じ行を更新しようとしたときです。読み取りが原因でロック待ちになることは通常ありません。裏を返せば、ロック待ちが出ているなら、必ず更新処理が絡んでいます。
PostgreSQLも同じくMVCCですが、古いバージョンをテーブル本体に残すため、VACUUMという別の運用課題が生まれます。この違いは移行時に必ず表面化します。詳しくはOracle から PostgreSQL 移行でハマるポイント|症状別の切り分けと対処で扱っています。
マルチテナント(CDB/PDB)でつまずきやすい前提の違い
12c以降のマルチテナント構成では、CDB(コンテナDB)とPDB(プラガブルDB)の階層があり、19cではマルチテナントが標準です。従来の非CDB構成を前提とした手順がそのまま通らない箇所があります。
接続先を間違えるパターン。SYSでローカル接続すると、デフォルトでCDB$ROOTに入ります。ここでテーブルを探しても、PDB内のオブジェクトは見えません。「確かに作ったはずのテーブルが無い」の大半はこれです。
-- 今どこにいるか
SELECT sys_context('USERENV','CON_NAME') AS container,
sys_context('USERENV','CON_ID') AS con_id
FROM dual;
-- PDBの一覧と状態
SELECT con_id, name, open_mode, restricted FROM v$pdbs;
-- コンテナを切り替える
ALTER SESSION SET CONTAINER = orclpdb1;
PDBがOPENしていないパターン。CDBを起動してもPDBは自動でOPENしないことがあります(SAVE STATEしていなければ)。CDBは正常なのにアプリがつながらない、という状況の定番です。
ALTER PLUGGABLE DATABASE orclpdb1 OPEN;
ALTER PLUGGABLE DATABASE orclpdb1 SAVE STATE; -- 次回起動時も自動OPEN
⚠️ 共有されるものと分離されるものの区別も要注意です。SGA、REDOログ、UNDO表領域(ローカルUNDO構成でない場合)、制御ファイルはCDBで共有されます。一方、データファイル、SYSTEM/SYSAUX表領域、ユーザーとオブジェクトはPDBごとです。つまり、あるPDBの暴走セッションがCDB全体のSGAやREDOを圧迫します。「PDBを分けたから影響が分離される」わけではありません。リソース制限が必要なら、Resource Managerで明示的に設定します。
表領域とサービス名の扱いも変わります。PDBごとに表領域が独立するため、領域監視も全PDBを回す必要があります。接続はサービス名経由が基本で、SID指定のTNSエントリはPDBには使えません。
🔌 起動・接続まわりのトラブルを切り分ける
インスタンスが起動しない:mount/openのどの段階で落ちたかを見る
Oracleの起動は三段階です。この段階を意識せずにstartupを繰り返すと、原因が見えません。
| 段階 | 必要なもの | 失敗時に疑うもの |
|---|---|---|
| NOMOUNT(インスタンス起動) | 初期化パラメータファイル(spfile/pfile) | パラメータの記述ミス、メモリ不足、共有メモリの残骸 |
| MOUNT(DBマウント) | 制御ファイル | 制御ファイルの欠損・破損、パスの不一致 |
| OPEN(DBオープン) | データファイル、REDOログファイル | データファイル欠損、リカバリ未完了、ファイルのSCN不整合 |
段階を手動で進めて、どこで落ちるか確定させます。
-- 段階的に起動して切り分ける
STARTUP NOMOUNT; -- ここで落ちる → パラメータかメモリ
ALTER DATABASE MOUNT; -- ここで落ちる → 制御ファイル
ALTER DATABASE OPEN; -- ここで落ちる → データファイル/REDO/リカバリ
NOMOUNTで落ちる場合、エラーメッセージを素直に読みます。ORA-27102(out of memory)ならOSの共有メモリ設定やhugepages、SGA_TARGETの値。ORA-01078はパラメータファイルの読み込み失敗で、ファイルパスとパーミッションを確認します。前回の異常終了で共有メモリセグメントが残っていることもあるので、ipcs -mで確認します。
MOUNTで落ちる場合、control_filesパラメータのパスにファイルが実在するかを確認します。多重化している制御ファイルの一つが壊れているなら、健全なコピーから複製して起動できます。
OPENで落ちる場合は、どのファイルが問題かを特定します。
-- リカバリが必要なファイル、欠損しているファイルの特定
SELECT file#, status, error, recover, change#, time
FROM v$recover_file;
SELECT file#, name, status FROM v$datafile_header
WHERE status != 'ONLINE' OR error IS NOT NULL;
v$recover_fileに行が出ていれば、そのファイルにリカバリが必要です。アーカイブログがあればRECOVER DATABASEで適用します。業務上不要な表領域のファイルなら、ALTER DATABASE DATAFILE ... OFFLINEで切り離してOPENし、後から対処する判断もあり得ます(ただしSYSTEM表領域では不可)。
いずれの段階でも、alert.logが一次情報です。GUIやスクリプトの出力より、alert.logの当該時刻の前後20行を読むほうが早いです。
# alert.logの場所を特定
# SQL> show parameter diagnostic_dest
# → $ORACLE_BASE/diag/rdbms/<db_name>/<instance>/trace/alert_<instance>.log
tail -200 $ORACLE_BASE/diag/rdbms/orcl/orcl/trace/alert_orcl.log
# ADRCIでインシデントを一覧
adrci exec="show problem"
adrci exec="show incident"
リスナー経由で接続できない(ORA-12154/12541/12514の読み分け)
このORA-125xx系は、番号ごとに「どこまで到達したか」が違います。番号を読み分けるだけで調査範囲が三分の一になります✨
ORA-12154(TNS:could not resolve the connect identifier specified)は、クライアント側の名前解決の失敗です。まだネットワークに出ていません。tnsnames.oraに該当エントリが無い、TNS_ADMINが想定と違う場所を指している、接続文字列のタイプミス。まずクライアント側だけを見ます。
ORA-12541(TNS:no listener)は、指定のホスト・ポートに到達したが、リスナーが応答しない状態です。リスナーが起動していない、ポート番号違い、ファイアウォールでブロック。サーバー側のリスナーを確認します。
ORA-12514(TNS:listener does not currently know of service requested)は、リスナーには届いたが、要求されたサービス名を知らない状態です。DBインスタンスが起動していない(動的登録されていない)、PDBがOPENしていない、サービス名の綴り違い、静的登録のSID_LISTの設定漏れ。リスナーは生きていてDB側に問題があることを示すので、この番号が出たらサーバー側のDBを見ます。
ORA-12505は、SIDを解決できないケース。CDB環境でSID指定してPDBに繋ごうとした場合によく出ます。サービス名指定に変えます。
確認コマンドはこの順で。
# リスナーが認識しているサービスを一覧(12514の一次切り分け)
lsnrctl status
lsnrctl services
# クライアント側の名前解決と到達性
tnsping ORCLPDB1 3
# 接続文字列を直書きして tnsnames.ora を迂回(12154の切り分け)
sqlplus scott/tiger@//dbhost:1521/orclpdb1.example.com
💡 lsnrctl servicesの出力で、目的のサービス名がStatus READYになっているかを見ます。UNKNOWNは静的登録、READYは動的登録です。サービス名が一覧に無ければ、DB側で確認します。
SELECT name, network_name, pdb FROM v$services ORDER BY pdb, name;
SHOW PARAMETER local_listener
SHOW PARAMETER service_names
リスナーログも見ます。接続試行が記録されていれば、少なくともネットワークは通っています。記録がなければクライアントから到達していません。
# リスナーログ(TNS-エラーの詳細が残る)
tail -100 $ORACLE_BASE/diag/tnslsnr/$(hostname)/listener/trace/listener.log
セッション数・プロセス数の上限に当たったときの確認と暫定対処
ORA-00018(maximum number of sessions exceeded)やORA-00020(maximum number of processes exceeded)は、上限値の問題として現れますが、本当の問題はセッションが減らない理由のほうです。
まず現状を把握します。
-- リソースの現在値・最高値・上限値
SELECT resource_name, current_utilization, max_utilization, limit_value
FROM v$resource_limit
WHERE resource_name IN ('processes','sessions','transactions','enqueue_locks');
max_utilizationがlimit_valueに張り付いているなら上限に達しています。次に、何が占めているかを見ます。
-- 接続元・プログラム別のセッション数(急増源の特定)
SELECT machine, program, status, COUNT(*) AS cnt
FROM v$session
WHERE type = 'USER'
GROUP BY machine, program, status
ORDER BY cnt DESC;
-- 長時間INACTIVEなセッション(接続リークの候補)
SELECT sid, serial#, username, machine, program,
last_call_et/60 AS idle_min, logon_time
FROM v$session
WHERE status = 'INACTIVE' AND type = 'USER'
AND last_call_et > 3600
ORDER BY last_call_et DESC;
判断基準です。INACTIVEが大量にありlast_call_etが長いなら、アプリのコネクションプール設定か、接続を閉じていないコードが原因です。この場合、processesを増やすのは時間稼ぎにしかなりません。増やした分だけリークが埋めます。
一方、ACTIVEが大量にあり同じ待機イベントで詰まっているなら、上限の問題ではなく、その待機の解消が本題です。ロック待ちで後続セッションが溜まっているケースが典型です。
暫定対処として個別セッションを切る場合、影響を確認してから実行します。
-- 特定セッションを終了(未コミットはロールバックされる)
ALTER SYSTEM KILL SESSION '1234,56789' IMMEDIATE;
⚠️ 大きなトランザクションを持つセッションをKILLすると、ロールバックに長時間かかり、その間v$transactionのused_ublkが減っていくのを待つことになります。KILLは万能ではありません。
恒久対処でprocessesを上げる場合、sessionsはprocessesから自動導出されることが多く、OS側のプロセス数上限(ulimit -u、kernel.pid_max)やメモリも合わせて見ます。PGAはセッションごとに消費されるため、セッション数を増やせばメモリ設計も変わります。processesは静的パラメータなので再起動が必要です。
ALTER SYSTEM SET processes = 800 SCOPE = SPFILE;
-- 再起動後に有効
パスワード期限切れ・アカウントロックで突然つながらなくなる
「何も変えていないのに、ある日突然アプリが接続できなくなった」の最有力候補です。11g以降、デフォルトプロファイルのPASSWORD_LIFE_TIMEが180日に設定されているため、放置したアカウントは半年で期限切れになります。ORA-28001(the password has expired)、ORA-28000(the account is locked)が出ます。
-- アカウントの状態と期限
SELECT username, account_status, lock_date, expiry_date, profile
FROM dba_users
WHERE account_status != 'OPEN'
ORDER BY expiry_date;
-- プロファイルのパスワード関連設定
SELECT profile, resource_name, limit
FROM dba_profiles
WHERE resource_name LIKE 'PASSWORD%'
AND profile = 'DEFAULT';
💡 account_statusの読み方です。EXPIREDは期限切れで、パスワード変更すれば使えます。EXPIRED(GRACE)は猶予期間中で、まだ接続できるが警告が出ている状態(ここで気づければ計画的に対処できます)。LOCKED(TIMED)はFAILED_LOGIN_ATTEMPTS超過による自動ロックで、PASSWORD_LOCK_TIME経過後に自動解除されます。LOCKEDは手動ロックです。
LOCKED(TIMED)が繰り返し起きる場合、古いパスワードで接続を試み続けているアプリやバッチが残っていることを疑います。パスワードを変更したのに一部の設定ファイルを更新し忘れた、というパターンです。この場合、正しいパスワードのアプリも巻き込まれてロックされます。
-- ロック解除とパスワード再設定
ALTER USER app_user ACCOUNT UNLOCK;
ALTER USER app_user IDENTIFIED BY "新しいパスワード";
運用上の判断として、アプリ用アカウントには無期限のプロファイルを割り当てるか、パスワード更新を運用手順に組み込むかを決めておきます。無期限化はセキュリティポリシーとのトレードオフなので、組織の方針を確認してください。失敗ログイン試行はdba_audit_sessionや統合監査のビューで追えます。
💾 領域不足とデータファイルまわりの障害
表領域が拡張できない(ORA-01653系)の確認手順
ORA-01653(unable to extend table … in tablespace …)は「表領域に空きがない」ですが、正確には次のエクステントを確保できるだけの連続空き領域がない、という意味です。合計空き容量があっても、断片化していれば発生し得ます。加えて、原因は三系統に分かれます。
- 表領域内に空きがない(既存データファイルが満杯で
AUTOEXTENDも限界) - データファイルの上限に達した(
MAXSIZE、あるいはファイルサイズ上限) - OSのファイルシステム/ASMディスクグループに空きがない
この三つを順に確認します。
-- 表領域の使用率(AUTOEXTENDの最大まで考慮)
SELECT d.tablespace_name,
ROUND(SUM(d.bytes)/1024/1024) AS alloc_mb,
ROUND(SUM(GREATEST(d.bytes, d.maxbytes))/1024/1024) AS max_mb,
ROUND(NVL(f.free_mb,0)) AS free_mb,
ROUND((SUM(d.bytes)-NVL(f.free_bytes,0))
/ SUM(GREATEST(d.bytes,d.maxbytes)) * 100, 1) AS pct_used_vs_max
FROM dba_data_files d
LEFT JOIN (SELECT tablespace_name,
SUM(bytes) AS free_bytes,
SUM(bytes)/1024/1024 AS free_mb
FROM dba_free_space GROUP BY tablespace_name) f
ON d.tablespace_name = f.tablespace_name
GROUP BY d.tablespace_name, f.free_mb, f.free_bytes
ORDER BY pct_used_vs_max DESC;
ここで見るべきは、現在の割当量に対する使用率ではなく、AUTOEXTENDの最大値に対する使用率です。pct_used_vs_maxが90%を超えていれば要対応です。
-- データファイルごとのAUTOEXTEND設定と上限
SELECT tablespace_name, file_name,
bytes/1024/1024 AS size_mb,
autoextensible,
increment_by * (SELECT value FROM v$parameter WHERE name='db_block_size')
/1024/1024 AS next_mb,
maxbytes/1024/1024 AS max_mb
FROM dba_data_files
WHERE tablespace_name = 'USERS';
autoextensible = NOなら拡張されません。YESでもbytesがmaxbytesに達していれば同じです。maxbytesが0のときは無制限ですが、実質的にはファイルサイズ上限(smallfileでブロックサイズ×約4,194,303ブロック)とOSの空きが上限です。
対処は、データファイル追加が基本です。既存ファイルのリサイズより、追加のほうが安全です(ファイルサイズ上限や断片化を回避できる)。
-- データファイル追加(bigfileでなければこれが基本)
ALTER TABLESPACE users
ADD DATAFILE '/u02/oradata/orcl/users02.dbf'
SIZE 2G AUTOEXTEND ON NEXT 256M MAXSIZE 16G;
-- 既存ファイルの上限を引き上げる
ALTER DATABASE DATAFILE '/u02/oradata/orcl/users01.dbf'
AUTOEXTEND ON NEXT 256M MAXSIZE 32G;
その前にOS側の空きを確認します。ファイルシステムが満杯なら、ALTERも失敗します。
df -h /u02
# ASMの場合
# SQL> SELECT name, total_mb, free_mb, ROUND(free_mb/total_mb*100,1) pct_free FROM v$asm_diskgroup;
増やす前に、そもそも増え続けている原因も確認します。不要データの蓄積、削除されていないワークテーブル、LOBセグメントの肥大化。dba_segmentsでサイズ上位を見れば当たりが付きます。
SELECT owner, segment_name, segment_type, partition_name,
ROUND(bytes/1024/1024) AS mb
FROM dba_segments
WHERE tablespace_name = 'USERS'
ORDER BY bytes DESC
FETCH FIRST 20 ROWS ONLY;
⚠️ なお、DELETEしてもセグメントは縮まりません。ハイウォーターマークが下がらないためです。空き領域を返したいなら、ALTER TABLE ... SHRINK SPACE(行移動を有効にした上で)や再編成が必要です。
UNDO表領域とスナップショットが古すぎます(ORA-01555)の考え方
ORA-01555(snapshot too old)は、領域不足のエラーのように見えて、実は時間の問題です。長時間実行中のクエリが、自分の開始SCN時点のブロックを再構成しようとしたが、必要なUNDOが既に他のトランザクションに上書きされていた、という状態です。
発生条件は「長時間クエリ」×「UNDOの高速な回転」の組み合わせです。だから、UNDO表領域を大きくすれば緩和されますが、それだけでは根治しません。クエリが長すぎることが原因の半分です。
確認するのはこの三点です。
-- UNDOの保持設定と実績
SHOW PARAMETER undo_retention
SELECT tablespace_name, retention -- GUARANTEE か NOGUARANTEE か
FROM dba_tablespaces WHERE contents = 'UNDO';
-- 過去の最長クエリと自動調整された保持時間、01555発生数
SELECT to_char(begin_time,'MM-DD HH24:MI') AS t,
maxquerylen, tuned_undoretention, ssolderrcnt, nospaceerrcnt,
undoblks
FROM v$undostat
WHERE begin_time > sysdate - 2
ORDER BY begin_time DESC;
判断基準です。maxquerylenがtuned_undoretentionを超えている期間があれば、そこでORA-01555が起きる余地があります。ssolderrcntが0でなければ既に発生しています。nospaceerrcntが出ていればUNDO領域自体が足りていません。
UNDO_RETENTIONは、AUTOEXTENDが有効なUNDO表領域では「目標値」として扱われ、Oracleが自動調整します。逆にAUTOEXTEND無効(固定サイズ)の場合は、Oracleは領域を最大限使う方向に動き、UNDO_RETENTIONは尊重されない挙動になります。この違いを知らないと、「UNDO_RETENTIONを長くしたのに効かない」と混乱します。どうしても保持を保証したいならRETENTION GUARANTEEを設定しますが、その場合UNDOが枯渇すると更新側がORA-30036で失敗するリスクを引き受けることになります。
対処の優先順位は、この順で検討します。
- 長時間クエリ側を短くする。実行計画を改善して数十分のクエリを数分にすれば、それだけで発生確率が下がります
- クエリ対象の更新を減らす、または時間帯をずらす。バッチ更新とレポートクエリを同時に走らせない
- UNDO表領域を拡張し、
UNDO_RETENTIONを最長クエリ以上に設定する - フェッチ間コミット(fetch across commit)のアンチパターンを直す。カーソルでSELECTしながらループ内でコミットする処理は、自分で自分のUNDOを壊します。これは古典的なORA-01555の原因です
⚠️ 4番目は特に注意してください。「1000件ごとにコミットしてUNDOを節約する」という意図の実装が、かえってORA-01555を招きます。
一時表領域の枯渇はソートとハッシュ結合を疑う
ORA-01652(unable to extend temp segment)は、一時表領域の枯渇です。TEMPを使うのは、主にこの処理です。
- ソート(ORDER BY、GROUP BY、DISTINCT、集合演算、索引作成)
- ハッシュ結合のハッシュ表(ビルド側がメモリに収まらないとき)
- グローバル一時表
- ウィンドウ関数の処理
つまり、TEMPの枯渇はSQLがメモリ内で処理しきれない量のデータを扱っているというサインです。単にTEMPを増やす前に、何が食っているかを見ます。
-- 現在TEMPを消費しているセッションとSQL
SELECT s.sid, s.serial#, s.username, s.sql_id,
u.tablespace, u.segtype,
ROUND(u.blocks * ts.block_size / 1024/1024) AS used_mb
FROM v$sort_usage u
JOIN v$session s ON s.saddr = u.session_addr
JOIN dba_tablespaces ts ON ts.tablespace_name = u.tablespace
ORDER BY u.blocks DESC;
-- TEMPの使用率
SELECT tablespace_name,
tablespace_size * 8192/1024/1024 AS total_mb,
allocated_space * 8192/1024/1024 AS alloc_mb,
free_space * 8192/1024/1024 AS free_mb
FROM dba_temp_free_space;
segtypeがSORTならソート、HASHならハッシュ結合、LOB_DATAならLOBの一時領域です。sql_idが分かれば、そのSQLの実行計画を見て、どこでソートやハッシュ結合が発生しているかを確認します。
-- 実際のメモリ/TEMP使用量を実行計画に重ねて見る
SELECT * FROM table(
dbms_xplan.display_cursor('&sql_id', NULL, 'ALLSTATS LAST +MEMSTATS'));
💡 ここでOMem(最適メモリ)、1Mem(1パス実行に必要なメモリ)、Used-Memを見ます。Used-Memに(1)や(2)が付いていれば、1パス/マルチパスでディスクに溢れている証拠です。0Memのみで完結していればメモリ内処理です。
判断基準です。特定の重いバッチSQLが原因なら、まずSQLを直します。不要なソートを消す(適切な索引でORDER BYを回避、DISTINCTの見直し)、結合方法を変える、パラレル度を調整する。PGA_AGGREGATE_TARGETを増やせばメモリ内処理の余地は広がりますが、1セッションが使えるワークエリアには上限があるため、極端に大きなソートは結局TEMPへ溢れます。
複数セッションが同時に大量のTEMPを使うことが業務要件なら、TEMPの拡張が正解です。特定ユーザーの暴走を防ぎたいなら、ALTER USER ... QUOTAではなくResource ManagerやTEMPORARY TABLESPACE GROUPの設計を検討します。
アーカイブログ領域が溢れてDBが止まったときの復旧順序
ARCHIVELOGモードで運用していて、アーカイブ先が満杯になると、更新系の処理が一斉にハングします。エラーではなくハングとして現れるのが厄介な点です。
なぜハングするのか。REDOログは複数グループを循環して使います。あるグループを再利用するには、その内容がアーカイブ済みである必要があります。アーカイブできなければ、ログスイッチが完了せず、REDOバッファへの書き込みも進まず、コミットが返らない。結果、更新セッションがlog file switch (archiving needed)で止まります。読み取りだけなら動き続けるので、「一部の処理だけ止まっている」ように見えます。
alert.logにはORA-00257(archiver error)やORA-16038、ARC0プロセスのエラーが記録されます。
確認はこの順で。
-- アーカイブ先とその状態
SELECT dest_id, destination, status, error, binding
FROM v$archive_dest
WHERE status != 'INACTIVE';
-- 高速リカバリ領域(FRA)の使用状況
SELECT name, space_limit/1024/1024/1024 AS limit_gb,
space_used/1024/1024/1024 AS used_gb,
ROUND(space_used/space_limit*100,1) AS pct_used,
space_reclaimable/1024/1024/1024 AS reclaimable_gb
FROM v$recovery_file_dest;
-- FRAの内訳(何が食っているか)
SELECT file_type, percent_space_used, percent_space_reclaimable,
number_of_files
FROM v$recovery_area_usage;
percent_space_reclaimableが大きければ、バックアップ済みで削除可能なファイルが溜まっています。この場合、RMANに削除させるのが正しい手順です。
復旧の順序を守ってください。焦ってOSコマンドでアーカイブログを削除すると、リカバリ不能な状態を作ります。
- FRAの空きを作る。まずRMANで、バックアップ済みかつ不要なアーカイブログを削除します
rman target /
CROSSCHECK ARCHIVELOG ALL;
DELETE NOPROMPT OBSOLETE;
DELETE NOPROMPT ARCHIVELOG ALL COMPLETED BEFORE 'SYSDATE-1'
BACKED UP 1 TIMES TO DISK;
- バックアップが取れていなければ、先にバックアップを取る。アーカイブログを削除すればポイントインタイムリカバリの連鎖が切れます。まだバックアップしていないログは、削除より退避が先です
BACKUP ARCHIVELOG ALL DELETE INPUT;
- 緊急避難としてFRAサイズを一時的に拡大する。ディスクに空きがあるなら、これが最も速い応急処置です
ALTER SYSTEM SET db_recovery_file_dest_size = 200G SCOPE = BOTH;
- OSで直接削除してしまった場合は、必ずカタログを整合させる。
CROSSCHECKで存在しないファイルをEXPIREDにし、DELETE EXPIREDでカタログから消します。これをしないと後続のRMAN操作がエラーになります
CROSSCHECK ARCHIVELOG ALL;
DELETE NOPROMPT EXPIRED ARCHIVELOG ALL;
予防策としては、FRAの使用率監視(80%で警告)と、ARCHIVELOG DELETION POLICYの明示的な設定です。スタンバイがある構成では、適用済みを条件にしないと削除してしまうので注意します。
-- バックアップ済みを条件に自動削除を許可
CONFIGURE ARCHIVELOG DELETION POLICY TO BACKED UP 1 TIMES TO DISK;
🔒 セッションが止まる:ロックと待機イベントの追いかけ方
行ロック待ち(enq: TX - row lock contention)をSQLで特定する
Oracleで最も頻繁に遭遇するロック待ちがenq: TX - row lock contentionです。同じ行を2つのトランザクションが更新しようとしたとき、後から来たほうが先行トランザクションのコミット/ロールバックを待ちます。タイムアウトはなく、無期限に待ちます(FOR UPDATE NOWAITやWAIT nを使わない限り)⏳
現在の待ちを特定します。
-- 待っている側と待たせている側のペア
SELECT w.sid AS waiting_sid,
w.username AS waiting_user,
w.event, w.seconds_in_wait,
w.sql_id AS waiting_sql_id,
b.sid AS blocking_sid,
b.username AS blocking_user,
b.status AS blocking_status,
b.sql_id AS blocking_last_sql,
b.prev_sql_id AS blocking_prev_sql,
b.machine, b.program, b.last_call_et AS blocker_idle_sec
FROM v$session w
LEFT JOIN v$session b ON b.sid = w.blocking_session
WHERE w.blocking_session IS NOT NULL
ORDER BY w.seconds_in_wait DESC;
ここで見るべき判断ポイントが二つあります。
一つはブロッカーのstatusです。ACTIVEなら、まだ処理中です。長時間実行のSQLやバッチが原因で、終われば解放されます。INACTIVEかつlast_call_etが長い場合は深刻です。トランザクションを開いたままアプリが放置している、つまりコミットもロールバックもせずに待機している状態です。これは設計・実装の問題で、DB側でできることはKILLしかありません。
もう一つは、待機している行がどこかです。v$sessionのrow_wait_obj#などから、対象オブジェクトと行を特定できます。
-- 待機対象のオブジェクトと行
SELECT s.sid, o.owner, o.object_name, o.object_type,
s.row_wait_file#, s.row_wait_block#, s.row_wait_row#,
dbms_rowid.rowid_create(1, s.row_wait_obj#, s.row_wait_file#,
s.row_wait_block#, s.row_wait_row#) AS waited_rowid
FROM v$session s
JOIN dba_objects o ON o.object_id = s.row_wait_obj#
WHERE s.blocking_session IS NOT NULL;
得られたROWIDでその行を実際に見れば、どのデータで競合しているかが分かります。特定のマスタレコード(採番テーブルやカウンタなど)に集中していれば、設計上のホットスポットです。
ブロッカーのトランザクションサイズも確認します。KILLの判断材料になります。
-- トランザクションのUNDO使用量(KILL時のロールバック量の目安)
SELECT t.addr, s.sid, s.username, t.start_time,
t.used_ublk, t.used_urec, t.status
FROM v$transaction t
JOIN v$session s ON s.taddr = t.addr
ORDER BY t.used_ublk DESC;
⚠️ used_ublkが大きいトランザクションをKILLすると、ロールバックに実行時間と同等以上かかることがあります。「KILLすれば直る」と考える前に、この値を見てください。
ブロッキングセッションのツリーをたどって根本の待ち元を探す
実務で困るのは、ブロッキングが連鎖している場合です。AがBを待ち、BがCを待ち、CがDを待つ。このとき止めるべきはDです。しかしユーザーから報告が来るのはAです。
v$sessionのblocking_sessionを再帰的に辿って、根本のブロッカーを見つけます。
-- ブロッキングの連鎖をツリー表示
SELECT LPAD(' ', 2*(LEVEL-1)) || sid AS tree_sid,
LEVEL, username, status, event, seconds_in_wait,
sql_id, program, machine,
CONNECT_BY_ISLEAF AS is_leaf
FROM v$session
WHERE type = 'USER'
START WITH blocking_session IS NULL
AND sid IN (SELECT blocking_session FROM v$session
WHERE blocking_session IS NOT NULL)
CONNECT BY PRIOR sid = blocking_session
ORDER SIBLINGS BY seconds_in_wait DESC;
ツリーの最上位(LEVEL=1)が根本のブロッカーです。ここを解消すれば、連鎖が一気に解けます。
過去の事象を追う場合はASHを使います(Diagnostics Packのライセンスが必要です)。
-- 過去1時間のブロッカー別待機時間ランキング(ASH)
SELECT blocking_session, blocking_session_serial#,
COUNT(*) AS samples,
COUNT(*) AS approx_wait_sec, -- 1秒サンプリング前提
COUNT(DISTINCT session_id) AS victims,
MIN(sample_time), MAX(sample_time)
FROM v$active_session_history
WHERE sample_time > sysdate - 1/24
AND blocking_session IS NOT NULL
GROUP BY blocking_session, blocking_session_serial#
ORDER BY samples DESC;
ライセンスがない環境では、v$sessionを定期的にサンプリングして表に貯めておく自作の仕組みが有効です。障害時に「その瞬間」を捉えられないのが、この種の調査で最も困る点なので、平常時から仕込んでおく価値があります✨
enq: TX以外のロック系イベントも読み分けます。enq: TM - contentionはテーブルロックで、外部キー列に索引がない状態での親表更新・削除が典型的な原因です。enq: TX - allocate ITL entryはブロック内のITLスロット不足で、同一ブロックに多数のトランザクションが集中するときに出ます(INITRANSの設計問題)。enq: HW - contentionはハイウォーターマークの移動競合で、大量INSERTが集中するテーブルやLOBで発生します。イベント名が原因の場所まで教えてくれるので、名前をそのまま公式ドキュメントで引くのが最短です。
デッドロックはトレースファイルのどこを読むか
ORA-00060(deadlock detected while waiting for resource)が出ると、Oracleは片方のセッションのカレントステートメントをロールバックし、トレースファイルを出力します。alert.logに該当トレースファイルのパスが記録されるので、そこを開きます。
トレースファイルで読むべきは三箇所です。
一つ目はDeadlock graphです。どのセッションがどのリソースをどのモードで持ち、何を要求しているかが分かります。
Deadlock graph:
---------Blocker(s)-------- ---------Waiter(s)---------
Resource Name process session holds waits process session holds waits
TX-00090012-00000d4f-00000000-00000000 42 123 X 57 201 X
TX-0007000b-00001a22-00000000-00000000 57 201 X 42 123 X
リソース名の先頭がTXなら行ロックの相互待ち、TMならテーブルロックです。TMが出ている場合、外部キー索引の欠落が原因である可能性が高くなります。TXで、かつモードがS(共有)になっているケースは、ビットマップ索引や一意制約の重複INSERT待ちを疑います。
二つ目は、information for the other waiting sessions と Current SQL statement です。両セッションのSQLが載っています。ここを見れば、どの2つの処理が衝突したかが分かります。
三つ目は rows waited on で、対象オブジェクトと行の情報です。ここからテーブルと行を特定します。
デッドロックの根本原因はほぼ常にアクセス順序の不一致です。処理Aが表X→表Yの順にロックし、処理Bが表Y→表Xの順にロックすれば、タイミング次第で必ず起きます。同一テーブル内でも、更新する行の順序が違えば同じことが起きます💥
対処は設計側です。
- 複数テーブルを更新する処理で、アクセス順序を全処理で統一する
- 一括更新のバッチでは、
ORDER BYで主キー順に処理する - トランザクションを短くして、ロック保持時間を縮める
- 更新対象が競合しやすいなら、
SELECT FOR UPDATE NOWAITで早期に失敗させ、アプリでリトライする
⚠️ ORA-00060が出た場合、Oracleはステートメント単位のロールバックしかしないため、トランザクション全体は中途半端な状態で生き残ります。アプリ側でORA-00060を捕まえて明示的にROLLBACKする実装になっていないと、不整合なデータをコミットする危険があります。ここはアプリレビューで必ず確認する項目です。
ロック待ちを設計で減らす:主キー・外部キー索引・トランザクション粒度
ロック待ちの多くは、運用でなく設計で減らせます。効果の大きい四点です。
まず、外部キー列に索引を作ること。これが最も見落とされます。子表の外部キー列に索引がないと、親表の行をDELETEまたは主キー更新するとき、Oracleは子表全体にテーブルロック(TM)を取ります。子表への更新が一斉に止まります。索引があれば、必要な範囲だけのロックで済みます。
未索引の外部キーを洗い出すSQLです。
-- 外部キー制約の先頭列に索引がない(またはリーディング列が一致しない)ものを検出
SELECT c.owner, c.table_name, c.constraint_name,
LISTAGG(cc.column_name, ',') WITHIN GROUP (ORDER BY cc.position) AS fk_cols
FROM dba_constraints c
JOIN dba_cons_columns cc
ON cc.owner = c.owner AND cc.constraint_name = c.constraint_name
WHERE c.constraint_type = 'R'
AND c.owner = 'APPUSER'
GROUP BY c.owner, c.table_name, c.constraint_name
MINUS
SELECT ic.table_owner, ic.table_name, c.constraint_name,
LISTAGG(cc.column_name, ',') WITHIN GROUP (ORDER BY cc.position)
FROM dba_constraints c
JOIN dba_cons_columns cc
ON cc.owner = c.owner AND cc.constraint_name = c.constraint_name
JOIN dba_ind_columns ic
ON ic.table_owner = c.owner AND ic.table_name = c.table_name
AND ic.column_name = cc.column_name AND ic.column_position = cc.position
WHERE c.constraint_type = 'R' AND c.owner = 'APPUSER'
GROUP BY ic.table_owner, ic.table_name, c.constraint_name;
次に、採番テーブルをやめてシーケンスにすること。「MAX(id)+1」や採番専用テーブルの更新は、全プロセスが1行に集中するため、行ロック待ちの温床です。シーケンスを使い、CACHEを十分に取ります(デフォルトの20では足りない場合があります)。RACならNOORDERにしてノード間の調整を避けます。
三つ目はトランザクション粒度です。大きすぎるトランザクションはロック保持時間が長く、待ちを増やします。小さすぎるトランザクション(1行ごとコミット)はlog file syncを増やし、業務的な整合性も壊します。「業務上一括で取り消すべき単位」をトランザクションにするのが原則で、それが巨大になる場合は、業務設計側で分割を検討します。
四つ目はITLスロットの確保です。同一ブロックに多数のトランザクションが同時アクセスするテーブル(小さくて更新頻度が高いテーブル)では、INITRANSをデフォルトの2から引き上げます。enq: TX - allocate ITL entryが出ているならこれが該当します。ただし既存ブロックには効かないので、再編成が必要です。
もう一点、ユーザー操作をトランザクションに含めないこと。画面入力の途中でロックを保持し続ける実装(悲観ロックの誤用)は、人間の操作時間だけロックが続きます。楽観ロック(バージョン列でのチェック)に切り替えるのが定石です。
🐢 「昨日まで速かったのに遅い」を切り分ける性能調査
実行計画が変わったのかを確かめる(計画の履歴と実測値の突き合わせ)
「同じSQL、同じデータ量、なのに突然遅くなった」の最有力候補は実行計画の変動です。まずこれを確定させます。推測でSQLを書き換え始める前に、事実を押さえます。
-- 同一SQLに対する複数の実行計画の履歴と、1実行あたりの実測値
SELECT plan_hash_value,
SUM(executions_delta) AS execs,
ROUND(SUM(elapsed_time_delta)/GREATEST(SUM(executions_delta),1)/1000) AS ms_per_exec,
ROUND(SUM(buffer_gets_delta)/GREATEST(SUM(executions_delta),1)) AS lio_per_exec,
ROUND(SUM(disk_reads_delta)/GREATEST(SUM(executions_delta),1)) AS pio_per_exec,
MIN(s.begin_interval_time) AS first_seen,
MAX(s.end_interval_time) AS last_seen
FROM dba_hist_sqlstat st
JOIN dba_hist_snapshot s
ON s.snap_id = st.snap_id AND s.instance_number = st.instance_number
WHERE st.sql_id = '&sql_id'
AND s.begin_interval_time > sysdate - 14
GROUP BY plan_hash_value
ORDER BY first_seen;
💡 見るべきはplan_hash_valueが複数あるかどうか、そしてms_per_execやlio_per_execが計画によって桁違いかどうかです。lio_per_exec(1実行あたりの論理読み込み)は、環境のI/O性能に左右されにくいので、計画の良し悪しを比較する指標として信頼できます。時間だけ見ると負荷の影響を受けるので、必ずセットで見ます。
計画が変わっているなら、両方の中身を比べます。
-- 履歴から特定の計画を表示
SELECT * FROM table(
dbms_xplan.display_awr('&sql_id', &plan_hash_value, NULL, 'ALL'));
-- 現在キャッシュにある計画を、実測行数つきで表示(最も情報量が多い)
SELECT * FROM table(
dbms_xplan.display_cursor('&sql_id', NULL, 'ALLSTATS LAST +PEEKED_BINDS'));
ALLSTATS LASTの出力で、E-Rows(見積り行数)とA-Rows(実際の行数)を比較します。これが実行計画調査の核心です。差が1桁以内なら見積りは妥当で、計画は概ね合理的です。2桁以上ずれている箇所があれば、そこがオプティマイザの誤算で、計画が壊れた起点です。誤算の原因は統計情報の劣化、複合条件の相関、バインド変数の値、関数適用による選択率推定の失敗などです🎯
A-TimeとStartsも重要です。Startsが想定外に大きいネステッドループの内側は、駆動表の行数見積りを外している証拠です。
統計情報の鮮度・ヒストグラム・バインドピークが引き起こす計画変動
「何も変えていない」のに計画が変わる理由の大半が、統計情報です。Oracleはデフォルトで夜間の自動メンテナンスウィンドウに統計情報を収集します。つまり毎晩、計画が変わる可能性があります。
まず鮮度を確認します。
-- 統計情報の最終収集日時と、収集後の変更量
SELECT t.owner, t.table_name, t.num_rows, t.last_analyzed, t.stale_stats,
m.inserts, m.updates, m.deletes,
ROUND((NVL(m.inserts,0)+NVL(m.updates,0)+NVL(m.deletes,0))
/ GREATEST(t.num_rows,1) * 100, 1) AS pct_changed
FROM dba_tab_statistics t
LEFT JOIN dba_tab_modifications m
ON m.table_owner = t.owner AND m.table_name = t.table_name
AND NVL(m.partition_name,'-') = NVL(t.partition_name,'-')
WHERE t.owner = 'APPUSER' AND t.object_type = 'TABLE'
ORDER BY t.last_analyzed NULLS FIRST;
stale_stats = 'YES'は、デフォルトで10%以上変更されたことを示します。判断基準としては、last_analyzedが古い(数週間以上)か、pct_changedが大きいテーブルが遅いSQLに関わっていれば、統計情報が原因の候補です。
ただし「統計情報が古いから遅い」とは限りません。新しく取った統計情報で計画が悪化するケースも同じくらいあります。だから闇雲に再収集するのではなく、以下を意識します。
ヒストグラムの有無。列値の分布が偏っているとき(特定の値が大半を占める、ステータス列など)、ヒストグラムがないとオプティマイザは均等分布を仮定し、選択率を誤ります。一方、バインド変数を使うSQLでヒストグラムがあると、バインドピーク(初回実行時のバインド値を覗いて計画を作る)により、たまたま最初に来た値に最適化された計画が共有され、他の値では壊滅的に遅くなります。これが「たまに極端に遅い」の原因です。
-- 列統計とヒストグラムの有無
SELECT column_name, num_distinct, num_nulls, density,
histogram, num_buckets, last_analyzed
FROM dba_tab_col_statistics
WHERE owner = 'APPUSER' AND table_name = 'ORDERS'
ORDER BY column_name;
アダプティブ機能とSQL Plan Management。11g以降のカーディナリティフィードバック、12c以降のアダプティブ計画・アダプティブ統計は、実行中や再実行時に計画を変える機能です。改善するはずの機能ですが、「同じSQLの2回目から計画が変わる」という挙動を生むため、原因調査を混乱させます。バージョンによってデフォルト値が異なる(optimizer_adaptive_featuresは12.1、optimizer_adaptive_plans/optimizer_adaptive_statisticsは12.2以降)ので、自環境の設定を確認してください。
計画を固定したい場合は、SQL Plan Management(SPM)でベースラインを登録します。ヒントの直書きよりも、アプリ改修なしで適用でき、将来より良い計画が見つかったときに検証して切り替えられる点で運用に向いています✨
-- キャッシュ上の良い計画をベースラインとして登録
DECLARE
n PLS_INTEGER;
BEGIN
n := dbms_spm.load_plans_from_cursor_cache(
sql_id => '&sql_id',
plan_hash_value => &good_plan_hash);
dbms_output.put_line('loaded: ' || n);
END;
/
-- 登録状況の確認
SELECT sql_handle, plan_name, enabled, accepted, fixed, created
FROM dba_sql_plan_baselines
ORDER BY created DESC;
💡 統計情報を再収集する前に、現在の統計をエクスポートしておくのが安全です。悪化したときに戻せます。
-- 収集前に統計をバックアップ(戻せるようにする)
BEGIN
dbms_stats.export_table_stats(
ownname => 'APPUSER', tabname => 'ORDERS',
statown => 'APPUSER', stattab => 'STATS_BACKUP',
statid => 'BEFORE_20260927');
END;
/
-- 収集
BEGIN
dbms_stats.gather_table_stats(
ownname => 'APPUSER',
tabname => 'ORDERS',
estimate_percent => dbms_stats.auto_sample_size,
method_opt => 'FOR ALL COLUMNS SIZE AUTO',
cascade => TRUE,
degree => 4);
END;
/
dbms_stats.restore_table_statsで過去の統計に戻すこともできます(保持期間はデフォルト31日)。「統計を取り直したら遅くなった」ときの最初の手段です。
AWR/ASH/Statspackで時間帯とセッションを絞り込む
インスタンス全体が遅いときは、個別SQLを追う前に「その時間帯、DBは何に時間を使っていたか」を見ます。AWRレポートを対象時間帯で出すのが基本です(Diagnostics Packライセンスが必要。ない環境ではStatspackを使います)。
# AWRレポート(対話形式でスナップショット範囲を指定)
sqlplus / as sysdba @?/rdbms/admin/awrrpt.sql
# ASHレポート(短時間のスパイク調査に向く)
sqlplus / as sysdba @?/rdbms/admin/ashrpt.sql
# Statspack(Diagnostics Packがない環境)
sqlplus perfstat/xxx @?/rdbms/admin/spreport.sql
AWRレポートで最初に見るべき箇所は決まっています。
- DB Time と Elapsed Time の比。DB TimeがElapsed Time×CPUコア数に近ければ、DBは飽和しています。DB Timeが小さければ、DBは暇でボトルネックは外部(アプリ、ネットワーク)です。この判断を飛ばして中身を読むと、無関係なチューニングに時間を使います
- Top 10 Foreground Events by Total Wait Time。何を待っていたか。ここで方向が決まります
- Load Profile。前回スナップショットとの比較で、論理読み込み、物理読み込み、実行回数、パース回数の増減を見ます。ハードパース回数が多ければリテラルSQLの問題です
- SQL ordered by Elapsed Time / Gets。時間を食っているSQLの特定。ただし1実行あたりの値と実行回数を分けて見ます。「1回が遅いSQL」と「速いが100万回実行されるSQL」は対処が全く違います
短時間のスパイク(数分間だけ固まった)はAWRの1時間平均では埋もれます。このときはASHを直接クエリします。
-- 時間帯を絞ってイベント別の待機時間分布を見る
SELECT to_char(sample_time,'HH24:MI') AS minute,
NVL(event, 'ON CPU') AS event,
COUNT(*) AS active_sessions
FROM v$active_session_history
WHERE sample_time BETWEEN to_date('2026-09-26 02:00','YYYY-MM-DD HH24:MI')
AND to_date('2026-09-26 02:30','YYYY-MM-DD HH24:MI')
GROUP BY to_char(sample_time,'HH24:MI'), NVL(event,'ON CPU')
ORDER BY minute, active_sessions DESC;
v$active_session_historyはメモリ上のため保持期間が短く(数十分〜数時間)、それより前はdba_hist_active_sess_history(10秒サンプリングに間引かれたもの)を見ます。「昨日の深夜」の調査なら後者です。
I/O待ちかCPU待ちか:待機イベント分類で打ち手を変える
待機イベントのwait_classを見れば、打ち手の方向が決まります。イベント名を個別に覚えるより、まずクラスで分類する習慣のほうが実用的です。
| wait_class | 主なイベント | 意味と打ち手の方向 |
|---|---|---|
| User I/O | db file sequential read, db file scattered read, direct path read | データブロックの物理読み込み。SQLが読む量を減らす(索引、パーティション)か、ストレージ性能を上げる |
| System I/O | log file parallel write, db file parallel write, control file sequential read | LGWR/DBWnなどの書き込み。ストレージの書き込み性能、REDOの配置 |
| Commit | log file sync | コミット完了待ち。コミット頻度が高すぎるか、REDO書き込み先が遅い |
| Concurrency | latch:, library cache, buffer busy waits, cursor: pin S | 内部構造の競合。リテラルSQL、ホットブロック、過度な同時実行 |
| Application | enq: TM, enq: TX - row lock contention | アプリのロック設計。前章参照 |
| Configuration | log file switch, enq: HW, free buffer waits | 設定・領域の問題。REDOログサイズ、表領域、FRA |
| Cluster | gc buffer busy, gc cr block | RACのノード間転送。データの局所性、サービス分割 |
| Idle | SQL*Net message from client など | 待機に見えるがDBは暇。これを集計に含めると誤診する |
⚠️ Idleを除外して集計するのが原則です。「SQL*Net message from clientが最大の待機です」は、DBがクライアントの次の指示を待っているだけで、何の問題も示しません。
CPUかI/Oかの判断は、この対比で行います。
-- CPU上にいた時間 vs 待機していた時間(ASHで比較)
SELECT CASE WHEN session_state = 'ON CPU' THEN 'ON CPU'
ELSE NVL(wait_class,'OTHER') END AS category,
COUNT(*) AS samples,
ROUND(COUNT(*) * 100 / SUM(COUNT(*)) OVER (), 1) AS pct
FROM v$active_session_history
WHERE sample_time > sysdate - 1/24
GROUP BY CASE WHEN session_state = 'ON CPU' THEN 'ON CPU'
ELSE NVL(wait_class,'OTHER') END
ORDER BY samples DESC;
ON CPUが支配的なら、SQLの処理量そのものが多いか、パースが多いか、CPU本数が足りません。この状態で索引を追加してI/Oを減らしても効果は薄く、むしろ処理ロジックやSQL本数の削減が効きます。OS側でvmstatやtopを見て、DB以外のプロセスがCPUを食っていないかも確認します。
User I/Oが支配的なら、どのオブジェクトへのI/Oかを特定します。
-- セグメント別の物理読み込み(AWR、上位のみ)
SELECT o.owner, o.object_name, o.subobject_name, o.object_type,
SUM(st.physical_reads_delta) AS phy_reads
FROM dba_hist_seg_stat st
JOIN dba_hist_seg_stat_obj o
ON o.obj# = st.obj# AND o.dataobj# = st.dataobj#
AND o.dbid = st.dbid
JOIN dba_hist_snapshot s ON s.snap_id = st.snap_id AND s.dbid = st.dbid
WHERE s.begin_interval_time > sysdate - 1
GROUP BY o.owner, o.object_name, o.subobject_name, o.object_type
HAVING SUM(st.physical_reads_delta) > 0
ORDER BY phy_reads DESC
FETCH FIRST 15 ROWS ONLY;
db file scattered readが多ければマルチブロック読み込み=フルスキャンです。意図したフルスキャンか(大量集計なら妥当)、索引が使われていないだけか(不適切なら索引や述語の見直し)を判断します。db file sequential readが多ければシングルブロック読み込みで、索引経由のアクセスです。索引を使っているのに遅いなら、クラスタリング係数が悪く1行ごとにランダムI/Oが発生しているか、そもそも取得行数が多すぎてフルスキャンのほうが速いケースです。
1回のI/O待ちの平均時間も見ます。これが数ミリ秒を大きく超えているなら、SQLの問題ではなくストレージ側の問題です。
SELECT event, total_waits, time_waited_micro/total_waits/1000 AS avg_ms
FROM v$system_event
WHERE event LIKE 'db file%'
AND total_waits > 0
ORDER BY time_waited_micro DESC;
🛡️ バックアップ・リカバリと日常運用で落とさないための備え
RMANバックアップの構成と「戻せるか」の定期確認
バックアップ運用で最も危険な状態は、「バックアップを取っている」ことを確認して満足し、「戻せる」ことを確認していない状態です。この二つは別物です。
まず構成を確認します。
-- RMANの設定一覧
RMAN> SHOW ALL;
確認すべき項目と判断基準です。
CONFIGURE CONTROLFILE AUTOBACKUPがONか。制御ファイルを失ったときのリカバリ難易度が段違いに変わります。ONにしていない理由はほぼありませんRETENTION POLICYはRECOVERY WINDOW OF n DAYSかREDUNDANCY nか。業務要件のRPO/RTOと整合しているかARCHIVELOG DELETION POLICYは、スタンバイ構成なら適用済み条件を含めているか- バックアップ先がDBと同じストレージになっていないか。同じディスク障害で両方失います
バックアップの実績を確認します。
-- 直近のバックアップ実行履歴と結果
SELECT session_key, input_type, status,
to_char(start_time,'MM-DD HH24:MI') AS start_time,
to_char(end_time,'MM-DD HH24:MI') AS end_time,
elapsed_seconds,
ROUND(input_bytes/1024/1024/1024,1) AS input_gb,
ROUND(output_bytes/1024/1024/1024,1) AS output_gb
FROM v$rman_backup_job_details
WHERE start_time > sysdate - 14
ORDER BY start_time DESC;
⚠️ statusがCOMPLETED WITH ERRORSやCOMPLETED WITH WARNINGSになっているものを見逃さないことです。「完了」という文字だけ見て安心すると、一部のデータファイルがバックアップされていないまま日が過ぎます。
そして最重要の「戻せるか」の確認です。実際にリストアせずに検証できる手段があります。
-- バックアップの物理的な健全性を検証(実際には復元しない)
RMAN> VALIDATE BACKUPSET 1234;
RMAN> RESTORE DATABASE VALIDATE;
RMAN> RESTORE ARCHIVELOG ALL VALIDATE;
-- データベース自体の物理・論理破損チェック
RMAN> VALIDATE CHECK LOGICAL DATABASE;
-- 破損ブロックの記録を確認(0件であること)
SELECT * FROM v$database_block_corruption;
RESTORE ... VALIDATEは、バックアップピースを読んで復元可能かを検証します。これを定期ジョブに組み込むことで、「テープが読めない」「バックアップピースが欠けている」を本番障害の前に検知できます✨
ただし、VALIDATEが通っても、実際のリストア手順が回るかは別問題です。最終的には、別サーバーへの復元テストを定期的に行うのが唯一の確実な確認です。手順書のコマンドが現環境で動くか、所要時間がRTOに収まるか、これは実際にやらないと分かりません。
ポイントインタイムリカバリの流れと必要なログの範囲
「誤ってテーブルを削除した」「間違ったUPDATEをコミットした」というとき、どの時点まで戻すかを決めてリカバリします。必要なものは三つです。
- 目標時点より前に取得したデータファイルのバックアップ
- そのバックアップ時点から目標時点までの全アーカイブログ(1本でも欠けるとそこで止まります)
- 制御ファイル(目標時点の構成を反映したもの、または自動バックアップから復元)
まず、どこまで戻せるかを確認します。
-- 利用可能なアーカイブログの範囲
SELECT thread#, MIN(sequence#) AS min_seq, MAX(sequence#) AS max_seq,
MIN(first_time) AS oldest, MAX(next_time) AS newest
FROM v$archived_log
WHERE deleted = 'NO'
GROUP BY thread#;
-- 連番の欠けを確認(欠けていればそこから先へ進めない)
SELECT sequence#, first_time, next_time, deleted, status
FROM v$archived_log
WHERE thread# = 1 AND sequence# BETWEEN &from_seq AND &to_seq
ORDER BY sequence#;
DB全体のポイントインタイムリカバリの流れです。
RMAN> SHUTDOWN IMMEDIATE;
RMAN> STARTUP MOUNT;
RMAN> RUN {
SET UNTIL TIME "TO_DATE('2026-09-26 14:30:00','YYYY-MM-DD HH24:MI:SS')";
RESTORE DATABASE;
RECOVER DATABASE;
}
RMAN> ALTER DATABASE OPEN RESETLOGS;
RESETLOGSを実行すると、そこで新しいインカネーションが始まり、それ以降のアーカイブログは従来の連番とは別系列になります。RESETLOGS後は必ず全体バックアップを取り直すのが原則です。これを忘れると、次の障害時に復旧経路がありません。
時刻ではなく、特定の操作の直前まで戻したい場合はSCNやログシーケンスを指定します。誤ったDDLのSCNはv$log_historyや監査ログ、フラッシュバックの機能で特定します。
-- 現在のSCNと時刻の対応(過去の時刻からSCNを引く)
SELECT timestamp_to_scn(
to_date('2026-09-26 14:30:00','YYYY-MM-DD HH24:MI:SS')) AS scn
FROM dual;
💡 DB全体を戻す前に、軽い手段を検討してください。全体のPITRは影響範囲が大きく、他の正常な更新も巻き戻します。
- フラッシバッククエリ。UNDOが残っている範囲なら、
SELECT ... AS OF TIMESTAMPで過去のデータを読み、必要な行だけ戻せます。DBを止めません - フラッシュバックテーブル。
FLASHBACK TABLE ... TO TIMESTAMPで特定テーブルだけ巻き戻せます(行移動の有効化が前提) - フラッシュバックドロップ。DROPしたテーブルは、リサイクルビンにあれば
FLASHBACK TABLE ... TO BEFORE DROPで復活します - 表領域単位のPITR(TSPITR)。影響を表領域に限定できます
- RMANによる表単位のリカバリ。12c以降、
RECOVER TABLEで特定表だけを補助インスタンス経由で復元できます
-- 影響範囲を限定した復旧の例
SELECT * FROM orders AS OF TIMESTAMP (SYSTIMESTAMP - INTERVAL '30' MINUTE)
WHERE order_id = 12345;
FLASHBACK TABLE orders TO TIMESTAMP (SYSTIMESTAMP - INTERVAL '30' MINUTE);
フラッシュバッククエリが使えるかはUNDOの保持期間に依存します。だからこそ、前章のUNDO設計が「トラブル時の選択肢の数」を決めます。フラッシュバックデータベース(FLASHBACK DATABASE)を使いたいなら、事前にフラッシュバックログの有効化が必要です。障害が起きてから有効化はできません😇
alert.logとトレースファイルから拾うべき兆候
alert.logは、障害後に読むものではなく、平常時から監視するものです。ここに出る兆候のうち、放置すると障害になるものを挙げます。
ORA-01555、ORA-01652、ORA-01653は領域とUNDOの警告です。件数が増えていれば、近い将来の障害予告です。
Checkpoint not complete は、REDOログを一周する速度がチェックポイントより速い状態です。ログスイッチ時に待ちが発生します。REDOログのサイズが小さすぎるか、グループ数が足りません。判断基準としては、ログスイッチが15〜20分に1回程度になるサイズが一般的な目安です。頻発しているならREDOログの見直しです。
-- 時間帯別のログスイッチ回数(頻度の確認)
SELECT to_char(first_time,'MM-DD HH24') AS hour, COUNT(*) AS switches
FROM v$log_history
WHERE first_time > sysdate - 3
GROUP BY to_char(first_time,'MM-DD HH24')
ORDER BY hour;
ORA-00600 / ORA-07445 は内部エラーです。自力での原因特定はほぼ不可能で、トレースファイルを添えてサポートに問い合わせる対象です。ただし、発生条件(直前のSQL、操作)を記録しておくことが問い合わせの成否を分けます。
ORA-04031 は共有プール不足。前述のとおり、リテラルSQLの乱発を疑います。
ORA-00257 はアーカイバエラーで、即対応が必要です。
ORA-01578(データブロック破損)が出たら、v$database_block_corruptionを確認し、RMANのBLOCKRECOVERでブロック単位の復旧を試みます。ストレージやメモリのハードウェア障害の兆候である可能性も考えます。
ADRCIを使うと、トレースファイルを手で探すより早く整理できます。
adrci
adrci> show homes
adrci> set homepath diag/rdbms/orcl/orcl
adrci> show alert -tail 100 # alert.logの末尾
adrci> show alert -p "message_text like '%ORA-%'" # ORAエラーだけ抽出
adrci> show incident # インシデント一覧
adrci> show problem # 問題(ORA-600など)
adrci> ips pack incident 12345 in /tmp # サポート提出用パッケージ作成
ips packは、サポートへの問い合わせ時に必要なファイルをまとめてzipにしてくれます。個別にファイルを探して添付するより確実です。
監視の実装としては、alert.logのORA-エラー行を拾って通知する仕組みが最低線です。ただし、ORA-00060(デッドロック)やORA-01555のように「発生しても運用が回ってしまう」エラーは通知しても放置されがちなので、週次でカウントを集計して傾向を見る運用のほうが実効性があります。
パッチ適用・パラメータ変更時に取っておく退避情報
変更作業で「戻せない」状況を作らないために、事前に取っておくものが決まっています。
パラメータ変更時に必要なのは、変更前の値と、spfileのバックアップです。
-- 現在の全パラメータ(デフォルト以外を明示的に記録)
CREATE PFILE = '/backup/pfile_before_20260927.ora' FROM SPFILE;
-- 非デフォルト値のパラメータ一覧
SELECT name, value, isdefault, ismodified
FROM v$parameter
WHERE isdefault = 'FALSE'
ORDER BY name;
-- 隠しパラメータも含めて確認したい場合(サポート指示がある場合に限る)
SELECT a.ksppinm AS name, b.ksppstvl AS value, b.ksppstdf AS isdefault
FROM x$ksppi a, x$ksppcv b
WHERE a.indx = b.indx AND a.ksppinm LIKE '\_%' ESCAPE '\'
AND b.ksppstdf = 'FALSE';
💡 SCOPEの使い分けも判断ポイントです。MEMORYは即時のみ(再起動で消える)、SPFILEは次回起動から、BOTHは両方。検証段階ではMEMORYで試し、確定したらBOTHにするのが安全です。静的パラメータはSPFILEしか指定できません。
パッチ適用時は、OPatchの現状を記録し、バイナリと設定を退避します。
# 適用済みパッチの一覧(適用前後で比較する)
$ORACLE_HOME/OPatch/opatch lsinventory > /backup/lsinv_before_20260927.txt
# 競合チェック(適用前に必ず実行)
$ORACLE_HOME/OPatch/opatch prereq CheckConflictAgainstOHWithDetail \
-phBaseDir /stage/patch/36000000
# OPatch自体のバージョン確認(古いと適用に失敗する)
$ORACLE_HOME/OPatch/opatch version
RUの適用では、バイナリ適用(opatch apply)とDB側の更新(datapatch -verbose)の二段階があります。datapatchを忘れると、バイナリとディクショナリの状態が食い違い、後で原因不明の不具合として現れます。
-- datapatchの適用状況を確認
SELECT patch_id, patch_type, action, status,
to_char(action_time,'YYYY-MM-DD HH24:MI') AS applied_at, description
FROM dba_registry_sqlpatch
ORDER BY action_time DESC;
共通して取っておくものは次のとおりです。
- 統計情報(
dbms_stats.export_*_stats)。変更後に計画が変わったときの比較・復元用 - SQL Plan Baselineの状態、または主要SQLの現在の
plan_hash_valueと実行計画。「変更後に遅くなった」を客観的に判定するため - 現在の性能ベースライン。AWRレポートを平常時に取得しておく。比較対象がないと「遅くなった」を証明できません
- 無効オブジェクトの一覧。パッチ後に増えた分を切り分けるため
-- 変更前の無効オブジェクトを記録(後で増分だけ追える)
SELECT owner, object_name, object_type, status
FROM dba_objects WHERE status = 'INVALID' ORDER BY owner, object_name;
💡 そして最も重要なのが、切り戻し手順を先に書くことです。「適用してから考える」と、夜間作業で判断を誤ります。どの時点でどう戻すか、それに何分かかるか、業務再開の判断は誰がするか。これを決めてから作業に入ります。
🔄 PostgreSQLなど他エンジンへの移行を見据えるとき
Oracleの前提がそのまま通じない領域(NULL・日付型・識別子・トランザクション)
コスト見直しやクラウド移行で、OracleからPostgreSQLへの移行を検討する場面が増えています。SQL標準に沿っている部分は素直に動きますが、Oracle独自の挙動に依存した実装が静かに壊れる領域があります。エラーで落ちれば気づけますが、エラーにならずに結果が変わるものが最も危険です😱
空文字とNULL。Oracleは''をNULLとして扱います。PostgreSQLは空文字とNULLを別物として区別します。WHERE col IS NULLで拾えていた行が拾えなくなる、NOT NULL制約に空文字が通ってしまう、といった形で表面化します。件数が微妙に合わない不具合の典型です。
日付型。OracleのDATEは時刻(秒まで)を含みます。PostgreSQLのdateは日付のみで、時刻を持たせるならtimestampです。「DATE型だから同じ」と思って移すと、時刻が切り捨てられます。日付比較のロジックも変わります。
識別子の大文字小文字。Oracleは引用符なしの識別子を大文字に正規化し、PostgreSQLは小文字に正規化します。移行ツールが引用符付きで大文字のまま作ると、以降すべてのSQLで引用符が必要になります。
トランザクションの扱い。OracleはDDLで暗黙コミットしますが、PostgreSQLはDDLもトランザクション内でロールバックできます。逆に、PostgreSQLではエラーが起きるとトランザクション全体がアボート状態になり、以降の文がすべて失敗します(Oracleは文単位でエラーになっても、トランザクションは継続できます)。エラーを無視して処理を続ける実装は、この違いで動かなくなります。
MVCCの実装差。両者ともMVCCですが、Oracleは旧バージョンをUNDOに置き、PostgreSQLはテーブル本体に置いて後からVACUUMで回収します。結果、PostgreSQLには「テーブル膨張」「トランザクションIDの周回」というOracleにない運用課題が生まれます。移行後の性能劣化の多くはここに関係します。
移行後に「データが合わない」「遅い」が起きる典型パターン
移行直後のトラブルは、大きく三系統に分かれます。
データが合わないケース。件数や集計値が一致しない。空文字/NULLの扱い、日付の切り捨て、文字コード変換、数値型の精度、ソート順(照合順序)の違いが原因です。移行検証では、件数だけでなく主要カラムのチェックサムや集計値を突き合わせる必要があります。
エラーで落ちるケース。関数名や構文の非互換(NVL、DECODE、ROWNUM、CONNECT BY、階層問い合わせ、(+)外部結合、シーケンスのNEXTVAL構文)、暗黙型変換の差、識別子の解決失敗。これは比較的発見しやすい部類です。
遅いケース。移行後の性能劣化は、原因が複合的です。統計情報が未収集、実行計画の傾向の違い(PostgreSQLのプランナはOracleと異なる選択をします)、ヒントが効かない(PostgreSQLは標準でオプティマイザヒントを持ちません)、索引設計の前提差、そしてVACUUM/autovacuumの設定不足によるテーブル膨張。「Oracleでは索引が使われていたのに使われない」という調査には、EXPLAIN (ANALYZE, BUFFERS)で見積りと実測を比べる、OracleのALLSTATS LASTと同じ発想が使えます。
症状別の切り分けは移行ポイント解説記事へ
移行時に発生する個々の症状について、原因の特定方法と具体的な書き換え方をまとめた記事があります。空文字とNULLの扱い、DATE型の変換、識別子の大文字小文字、実行計画とVACUUMまで、症状から入って対処にたどり着ける構成にしています。移行の検討段階でも、移行後のトラブル対応中でも使えるので、該当する症状があればOracle から PostgreSQL 移行でハマるポイント|症状別の切り分けと対処を参照してください。
Oracleで培った「待機イベントから原因を辿る」「実行計画の見積りと実測を比べる」という考え方自体は、他のエンジンでもそのまま通用します。ビューの名前とツールが変わるだけです。エンジン固有の知識より、この思考の型のほうが長く使えます。
❓ よくある質問
ORA-01555はUNDO表領域を大きくすれば直る?
記事の説明では、ORA-01555は領域不足というより時間の問題で、長時間クエリと UNDO の高速な回転が重なったときに起きます。UNDO表領域を大きくすれば緩和はできますが、それだけでは根治しません。対処は実行計画を改善してクエリ自体を短くする、更新とレポートの時間帯をずらす、UNDO表領域と UNDO_RETENTION を見直す、という優先順位で検討します。カーソルでSELECTしながらループ内でコミットするフェッチ間コミットも、自分でUNDOを壊す古典的な原因として挙げています。
ORA-12154・ORA-12541・ORA-12514はどう読み分ける?
番号ごとに「どこまで到達したか」が違うため、番号を見るだけで調査範囲を絞れます。ORA-12154はクライアント側の名前解決の失敗で、まだネットワークに出ていない状態なので tnsnames.ora や TNS_ADMIN を確認します。ORA-12541はホスト・ポートには到達したがリスナーが応答しない状態、ORA-12514はリスナーには届いたが要求されたサービス名を知らない状態で、DBインスタンス未起動やPDBがOPENしていないなどサーバー側のDBを見ることになります。
ORA-00020が出たら processes を増やせばいい?
記事では、上限値そのものより「セッションが減らない理由」が本題だとしています。INACTIVEが大量にあり last_call_et が長いなら、コネクションプール設定か接続を閉じていないコードが原因で、processes を増やしてもリークが埋めるだけの時間稼ぎになります。逆にACTIVEが同じ待機イベントで詰まっているなら、その待機の解消が本題です。恒久的に上げる場合は OS のプロセス数上限やPGAのメモリ設計も合わせて見直し、processes は静的パラメータなので再起動が必要です。
CDB環境で作ったはずのテーブルが見つからないのはなぜ?
SYSでローカル接続するとデフォルトで CDB$ROOT に入るため、そこで探してもPDB内のオブジェクトは見えません。記事では、sys_context(‘USERENV’,‘CON_NAME’) で今どこにいるかを確認し、ALTER SESSION SET CONTAINER でPDBに切り替える手順を示しています。またCDBを起動してもPDBが自動でOPENしないことがあり、その場合は OPEN したうえで SAVE STATE しておきます。
📚 これからOracle運用を学ぶ人のための進め方
手を動かせる検証環境の作り方と壊して覚える練習メニュー
Oracle運用スキルは、本を読むだけでは身につきません。壊して直す経験が必要で、それは本番ではできません。だから検証環境を持つことが出発点です。
構築の選択肢としては、Oracle Database Free(旧XE)をLinux上に入れる、公式のコンテナイメージを使う、クラウドの無料枠を使うといった方法があります。ライセンス条件や利用可能な機能(Diagnostics Packが使えるかなど)はエディションによって違うので、公式ドキュメントとライセンス条項を確認してください。学習目的でもライセンスの範囲は確認する習慣をつけたほうがよいです。
練習メニューとして、この順で「壊して直す」のが実務に直結します。
- 起動失敗を作る。pfileのパラメータをわざと不正にする、制御ファイルをリネームする、データファイルをリネームする。それぞれ
startupがどの段階で止まり、alert.logに何が出るかを確認する - 表領域を満杯にする。小さい表領域を作り、AUTOEXTENDを切って、大量INSERTでORA-01653を出す。
dba_free_spaceとdba_data_filesの値がどう変化するかを見る - ロック待ちとデッドロックを作る。2セッションで同じ行をUPDATEして
enq: TXを観察する。次に、互い違いの順序で2テーブルを更新してORA-00060を発生させ、トレースファイルを実際に読む - ORA-01555を出す。UNDO表領域を小さく固定し、
UNDO_RETENTIONを短くして、長時間のSELECTを回しながら別セッションで大量更新する - バックアップとリストアを一周する。RMANで全体バックアップを取り、データファイルを削除して復旧する。次に、テーブルを誤って削除してからポイントインタイムリカバリで戻す。RESETLOGS後の再バックアップまでやる
- 実行計画を壊す。統計情報を意図的に古くする/削除する、大量データを投入して再収集しないまま実行する。
ALLSTATS LASTでE-RowsとA-Rowsのずれを目で見る
特に5番は、手順書を見ながらでも最後まで通せるかどうかで、本番障害時の落ち着きが変わります。所要時間も体感できます💪
公式ドキュメントとエラーメッセージの調べ方を習慣にする
💡 Oracleのトラブル対応で最も費用対効果が高い習慣は、エラー番号をそのまま公式のエラーメッセージリファレンスで引くことです。ORA-エラーには番号ごとに「原因」と「処置」が記載されており、Web検索で出てくる断片的な記事よりも正確です。
サーバー上ではoerrコマンドで即座に引けます。
# ORA-01653 の原因と処置を表示
oerr ora 1653
# TNS-12514
oerr tns 12514
待機イベントも同じです。イベント名を公式のリファレンスで引けば、そのイベントが何を待っているか、どのパラメータ(P1/P2/P3)が何を意味するかが書かれています。イベント名を暗記するより、引き方を覚えるほうが実用的です。
もう一つの習慣は、バージョンを意識することです。Oracleは12c、19c、21c、23aiと機能とデフォルト値が変わります。「ネットで見た手順が動かない」の原因が、バージョン差であることは頻繁にあります。調べるときは、自環境のバージョンを確認してから、そのバージョンのドキュメントを見ます。
SELECT banner_full FROM v$version;
SELECT * FROM v$instance; -- version, version_full
サポート契約があるなら、My Oracle Support(MOS)のナレッジベースが最も情報量があります。ORA-00600のような内部エラーは、MOSで該当する既知バグを探すのが正攻法です。
そして、調査の過程を記録に残すこと。「このエラーはこう調べてこう対処した」を自分用に蓄積すると、二度目が圧倒的に速くなります。同じ障害は繰り返し起きます。
資格学習(ORACLE MASTER)と実務スキルの補い合い方
ORACLE MASTERの学習は、実務と補完関係にあります。どちらか一方では埋まらない穴があります。
資格学習が埋めてくれるのは、アーキテクチャの体系的な理解、自分が触ったことのない領域(RAC、Data Guard、マルチテナント、セキュリティ機能)の存在を知ること、用語の正確な定義です。実務では、自分の担当範囲だけが深くなり、隣の領域が空白になりがちです。試験範囲は一通り網羅されているので、空白を見つけるのに向いています。「ここは知らない」と自覚できるだけでも価値があります。
実務でしか身につかないのは、障害時の判断(どの情報から集めるか、どこで切り戻すか、影響範囲をどう見積もるか)、性能問題の切り分けの勘所、alert.logの読み慣れ、業務要件とのトレードオフ判断です。試験問題には「上司にどう報告するか」も「深夜3時の判断」も出てきません。
進め方としては、資格学習で範囲を把握し、検証環境で手を動かして確認するのが効率的です。テキストで「REDOログのグループとメンバー」を読んだら、実際にグループを追加し、メンバーを削除して何が起きるか試す。これで知識が判断力に変わります。逆に、実務で遭遇した事象を資格のテキストで裏取りするのも有効です。「なぜそうなったか」が体系の中で位置づけられます。
試験の構成、出題範囲、対象バージョン、認定要件は改定されます。受験を検討する際は、必ず公式サイトで最新の情報を確認してください。過去の情報をもとに準備すると、範囲外を勉強することになります。
最後に、Oracle運用で長く通用するのは、個別のコマンドや設定値の暗記ではありません。「症状から仮説を立て、ビューやログで事実を確認し、原因を絞る」という手順です。このページで挙げたSQLは、そのための道具です。バージョンが変わってもビューの名前が変わっても、この手順は変わりません。目の前の障害を、次に活きる形で片付けていくのが、結局いちばん速い上達の道になります。
このテーマの記事一覧(1本)
- データベースOracle
から PostgreSQL 移行 で ハマる ポイント | 症状 別 の 切り分け と 対処 OracleからPostgreSQLへ移行した直後に起きる「データが合わない」「エラーで落ちる」「遅い」を症状別に切り分ける手順をまとめました。空文字とNULL、DATE型、識別子の大文字小文字、実行計画とVACUUMまで、原因と書き換え方を具体的に解説します。