- 設計
バッチ設計書の書き方と項目一覧|リランと異常終了時の設計サンプル
バッチ設計書とは、決まった時刻や条件でまとめて動かす処理(バッチ)について、「いつ動き、何を読み、何を書き、どう終わり、止まったときにどう動かし直すか」を書いた設計書です。 画面のある処理と違って人が見ていない時間に動くため、止まったあとの動かし直し(リラン)まで設計書に書いておくことが、いちばんの価値になります。
この記事では、ジョブ一覧・起動条件・入出力・処理フロー・終了コード・コミット単位・リラン手順を持つテンプレートをMarkdownとタブ区切りで用意し、架空の日次売上集計バッチで全項目を記入します。あわせて、異常終了のパターン別にリラン方式を選ぶ判断表と、2回実行しても結果が変わらないSQLとシェルスクリプトを、実行結果と一緒に示します。
確認環境:Ubuntu 24.04、PostgreSQL 16.15(psql 16.15)、GNU bash 5.2.21、flock(util-linux 2.39.3)。掲載したSQL・シェルスクリプトの出力は、すべてこの環境で実行したものです。
バッチ設計書の項目早見表
| 項目 | 決めること | 記入例(日次売上集計) | 決めていないと起きること |
|---|---|---|---|
| ジョブ一覧 | ジョブID、名前、順番 | JOB-S005 → JOB-S010 → JOB-S020 | どのジョブの後に動くのかが人によって違う |
| 起動条件 | 時刻、先行ジョブ、引数 | JOB-S005 が終了コード0で終了したら起動 | 入力がそろう前に動く |
| 入出力 | 読むもの、書くもの、キー | sales を読み、daily_sales に書く | リランで消してよい範囲がわからない |
| 処理フロー | 処理の順番と分岐 | 検査 → ロック → 削除 → 集計登録 → 件数確認 | 止まった場所から状態を推測できない |
| 終了コード | 数字と意味、後続の扱い | 0 正常、1 警告、10・20・30 異常 | 後続ジョブを動かしてよいか判断できない |
| コミット単位 | どこまでを1つの確定とするか | 対象日1日分を1トランザクション | 途中で止まると半分だけ書かれた状態が残る |
| リラン手順 | 動かし直す方式とコマンド | 同じ対象日を引数にして先頭から再実行 | 夜間の対応者が手順を調べるところから始まる |
| 二重起動の防止 | 同時に2つ動かない仕組み | ロックファイルを flock で取る | 同じ日の集計が同時に走り、結果が入り混じる |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「バッチ設計書の書き方と項目一覧|リランと異常終了時の設計サンプル」
バッチ設計書の構成と用語
バッチ設計書は、全体を見渡す「ジョブ一覧」と、ジョブ1本ごとの「個別設計」の2段で書きます。
| 用語 | 意味 |
|---|---|
| ジョブ | バッチ処理の1単位。1本のスクリプトやプログラムに当たる |
| ジョブネット | ジョブを順番や条件でつないだもの |
| 先行ジョブ | このジョブより先に終わっている必要があるジョブ |
| 終了コード | プログラムが終わるときに返す数字。0が正常、0以外が正常以外 |
| リラン | 止まったジョブ、または結果を直したいジョブを動かし直すこと |
| 冪等性(べきとうせい) | 同じ処理を何回実行しても結果が同じになる性質 |
| コミット単位 | データベースへの変更を確定させる区切り |
リランを簡単にする近道は、ジョブを冪等に作ることです。冪等なら、止まったときの手順は「同じ引数でもう一度動かす」の1行で済みます。
テンプレート1:ジョブ一覧
| ジョブID | ジョブ名 | 起動条件 | 先行ジョブ | 入力 | 出力 | 想定時間 | リラン方式 |
|---|---|---|---|---|---|---|---|
| JOB-0000 | (ジョブ名) | (時刻または先行ジョブの終了) | (ジョブID) | (テーブル・ファイル) | (テーブル・ファイル) | (分) | 先頭から再実行 |
表計算ソフトに貼るときは、次のタブ区切りを使います。
ジョブID ジョブ名 起動条件 先行ジョブ 入力 出力 想定時間 リラン方式
JOB-0000 (ジョブ名) (時刻または先行ジョブの終了) (ジョブID) (テーブル・ファイル) (テーブル・ファイル) (分) 先頭から再実行
テンプレート2:ジョブ個別設計
## JOB-0000 (ジョブ名)
| 項目 | 内容 |
|---|---|
| ジョブID | |
| ジョブ名 | |
| 概要 | |
| 起動条件 | |
| 起動コマンド | |
| 引数 | |
| 入力 | |
| 出力 | |
| 処理フロー | |
| コミット単位 | |
| 終了コード | |
| リラン方式 | |
| リラン手順 | |
| 二重起動の防止 | |
| 想定時間と打ち切り | |
| 異常時の連絡 | |
項目 内容
ジョブID
ジョブ名
概要
起動条件
起動コマンド
引数
入力
出力
処理フロー
コミット単位
終了コード
リラン方式
リラン手順
二重起動の防止
想定時間と打ち切り
異常時の連絡
終了コードは別の表にします。
| 終了コード | 区分 | 意味 | 後続ジョブ | 対応 |
|---|---|---|---|---|
| 0 | 正常 | | 起動する | |
| 1 | 警告 | | 起動する | |
| 10 | 異常 | | 起動しない | |
終了コード 区分 意味 後続ジョブ 対応
0 正常 起動する
1 警告 起動する
10 異常 起動しない
記入例:日次売上集計バッチ
架空の店舗売上を題材に、3本のジョブを並べます。
ジョブ一覧の記入例
| ジョブID | ジョブ名 | 起動条件 | 先行ジョブ | 入力 | 出力 | 想定時間 | リラン方式 |
|---|---|---|---|---|---|---|---|
| JOB-S005 | 売上明細取込 | 毎日 01:00 | なし | 売上明細ファイル | sales テーブル | 10分 | 先頭から再実行 |
| JOB-S010 | 日次売上集計 | JOB-S005 が終了コード0で終了 | JOB-S005 | sales テーブル | daily_sales テーブル | 5分 | 先頭から再実行 |
| JOB-S020 | 集計レポート出力 | JOB-S010 が終了コード0で終了 | JOB-S010 | daily_sales テーブル | レポートファイル(CSV) | 3分 | 先頭から再実行 |
ジョブID ジョブ名 起動条件 先行ジョブ 入力 出力 想定時間 リラン方式
JOB-S005 売上明細取込 毎日 01:00 なし 売上明細ファイル sales テーブル 10分 先頭から再実行
JOB-S010 日次売上集計 JOB-S005 が終了コード0で終了 JOB-S005 sales テーブル daily_sales テーブル 5分 先頭から再実行
JOB-S020 集計レポート出力 JOB-S010 が終了コード0で終了 JOB-S010 daily_sales テーブル レポートファイル(CSV) 3分 先頭から再実行
個別設計の記入例:JOB-S010 日次売上集計
| 項目 | 内容 |
|---|---|
| ジョブID | JOB-S010 |
| ジョブ名 | 日次売上集計 |
| 概要 | 対象日1日分の売上明細を店舗ごとに集計し、件数と合計額を日次売上集計テーブルに登録する |
| 起動条件 | JOB-S005 が終了コード0で終了したとき。手動で起動するときは対象日を引数で渡す |
| 起動コマンド | ./daily_sales.sh [対象日] |
| 引数 | 第1引数:対象日(YYYY-MM-DD)。省略時は実行日の前日 |
| 入力 | sales(売上明細)。sold_at が対象日の 00:00:00 以上、翌日の 00:00:00 未満の行 |
| 出力 | daily_sales(日次売上集計)。キーは sales_date と store_id |
| 処理フロー | 1. 引数の検査 → 2. ロックの取得 → 3. 対象日の集計行を削除 → 4. 集計して登録 → 5. コミット → 6. 件数の確認(下の図) |
| コミット単位 | 手順3〜4を1トランザクションにする(対象日1日分で1回コミット) |
| 終了コード | 0 正常/1 警告/10・20・30 異常(下の表) |
| リラン方式 | 先頭から再実行(何回実行しても同じ結果になる) |
| リラン手順 | 原因を取り除いたあと、同じ対象日を引数にして起動コマンドを実行する。事前のデータ削除は要らない |
| 二重起動の防止 | ロックファイル /tmp/job_s010.lock を flock で取る。取れなければ終了コード20で終わる |
| 想定時間と打ち切り | 想定5分。15分を超えたらジョブ管理ツールから通知する |
| 異常時の連絡 | 終了コード10以上で運用担当へ通知。JOB-S020 は起動しない |
処理フローは図にしておくと、終了コードがどこで決まるかが一目でわかります。
flowchart TD
A(["開始"]) --> B{"対象日の形式は正しいか"}
B -->|"いいえ"| X10(["終了コード10"])
B -->|"はい"| C{"ロックを取れたか"}
C -->|"いいえ"| X20(["終了コード20"])
C -->|"はい"| D["対象日の集計行を削除"]
D --> E["売上明細を店舗ごとに集計して登録"]
E --> F{"SQLは最後まで通ったか"}
F -->|"いいえ"| R["ロールバック"] --> X30(["終了コード30"])
F -->|"はい"| G["コミット"]
G --> H{"出力件数は1件以上か"}
H -->|"いいえ"| X1(["終了コード1"])
H -->|"はい"| X0(["終了コード0"])
終了コード一覧の記入例
| 終了コード | 区分 | 意味 | 後続ジョブ | 対応 |
|---|---|---|---|---|
| 0 | 正常 | 集計して1件以上を出力した | 起動する | なし |
| 1 | 警告 | 対象日の売上明細が0件だった | 起動しない | 休業日なら対応なし。営業日なら JOB-S005 の結果を確認 |
| 10 | 異常 | 引数(対象日)の形式が正しくない | 起動しない | 引数を直して再実行 |
| 20 | 異常 | 同じジョブが実行中だった | 起動しない | 先に動いているジョブの終了を待つ。再実行は不要 |
| 30 | 異常 | SQLまたはデータベース接続でエラーになった | 起動しない | ログのエラー文で原因を取り除き、同じ対象日で再実行 |
自分で決める終了コードは、原因ごとに10刻みで分けると、あとから間に番号を足せます。シェルが自動で返す番号とは重ならないようにします。今回の環境の bash で確かめた番号は次のとおりです。
| 終了コード | 返る場面(bash 5.2.21 で確認) |
|---|---|
| 126 | 実行権限のないファイルを実行しようとした(Permission denied) |
| 127 | コマンドが見つからない(command not found) |
| 137 | KILL シグナルで止められた(128+9) |
| 143 | TERM シグナルで止められた(128+15) |
psql の終了コードも設計に関わります。同じ環境で確かめた結果です。
| 場面 | psql の終了コード |
|---|---|
-f で渡したSQLが途中でエラー、ON_ERROR_STOP の指定なし |
0(エラーの次の文へ進み、最後まで実行される) |
-f で渡したSQLが途中でエラー、-v ON_ERROR_STOP=1 を指定 |
3(その場で止まる) |
| データベースに接続できない | 2 |
ON_ERROR_STOP=1 を付けないと、SQLがエラーになっても終了コードは0になります。 バッチから psql を呼ぶときは必ず付けます。
二重実行しても結果が変わらない書き方(SQL)
準備するテーブルとデータ
DROP TABLE IF EXISTS sales, daily_sales, daily_sales_append;
-- 入力:売上明細
CREATE TABLE sales (
sale_id integer PRIMARY KEY,
sold_at timestamp NOT NULL,
store_id text NOT NULL,
amount integer NOT NULL
);
-- 出力:日次売上集計(集計日と店舗で一意)
CREATE TABLE daily_sales (
sales_date date NOT NULL,
store_id text NOT NULL,
sale_count integer NOT NULL,
total_amount integer NOT NULL,
updated_at timestamp NOT NULL DEFAULT now(),
PRIMARY KEY (sales_date, store_id)
);
-- 比較用:一意キーを持たない出力テーブル
CREATE TABLE daily_sales_append (
sales_date date NOT NULL,
store_id text NOT NULL,
sale_count integer NOT NULL,
total_amount integer NOT NULL
);
INSERT INTO sales VALUES
(1, '2026-10-04 10:15', 'S01', 1200),
(2, '2026-10-04 11:40', 'S01', 800),
(3, '2026-10-04 13:05', 'S02', 3000),
(4, '2026-10-04 18:30', 'S02', 500),
(5, '2026-10-04 19:10', 'S03', 2400),
(6, '2026-10-05 09:20', 'S01', 1000);
追記するだけのSQLを2回実行すると、行が2倍になる
まず、集計結果を追加するだけのSQLです。
INSERT INTO daily_sales_append (sales_date, store_id, sale_count, total_amount)
SELECT sold_at::date, store_id, count(*), sum(amount)
FROM sales
WHERE sold_at >= :'target'::date
AND sold_at < :'target'::date + 1
GROUP BY sold_at::date, store_id;
psql -q -v target=2026-10-04 -f append.sql
psql -q -v target=2026-10-04 -f append.sql
psql -c "SELECT * FROM daily_sales_append ORDER BY store_id"
sales_date | store_id | sale_count | total_amount
------------+----------+------------+--------------
2026-10-04 | S01 | 2 | 2000
2026-10-04 | S01 | 2 | 2000
2026-10-04 | S02 | 2 | 3500
2026-10-04 | S02 | 2 | 3500
2026-10-04 | S03 | 1 | 2400
2026-10-04 | S03 | 1 | 2400
(6 rows)
同じ行が2つずつ入りました。この作りでリランするには「先に対象日の行を手で消す」という手順が必要になり、消し忘れると合計が2倍になります。
直し方1:対象日の分を消してから入れ直す
削除と登録を1つのトランザクションにまとめると、何回実行しても対象日の行は最新の集計結果だけになります。daily_sales.sql として保存します。
-- 日次売上集計:対象日の分を消してから入れ直す(1トランザクション)
BEGIN;
DELETE FROM daily_sales
WHERE sales_date = :'target'::date;
INSERT INTO daily_sales (sales_date, store_id, sale_count, total_amount)
SELECT sold_at::date, store_id, count(*), sum(amount)
FROM sales
WHERE sold_at >= :'target'::date
AND sold_at < :'target'::date + 1
GROUP BY sold_at::date, store_id;
COMMIT;
-- 出力件数をシェルへ返す
SELECT count(*) FROM daily_sales WHERE sales_date = :'target'::date;
:'target' は psql の変数で、-v target=2026-10-04 で渡した値が文字列として入ります。
直し方2:キーが重なったら上書きする(UPSERT)
出力テーブルに一意キーがあれば、INSERT ... ON CONFLICT でも冪等にできます。
INSERT INTO daily_sales (sales_date, store_id, sale_count, total_amount)
SELECT sold_at::date, store_id, count(*), sum(amount)
FROM sales
WHERE sold_at >= :'target'::date
AND sold_at < :'target'::date + 1
GROUP BY sold_at::date, store_id
ON CONFLICT (sales_date, store_id) DO UPDATE
SET sale_count = EXCLUDED.sale_count,
total_amount = EXCLUDED.total_amount,
updated_at = now();
2回実行したあとの結果です。行は増えません。
sales_date | store_id | sale_count | total_amount
------------+----------+------------+--------------
2026-10-04 | S01 | 2 | 2000
2026-10-04 | S02 | 2 | 3500
2026-10-04 | S03 | 1 | 2400
(3 rows)
2つの直し方の違い:入力の行が減ったとき
リランの理由が「入力データの訂正」のときに差が出ます。店舗 S03 の売上明細(sale_id = 5)が取り消されたとして、明細を削除してからそれぞれを実行しました。
DELETE FROM sales WHERE sale_id = 5;
UPSERT(直し方2)を実行したあとです。S03 の行が残ります。
sales_date | store_id | sale_count | total_amount
------------+----------+------------+--------------
2026-10-04 | S01 | 2 | 2000
2026-10-04 | S02 | 2 | 3500
2026-10-04 | S03 | 1 | 2400
(3 rows)
削除してから登録(直し方1)を実行したあとです。S03 の行が消え、入力と一致します。
sales_date | store_id | sale_count | total_amount
------------+----------+------------+--------------
2026-10-04 | S01 | 2 | 2000
2026-10-04 | S02 | 2 | 3500
(2 rows)
| 書き方 | 2回実行した結果 | 入力の行が減ったとき | 向いている場面 |
|---|---|---|---|
| 追記のみ | 行が2倍になる | 以前の行が残る | 履歴をそのまま積むログ(リラン前に削除が必要) |
| 削除してから登録 | 変わらない | 出力からも消える | 対象日などの範囲で丸ごと作り直せる集計 |
| UPSERT | 変わらない | 以前の行が残る | 行が減らないマスターの反映、1件ずつの更新 |
日次集計のように「対象日」で範囲を区切れる出力は、削除してから登録が扱いやすい選び方です。
二重実行しても結果が変わらない書き方(シェルスクリプト)
SQLを呼び出すシェルスクリプトです。daily_sales.sh として daily_sales.sql と同じフォルダに保存し、chmod +x daily_sales.sh で実行できるようにします。接続先は環境変数(PGHOST、PGUSER、PGDATABASE、PGPASSWORD)で渡します。
#!/usr/bin/env bash
# JOB-S010 日次売上集計
# 使い方: ./daily_sales.sh [対象日 YYYY-MM-DD] 省略時は前日
# 接続先は環境変数 PGHOST / PGUSER / PGDATABASE / PGPASSWORD で渡す
set -euo pipefail
readonly EXIT_OK=0 # 正常終了
readonly EXIT_WARN=1 # 警告終了(入力0件)
readonly EXIT_ARG=10 # 引数の誤り
readonly EXIT_LOCKED=20 # 同じジョブが実行中
readonly EXIT_DB=30 # SQLまたは接続のエラー
DIR="$(cd "$(dirname "$0")" && pwd)"
LOCK="${LOCK_FILE:-/tmp/job_s010.lock}"
log() { printf '%s [%s] JOB-S010 %s\n' "$(date '+%F %T')" "$1" "$2"; }
# 1) 対象日を決める(引数で渡せるようにしておくと、過去日のリランに使える)
TARGET="${1:-$(date -d yesterday +%F)}"
if ! [[ "$TARGET" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || ! date -d "$TARGET" >/dev/null 2>&1; then
log ERROR "対象日の形式が正しくありません: $TARGET"
exit "$EXIT_ARG"
fi
# 2) 二重起動を防ぐ(ロックを取れなければ何もせず終わる)
exec 9>"$LOCK"
if ! flock -n 9; then
log ERROR "同じジョブが実行中のため起動しません"
exit "$EXIT_LOCKED"
fi
log INFO "開始 対象日=$TARGET"
# 3) 集計(SQLの中で1トランザクション。途中で止まれば全体が取り消される)
if ! out="$(psql -X -q -At -v ON_ERROR_STOP=1 -v target="$TARGET" -f "$DIR/daily_sales.sql" 2>&1)"; then
log ERROR "集計が止まりました: $out"
exit "$EXIT_DB"
fi
# 4) 件数を確かめて終了コードを決める
if [[ "$out" -eq 0 ]]; then
log WARN "終了 出力0件(対象日の売上明細がありません)"
exit "$EXIT_WARN"
fi
log INFO "終了 出力=${out}件"
exit "$EXIT_OK"
2回実行した結果
./daily_sales.sh 2026-10-04; echo "exit=$?"
psql -c "SELECT sales_date, store_id, sale_count, total_amount FROM daily_sales ORDER BY 1,2"
./daily_sales.sh 2026-10-04; echo "exit=$?"
psql -c "SELECT sales_date, store_id, sale_count, total_amount FROM daily_sales ORDER BY 1,2"
2026-10-05 18:45:29 [INFO] JOB-S010 開始 対象日=2026-10-04
2026-10-05 18:45:29 [INFO] JOB-S010 終了 出力=3件
exit=0
sales_date | store_id | sale_count | total_amount
------------+----------+------------+--------------
2026-10-04 | S01 | 2 | 2000
2026-10-04 | S02 | 2 | 3500
2026-10-04 | S03 | 1 | 2400
(3 rows)
2026-10-05 18:45:29 [INFO] JOB-S010 開始 対象日=2026-10-04
2026-10-05 18:45:29 [INFO] JOB-S010 終了 出力=3件
exit=0
sales_date | store_id | sale_count | total_amount
------------+----------+------------+--------------
2026-10-04 | S01 | 2 | 2000
2026-10-04 | S02 | 2 | 3500
2026-10-04 | S03 | 1 | 2400
(3 rows)
1回目と2回目で結果は同じです。リラン手順に「同じ対象日で再実行する」と1行書けば足ります。
終了コードごとの動き
設計書に書いた終了コードが、そのとおり返ることを確かめます。
引数の形式が正しくないとき(終了コード10):
$ ./daily_sales.sh 2026-13-01; echo "exit=$?"
2026-10-05 18:45:29 [ERROR] JOB-S010 対象日の形式が正しくありません: 2026-13-01
exit=10
対象日の売上明細が0件のとき(終了コード1):
$ ./daily_sales.sh 2026-10-01; echo "exit=$?"
2026-10-05 18:45:29 [INFO] JOB-S010 開始 対象日=2026-10-01
2026-10-05 18:45:29 [WARN] JOB-S010 終了 出力0件(対象日の売上明細がありません)
exit=1
別のプロセスがロックを持っている間に起動したとき(終了コード20)。ここでは flock コマンドでロックを3秒間持たせています。
$ flock -n /tmp/job_s010.lock sleep 3 &
$ ./daily_sales.sh 2026-10-04; echo "exit=$?"
2026-10-05 18:45:30 [ERROR] JOB-S010 同じジョブが実行中のため起動しません
exit=20
存在しないデータベース名を指定したとき(終了コード30):
$ PGDATABASE=nodb ./daily_sales.sh 2026-10-04; echo "exit=$?"
2026-10-05 18:45:32 [INFO] JOB-S010 開始 対象日=2026-10-04
2026-10-05 18:45:32 [ERROR] JOB-S010 集計が止まりました: psql: error: connection to server at "127.0.0.1", port 5432 failed: FATAL: database "nodb" does not exist
exit=30
スクリプトの書き方のポイント
| 書き方 | ねらい |
|---|---|
set -euo pipefail |
コマンドのエラー、未定義の変数、パイプ途中のエラーで止める |
| 対象日を引数で受け取る | 過去日のリランを同じコマンドでできるようにする。処理の中で「今日」を使わない |
exec 9>"$LOCK" と flock -n 9 |
ロックを取れなければすぐ終わる。プロセスが終わるとロックは自動で外れる |
psql -X |
利用者ごとの設定ファイル(~/.psqlrc)を読まず、どの環境でも同じ動きにする |
-v ON_ERROR_STOP=1 |
SQLのエラーで止め、0以外の終了コードを返させる |
-q -At |
余分な表示を消し、件数だけを受け取る |
| ログに時刻・レベル・ジョブID・対象日を出す | あとから「いつ・どの日の分が・どう終わったか」を追えるようにする |
コミット単位の決め方と、途中で止まったときの状態
コミット単位は、止まったときにデータがどの状態で残るかを決めます。削除のあとに登録の前で止まる場合を、2通りで確かめました(3行目にわざとエラーになる文を入れています)。
BEGIN;
DELETE FROM daily_sales WHERE sales_date = :'target'::date;
SELECT 1/0; -- ここで止まる想定
INSERT INTO daily_sales (sales_date, store_id, sale_count, total_amount)
SELECT sold_at::date, store_id, count(*), sum(amount) FROM sales
WHERE sold_at >= :'target'::date AND sold_at < :'target'::date + 1
GROUP BY sold_at::date, store_id;
COMMIT;
トランザクションあり(上のSQL)で実行した結果です。削除が取り消され、3行とも残ります。
psql:stop_tx.sql:3: ERROR: division by zero
exit=3
残っている行数
----------------
3
(1 row)
BEGIN; と COMMIT; の2行を外して実行した結果です。削除だけが確定し、対象日の行が0件になります。
psql:stop_notx.sql:2: ERROR: division by zero
exit=3
残っている行数
----------------
0
(1 row)
後者の状態で後続のレポート出力が動くと、空のレポートができてしまいます。どちらの場合も、原因を取り除いて ./daily_sales.sh 2026-10-04 を実行すると3行に戻ることを確かめています。
コミット単位は次の3つから選びます。
| コミット単位 | 止まったときの状態 | リラン方式 | 向いている処理 |
|---|---|---|---|
| ジョブ全体で1回 | 実行前の状態に戻る | 先頭から再実行 | 件数が少なく、短時間で終わる集計 |
| 一定件数ごと(例:1,000件ごと) | 途中まで確定している | 途中から再開(どこまで済んだかの記録が必要) | 件数が多く、全体を1回で確定させると時間がかかりすぎる処理 |
| 1件ごと | 止まった1件の手前まで確定している | 途中から再開、またはエラーの1件をスキップ | 外部への送信など、1件ずつ結果が決まる処理 |
途中から再開する方式を選ぶときは、「どこまで済んだか」を残す場所(処理済みフラグの列、最後に処理したキーを持つ管理テーブルなど)も設計書の出力欄に書きます。
異常終了のパターン別:リラン方式の判断表
| 異常終了のパターン | データの状態 | 選ぶ方式 | 手順 |
|---|---|---|---|
| 引数の誤りで起動直後に終了(終了コード10) | 変更なし | 先頭から再実行 | 引数を直して起動する |
| 同じジョブが実行中(終了コード20) | 変更なし | 再実行しない | 先に動いているジョブの結果を確認する |
| SQL・接続のエラー、ジョブ全体で1回コミット(終了コード30) | 実行前に戻っている | 先頭から再実行 | 原因を取り除き、同じ対象日で起動する |
| シグナルで止められた(終了コード137・143)、ジョブ全体で1回コミット | 実行前に戻っている | 先頭から再実行 | 止めた理由を確認し、同じ対象日で起動する |
| 一定件数ごとにコミットする処理が途中で終了 | 途中まで確定 | 途中から再開 | 処理済みの記録を確認し、続きから起動する |
| 特定の1件だけがデータの不備でエラー | その1件の手前まで確定 | スキップ | その1件をエラー一覧に出して先へ進め、あとで個別に直す |
| 入力データが訂正された(ジョブは正常終了していた) | 以前の結果が入っている | 先頭から再実行 | 同じ対象日で起動し、後続ジョブも順に動かし直す |
| 先行ジョブが止まっていて入力がない(終了コード1) | 変更なし | 先行ジョブから再実行 | 先行ジョブをリランし、終わってから起動する |
3つの方式の選び方をまとめます。
| 方式 | 選ぶ条件 | 設計書に書いておくこと |
|---|---|---|
| 先頭から再実行 | ジョブが冪等で、全体をやり直しても時間内に終わる | 起動コマンドと引数 |
| 途中から再開 | 全体をやり直すと時間内に終わらない | 処理済みの記録の場所、再開の起動コマンド |
| スキップ | 1件の不備で全体を止めたくない。あとから1件ずつ直せる | スキップしてよい条件、上限件数、エラー一覧の出力先 |
迷ったら「先頭から再実行」で済む設計を第一候補にします。 手順がいちばん短く、夜間に対応する人が判断することが少なくなります。
レビュー用チェックリスト
- [ ] ジョブ一覧に、起動条件と先行ジョブが全ジョブ分書かれている
- [ ] 対象日などの処理範囲を、引数で指定できる
- [ ] 処理の中で「現在の日付」を直接使っていない(引数の省略時だけ使う)
- [ ] 入力と出力に、テーブル名・ファイル名とキーが書かれている
- [ ] 終了コードごとに、意味・後続ジョブの扱い・対応が決まっている
- [ ] SQLのエラーが終了コードに反映される(psql なら
ON_ERROR_STOP=1) - [ ] コミット単位と、途中で止まったときのデータの状態が書かれている
- [ ] 同じ引数で2回実行して、結果が変わらないことを確かめた
- [ ] 同時に2つ起動したときの動きが決まっている
- [ ] リラン手順が、コマンドをそのまま実行できる形で書かれている
- [ ] ログに、ジョブID・対象日・件数・終了の区分が出る
よくある質問
Q. リランと再実行は違うものですか? A. 同じ意味で使われます。この記事では、止まったジョブや結果を直したいジョブを動かし直すこと全般をリランと呼び、その方式を「先頭から再実行」「途中から再開」「スキップ」の3つに分けました。
Q. 警告終了(終了コード1)のとき、後続ジョブは動かしますか? A. ジョブごとに決めて、終了コード一覧の「後続ジョブ」の列に書きます。この記事の例では、集計結果が0件のときにレポートを作っても中身がないため、起動しない設計にしました。
Q. 削除してから登録する方式は、集計中にテーブルが空に見えませんか? A. 削除と登録を1つのトランザクションにしているので、コミットするまで削除は確定しません。途中で止まった場合に実行前の行がそのまま残ることは、上の「途中で止まったときの状態」で確かめたとおりです。
Q. ロックファイルは、ジョブが異常終了したら消す必要がありますか?
A. flock はファイルの有無ではなく、開いているファイルに掛けたロックで判断します。プロセスが終わればロックは外れるので、ファイルが残っていても次の起動に影響しません。上の実行例でも、ロックを持つプロセスが終わったあとは通常どおり起動できています。
まとめ
- バッチ設計書は「ジョブ一覧」と「ジョブ個別設計」の2段で書く
- 個別設計には起動条件・入出力・処理フロー・終了コード・コミット単位・リラン手順を必ず入れる
- 終了コードは意味・後続ジョブの扱い・対応をセットで一覧にする
- 同じ引数で2回実行しても結果が変わらない(冪等)ように作ると、リラン手順が1行になる
- 範囲で区切れる集計は「削除してから登録」を1トランザクションで行う。UPSERTは入力の行が減ったときに以前の行が残る
- psql をバッチから呼ぶときは
-v ON_ERROR_STOP=1を付け、SQLのエラーを終了コードに反映させる - リラン方式は「先頭から再実行」を第一候補にし、時間内に終わらない処理だけ「途中から再開」を選ぶ
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「バッチ設計書の書き方と項目一覧|リランと異常終了時の設計サンプル」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。