バッチ処理でSYSDATEをそのまま使うと、運用日付がぶれやすくなります。休日に実行したら前営業日を処理するのか、月初に前月末を締めるのか、再実行時も同じ対象日になるのか。この決め方が曖昧なままSQLを書くと、差分処理や集計結果が毎回変わってしまいます。
この記事では、PL/SQLで業務日付・営業日カレンダーを設計する方法を整理します。単に曜日を判定するのではなく、休日テーブル、営業日フラグ、前営業日・翌営業日、月末締め、処理対象日の固定までをバッチ運用で使える形にします。差分処理は 差分抽出・増分処理の設計、集計処理は サマリーテーブル・集計バッチ設計 と組み合わせると効果的です。
- 業務日付とシステム日付の違い
- 営業日カレンダーテーブルの設計
- 休日・祝日・臨時営業日の管理
- 前営業日・翌営業日の取得
- 月末締め・月初処理の判定
- バッチ処理対象日の固定
- 再実行時に日付がぶれない設計
業務日付とシステム日付を分ける
最初に、業務日付とシステム日付を分けて考えます。SYSDATEはサーバーの現在日時ですが、業務上の処理対象日とは限りません。夜間バッチでは、実行日時は翌日でも処理対象は前日ということがよくあります。
日付の切り捨てや月初・月末の基本は OracleのTRUNC関数 や LAST_DAY関数で月末を取得する方法 も参考になります。この記事では、それらを業務カレンダーとして運用する部分に絞ります。
営業日カレンダーテーブルを設計する
曜日計算だけで営業日を判定すると、祝日、年末年始、会社独自休日、臨時営業日に対応できません。実務では、日付ごとに営業日かどうかを持つカレンダーテーブルを作る方が安定します。
CREATE TABLE business_calendar (
calendar_date DATE PRIMARY KEY,
is_business_day CHAR(1) NOT NULL,
holiday_name VARCHAR2(100),
business_month VARCHAR2(6) NOT NULL,
month_end_flag CHAR(1) DEFAULT 'N' NOT NULL,
note VARCHAR2(500),
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
CONSTRAINT ck_bc_business_day
CHECK (is_business_day IN ('Y','N')),
CONSTRAINT ck_bc_month_end
CHECK (month_end_flag IN ('Y','N'))
);
CREATE INDEX ix_bc_business_day
ON business_calendar(is_business_day, calendar_date);
CREATE INDEX ix_bc_business_month
ON business_calendar(business_month, calendar_date);
business_monthは、会計月や締め月を管理するための列です。暦の月と業務上の月が一致するならYYYYMMで十分ですが、締め日が月末以外の場合は業務ルールに合わせて設定します。
カレンダー初期データを作る
まずは対象期間の日付を作り、土日を非営業日にします。この後で祝日や会社休日、臨時営業日を上書きします。毎年手入力するのではなく、初期生成してから例外だけ登録する形にすると保守しやすくなります。
INSERT INTO business_calendar (
calendar_date,
is_business_day,
holiday_name,
business_month,
month_end_flag
)
SELECT
DATE '2026-01-01' + LEVEL - 1 AS calendar_date,
CASE
WHEN TO_CHAR(DATE '2026-01-01' + LEVEL - 1, 'DY', 'NLS_DATE_LANGUAGE=ENGLISH')
IN ('SAT','SUN')
THEN 'N'
ELSE 'Y'
END AS is_business_day,
NULL AS holiday_name,
TO_CHAR(DATE '2026-01-01' + LEVEL - 1, 'YYYYMM') AS business_month,
'N' AS month_end_flag
FROM dual
CONNECT BY LEVEL <= DATE '2026-12-31' - DATE '2026-01-01' + 1;
TO_CHAR(..., 'DY')は言語設定の影響を受けるため、例ではNLS_DATE_LANGUAGE=ENGLISHを明示しています。曜日判定を本番バッチで使う場合は、NLS設定に依存しないようにしておくと安全です。
祝日・会社休日・臨時営業日を登録する
土日以外の休業日は、カレンダーに上書きします。祝日だけでなく、創立記念日、年末年始、システム停止日、棚卸日なども同じテーブルで扱えます。逆に土曜日を臨時営業日にする場合はis_business_dayをYに戻します。
UPDATE business_calendar SET is_business_day = 'N', holiday_name = '年末年始休業', updated_at = SYSTIMESTAMP WHERE calendar_date BETWEEN DATE '2026-12-29' AND DATE '2026-12-31'; UPDATE business_calendar SET is_business_day = 'N', holiday_name = '会社休日', updated_at = SYSTIMESTAMP WHERE calendar_date = DATE '2026-08-14'; UPDATE business_calendar SET is_business_day = 'Y', holiday_name = NULL, note = '臨時営業日', updated_at = SYSTIMESTAMP WHERE calendar_date = DATE '2026-09-12';
祝日データを外部から取り込む場合でも、最終的にはカレンダーテーブルに反映しておくとSQLが単純になります。取込前後の検証は ステージングテーブル設計 や データ品質チェック・整合性検証バッチ設計 と相性が良いです。
前営業日・翌営業日を取得する
営業日カレンダーの価値が最も出るのは、前営業日や翌営業日を取得する処理です。休日をまたぐ差分処理、月初の前営業日処理、入金消込の対象期間などで使います。
CREATE OR REPLACE FUNCTION get_prev_business_day (
p_base_date IN DATE
) RETURN DATE
IS
v_business_date DATE;
BEGIN
SELECT MAX(calendar_date)
INTO v_business_date
FROM business_calendar
WHERE calendar_date < TRUNC(p_base_date)
AND is_business_day = 'Y';
RETURN v_business_date;
END;
/
CREATE OR REPLACE FUNCTION get_next_business_day (
p_base_date IN DATE
) RETURN DATE
IS
v_business_date DATE;
BEGIN
SELECT MIN(calendar_date)
INTO v_business_date
FROM business_calendar
WHERE calendar_date > TRUNC(p_base_date)
AND is_business_day = 'Y';
RETURN v_business_date;
END;
/
関数化すると便利ですが、呼び出し頻度が高いSQLでは結合やサブクエリで直接取得した方が速い場合もあります。画面検索の大量行に対して1行ずつ関数を呼ぶのではなく、バッチ開始時に対象日を1回だけ確定する使い方が向いています。
当日が営業日か判定する
休日にバッチを止めるだけなら、営業日判定は単純です。ただし、休日に実行しないのか、休日に前営業日を処理するのか、休日明けにまとめて処理するのかを先に決めます。
DECLARE
v_is_business_day business_calendar.is_business_day%TYPE;
BEGIN
SELECT is_business_day
INTO v_is_business_day
FROM business_calendar
WHERE calendar_date = TRUNC(SYSDATE);
IF v_is_business_day = 'N' THEN
raise_application_error(-20010, '本日は非営業日のため処理を実行しません。');
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
raise_application_error(
-20011,
'営業日カレンダーに本日の日付が登録されていません。'
);
END;
/
非営業日に何もしない設計は分かりやすい一方で、データ連携や監視が止まるリスクがあります。実務では、ジョブ自体は起動し、処理対象日がないことをログに残す方が運用しやすい場合もあります。
処理対象日を決める
夜間バッチでは、実行日ではなく処理対象日を明示的に決めます。たとえば営業日にだけ前営業日を処理するなら、バッチ開始時に対象日を確定し、その値を後続処理へ渡します。
CREATE OR REPLACE PROCEDURE resolve_target_business_date (
p_run_date IN DATE,
p_target_date OUT DATE
)
IS
v_is_business_day business_calendar.is_business_day%TYPE;
BEGIN
SELECT is_business_day
INTO v_is_business_day
FROM business_calendar
WHERE calendar_date = TRUNC(p_run_date);
IF v_is_business_day = 'Y' THEN
p_target_date := get_prev_business_day(p_run_date);
ELSE
p_target_date := NULL;
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
raise_application_error(
-20012,
'営業日カレンダーに実行日が登録されていません。'
);
END;
/
この例では、非営業日は処理対象日なしにしています。休日にも前営業日を処理したい場合は、v_is_business_dayに関係なくget_prev_business_dayを返すようにします。大切なのは、ルールをコード内で散らさず、1箇所で決めることです。
処理対象日をログに固定する
処理対象日は、バッチの最初に決めたらログテーブルへ保存します。後続SQLで毎回SYSDATEやget_prev_business_dayを呼ぶと、再実行時に対象日が変わる可能性があります。
CREATE TABLE batch_date_log ( batch_id NUMBER PRIMARY KEY, job_name VARCHAR2(100) NOT NULL, run_date DATE NOT NULL, target_date DATE, status VARCHAR2(20) NOT NULL, started_at TIMESTAMP NOT NULL, finished_at TIMESTAMP, message VARCHAR2(1000) );
batch_idをシーケンスで採番する場合は、ログ用のシーケンスも用意します。既存の共通ジョブログがある場合は、その採番方式に合わせてください。
CREATE SEQUENCE batch_log_seq START WITH 1 INCREMENT BY 1 NOCACHE;
DECLARE
v_batch_id NUMBER := batch_log_seq.NEXTVAL;
v_target_date DATE;
BEGIN
resolve_target_business_date(
p_run_date => SYSDATE,
p_target_date => v_target_date
);
INSERT INTO batch_date_log (
batch_id,
job_name,
run_date,
target_date,
status,
started_at,
message
)
VALUES (
v_batch_id,
'DAILY_SALES_SUMMARY',
TRUNC(SYSDATE),
v_target_date,
CASE WHEN v_target_date IS NULL THEN 'SKIPPED' ELSE 'RUNNING' END,
SYSTIMESTAMP,
CASE WHEN v_target_date IS NULL THEN '処理対象日なし' END
);
COMMIT;
END;
/
ジョブ実行履歴を共通化している場合は、専用テーブルではなく共通ログにtarget_dateを追加しても構いません。実行履歴の基本設計は ジョブ実行履歴テーブル設計 と同じ考え方です。
差分処理の対象期間に使う
対象日を固定したら、その日付から処理対象期間を作ります。前回実行日時ベースの差分処理でも、業務日付ベースのバッチでは対象日をログに残しておくと再実行が安定します。
INSERT INTO sales_daily_summary ( summary_date, store_id, sales_amount, created_at ) SELECT l.target_date, s.store_id, SUM(s.amount), SYSTIMESTAMP FROM batch_date_log l JOIN sales_detail s ON s.sales_date >= l.target_date AND s.sales_date < l.target_date + 1 WHERE l.batch_id = :batch_id AND l.status = 'RUNNING' GROUP BY l.target_date, s.store_id;
対象期間をSQL内でTRUNC(SYSDATE) - 1のように計算しないのがポイントです。処理対象日はログから取得し、同じbatch_idなら何度実行しても同じ範囲を処理します。集計後の検算は データ品質チェック・整合性検証バッチ設計 につなげられます。
月末締めを判定する
月末締めは、単純な暦月末ではなく、月内最後の営業日で判定することが多いです。カレンダーテーブルにmonth_end_flagを持たせると、月末処理の条件が分かりやすくなります。再計算するときは、古い月末フラグが残らないように先にリセットします。
UPDATE business_calendar
SET month_end_flag = 'N';
UPDATE business_calendar c
SET month_end_flag = 'Y'
WHERE c.is_business_day = 'Y'
AND c.calendar_date = (
SELECT MAX(c2.calendar_date)
FROM business_calendar c2
WHERE c2.business_month = c.business_month
AND c2.is_business_day = 'Y'
);
SELECT CASE
WHEN month_end_flag = 'Y' THEN 'RUN'
ELSE 'SKIP'
END AS month_end_batch_decision
FROM business_calendar
WHERE calendar_date = :target_date;
月末フラグを事前に持たせると、締め日の例外にも対応しやすくなります。たとえば会社都合で月末締めを前倒しする場合も、対象日だけmonth_end_flagを変更すればバッチ側のSQLを変えずに済みます。
会計月が暦月と違う場合
締め日が20日や25日の会社では、暦月と業務月が一致しません。この場合はbusiness_monthをカレンダーに持たせ、どの業務月として集計するかを明示します。
UPDATE business_calendar
SET business_month =
CASE
WHEN TO_NUMBER(TO_CHAR(calendar_date, 'DD')) <= 20
THEN TO_CHAR(calendar_date, 'YYYYMM')
ELSE TO_CHAR(ADD_MONTHS(calendar_date, 1), 'YYYYMM')
END;
この例では21日以降を翌業務月として扱っています。実際の締め日、休日前倒し、月末前倒しの扱いは会社ごとに違うため、カレンダーの値として確定させておく方が安全です。
マスタ同期や履歴管理とつなげる
営業日カレンダーは、マスタの有効期間判定にも使えます。たとえば価格マスタや商品マスタを営業日単位で切り替える場合、システム日付ではなく処理対象日で有効行を選びます。
SELECT m.item_id,
m.item_name,
m.price
FROM item_price_master m
JOIN batch_date_log l
ON l.batch_id = :batch_id
WHERE l.target_date >= m.valid_from
AND l.target_date < NVL(m.valid_to, DATE '9999-12-31');
マスタの履歴管理は マスタ同期・履歴管理の設計 と組み合わせると、日付境界のバグを減らせます。どの時点のマスタで処理したかを説明できる状態にしておくことが重要です。
再実行時に日付を変えない
再実行で最も避けたいのは、同じバッチなのに対象日が変わることです。失敗した翌日に再実行したら、前営業日が別の日になってしまう。これを防ぐために、再実行では既存ログのtarget_dateを使います。
SELECT target_date INTO :target_date FROM batch_date_log WHERE batch_id = :batch_id; UPDATE batch_date_log SET status = 'RUNNING', started_at = SYSTIMESTAMP, finished_at = NULL, message = '再実行' WHERE batch_id = :batch_id;
差分処理や集計処理では、再実行時の重複防止も必要です。対象日を固定したうえで、同じ対象日の集計結果を削除して作り直すのか、履歴として残すのかを決めます。再実行と重複防止の考え方は 差分抽出・増分処理の設計 にもつながります。
カレンダー不足を検出する
営業日カレンダーは、先の日付まで登録されていないとバッチが失敗します。年末に翌年分を作り忘れる、祝日変更を反映し忘れる、月末フラグがないといった問題を事前に検出します。
SELECT 'MISSING_NEXT_30_DAYS' AS check_name,
COUNT(*) AS missing_count
FROM (
SELECT TRUNC(SYSDATE) + LEVEL - 1 AS calendar_date
FROM dual
CONNECT BY LEVEL <= 30
) d
LEFT JOIN business_calendar c
ON c.calendar_date = d.calendar_date
WHERE c.calendar_date IS NULL
UNION ALL
SELECT 'NO_MONTH_END_FLAG',
COUNT(*)
FROM (
SELECT business_month
FROM business_calendar
GROUP BY business_month
HAVING SUM(CASE WHEN month_end_flag = 'Y' THEN 1 ELSE 0 END) = 0
);
この検証は、日次の品質チェックや月初の運用確認に組み込むと効果的です。カレンダーは地味ですが、間違えるとすべてのバッチ処理対象がずれます。
やってはいけない日付設計
最後に、実務で避けたい日付設計を整理します。日付まわりは一見簡単に見えますが、障害になると原因調査が難しくなります。
設計チェックリスト
PL/SQLで業務日付・営業日カレンダーを設計するときは、次の点を確認します。
- システム日付と業務日付を分けている
- 営業日カレンダーに休日・臨時営業日を持っている
- 前営業日・翌営業日を取得できる
- 処理対象日をバッチ開始時に固定している
- 対象日をジョブログに保存している
- 月内最終営業日を判定できる
- 暦月と業務月が違う場合に対応している
- 再実行時に同じ対象日を使う
- カレンダー不足や月末フラグ不足を検出している
- 日付ルールをSQLごとに散らしていない
まとめ
PL/SQLのバッチ処理では、SYSDATEをそのまま使うのではなく、業務日付と処理対象日を明示的に決めることが重要です。営業日カレンダーを用意し、前営業日、翌営業日、月末締め、業務月をテーブルで管理すると、日付境界のバグを減らせます。
特に大切なのは、バッチ開始時に処理対象日を固定し、ジョブログに残すことです。同じbatch_idなら同じ対象日を処理する。このルールがあるだけで、差分処理、集計処理、品質チェック、再実行の設計がかなり安定します。
