ジョブを安定運用するうえで最も重要なのは「いつ・何が・どれくらい動いて・成功か失敗か・遅延や長時間化はないか」を一元的に把握できることだ。DBMS_SCHEDULERはデフォルトでも実行履歴ビューを提供するが、アプリ都合の識別子やSLA、入力パラメータ、ビジネス件数などを含めた“運用に効く粒度”での集中ロギングを別途設計しておくと、異常検知・原因追跡・リカバリが格段に速くなる。この記事では、ジョブ実行のラッピングと自治ロギング、ヘルスチェック用ハートビート、遅延・超過・未実行の検知クエリ、SLA監視ビュー化、再実行とサマリの自動化までを、一貫したコードで示す。
- 集中ロギングの土台を作る(メタ・実行・ハートビート)
- ロギングを一元化するパッケージ(自治トランザクションで確実に確定)
- ビジネス処理をラップする実行テンプレート(例外を握らず記録して再送可能に)
- DBMS_SCHEDULERのジョブ定義と引数受け渡し(スケジュールとアドホック両方)
- 遅延・長時間化・未実行の異常検知(SLAを満たさないケースをSQLで抽出)
- ハングや停滞の検知(ハートビートが止まったらアラート)
- デフォルト履歴との突き合わせ(USER_SCHEDULER_*ビューと自前ログの整合)
- 月次サマリと再実行支援(集計ビューと再送API)
- 保持期間とアーカイブ(テーブル肥大化を避けて軽量運用)
- まとめ
集中ロギングの土台を作る(メタ・実行・ハートビート)
ジョブ定義と運用メトリクスを分けて記録する。メタは期待スケジュールやSLA、実行は開始・終了・結果コードや件数、ハートビートは長時間ジョブの生存信号を残す。まずは最小のスキーマから始める。
CREATE TABLE job_meta (
job_name VARCHAR2(128) PRIMARY KEY,
cron_expr VARCHAR2(100), -- 期待スケジュール(参考用)
sla_seconds NUMBER, -- 期待完了時間
enabled_flag CHAR(1) DEFAULT 'Y', -- 運用ON/OFF
notify_email VARCHAR2(320) -- 通知先(任意)
);
CREATE TABLE job_run (
run_id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
job_name VARCHAR2(128) NOT NULL,
corr_id VARCHAR2(64), -- 相関ID(APIやバッチ全体で共有)
started_at TIMESTAMP DEFAULT SYSTIMESTAMP,
finished_at TIMESTAMP,
status VARCHAR2(30), -- SUCCESS/ERROR/TIMEOUT/CANCELLED
error_code NUMBER,
error_message VARCHAR2(4000),
input_args CLOB, -- 文字列 or JSON
output_stats CLOB -- 処理件数などをJSONで
);
CREATE TABLE job_heartbeat (
job_name VARCHAR2(128) NOT NULL,
run_id NUMBER NOT NULL,
beat_at TIMESTAMP DEFAULT SYSTIMESTAMP,
note VARCHAR2(200),
CONSTRAINT pk_job_heartbeat PRIMARY KEY(job_name, run_id, beat_at)
);
ロギングを一元化するパッケージ(自治トランザクションで確実に確定)
実ジョブの成功・失敗にかかわらず履歴が残るよう、記録系はAUTONOMOUS_TRANSACTIONで独立させる。開始・進捗・終了の三つを用意しておけば十分だ。
CREATE OR REPLACE PACKAGE job_log AS
FUNCTION start_run(p_job VARCHAR2, p_corr VARCHAR2 := NULL, p_args CLOB := NULL) RETURN NUMBER;
PROCEDURE beat(p_run_id NUMBER, p_job VARCHAR2, p_note VARCHAR2 := NULL);
PROCEDURE finish_ok(p_run_id NUMBER, p_stats CLOB := NULL);
PROCEDURE finish_err(p_run_id NUMBER, p_code NUMBER, p_msg VARCHAR2);
END job_log;
/
CREATE OR REPLACE PACKAGE BODY job_log AS
FUNCTION start_run(p_job VARCHAR2, p_corr VARCHAR2, p_args CLOB) RETURN NUMBER IS
PRAGMA AUTONOMOUS_TRANSACTION;
v_id NUMBER;
BEGIN
INSERT INTO job_run(job_name, corr_id, input_args) VALUES(p_job, p_corr, p_args)
RETURNING run_id INTO v_id;
COMMIT;
RETURN v_id;
END;
PROCEDURE beat(p_run_id NUMBER, p_job VARCHAR2, p_note VARCHAR2) IS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
INSERT INTO job_heartbeat(job_name, run_id, note) VALUES(p_job, p_run_id, p_note);
COMMIT;
END;
PROCEDURE finish_ok(p_run_id NUMBER, p_stats CLOB) IS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
UPDATE job_run
SET finished_at = SYSTIMESTAMP, status = 'SUCCESS', output_stats = p_stats
WHERE run_id = p_run_id;
COMMIT;
END;
PROCEDURE finish_err(p_run_id NUMBER, p_code NUMBER, p_msg VARCHAR2) IS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
UPDATE job_run
SET finished_at = SYSTIMESTAMP, status = 'ERROR', error_code = p_code,
error_message = SUBSTR(p_msg,1,4000)
WHERE run_id = p_run_id;
COMMIT;
END;
END job_log;
/
ビジネス処理をラップする実行テンプレート(例外を握らず記録して再送可能に)
運用に乗せるすべてのジョブは共通ラッパを通す。相関IDを発行し、開始→定期ハートビート→終了の流れを統一する。失敗時は例外をロギングして再送判断は上位へ返す。
CREATE OR REPLACE PROCEDURE run_monthly_agg(p_ym IN DATE) IS
v_run NUMBER;
v_corr VARCHAR2(64) := RAWTOHEX(SYS_GUID());
v_done NUMBER := 0;
BEGIN
v_run := job_log.start_run('RUN_MONTHLY_AGG',
p_corr => v_corr,
p_args => JSON_OBJECT('ym' VALUE TO_CHAR(TRUNC(p_ym,'MM'),'YYYY-MM') RETURNING CLOB));
DBMS_APPLICATION_INFO.SET_MODULE('JOB','RUN_MONTHLY_AGG');
DBMS_SESSION.SET_IDENTIFIER(v_corr);
-- 集計の例(チャンクごとにハートビート)
FOR r IN (SELECT /*+ PARALLEL(4) */ cust_id FROM mv_sales_cust_m WHERE ym = TRUNC(p_ym,'MM')) LOOP
-- ここで集計や書き戻しを行う(省略)
v_done := v_done + 1;
IF MOD(v_done, 10000)=0 THEN job_log.beat(v_run, 'RUN_MONTHLY_AGG', 'rows='||v_done); END IF;
END LOOP;
job_log.finish_ok(v_run, JSON_OBJECT('rows' VALUE v_done RETURNING CLOB));
EXCEPTION
WHEN OTHERS THEN
job_log.finish_err(v_run, SQLCODE, SQLERRM);
RAISE;
END;
/
DBMS_SCHEDULERのジョブ定義と引数受け渡し(スケジュールとアドホック両方)
定期実行はスケジュールへ、臨時実行はジョブ引数で受ける。プロシージャ引数を使うパターンに統一すると履歴の比較が容易になる。
BEGIN
DBMS_SCHEDULER.CREATE_JOB(
job_name => 'JOB_MONTHLY_AGG',
job_type => 'STORED_PROCEDURE',
job_action => 'RUN_MONTHLY_AGG',
start_date => SYSTIMESTAMP,
repeat_interval => 'FREQ=DAILY;BYHOUR=2;BYMINUTE=0;BYSECOND=0',
enabled => TRUE);
END;
/
-- アドホック実行例(特定月のみ)
BEGIN
RUN_MONTHLY_AGG(p_ym => DATE '2025-10-01');
END;
/
遅延・長時間化・未実行の異常検知(SLAを満たさないケースをSQLで抽出)
メタのSLAとrun履歴を結び、閾値超過や次回予定時刻からの遅延を検知する。最小限のビューを用意するとダッシュボード作成が容易になる。
CREATE OR REPLACE VIEW v_job_health AS
SELECT m.job_name,
m.sla_seconds,
(SELECT MAX(started_at) FROM job_run r WHERE r.job_name = m.job_name) AS last_start,
(SELECT MAX(finished_at) FROM job_run r WHERE r.job_name = m.job_name) AS last_finish,
(SELECT MAX(status) KEEP (DENSE_RANK LAST ORDER BY started_at)
FROM job_run r WHERE r.job_name = m.job_name) AS last_status,
CASE
WHEN (SELECT MAX(finished_at) FROM job_run r WHERE r.job_name=m.job_name) IS NULL
THEN NULL
ELSE EXTRACT(SECOND FROM NUMTODSINTERVAL(
(SELECT (MAX(finished_at)-MAX(started_at))*86400 FROM job_run r WHERE r.job_name=m.job_name),'SECOND'))
END AS last_duration_sec
FROM job_meta m;
-- 遅延(最後の開始が24h超空いている等、基準は用途に応じて調整)
SELECT job_name, last_start FROM v_job_health
WHERE last_start IS NULL OR SYSTIMESTAMP - last_start > INTERVAL '1' DAY;
-- 長時間化(SLA超過の最新実行を抽出)
SELECT job_name, last_duration_sec, sla_seconds
FROM v_job_health
WHERE last_status = 'SUCCESS' AND last_duration_sec > sla_seconds;
-- 未実行(有効ジョブだが履歴ゼロ)
SELECT m.job_name
FROM job_meta m
WHERE m.enabled_flag='Y'
AND NOT EXISTS (SELECT 1 FROM job_run r WHERE r.job_name=m.job_name);
ハングや停滞の検知(ハートビートが止まったらアラート)
実行中のrun_idで最新ビート時刻を見て、閾値を超えたら停止疑いとして扱う。ジョブが長尺でも小刻みにbeatを打てば誤検知を避けられる。
-- 実行中runの最新ビートを追跡
WITH running AS (
SELECT r.run_id, r.job_name, r.started_at
FROM job_run r
WHERE r.finished_at IS NULL
),
last_beat AS (
SELECT job_name, run_id, MAX(beat_at) AS last_beat_at
FROM job_heartbeat GROUP BY job_name, run_id
)
SELECT x.job_name, x.run_id, NVL(b.last_beat_at, x.started_at) AS last_signal
FROM running x LEFT JOIN last_beat b
ON b.job_name = x.job_name AND b.run_id = x.run_id
WHERE SYSTIMESTAMP - NVL(b.last_beat_at, x.started_at) > INTERVAL '5' MINUTE;
デフォルト履歴との突き合わせ(USER_SCHEDULER_*ビューと自前ログの整合)
DB標準履歴と自前ログの差を定期的に比較し、欠落や二重記録を検知する。ジョブ異常時の一次情報は標準ビューに残るため、相互参照を用意すると原因特定が速い。
-- 最新失敗の要約
SELECT job_name, status, additional_info, log_date
FROM user_scheduler_job_run_details
WHERE status <> 'SUCCEEDED'
ORDER BY log_date DESC FETCH FIRST 20 ROWS ONLY;
-- 自前ログとの突き合わせ(近時刻の相関)
SELECT d.job_name, d.status, r.status AS custom_status, d.log_date, r.started_at
FROM user_scheduler_job_run_details d
JOIN job_run r
ON r.job_name = d.job_name
AND ABS((CAST(d.log_date AS DATE) - CAST(r.started_at AS DATE))*86400) < 60;
月次サマリと再実行支援(集計ビューと再送API)
運用レビュー用にジョブ別・日別の成功率と平均時間をまとめ、失敗runの再実行入口を用意する。再実行は冪等性前提で入力引数をそのまま渡す。
CREATE OR REPLACE VIEW v_job_daily AS
SELECT job_name,
CAST(TRUNC(started_at) AS DATE) AS d,
COUNT(*) AS runs,
SUM(CASE WHEN status='SUCCESS' THEN 1 ELSE 0 END) AS ok,
ROUND(AVG((finished_at - started_at)*86400)) AS avg_sec
FROM job_run
GROUP BY job_name, TRUNC(started_at);
-- 失敗runの再実行(引数を再利用)
CREATE OR REPLACE PROCEDURE retry_failed_run(p_run_id NUMBER) IS
v_job job_run.job_name%TYPE;
v_args CLOB;
v_ym DATE;
BEGIN
SELECT job_name, input_args INTO v_job, v_args FROM job_run WHERE run_id = p_run_id;
IF v_job = 'RUN_MONTHLY_AGG' THEN
SELECT TO_DATE(JSON_VALUE(v_args,'$.ym'), 'YYYY-MM') INTO v_ym FROM dual;
RUN_MONTHLY_AGG(v_ym);
ELSE
RAISE_APPLICATION_ERROR(-20001, 'unsupported job for retry');
END IF;
END;
/
保持期間とアーカイブ(テーブル肥大化を避けて軽量運用)
履歴はSLAに沿って保持し、古い行は週次アーカイブへ退避する。サイズと参照頻度のバランスで日次・月次に分けてもよい。最小の運用タスク例を示す。
BEGIN
DELETE FROM job_heartbeat WHERE beat_at < SYSTIMESTAMP - INTERVAL '30' DAY;
DELETE FROM job_run WHERE started_at < SYSTIMESTAMP - INTERVAL '180' DAY;
COMMIT;
END;
/
まとめ
DBMS_SCHEDULERが提供する標準履歴は強力だが、運用の意思決定にはドメイン依存のメタと相関ID、入力・件数、SLA準拠状況を一体で見える化する専用ロギングが不可欠になる。開始・ビート・終了を自治トランザクションで確実に刻み、遅延・超過・未実行・停止疑いをSQLだけで抽出できる形に整えれば、ダッシュボードや通知は薄く実装できる。再実行は冪等性を前提に引数を保存し、標準ビューと自前ログの突き合わせで一次情報と運用指標を両立させる。これらをテンプレート化してすべてのジョブに適用すれば、履歴の集中管理と異常検知は“仕組み”として回り続け、運用の暗黙知から確かなデータ駆動へと移行できる。