月次締めは、単に集計SQLを実行して終わりではありません。締め対象月を確定し、締め前チェックを通し、処理中の排他を取り、集計結果を固定し、必要なときは締め戻しと再締めができるようにする必要があります。この流れが曖昧なままだと、締め後にデータが変わる、再締めで二重集計する、誰が締め戻したか分からない、といった問題が起きます。
この記事では、PL/SQLで締め処理・締め戻しを設計する方法を整理します。OPEN、CLOSING、CLOSED、REOPENEDの締め状態を管理し、月次確定、締め前検証、排他制御、履歴ログ、再締めまでを実務で使える形にします。業務日付は 業務日付・営業日カレンダー設計、集計バッチは サマリーテーブル・集計バッチ設計 と組み合わせると効果的です。
- 締め処理が必要になる場面
- OPEN、CLOSING、CLOSED、REOPENEDの状態設計
- 締め管理テーブルと履歴テーブルのDDL
- 締め前チェックとデータ品質検証
- 締め中の排他制御
- 月次集計の確定と再作成
- 締め戻し、再締め、監査ログ
締め処理が必要になる場面
締め処理は、一定期間のデータを確定し、後続工程へ渡すための処理です。売上、入金、在庫、請求、会計連携、ポイント付与など、締め後に値が変わると困る処理で必要になります。締める対象は月次だけとは限らず、日次締め、週次締め、店舗別締め、部門別締めの場合もあります。
締め対象日や月内最終営業日の考え方は 業務日付・営業日カレンダー設計 と合わせて決めます。処理対象レコード単位の状態管理は 処理ステータス管理テーブル設計 が参考になります。
締め状態を定義する
締め状態は少なく、しかし運用判断に必要な粒度で定義します。OPENは未締め、CLOSINGは締め処理中、CLOSEDは締め済み、REOPENEDは締め戻し済みとして扱います。
状態を増やしすぎると、どの状態からどの状態へ進めるのか分かりにくくなります。まずはこの4状態にして、必要ならCANCELLEDやFAILEDを追加します。
締め管理テーブルを設計する
締め対象月、状態、締めバージョン、締め実行者、締め戻し理由を管理するテーブルを作ります。締め状態は期間ごとに1行で管理し、再締めのたびにclose_versionを増やします。
CREATE TABLE monthly_closing_period (
period_month VARCHAR2(6) PRIMARY KEY,
target_from DATE NOT NULL,
target_to DATE NOT NULL,
status VARCHAR2(20) DEFAULT 'OPEN' NOT NULL,
close_version NUMBER DEFAULT 0 NOT NULL,
closing_started_at TIMESTAMP,
closed_at TIMESTAMP,
closed_by VARCHAR2(100),
reopened_at TIMESTAMP,
reopened_by VARCHAR2(100),
reopen_reason VARCHAR2(1000),
lock_owner VARCHAR2(100),
lock_at TIMESTAMP,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
CONSTRAINT ck_mcp_status
CHECK (status IN ('OPEN','CLOSING','CLOSED','REOPENED')),
CONSTRAINT ck_mcp_period
CHECK (target_from < target_to)
);
CREATE INDEX ix_mcp_status
ON monthly_closing_period(status, period_month);
target_toは終了日そのものではなく、検索条件で< target_toとして使う境界日にします。2026年5月の締めなら、target_from = DATE '2026-05-01'、target_to = DATE '2026-06-01'のように持ちます。
締め履歴テーブルを設計する
締め処理は、誰がいつ実行したか、なぜ締め戻したかを後から説明できる必要があります。現在状態だけでなく、状態遷移の履歴を残します。
CREATE TABLE monthly_closing_history ( history_id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, period_month VARCHAR2(6) NOT NULL, old_status VARCHAR2(20), new_status VARCHAR2(20) NOT NULL, close_version NUMBER NOT NULL, changed_by VARCHAR2(100), changed_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL, reason VARCHAR2(1000), message VARCHAR2(2000) ); CREATE INDEX ix_mch_period ON monthly_closing_history(period_month, changed_at);
ジョブ全体の実行履歴は ジョブ実行履歴テーブル設計、締め状態の遷移はこの履歴テーブル、というように役割を分けます。
締め対象月を登録する
締め処理を行う前に、対象月の管理行を作成します。月次締めなら対象月ごとに1行、店舗別や部門別に締める場合はキーを追加します。
INSERT INTO monthly_closing_period ( period_month, target_from, target_to, status ) VALUES ( '202605', DATE '2026-05-01', DATE '2026-06-01', 'OPEN' );
締め対象月の決定は、営業日カレンダーや業務月に合わせます。会計月が暦月と違う場合は、締め対象範囲をテーブル値として確定させると、SQLごとに日付計算を散らさずに済みます。
締め前チェックを通す
締め処理を始める前に、未処理データ、失敗データ、データ不整合が残っていないかを確認します。締め前チェックを通さずに締めると、後から締め戻しが必要になりやすくなります。
SELECT 'FAILED_WORK_ITEMS' AS check_name,
COUNT(*) AS issue_count
FROM batch_work_item
WHERE target_date >= :target_from
AND target_date < :target_to
AND status IN ('FAILED','RETRY_WAIT','RUNNING')
UNION ALL
SELECT 'QUALITY_ERRORS',
COUNT(*)
FROM data_quality_result q
JOIN data_quality_rule r
ON r.rule_id = q.rule_id
WHERE q.detected_at >= :target_from
AND q.detected_at < :target_to
AND r.severity = 'ERROR';
品質チェックの設計は データ品質チェック・整合性検証バッチ設計 と組み合わせます。締め前チェックは、失敗件数が0であることだけでなく、確認済みの例外をどう扱うかも決めておきます。
締め処理を開始する
締め処理を開始するときは、締め管理行をロックし、状態をCLOSINGに変更します。同じ月を二重に締めないように、SELECT FOR UPDATEで対象行をロックします。
CREATE OR REPLACE PROCEDURE begin_monthly_closing (
p_period_month IN VARCHAR2,
p_operator IN VARCHAR2
)
IS
v_status monthly_closing_period.status%TYPE;
v_version monthly_closing_period.close_version%TYPE;
BEGIN
SELECT status, close_version
INTO v_status, v_version
FROM monthly_closing_period
WHERE period_month = p_period_month
FOR UPDATE;
IF v_status NOT IN ('OPEN','REOPENED') THEN
raise_application_error(
-20030,
'締め開始できない状態です: ' || v_status
);
END IF;
UPDATE monthly_closing_period
SET
status = 'CLOSING',
close_version = close_version + 1,
closing_started_at = SYSTIMESTAMP,
lock_owner = p_operator,
lock_at = SYSTIMESTAMP,
updated_at = SYSTIMESTAMP
WHERE period_month = p_period_month;
INSERT INTO monthly_closing_history (
period_month,
old_status,
new_status,
close_version,
changed_by,
message
)
VALUES (
p_period_month,
v_status,
'CLOSING',
v_version + 1,
p_operator,
'締め処理を開始しました。'
);
COMMIT;
END;
/
ジョブ全体の二重起動防止には DBMS_LOCKで二重起動防止する方法 も有効です。締め対象月単位の排他は、締め管理行のロックと状態管理で行います。
締め対象の更新を防ぐ
締め済み期間や締め処理中の期間のデータが後から更新されると、締め結果と明細がずれます。更新処理側で締め状態を確認するか、必要に応じてトリガーで防ぎます。
CREATE OR REPLACE TRIGGER trg_sales_no_update_closed
BEFORE INSERT OR UPDATE OR DELETE ON sales_detail
FOR EACH ROW
DECLARE
v_status monthly_closing_period.status%TYPE;
v_sales_date DATE;
BEGIN
v_sales_date := TRUNC(
CASE
WHEN INSERTING OR UPDATING THEN :NEW.sales_date
ELSE :OLD.sales_date
END
);
SELECT status
INTO v_status
FROM monthly_closing_period
WHERE v_sales_date >= target_from
AND v_sales_date < target_to;
IF v_status IN ('CLOSING','CLOSED') THEN
raise_application_error(-20031, '締め中または締め済み期間の売上データは更新できません。');
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
NULL;
END;
/
トリガーは強力ですが、すべてをトリガーに寄せると原因調査が難しくなることもあります。更新処理側で締め状態を確認する設計を基本にし、重要なテーブルだけ防御的にトリガーを使うのが現実的です。
月次集計を作り直す
締め処理では、対象月の集計結果を作成します。この記事では、締め開始時にclose_versionを増やし、その締め中バージョンの集計結果を作り直す例にします。同じバージョンを再実行する場合は削除して再作成し、締め戻し後の再締めでは新しいバージョンを残します。
DELETE FROM sales_monthly_summary WHERE period_month = :period_month AND close_version = :close_version; INSERT INTO sales_monthly_summary ( period_month, close_version, store_id, sales_amount, return_amount, created_at ) SELECT :period_month, :close_version, s.store_id, SUM(CASE WHEN s.sales_type = 'SALE' THEN s.amount ELSE 0 END), SUM(CASE WHEN s.sales_type = 'RETURN' THEN s.amount ELSE 0 END), SYSTIMESTAMP FROM sales_detail s JOIN monthly_closing_period p ON p.period_month = :period_month WHERE s.sales_date >= p.target_from AND s.sales_date < p.target_to GROUP BY s.store_id;
集計バッチの基本設計は サマリーテーブル・集計バッチ設計 と同じです。締め処理では、再締め時にどの集計結果が最新なのかを説明できるように、close_versionを持たせるのがポイントです。
締め結果を検算する
締め集計を作ったら、明細合計と締めサマリーが一致しているか確認します。締め済みにする前に検算を通すことで、締め戻しの回数を減らせます。
WITH detail_sum AS (
SELECT
s.store_id,
SUM(CASE WHEN s.sales_type = 'SALE' THEN s.amount ELSE 0 END) AS sales_amount,
SUM(CASE WHEN s.sales_type = 'RETURN' THEN s.amount ELSE 0 END) AS return_amount
FROM sales_detail s
JOIN monthly_closing_period p
ON p.period_month = :period_month
WHERE s.sales_date >= p.target_from
AND s.sales_date < p.target_to
GROUP BY s.store_id
)
SELECT
NVL(d.store_id, m.store_id) AS store_id,
NVL(d.sales_amount, 0) - NVL(m.sales_amount, 0) AS sales_diff,
NVL(d.return_amount, 0) - NVL(m.return_amount, 0) AS return_diff
FROM detail_sum d
FULL OUTER JOIN sales_monthly_summary m
ON m.period_month = :period_month
AND m.close_version = :close_version
AND m.store_id = d.store_id
WHERE NVL(d.sales_amount, 0) <> NVL(m.sales_amount, 0)
OR NVL(d.return_amount, 0) <> NVL(m.return_amount, 0);
検算で差異が出た場合は、CLOSEDにせず、原因を調べます。差異検出を定型化するなら、データ品質チェックのルールとして登録しても構いません。
締め処理を完了する
締め前チェック、集計、検算が通ったら、状態をCLOSEDに変更します。検算が通っていない状態で誤って締め完了しないように、完了処理には検算OKの引数を渡します。締め完了時にはロック情報をクリアします。
CREATE OR REPLACE PROCEDURE finish_monthly_closing (
p_period_month IN VARCHAR2,
p_operator IN VARCHAR2,
p_validation_ok IN CHAR
)
IS
v_version monthly_closing_period.close_version%TYPE;
BEGIN
IF p_validation_ok <> 'Y' THEN
raise_application_error(
-20032,
'締め結果の検算が完了していないため、締め完了できません。'
);
END IF;
SELECT close_version
INTO v_version
FROM monthly_closing_period
WHERE period_month = p_period_month
AND status = 'CLOSING'
FOR UPDATE;
UPDATE monthly_closing_period
SET
status = 'CLOSED',
closed_at = SYSTIMESTAMP,
closed_by = p_operator,
lock_owner = NULL,
lock_at = NULL,
updated_at = SYSTIMESTAMP
WHERE period_month = p_period_month;
INSERT INTO monthly_closing_history (
period_month,
old_status,
new_status,
close_version,
changed_by,
message
)
VALUES (
p_period_month,
'CLOSING',
'CLOSED',
v_version,
p_operator,
'締め処理を完了しました。'
);
COMMIT;
END;
/
この設計では、締め開始時に決まったclose_versionを、集計結果と締め履歴の両方で使います。大切なのは、集計結果と締め履歴が同じバージョンを指すことです。
締め戻しを設計する
締め後に補正が必要になった場合は、締め戻しを行います。締め戻しは、締め済みデータを無条件に更新可能にする操作ではありません。理由、実行者、対象月、影響範囲を履歴に残してからREOPENEDにします。
CREATE OR REPLACE PROCEDURE reopen_monthly_closing (
p_period_month IN VARCHAR2,
p_operator IN VARCHAR2,
p_reason IN VARCHAR2
)
IS
v_version monthly_closing_period.close_version%TYPE;
BEGIN
SELECT close_version
INTO v_version
FROM monthly_closing_period
WHERE period_month = p_period_month
AND status = 'CLOSED'
FOR UPDATE;
UPDATE monthly_closing_period
SET
status = 'REOPENED',
reopened_at = SYSTIMESTAMP,
reopened_by = p_operator,
reopen_reason = p_reason,
updated_at = SYSTIMESTAMP
WHERE period_month = p_period_month;
INSERT INTO monthly_closing_history (
period_month,
old_status,
new_status,
close_version,
changed_by,
reason,
message
)
VALUES (
p_period_month,
'CLOSED',
'REOPENED',
v_version,
p_operator,
p_reason,
'締め戻しを実行しました。'
);
COMMIT;
END;
/
締め戻し後に本番データを補正する場合は 本番データ補正スクリプトの作り方 と同じく、対象条件、バックアップ、検証SQLを用意してから実行します。
再締めの流れを決める
締め戻し後は、補正、品質チェック、集計再作成、検算、締め完了という流れで再締めします。初回締めと同じ手順を通すことで、締め戻し後だけチェックが甘くなることを防ぎます。
BEGIN
begin_monthly_closing(
p_period_month => '202605',
p_operator => :operator_name
);
-- 1. 締め前チェック
-- 2. 月次集計の再作成
-- 3. 締め結果の検算
finish_monthly_closing(
p_period_month => '202605',
p_operator => :operator_name,
p_validation_ok => 'Y'
);
END;
/
再締めでは、前回の締め結果を削除して置き換えるのか、締めバージョンを増やして履歴として残すのかを決めます。監査や会計連携が関わる場合は、古い締め結果も残せる設計の方が説明しやすくなります。
CLOSINGのまま残った状態を検出する
締め処理中にジョブが落ちると、状態がCLOSINGのまま残ることがあります。締め処理中の滞留は、通常の処理ステータスより影響が大きいため、監視SQLを用意します。
SELECT period_month, status, closing_started_at, lock_owner, lock_at FROM monthly_closing_period WHERE status = 'CLOSING' AND lock_at < SYSTIMESTAMP - INTERVAL '2' HOUR;
CLOSINGを自動でOPENへ戻すかどうかは慎重に決めます。締め処理は影響が大きいため、自動戻しよりも、運用者がログと集計結果を確認してから戻す方が安全な場合が多いです。
やってはいけない締め処理
最後に、実務で避けたい締め処理を整理します。締め処理は障害時の説明責任が大きいため、曖昧な設計のまま運用に乗せないことが重要です。
設計チェックリスト
PL/SQLで締め処理・締め戻しを設計するときは、次の点を確認します。
- 締め対象期間をテーブルで管理している
- OPEN、CLOSING、CLOSED、REOPENEDを区別している
- 締め前チェックを通してから締めている
- 締め中の二重実行を防いでいる
- 締め済み期間の更新を防いでいる
- 締め履歴と締め戻し理由を残している
- 集計結果と締めバージョンが紐づいている
- 再締め時も初回締めと同じ検証を通している
- CLOSING滞留を検出できる
- 補正、検算、再締めの手順が決まっている
まとめ
PL/SQLで締め処理を設計するときは、集計SQLだけでなく、締め状態、排他、締め前チェック、履歴、締め戻し、再締めまでを一つの流れとして考えることが重要です。締め済みデータは後続工程の前提になるため、いつ誰が何を確定したのかを説明できる設計にします。
特に大切なのは、締め対象期間を固定すること、CLOSINGで二重実行を防ぐこと、締め戻し理由を履歴に残すことです。締め処理は地味ですが、業務データの信頼性を支える最後の関門です。
