1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

rex0220 kSQL アクションプラグイン - ⑧ AI に請求データを作らせる(全体再計算で整合性を保つ)

1
Last updated at Posted at 2026-11-03

「rex0220 kSQL アクションプラグイン」の実践編です。

⑦ オーサリング編 では、チェックボックスに印を付けるだけの小さなアクションを AI に作ってもらいました。今回はもう一歩進めて、受注から請求データを作る処理を AI に作らせます。アプリを 3 つまたぎ、作り直しと削除まで含む、業務で実際に使う形です。

シリーズ: 概要 / ① 詳細画面 / ② 一覧 / ③ 集計 / ④ 運用 / ⑤ 事例 / ⑥ イベント / ⑦ オーサリング / ⑧ 請求データ(この記事) / ⑨ 締めの画面 / ⑩ どこに出すか / ⑪ 多言語 / ⑫ オーサリング改善

今回作るもの

受注アプリから、顧客 × 請求月ごとの請求データを作ります。

ボタンは 2 つです。

ボタン いつ使うか
請求データを作成・更新 受注を 1 件直したとき。その顧客 × 請求月だけを集計し直す
請求を全体再計算 月末など。その請求月ぜんぶを受注から作り直す

受注アプリの一覧パネル

なぜ「全体再計算」なのか

kintone には、複数レコード・複数アプリにまたがるトランザクションがありません。 請求データを 5 件書いている途中で失敗したら、3 件だけ書かれた状態で止まります。

ここで「失敗した 1 件を追いかけて直す」方式にすると、作りも運用も複雑になります。この記事では別の考え方を採ります。

受注を正本とし、未確定の請求は「いつでも作り直せる派生データ」として扱う。おかしくなったら、その月をまるごと作り直す。

こうすると、次のどれが起きても同じ 1 つのボタンで直せます。

  • 通常処理の途中で失敗した
  • 受注の金額や顧客を後から直した
  • 請求対象から外した受注がある
  • もう対象でなくなった請求データが残っている

そのかわり、作り直してはいけないデータ(確定済み・発行済み)は、はっきり分けておく必要があります。この記事では確定したら別のアプリに移すことで分けました。

アプリ

アプリ 役割 主な項目
受注 正本 受注日・顧客コード・顧客名・金額・請求対象月・請求対象(対象 / 対象外)
請求(未確定) 派生データ 請求キー(重複禁止)・顧客コード・顧客名・請求月・件数・金額・集計日時
請求確定 確定したもの 同じ項目 + 確定日時

請求キーは 顧客コード-請求月 です(C001-2026-10)。UPSERT のキーになるので、請求アプリ側で「値の重複を禁止する」にしておきます。

請求対象月は、受注日から計算せず項目として持ちます。締めの都合で月をずらすことが実務ではよくあるためです。

Claude Code に頼む

kSQL アクションの設定を作ってください。

アプリ 4387「受注」から、アプリ 4400「請求(未確定)」に請求データを作ります。
請求は 顧客コード × 請求対象月 の単位で、請求キーは「顧客コード-請求月」です。
金額は受注の金額の合計、件数は受注の件数です。
請求対象が「対象外」の受注は数えません。

ボタンは 2 つ欲しいです。
1. 受注の詳細画面で、その受注の顧客 × 請求対象月を集計し直すボタン
2. 一覧から、指定した請求月をまるごと作り直すボタン(請求月は実行時に入力)

2 のときは、受注が無くなった顧客の請求データも消してください。
アプリ 4401「請求確定」に同じ請求月のデータがあるときは、1 も 2 も実行できないようにしてください。

アプリ番号・項目コード・業務ルールはこちらが書きます。AI はこれらを推測しません。

AI が作った SQL

通常処理(1 件ぶん)

-- @ksql dialect: 1
DECLARE @cust = '';
DECLARE @m = '';
DECLARE @key = '';

-- 0) 確定済みの月は触らない(確定アプリに移したものを未確定に復活させない)
ASSERT (SELECT COUNT(*) FROM LAPP_請求確定 WHERE 請求月 = @m) = 0, 'この請求月は確定済みです。作成・更新できません';

-- 1) その顧客 × 請求月を集計し直す(対象が 0 件でも 1 行(0 埋め)が書かれる)
UPSERT INTO LAPP_請求 (請求キー, 顧客コード, 顧客名, 請求月, 件数, 金額, 集計日時)
SELECT @key AS 請求キー, @cust AS 顧客コード, MAX(顧客名) AS 顧客名, @m AS 請求月,
       COUNT(*) AS 件数, SUM(金額) AS 金額, @NOW() AS 集計日時
  FROM LAPP_受注
 WHERE 顧客コード = @cust AND 請求対象月 = @m AND 請求対象 = '対象'
ON DUPLICATE (請求キー);

-- 対象が 0 件になったら、未確定の行を残さない
DELETE FROM LAPP_請求 WHERE 請求キー = @key AND 件数 = 0;

パラメーターは 3 つとも計算式です。

名前 計算式
cust 顧客コード
m 請求対象月
key 顧客コード & "-" & 請求対象月

GROUP BY を使わず @cust AS 顧客コード と書くのが要点です。こうすると集計元が 0 件でも 1 行(0 埋め)が書かれるので、「最後の受注を対象外にした」ときに古い請求が残りません。0 になった行は次の DELETE が片づけます。

全体再計算

-- @ksql dialect: 1
DECLARE @m = '';

-- 0) 確定済みの月は作り直さない(確定アプリに移したものを復活させない)
ASSERT (SELECT COUNT(*) FROM LAPP_請求確定 WHERE 請求月 = @m) = 0, 'この請求月は確定済みです。再計算できません';

-- 1) 集める。受注を顧客ごとに集計し、今の請求を「0 の行」として足す
CREATE TEMP TABLE #raw AS
SELECT 顧客コード, MAX(顧客名) AS 顧客名, COUNT(*) AS 件数, SUM(金額) AS 金額
  FROM LAPP_受注 WHERE 請求対象月 = @m AND 請求対象 = '対象'
 GROUP BY 顧客コード
UNION ALL
SELECT 顧客コード, '' AS 顧客名, 0 AS 件数, 0 AS 金額
  FROM LAPP_請求 WHERE 請求月 = @m;

-- 2) まとめる(kintone には行かない)
CREATE TEMP TABLE #agg AS
SELECT 顧客コード, MAX(顧客名) AS 顧客名, SUM(件数) AS 件数, SUM(金額) AS 金額
  FROM #raw GROUP BY 顧客コード;

-- 3) 書き込む(受注が無くなった顧客は 0 の行だけが残るので 0 で更新される)
UPSERT INTO LAPP_請求 (請求キー, 顧客コード, 顧客名, 請求月, 件数, 金額, 集計日時)
SELECT CONCAT(顧客コード, '-', @m) AS 請求キー, 顧客コード, 顧客名, @m AS 請求月,
       件数, 金額, @NOW() AS 集計日時
  FROM #agg
ON DUPLICATE (請求キー);

-- 4) 0 のまま残った行(もう対象でない)を消す
DELETE FROM LAPP_請求 WHERE 請求月 = @m AND 件数 = 0;

この形が今回いちばん伝えたいところです。

  1. 集める — 受注を顧客ごとに集計し、今の請求を「件数 0・金額 0 の行」として足す
  2. まとめる — 足し合わせる。受注が無くなった顧客は 0 の行だけなので 0 になる
  3. 書き込む — 0 になった顧客も含めて UPSERT
  4. 消す — 0 のまま残った行(もう対象でない顧客)を消す

「0 の行を足す」のが肝です。 GROUP BY で集計すると、元が 0 件になったキーは1 行も返りません。放っておくと、最後の受注を対象外にした顧客の請求が古い金額のまま残ります。今の請求を 0 の行として混ぜておけば、その顧客も「0 件」として結果に現れます。

この形はシリーズを通して同じです(③ 集計編 / ⑤ 事例編 / ⑥ イベント編)。

「先に 0 にする」形はやめた

最初は、先に UPDATE で 0 を埋める形で書いていました。

-- 旧: 2 文で同じことをしていた
UPDATE LAPP_請求 SET 件数 = 0, 金額 = 0 WHERE 請求月 = @m;
UPSERT INTO LAPP_請求 (…) SELECT … FROM LAPP_受注 WHERE … GROUP BY 顧客コード ON DUPLICATE (請求キー);

結果は同じですが、UNION ALL の形に替えて 3 つ良くなりました。

先に 0 にする 0 の行を UNION ALL
書き込み 2 倍(0 にする N + 書き直す N) N
途中で失敗したら 0 のまま残る 前の値のまま(0 にする文が無い)
レコードの更新履歴 1 回の再計算で 2 行(途中に「0 円」が残る) 1 行

3 つ目は実際の履歴を見るのがいちばん早いです。同じ 21:01 の 4 → 5 が旧の形で、1 回の再計算で「999,999 円 → 0 円」「0 円 → 185,000 円」と 2 行付いています。8:27 の 6 が新しい形で、1 行だけです。

レコードの変更履歴(旧は 1 回で 2 行、新は 1 行)

読む量は変わりません。先に 0 にする形も請求を読んでから更新するので、kintone に行く回数は同じです。減るのは書き込みのほうです。

「もう対象でなくなったデータ」を消すのに、NOT IN (SELECT …) を使っていません。理由は次の節のとおり、使えないからです。

つまずいたところ

NOT IN (SELECT …) は DELETE の条件に書けない

先に断っておくと、NOT IN そのものは書けます。書けないのはサブクエリを中に入れた形だけです。

書き方 書き込む文の WHERE
確定 NOT IN ('確定')(値のリスト) ○ kintone の not in にそのまま乗ります
顧客コード NOT IN @keys(配列) ○ ただし空配列は全件に当たります
顧客コード NOT IN (SELECT …)(サブクエリ) × この節の話

最初はこう書こうとしました。

-- これは動きません
DELETE FROM LAPP_請求
 WHERE 請求月 = @m AND 顧客コード NOT IN (SELECT 顧客コード FROM LAPP_受注 WHERE 請求対象月 = @m);

設定の検査(kact_check)は通ります。ところが実行計画を見ると止まります。

WHERE predicate cannot be represented by kintone REST
  (field=顧客コード, type=SINGLE_LINE_TEXT, operator=not in, reason=WHERE_RESIDUAL)

kSQL がこの形を実装していないためです。別アプリのサブクエリでも、一時テーブルに貯めてからでも同じでした(一時テーブルのほうは temp table references in DELETE are not supported yet と出ます)。

ここは SQL として書ける形(言語仕様)と、kintone の REST にどう変換するか(実装)が別だということです。だから kact_check は通り、実行計画で初めて分かります。書き込みの WHERE に kintone へ渡せない条件が残ると(reason=WHERE_RESIDUAL)、JavaScript 側で絞るしかなく、それは全件取得して削除することになるのでエンジンが止めます。

将来この変換が入れば書けるようになる類のものですが、今は書けません。だから「0 の行を足して集計し、0 になった行を消す」形に落ち着きました。消す条件が 件数 = 0 という自分のアプリの項目だけになるので、kintone の絞り込みにそのまま渡せます。

WHERE に関数を書くと実行時に落ちる

これも検査は通り、実行計画で分かる類のものです。

-- × 実行時に DmlConvertError
DELETE FROM LAPP_請求 WHERE 請求キー = CONCAT(@cust, '-', @m) AND 件数 = 0;

-- ○ キーは計算式で組んで渡す
DELETE FROM LAPP_請求 WHERE 請求キー = @key AND 件数 = 0;

SELECT の列に書く CONCAT() は問題ありません。 関数が書けないのは WHERE の側です。私は全体再計算の実行計画だけを見て通常処理のほうを見ていなかったので、実機で初めて気づきました。実行計画はアクションごとに見てください。

実行計画で確かめる

取り込む前に kact_explain で見ます。全体再計算はこうなりました。

[ASSERT]         kintone query: 請求月 = "@m" limit 1
                 fetch: COUNT_ONLY / REST execution: single GET
[CREATE TEMP #raw]
  [union:1] 受注  kintone query: 請求対象月 = "@m" and 請求対象 in ("対象")
                 pushdown applied / group key 顧客コード: PHYSICAL
  [union:2] 請求  kintone query: 請求月 = "@m"
[CREATE TEMP #agg]
                 records API: none(手元の計算。kintone には行かない)
[UPSERT SELECT]  一時テーブル #agg → 照合してから insert + update
[DELETE]         kintone query: 請求月 = "@m" and 件数 = 0
                 selection: exact native pushdown; JS residual none

kintone に行く文はすべて条件が押し下がっています(pushdown applied / exact native pushdown)。つまり全件を取ってから JavaScript で絞ってはいません。月単位の処理なので、受注が増えても読む件数は 1 か月ぶんです。

#agg の行に records API: none と出ているのが、集計は手元の計算で、kintone の読み書きを増やさないことの確認です。GROUP BY を 2 回書いても API は増えません。

UNION ALL の前に GROUP BY 顧客コード を書いても、実行計画のとおり前の枝(受注)にだけ掛かります。受注を先に顧客ごとにまとめておくことで、一時テーブルに積む行が「受注の件数」ではなく「顧客の数」で済みます(一時テーブルの上限は 1 表 10,000 行が既定。共通設定で変えられます)。

なお、これは静的な計画です。実際に何件書き換わるか、権限エラーが出ないかは分かりません。

動かす

一覧の[請求を全体再計算]を押すと、請求月を聞かれます。ここでは 2026-11(受注 10 件・顧客 5 件の月)で動かします。

請求月の入力

書き込む文ごとに確認が出ます。

確認(ステップ 1/2)

「ステップ 1 / 2」は、書き込む 2 つの文のうち 1 つ目という意味です。件数が 0 の文では確認が出ません。消す行が無ければステップ 2 は飛びます。

結果(追加 0 / 更新 5 / 削除 0)

できた請求データ

試した結果

実際に動かした記録です(受注 10 件・顧客 5 件の小さな月。200 件の場合は次の節)。

試したこと 結果 時間
初回作成(請求がまだ 0 件) 追加 5 / 更新 0 / 削除 0 1 秒
同じ処理をもう一度 追加 0 / 更新 5 / 削除 0 2 秒
受注の金額を 150,000 → 999,000 に変更 更新 5。請求が 185,000 → 1,034,000 に 0.9 秒
受注 1 件を「対象外」に 更新 5 / 削除 1(その顧客の請求が消えた) 1 秒
その受注を「対象」に戻す 追加 1 / 更新 4 / 削除 0(請求が戻った) 2 秒
通常処理(詳細画面から 1 件) 更新 1(その顧客だけ) 1 秒
確定済みの月を再計算 中止(書き込み 0) —

受注 1 件を対象外にしたとき(更新 5 / 削除 1)

200 件でも試す

小さな月では速くて当たり前なので、受注 約 200 件・顧客 21 件(うち 1 件は対象外)の月(2026-12)も測りました。

200 件の確認(ステップ 1 / 2:21 件 更新)

試したこと 結果 時間
同じ処理をもう一度(受注 約 200 件 → 請求 21 件) 追加 0 / 更新 21 / 削除 0 2 秒

200 件の結果

小さな月と変わりません。 読む受注が 10 件から 200 件に増えても、kintone の取得は 1 回(500 件まで)で済み、書き込む請求は顧客の数(21 件)だけだからです。この処理で効くのは受注の件数より顧客の数で、顧客が 100 を超えたあたりから書き込みの回数(100 件ごと)が増えていきます。

更新の件数が顧客の数とちょうど同じなのが、UNION ALL の形にした効果です。先に 0 を埋める形だと、ここが 42 件(0 にする 21 + 書き直す 21)になります。

ただしこれも小さな部類です。 読み取りの上限は既定 10,000 件で、共通設定の「読み取りの上限件数(1 文)」で 100,000 件まで上げられます。それを超える規模では、月をさらに分けるか kSQL Flow 側で回すことになります(月を分けて続けて流すのは、後述の「ループ」で 1 ボタンにできます)。

共通設定 既定 変えられる範囲
読み取りの上限件数(1 文) 10,000 1 〜 100,000
一時テーブルの上限行数(1 表) 10,000 1 〜 100,000
一時テーブルの合計行数 20,000 1 〜 200,000

上げれば読めますが、1 回の実行が長くなり、kintone の API も使います。ブラウザーの中で動く処理なので、上げる前に「月をもっと細かく分ける」「GROUP BY で先に集計して一時テーブルを小さくする」で足りないかを見てください。

2 行目が「再実行しても重複しない」ことの確認です。更新 5 で、レコードは増えていません。

4 行目と 5 行目が「0 の行」の効き目です。対象外にすると請求が消え、対象に戻すと請求が戻ります。どちらも同じ 1 つのボタンで、NOT IN も「先に消す」文もありません。削除は確認の「ステップ 2 / 2:レコードを 1 件 削除します」を経るので、消える件数を見てから実行できます。

確定したものは守る

確定すると、未確定アプリから確定アプリへ移ります。その月をもう一度再計算しようとすると、止まります。下は 2026-10 を確定済みにしてあるときの画面です。

確定済みの月は再計算できない

請求を全体再計算: この請求月は確定済みです。再計算できません
書き込みはしていません。

歯止めは SQL の先頭の 1 行だけです。

ASSERT (SELECT COUNT(*) FROM LAPP_請求確定 WHERE 請求月 = @m) = 0, 'この請求月は確定済みです。再計算できません';

ASSERT のメッセージは画面に出ます(このプラグインで SQL の文面を画面に出す唯一の例外です)。書き込みは 1 件も起きません。2 行目の「書き込みはしていません。」はプラグインが付けます(途中まで書いて止まったときは、書いた件数が出ます)。

通常処理にも同じ歯止めが要る

最初はここを間違えました。 全体再計算にだけ ASSERT を置いて、通常処理(1 件ぶんのボタン)には置いていなかったのです。

それだと、確定して別アプリに移した後で受注の詳細画面からボタンを押すと、その顧客の請求だけが未確定アプリに復活します。確定を分けた意味がなくなります。

確定済みの月は通常処理も止まる

通常処理の SQL にも同じ 1 行を置いて、こうなりました。

請求データを作成・更新: この請求月は確定済みです。作成・更新できません
書き込みはしていません。

未確定の月(2026-11)では、これまでどおり動きます(更新 1 件・2 秒)。

確定済みのデータを守るときは、「書き込む口」を全部ふさいでください。 入口が 2 つあるなら、歯止めも 2 つ要ります。

アプリを分けずに「確定フラグ」ではどうか

「確定アプリを別に作らず、請求アプリに確定フラグを 1 つ持たせればいいのでは」と聞かれました。もっともな案です。確定も取り消しもずっと軽くなります。

アプリを分ける(この記事) 1 アプリ + 確定フラグ
確定する 確定アプリに INSERT … SELECT して元を消す UPDATE 請求 SET 確定 = '確定' WHERE 請求月 = @m の 1 文
確定を取り消す 確定アプリのレコードを消す(削除の権限が要る) フラグを戻すだけ
アプリの数 2 つ。項目を足すときは両方 1 つ

確定と取り消しの軽さは、はっきりフラグの勝ちです。

試したら、確定した行が書き換わった

請求アプリに 確定 を足して、C001 の 2026-11 を確定にし、金額を 999,999 円・件数を 99 件に変えてから、この記事の全体再計算をそのまま動かしました。

確定にした行はこうなりました。

確定 件数 金額
実行前 確定 99 999,999
実行後 確定 2 185,000

フラグは残ったのに、中身だけ元に戻りました。エラーも警告も出ません。

理由は、どの文にもフラグの条件を書いていなかったことです。0 の行は確定済みも拾いますし、UPSERT の ON DUPLICATE (請求キー) はキーだけで突き合わせるので、相手が確定済みでも更新の対象になります。確定フラグはただの項目で、書いていない条件を SQL が察してくれるわけではありません。

守りは書ける。ただし 3 か所

書けないわけではありません。こう書けば確定した行は守れます(検査も実行計画も通りました)。

-- 1) 集める。守り① 確定済みの顧客は受注から数えない
--             守り② 0 の行に確定済みを混ぜない
CREATE TEMP TABLE #raw AS
SELECT 顧客コード, MAX(顧客名) AS 顧客名, COUNT(*) AS 件数, SUM(金額) AS 金額
  FROM LAPP_受注
 WHERE 請求対象月 = @m AND 請求対象 = '対象'
   AND 顧客コード NOT IN (SELECT 顧客コード FROM LAPP_請求 WHERE 請求月 = @m AND 確定 IN ('確定'))
 GROUP BY 顧客コード
UNION ALL
SELECT 顧客コード, '' AS 顧客名, 0 AS 件数, 0 AS 金額
  FROM LAPP_請求 WHERE 請求月 = @m AND 確定 NOT IN ('確定');

-- 2) まとめる・3) 書き込む は同じ

-- 4) 消す。守り③ 確定済みは消さない
DELETE FROM LAPP_請求
 WHERE 請求月 = @m AND 件数 = 0 AND 確定 NOT IN ('確定');

ドロップダウンなので <>(!=)ではなく NOT IN です(記事⑥と同じ決まり)。守り③ の DELETE に NOT IN が書けているのは、中身が値のリスト(('確定'))だからです。書けないのはサブクエリの形だけ、というのが前の節の話です。

代わりに失うものがあります。守り① はサブクエリなので JavaScript 側の判定になり(実行計画の WHERE_RESIDUAL)、受注の取得が EXACT から PREFILTERED に落ちて、請求アプリを 2 回読みます(守り① のサブクエリと、守り② の 0 の行)。守り③ の DELETE は exact native pushdown のまま、kintone の条件にそのまま乗ります。

そして何より、3 か所のうち 1 か所でも書き忘れると、黙って確定データが壊れます。さきほどの ASSERT の書き忘れを思い出してください。あれはアプリを分けていたので、起きたのは「未確定が復活する」ことだけで済みました。

守れるのも 1 回の実行の中だけです。3 か所の条件はどれも、その文を実行する時点のフラグを読みます。再計算の最中に別の人が確定すると取り合いになり、これは条件では防げません(後の「ASSERT は排他制御ではない」と同じ話です)。

どちらを選ぶか

アプリを分けると、全体再計算は未確定アプリにしか書きません。確定データには SQL が届かないので、条件の書き忘れが事故になりません。権限も分けられます(確定アプリは閲覧だけにする、削除を誰にも許さない、など)。ASSERT は二重の守りになります。

  • 確定の取り消しが日常的に起きる、請求が月に数件 → 1 アプリ + フラグで十分。ASSERT を全アクションに置き、3 か所の条件を必ず書く
  • 確定したら動かさない、再計算は気軽に回したい → アプリを分ける(この記事)

「作り直せる派生データ」と「作り直してはいけない正本」を物理的に分けることが、再計算を怖がらずに回せる理由そのものです。

SQL とパラメーターの不備で、大量に書き換えないために

全体再計算は「その月をまるごと作り直す」処理です。請求月を打ち間違えれば、別の月を作り直します。確定済みは ASSERT で守れますが、守れるのは確定済みだけで、打ち間違いそのものは止まりません。

歯止めは 4 段あります。今回の 2 つのアクションでは、こう使い分けました。

段 何を防ぐか 今回の設定
① 入力の必須 空のまま実行して、空の条件で広く当たること 全体再計算の「請求月」は必須。空では実行できません
② 実行条件(計算式) レコードの項目が空のまま押すこと 通常処理は AND(請求対象月 <> "", 顧客コード <> "")。満たさないと押しても止まり、理由が出ます
③ 書き込み上限 桁違いに多いときに、書く前に止める どちらも 300 件。超える文は書かずに中止します
④ 書き込みの確認 件数を見てから決める 全体再計算は確認あり(「レコードを 21 件 更新します」)

いちばん効くのは ③ です。 確認(④)は人が見落としますが、上限は機械が止めます。通常処理のように確認を出さない運用にするなら、上限の設定は必須になります。

① と ② は「空の条件で広く当たる」ための備えです。WHERE 請求月 = @m の @m が空なら、請求月が空のレコードに当たります。業務データでは空の行がまとまって存在しがちなので、空を通さないのが先です。

一覧の一括では、空配列の NOT IN は全件になります。「選択が空なら何もしない」は IN @_selected 側で書いてください(② 一覧編)。

そして取り込む前に kact_explain です。パラメーターが条件に効いていなければ kintone query: (全件取得) と出ますし、estimated API consumption の read で読む量の見積もりも分かります。今回の NOT IN も CONCAT() も、検査では分からず実行計画で分かりました。

詳しくは ④ 運用編(ログアプリ・二重実行の防止・権限)

何か月分もまとめて作り直す(ループ)

全体再計算は「その月をまるごと作り直す」処理です。3 か月分をやり直したければ、月を変えて 3 回押せば済みます。ループを使うと、それが 1 つのボタンになります。

ループは「同じ SQL を回数ぶん流す」仕組みです。設定の「条件とメッセージ」に ループ の式を書きます。

書いた式 流れる回数 回の値
空 1 回(ふつうのアクション) —
3 3 回 0・1・2
ARRAY("2026-09", "2026-10", "2026-11") 3 回 その要素

回の値は、パラメーターの計算式で $val として受け取ります。今回の全体再計算なら、請求月のパラメーターを書き換えるだけです。

入力で聞く形(これまで) ループで回す形
ループ (空) ARRAY("2026-09", "2026-10", "2026-11")
パラメーター m 入力項目「請求月」 計算式 $val

入力で聞く形でも、候補を計算式で出せます。ARRAY(DATE_ADD("TODAY", -1, "month", "YYYY-MM"), DATE_FORMAT("TODAY", "YYYY-MM")) と書けば、前月と当月から選ばせる形になり、打ち間違いが起きません(① 詳細画面編)。

SQL は 1 文字も変えません。 @m に入る値が回ごとに変わるだけです。

押すと、確認は実行の前に 1 回だけ出ます。

3 回に分けて実行する確認

実行中は回の数で進み、中止で止められます。

実行中(1 / 3 回・中止)

ループ値は必ず WHERE に入れる

ループが決めるのは「分ける軸」だけで、分ける仕事をするのは SQL の WHERE です。 今回の SQL は 4 つの文すべてが 請求対象月 = @m か 請求月 = @m で絞ってあるので、1 回ぶんはその月だけを読み、その月だけを書きます。

軸を入れ忘れると、こうなります。

書き忘れ 何が起きるか
読み取りの WHERE に入っていない 同じ量を回数ぶん読むだけ。分けた意味がありません
書き込みの WHERE に入っていない 後の回が前の回の結果を壊します(3 か月ぶん流したのに、最後の月しか残らない)

どちらも設定画面の[SQL を検査]が注意として出します。保存は止めませんが、出たら書き直してください。

効き目は「読み取りの上限が 1 回ぶんになる」こと

前に「読み取りの上限は既定 10,000 件、100,000 件まで上げられる」と書きました。ループを使うと、その上限が「1 回ぶん」の天井に変わります。上限を上げずに、分けた回数だけ扱えます。

⑤ 事例編の 10 年・9.4 万件の集計で試したところ、読み取りの上限は既定の 10,000 件のまま、10 年分(年ごとに 10 回)が通りました。まとめて 1 回で流せば 2 年目で上限に当たる量です。kintone の API を呼ぶ回数は「10 回押す」のと同じで、増えも減りもしません。

守り

  • 書き込み上限は起動 1 回の合計のまま(回ごとではありません)。上の表の③ がそのまま効きます
  • ループを使うときは、その書き込み上限が必須になります。確認が回ごとに出なくなるぶん、止める役が上限だけになるからです
  • 確認は実行の前に 1 回だけ出ます(「3 回に分けて実行します」)
  • 実行中は「2 / 3 回 …」と進み、中止を押せば回の切れ目で止まります
  • 1 つの対象あたりの回数(既定 100 回)と、起動 1 回の合計の回数(既定 1,000 回)にも上限があります(共通設定の「制限」)

確定済みの月が混ざったら

今回の SQL は、先頭の ASSERT で「確定済みの月は作り直さない」を守っています。ループで 3 か月を回すと、確定済みの月の回だけが止まります。残りをどうするかは、設定の「ループの回が失敗したとき」で選べます。

設定 途中の回が確定済みの月だったら
停止(既定) その回で止まり、後ろの回は流れません
続行 その回を失敗にして、後ろの回も流します。最後に「失敗あり」で終わります

下は、3 回のうち最後の回が確定済みの月だった例です。前の 2 か月は作り直され、最後の回だけが ASSERT で止まりました。

確定済みの月で止まる

どちらでも書いたものは残ります(確定済みより前の回は作り直されたままです)。ログには「3 回のうち 成功 2 / 失敗 1 / 未処理 0」のように回の内訳が残るので、どこで止まったかは後から分かります。

途中で止まったら

ループもトランザクションではありません。3 か月ぶんのうち 2 か月で止まることがあります。

今回の形(今のデータを 0 の行として足して UPSERT、0 を消す)は何度押しても結果が同じなので、もう一度押せば直ります。素の INSERT で書く形だと、押し直すたびに増えます。

回どうしが重ならないことも大事です。 今回は月で分けているので重なりません。どの回も「自分の月だけを読み、自分の月だけを書く」形になっているかを、kact_explain で確かめてください。

この方式で直せること・直せないこと

全体再計算で直るか
通常処理の途中で失敗した 直る(その月を作り直す)
受注の金額・顧客を後から直した 直る
請求対象から外した受注がある 直る(0 のまま残った行が消える)
もう受注が 1 件も無い顧客の請求が残っている 直る
全体再計算そのものが途中で失敗した もう一度押す(何度押しても同じ結果になる形なので安全)
再計算の最中に誰かが受注を直した 直らない。その受注は次の再計算で反映される
確定済みのデータ 対象外(止まる)
確定処理そのものが途中で失敗した 直らない(下記)

全体再計算もトランザクションではありません。 途中で失敗したら、もう一度押してください。どの文も「何度流しても同じ結果」の形で書いてあるので、途中からでも最後まででも、結果は変わりません。

確定処理の途中失敗は、再計算では直せない

確定(未確定 → 確定アプリへ移す)も 2 文あるので、途中で止まることがあります。たとえば確定アプリに 2 件書いたところで失敗すると、確定アプリに 2 件・未確定アプリに 5 件という状態になります。

この状態では、全体再計算は「確定済みがある」と見なして止まります。つまりこの傷は全体再計算では直りません。確定アプリと未確定アプリを見比べて、確定処理をやり直すなどの手当てが要ります。

これは全体再計算の弱点というより、確定済みを再構築の対象から外したことの裏側です。「作り直してよいもの」と「作り直してはいけないもの」を分けた以上、後者は人が面倒を見ることになります。この記事ではここまでを境界として示し、復旧のしくみは作っていません。

ASSERT は排他制御ではない

ASSERT は実行を始めた時点の確認です。再計算を始めた後に別の人が確定を実行すれば、どちらも条件を通ります。月次の再計算と請求確定は、同時に走らせない運用にしてください(④ 運用編 のログアプリのロックを併用すると、同じアクションの二重実行は防げます)。

なお、途中で 0 円の行が見えることはありません。UNION ALL の形には「いったん 0 にする」文が無く、書き込みは UPSERT の 1 回だけだからです。途中で失敗しても、書けなかった顧客は前の値のまま残ります。

結果の表示はダイアログに出るだけなので、月次の締めでは実行した人が結果を確かめ、失敗していたら押し直すところまでを手順にしてください。押し直しは何度でも安全です。

kSQL アクションと kSQL Flow の使い分け

このプラグインは画面の操作で動きます。件数にも上限があります。

kSQL アクション(この記事) kSQL Flow / kSQL ジョブ実行
起動 ボタン・保存イベント スケジュール・外部連携
読み取り 既定 10,000 件(共通設定で最大 100,000 件) 大量データ向け
一覧の一括 既定 300 件 —
向いている処理 月次の締め・現場の押し直し 毎晩の全件バッチ・他システム連携

「月末に担当者が押す」ならこのプラグインで足ります。 毎晩自動で全件を回すなら Flow 側の出番です。REST API や CSV 読み込みではイベントが動かないので、外部システムからの取り込みがあるアプリでは、月次の全体再計算を併用すると取りこぼしを拾えます。

まとめ

  • kintone にトランザクションが無くても、正本を決めて派生データを作り直せる形にすれば整合性は保てます
  • 「今のデータを 0 の行として足してから集計する」形にすると、更新も削除も何度押しても結果は同じになります。先に 0 を埋める形は勧めません。書き込みが 2 倍になり、途中で失敗すると 0 のまま残り、レコードの更新履歴にも余計な 0 更新が付きます
  • NOT IN (SELECT …) は DELETE の条件に書けません(値のリストや配列の NOT IN は書けます)。実行計画(kact_explain)をアクションごとに見てください
  • 確定したものは別アプリに移し、ASSERT で止めます。ただし書き込む口は全部ふさぐこと(全体再計算だけでなく、1 件ぶんのボタンにも要ります)
  • 確定処理そのものの途中失敗は、全体再計算では直りません。ASSERT は排他制御でもないので、再計算と確定は同時に走らせない運用にします
  • 打ち間違いは ASSERT では止まりません。入力の必須・実行条件で空を通さず、書き込み上限で桁違いを機械的に止めます
  • 何か月分もまとめて作り直すなら「ループ」です。パラメーターの式を $val にするだけで、SQL は変えません。ループ値を WHERE に入れるのと、書き込み上限を入れるのが条件です
  • ここまでの SQL と設定は、日本語の依頼から AI(kact MCP)が作りました。検査と実行計画を通してから、利用者が取り込みます

関連記事

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?