0
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?

【kSQL 実践 #0】kintone を SQL で扱う前に知る、標準 SQL との差分と実行モデル

0
Posted at

結論(3 行)

  • kintone に JOIN・GROUP BY・ウィンドウ関数・一括更新を SQL で持ち込めます。REST の 500 件ページングも SQL の裏に隠れます
  • ただし kintone は SQL データベースではありません。JOIN は等式 1 本、LIKE は JavaScript、数値は比較だけ 10 進厳密、トランザクション無し——差分は方言として明示されています
  • この記事は SQL を書ける人向けに、その差分の早見表と、SQL が kintone の REST API にどう翻訳されるか(EXPLAIN)を 30 分で読める形にまとめたものです

kSQL プラグインの実行画面。顧客管理と案件管理を JOIN した SQL と結果表

この記事の前提

対象は SQL を書ける開発者です。kintone のカスタマイズやプラグイン、データ連携、運用スクリプトを書いていて、次のどれかで困ったことがある人を想定しています。

  • 複数アプリをまたぐ集計を、REST で全件取ってから自前で突き合わせている
  • レコードの一括更新に dry-run が無く、本番前の確認が「目視」しかない
  • 「月別推移」「上位 N 社」「案件の無い顧客」のような集計を、kintone の一覧とグラフで作り切れない

逆に、SELECTGROUP BY の意味は書きません。書くのは標準 SQL との差分実行の仕組みだけです。

題材は kintone 公式のサンプルアプリ「SFA(営業支援)パック」(顧客管理・担当者管理・案件管理・活動履歴の 4 アプリ)です。
アプリストアから「サンプルデータを含める」をオンにして追加すると、そのまま使えます。
記事中のアプリ番号(顧客管理 APP4148、案件管理 APP4149)は私の環境のものなので、読み替えてください。
なお、件数や取得回数の実測は、第 6 回で扱う手順(CSV を IMPORT で流し込む)で顧客管理を 215 件に増やした環境で取っています。素のパック(顧客 9 件・案件 20 件)でも SQL はすべてそのまま動きます。

私は kintone を SQL 風の構文で操作する
kintone-sql-tools(kSQL)を開発しています。
以下は v3.77.0 時点の内容です。なお kSQL は Apache Kafka の ksqlDB とは無関係の別ツールです。

1. 実行モデル — SQL は REST API の呼び出し計画に翻訳される

kintone にはクエリエンジンがありません。kSQL がやっているのはこれです。

  1. SQL を解析し、kintone のクエリ文字列に変換できる条件を選り分ける
  2. アプリごとに records API を呼ぶ(通常は GET /k/v1/records.json のページング。KORDER BY の大規模窓では Cursor API)。変換できた条件はクエリに載せ、サーバー側で絞る(押し下げ
  3. 返ってきたレコードをインメモリで JOIN・集計・並べ替えする
  4. 集計・GROUP BYDISTINCT・ローカル並べ替えのように完全な入力が要るクエリは、上限(maxRecords)に達したら部分結果を返さずエラーで止まる(fail-closed)。単純な明細だけが onLimit の設定に従う

流れを 1 枚にするとこうなります。

fetch summary は、この分析と取得計画の結果です。WHERE 全体を kintone のクエリへ忠実に変換できれば EXACT
全体は変換できなくても AND でつながった安全な条件を一部押し下げられれば PREFILTERED(押し下げとインメモリ再評価の両方が働く)、
押し下げられる条件が無ければ ALL(それでも records API で全件取る)です。
JOIN や UNION ではソースごとに fetch が決まり、fetch summary はそれらを要約した値です。
JOIN ではさらに、先に取れた側の結合キーで相手アプリを絞る最適化が INNER JOIN にだけ加わります(第 1 回)。

つまり性能と安全性は「何が押し下がり、何件取りに行くか」で決まります。それを実行前に見せるのが EXPLAIN です。
取得量を知るには先頭の fetch summary 行、途中で止まる可能性まで見るなら complete input 行を読みます。

fetch 意味
COUNT_ONLY 件数だけ。全件を走査せず、limit 1 で最大 1 件だけ取得し、1 リクエストで totalCount を得る
EXACT WHERE 全体が kintone 側へ渡った。レコード取得を伴う方式の中では、最も絞り込みが効いている
PREFILTERED 一部だけ kintone 側で絞り、残りをインメモリで評価
ALL 全件取得。上限に注意
NONE kintone から取るソースが無い(一時テーブルや生成系列だけ)。構造化された計画上の値で、テキスト表示では fetch summary 行自体が出ないことがある

以下の 3 例は、プラグインの実行画面に SQL を貼って EXPLAIN ボタンを押した出力です(EXPLAIN を SQL の先頭に付けても同じ計画が出ます)。

例 1: 件数だけなら COUNT_ONLY

SELECT COUNT(*) FROM APP4149 WHERE 商談フェーズ IN ('受注')
fetch summary: COUNT_ONLY
metadata API: form definition APP4149
mode: COUNT_TOTAL_COUNT
app: APP4149 (4149)
kintone query: 商談フェーズ in ("受注") limit 1
fetch: COUNT_ONLY (limit 1)
fields: $id
fetch API: GET records.json (totalCount=true)
REST execution: single GET
record limit: maxRecords/onLimitReached not applied
fallback: full record scan when totalCount is missing or invalid
search abort: fail-closed (SearchAbortedError)

例 1 の EXPLAIN 出力。fetch summary: COUNT_ONLY

totalCount=true の 1 リクエストで終わり、全件分のページングは発生しません。転送されるレコードは limit 1 の最大 1 件(フィールドは $id だけ)で、maxRecords の影響も受けません。
件数に比例したレコード取得やページングが発生しないので、総件数の KPI は maxRecords を気にせず書けます。
ただし条件は狭く、物理アプリに対する SELECT COUNT(*) で、JOIN・サブテーブル・CTE・GROUP BYHAVINGDISTINCTLIMIT / OFFSET が無く、WHERE 全体を押し下げられる場合だけです。

例 2: 同じ WHERE でも GROUP BY を足すと取りに行く(EXACT

SELECT 商談フェーズ, COUNT(*) AS 件数, SUM(売上) AS 売上合計
FROM APP4149
WHERE 商談フェーズ IN ('受注')
GROUP BY 商談フェーズ
fetch summary: EXACT
metadata API: form definition APP4149
mode: FULL_SCAN
group key 商談フェーズ: PHYSICAL (source=0, field=商談フェーズ)
complete input: required (onLimit=truncate disabled)
complete input reason: GROUP_BY, AGGREGATE
onLimit=truncate: disabled
reason: GROUP BY あり, 集計関数(COUNT / SUM 等)あり
app: APP4149 AS APP4149 (4149)
kintone query: 商談フェーズ in ("受注")
fetch: EXACT
pushdown applied: 商談フェーズ in ("受注")
relation: exact
fields: 商談フェーズ, 売上

例 2 の EXPLAIN 出力。fetch summary: EXACT、complete input: required

集計はインメモリなので mode: FULL_SCAN になりますが、WHERE は丸ごと押し下がっているので(fetch: EXACT)、取ってくるのは受注フェーズの行だけです。
fields 行にも注目してください。この SQL の評価に必要なフィールドだけを取得します。必要な列は SELECT だけでなく WHEREJOINGROUP BYHAVINGORDER BY から決まります。
complete input: required は「集計は入力が欠けると値が誤るので、上限に達したら打ち切りではなくエラーにする」という宣言です。

例 3: フィールドを関数で包むと押し下がらない(ALL

SELECT 案件名, 売上 FROM APP4149 WHERE DATE_FORMAT(受注予定日, '%Y-%m') = '2026-08'
fetch summary: ALL
metadata API: form definition APP4149
mode: FULL_SCAN
reason: WHERE 句に JS 評価が必要な式, WHERE_EXPRESSION_LOCAL_ONLY
app: APP4149 AS APP4149 (4149)
kintone query: (全件取得)
fetch: ALL
fields: 案件名, 売上, 受注予定日

例 3 の EXPLAIN 出力。fetch summary: ALL、kintone query: (全件取得)

DATE_FORMAT(受注予定日, …) は kintone のクエリ構文に無いので、全件取ってからインメモリで評価します。
同じ意図なら WHERE 受注予定日 >= '2026-08-01' AND 受注予定日 <= '2026-08-31' か、相対日付関数 WHERE 受注予定日 = THIS_MONTH() と書けば EXACT になります。
「WHERE に書く日付」と「SELECT で整形する日付」は別物、というのが第 2 回の主題の一つです。

上限は fail-closed

取得上限は CLI 既定 500 件・MCP 既定 500 件・プラグイン既定 3,000 件・エンジン既定 10,000 件です。
上限に達したとき、集計・GROUP BY・DISTINCT・ローカル ORDER BY を含むクエリは部分結果を返さずエラーになります(誤った合計や誤った上位 N を返さないため)。
素の明細は onLimit=truncate を指定した場合だけ打ち切り表示します。
「動いていたのにデータが増えたら突然エラーになった」は、たいてい fetch: ALL のクエリが上限に当たったものです。EXPLAIN を見れば事前に分かります。

2. 標準 SQL との差分早見表

MySQL / PostgreSQL の感覚で書いて引っかかる点を、引っかかる順に並べました。★は「エラーにならず静かに違う結果になる」ものです。

項目 標準 SQL の感覚 kSQL 備考
テーブル名 orders APP4149。サブテーブルは APP4149$明細 の仮想テーブル CLI は APP4149@prod、論理名 LAPP_案件管理 も可
識別子 英数字 日本語フィールドコードをそのまま書ける。空白や予約語はバッククォート ラベル≠フィールドコード。「〒」のコードは 郵便番号DESCRIBE APP4148 で確認
JOIN 任意の条件 ON はフィールド同士の等式 1 本だけON a.顧客No = b.顧客No_ は可、ON a.x = b.x AND a.y = b.yON a.日付 BETWEEN b.開始 AND b.終了 は不可。ON に式や関数も書けない。INNER / LEFT / RIGHT / CROSS。FULL OUTER 無し 複合キーは kintone の計算フィールド、CTE、一時テーブルのどれかで CONCAT(a, '::', b) の連結キー列を先に作り、その列同士を結ぶ。区切り文字が元データに現れないことを保証する('A::'+'B''A'+'::B' は衝突する)。CTE 化すると取得計画が変わるので EXPLAIN で確認。今回使う SFA パックでは、ルックアップのコピー先フィールドが結合キーになる
派生テーブル FROM (SELECT …) t 不可WITH(CTE)か CREATE TEMP TABLE #t AS SELECT … 一時テーブルはバッチ(; 区切り)内で有効、既定 10,000 行まで
サブクエリ 相関可 非相関の IN / EXISTS / スカラーのみ。相関サブクエリ不可 DML の WHERE には IN (SELECT …) を書けない(第 5 回)
LIKE DB の照合順序 JavaScript の文字列意味論で評価し、LIKE 自体は押し下げない。単独なら ALL、押し下がる条件と AND で組めば PREFILTERED kintone の全文検索が欲しいときは別演算子 KLIKE(10 万件で打ち切りあり)
ORDER BY 照合順序 型ごとの正準順。テキストは Unicode コードポイント順(日本語の辞書順ではない) kintone の REST と同じ並びが要るときは KORDER BY
数値 ★ DECIMAL / DOUBLE 比較は最大 30 桁の 10 進厳密1.10 = 1.1)。算術・SUM / AVG は IEEE 754 倍精度3000 * 1.1 = 3300.0000000000005 精度が要る計算は kintone の計算フィールドに寄せる
空セル ★ NULL 未設定値は空文字として扱う。数値の空セルは算術では 0、型付き比較と並べ替えでは最小側 WHERE 金額 != '' で除外(押し下がる)。IS NULL / IS NOT NULL 自体は押し下がらず、その条件単独では ALL、押し下がる条件と AND で組めば PREFILTERED(v3.77.0 で実測)。選択系の未選択は IN ('') なら押し下がる
選択系の比較 = ドロップダウン・ラジオ・ステータスは = でよい(IN に正規化して押し下げ)。チェックボックス・複数選択・ユーザー選択は IN / NOT IN 必須 定義に無い値との = は正規化されず、その条件単独なら全件取得のうえ 0 行
日付の条件 date >= '…' リテラル比較は押し下がる。相対日付関数TODAY() / THIS_MONTH() / FROM_TODAY(n, unit) …)も押し下がるが WHERE 専用 DATE_FORMAT / YEAR() で包むと押し下がらない(例 3)。DATE_ADD(列, n, 'MONTH') の引数順に注意、DATE_SUBINTERVAL は無い
集計とウィンドウ 同じ SELECT に書ける 同じ SELECT に書けない。CTE で段を分ける GROUP BY にはエイリアスも書ける
結果列名 そのまま 英字は小文字に正規化される(AS ランクAランクa ダッシュボードの列設定で参照するときに効く
トランザクション BEGIN / ROLLBACK 無し。バッチは非アトミック、常に fail-fast。書き込み API は自動リトライしない 代わりに VALIDATE ONLYASSERTON ERROR SKIP で事前検証と早期停止(第 5 回)
静的検証 パーサ + カタログ MCP の ksql_validate(第 9 回)は構文と静的検査だけ。フォーム定義は読まないので、存在しないフィールドコードでも ok になりうる 最後に確かめるのは実行結果
文字列リテラル 'it''s' 同じ。シングルクォート、'' でエスケープ ダブルクォートは識別子ではなく使えない

★は、エラーにはならないものの、DB で期待した意味と異なる可能性がある項目です。値が返ってくるぶんエラーより厄介です。
特に数値は「比較は 10 進厳密、算術は倍精度」、空セルは「算術では 0、比較では最小側」と、操作によって意味が変わります。第 4 回で「静かに間違う」例をまとめて扱います。

3. 道具の使い分けと準備

向いていること 書き込み 接続・認証設定
kintone プラグイン 画面内で書いて実行し、その場で見る。EXPLAIN ボタン・履歴・表表示・フィルタ・; 区切りのバッチ・一時テーブル・IMPORT(ファイル選択) 文ごとの確認ダイアログ 不要(ログイン中のユーザー権限で動く)
CLI ksql スクリプト・CI・スケジューラ。-f file.sql--format json|csv、CSV export、profile で環境切替 --allow-dml で可(確認プロンプトあり) 設定ファイルと API トークン
MCP サーバー Claude などの AI クライアントから。read-only ツールと mutate ツールが分離 ksql_mutate のみ 同上
無償版 Dashboard プラグイン 集計 SQL を表と棒グラフのペインにして一覧画面に置く 不可(read-only) 不要(ログイン中の権限)
engine ライブラリ 自作プラグイン・カスタマイズに read-only エンジンを組み込む 不可 組み込み側からクライアントを渡す

このシリーズは第 6 回までプラグインで進めます。接続設定が要らず、EXPLAIN もバッチも一括更新も画面内で試せるからです。
このシリーズで CLI が必要になるのは「ファイルに書き出す」「別環境で同じ SQL を使う」「自動で回す」の 3 つで、その必要が出る第 7 回で導入します。

SFA パックを追加したら、kSQL 側の準備は 2 つの zip だけです。v3.77.0 の release フォルダ から
ksql-plugin-v3.77.0.zip を kintone のプラグイン画面で読み込み(kintone のシステム管理権限が要ります)、
ksql-app-template-v1.11.0.zip からテンプレートで専用アプリを作ります。
専用アプリは設定済みなので追加設定はありません。使い方の詳細は rex0220 kSQL プラグイン にあります。
第 5 回以降はレコードを書き換えるので、本番アプリではなく、サンプルデータを入れた検証環境で進めてください。

最初の 1 本を専用アプリの実行画面に貼って実行し、続けて同じ SQL のまま EXPLAIN ボタンを押してください。

SELECT 商談フェーズ, COUNT(*) AS 件数
FROM APP4149
GROUP BY 商談フェーズ
ORDER BY 件数 DESC

EXPLAIN は fetch summary: ALL になります。WHERE が無いので当然ですが、complete input reason: GROUP_BY, LOCAL_ORDER, AGGREGATE の行に、
集計と ORDER BY 件数 がどちらもインメモリで、入力が欠けたら止まる、という宣言が出ています。20 件のアプリでは何も起きませんが、
この行が出るクエリは、データが増えたときに上限の対象になる、と覚えておいてください。
プラグインの上限は「⚙ オプション → 取得」の「最大取得件数」(既定 3,000 件。下の画像の環境では 500 に下げてあります)で、以降の回ではこれを意図的に下げて fail-closed を実演します。

最初の SQL の EXPLAIN 出力。complete input reason: GROUP_BY, LOCAL_ORDER, AGGREGATE

他の面の導入は、必要になる回で扱います。

4. シリーズの構成

以降は業務課題 1 つにつき 1 本、「課題 → 標準 SQL ならこう書く → kSQL ではこう → EXPLAIN で仕組み → 落とし穴 → 運用に載せる」の型で進めます。

課題 主な差分・機能
1 アプリをまたいで集計する INNER JOIN・押し下げの 2 種(WHERE 述語と結合キー)・LEFT JOIN の上限
2 期間で集計する 相対日付・GENERATE_SERIES で無い月を 0 埋め・LAG 前月比
3 ABC 分析 ウィンドウ関数(順位・構成比・累積)・ROLLUP
4 データ品質を監査する VALIDATE APPn INTO #err・重複・文字列の罠(コードポイント順・サロゲートペア)
5 安全に一括更新する 一時テーブルで確保・ASSERTUPSERTVALIDATE ONLYON ERROR SKIP・冪等性
6 CSV を IMPORT して検証環境を大きくする IMPORT CSV/JSON(プラグインのファイル選択)・テストデータ増量
7 CLI を導入して定期運用に載せる CSV export(Shift_JIS)・--format jsonDECLARE @param・profile と LAPP_・スケジューラ
8 kSQL Flow へ載せる dialect 1・ASSERT / EXIT ゲート・as-of・再実行
9 AI に書かせて人間がレビューする MCP・構文カタログ・EXPLAIN をレビューに使う

Claude に頼むなら(MCP 導入済みの読者向け・任意)

各回の末尾に、同じ課題を Claude に頼むときの依頼文を 1 つ置きます。
kSQL MCP は第 9 回で導入します。すでに導入済みなら次の依頼文を試せます。未導入ならここは飛ばしてください。

kSQL MCP で、APP4149(案件管理)のうち受注予定日が今年の案件について、商談フェーズ別の件数と売上合計を出す SQL を書いてください。
日付条件には THIS_YEAR() を使い、YEAR() や DATE_FORMAT() は使わないでください。
ksql_validate で検証したうえで、ksql_explain の fetch summary 行が EXACT になっていることを確認してください。実行はしないでください。

次回は第 1 回「アプリをまたいで集計する」です。顧客管理と案件管理をルックアップのキーで結合し、EXPLAIN の「pushdown applied」と「join key prefilter」を別々に読みます。


リポジトリ・ドキュメント:

0
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
0
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?