Planとプッシュダウン
このページは、実行する前に、プログラムの処理がどこで動き、どれくらいコストがかかるかを知りたい作者のためのものです。explain が何を表示するか、どの部分を読み取り元のシステムに任せるかをActQLがどう決めるか、見積もり・上限・実績がどう違うかを説明します。
Planとは
ActQLは、データベースがSQLを読むのと同じようにプログラムを読みます。何が欲しいかを書けば、どう取るかはエンジンが決めます。Planは、演算、その型、外部システムに触れる境界、そして上限を固定します。すべての演算を物理的にどこで動かすかまでは固定しません。それは、Planが固定した意味の範囲で、HostがConnectorと計算エンジンのCapabilityから選びます。
配置は2つの要素に分けて表示します。
- Site:処理を実行する場所です。Connectorの先のシステム(
connector:snowflake)、手元のマシン(host:local)、リモートやCloudのワーカーがあります。 - Engine:その場所で計算するものです。読み取り元自身のクエリエンジン、ActQLの内蔵エンジン、DuckDBがあります。
データを読む前には決められない選択もあります。JOINをどのエンジンで実行するかは届く件数で変わるので、HostはRunの中で、入力の件数が分かった時点で選びます。変えられるのはまだ始まっていない処理だけで、プログラムの意味、上限、承認が必要なものは変わりません。
explain を読む
explain は、業務データを読まず、LLMも呼ばず、Runも作らずに計画を表示します。
$ actql explain churn-risk.aql
ActQL EXPLAIN · static
Check: valid Run: not started
Provider I/O: none
[1] READ salesforce.accounts
Site: connector:salesforce
Interface: Salesforce API
Projection: id, description
Pushdown: selected
[2] READ snowflake.usage_30d
Site: connector:snowflake
Engine: Snowflake SQL
Projection: account_id, active_users, trend
Pushdown: selected
[3] JOIN [1] + [2]
Condition: a.id = u.account_id
Site: host:local (fixed)
Engine: deferred
Candidates: ActQL RelA, DuckDB
Input rows: unknown
Output limit: 50,000 (Host policy)
[4] INFER churn-risk.v1
Input: [3]
Site: model provider
Calls: one per row of [3]; count unknown
[5] WHERE risk.outcome = 'ok'
AND risk.value.level = 'high'
Depends on: [4]
Site: host:local
Engine: deferred
Pushdown: not eligible
Reason: depends on INFER output
[6] EFFECT UPDATE salesforce.Account
Site: connector:salesforce
Preview: unavailable before execution
Policy: evaluated at runtime
Checkpoint: conditional
Estimates: unavailable (no statistics)
Details: actql explain churn-risk.aql --verbose番号付きの各ブロックが1つの演算です。Siteは実行する場所、Engineはそこで計算するものです(SaaSのAPIの場合はInterface)。まだ選ばれていない配置には、次の印が付きます。
| 配置 | 意味 |
|---|---|
fixed | 計画で決まっている |
deferred | まだ得られない情報をもとに、Runの中で決まる |
proposed | 現時点の第一候補。Runの中でもう一度確かめる |
candidate | 保留中の選択や第一候補の選択肢の1つ(--verbose) |
excluded | ここでは実行できない。理由も表示する(--verbose) |
realized | Runが実際に使ったもの(explain --run) |
認可は配置とは別に、Effectのところに表示します。Policy はいつ判断されるか、Checkpoint はRunが止まるかどうかです。none、conditional(実行時にポリシーが決める)、required のどれかになります。
Pushdown: selected は、その演算を読み取り元への要求に組み込むと計画が決めたことを意味します。eligible は読み取り元にできるが計画がまだ選んでいないこと、not eligible は理由とともに、送れないことを示します。このプログラムの WHERE はLLMの答えを読むので、INFER の後でしか実行できず、そのエンジンはJOINと同じくRunの中で選ばれます。ここで host:local が確定なのは、このプロジェクトがローカルのHostで動くからです。リモートのHostやActQL Cloudでは、Siteも保留になることがあります。Catalogの統計があれば、見積もりと proposed のエンジンが表示されます。
explain は run と同じパラメーター、接続、Hostの設定を使います。--json は同じ説明をデータとして返します(actql-explanation/0.1)。
プッシュダウンの決め方
Connectorは自分のCapabilityを宣言します。どの読み取りと書き込みを提供するか、そのシステム自身が何を計算できるか。絞り込み、列の選択、集計、並べ替え、件数の制限などです。プランナーが演算を読み取り元に任せるのは、次の3つがすべて成り立つときだけです。
- ConnectorがそのCapabilityを宣言している。
- 結果が、プログラムの意味が求めるものと正確に一致する。同じレコード、順序が意味を持つなら同じ順序、同じ失敗時の振る舞い、同じ権限の境界、そして観測するデータの範囲が広がらないこと。
- その演算を、依存関係と意味を保ったまま読み取り元で評価できる。プログラムに書いた順番で決まるわけではない。
だから、JOIN の後ろに書いた WHERE でも、その読み取り元の列だけに触れるなら読み取り元の中で動くことがありますし、集計はDWHにできるならDWHで動きます。一方、LLMの答えを読む条件は、依存している INFER より前には動けません。プッシュダウンできないものはローカルの計算で動きます。プッシュダウンが結果を変えることはありません。変わるのは、処理の場所と、動くデータの量です。
explain --verbose は、条件ごとの判断を表示します。LLMのステップの前に絞り込みを2つ足した例です。
SELECT a.id AS id, a.description AS notes, u.active_users, u.trend
FROM salesforce.accounts AS a
JOIN snowflake.usage_30d AS u ON a.id = u.account_id
WHERE a.status = 'Active' AND u.active_users < 20
|> INFER `model-task:churn-risk.v1`
INPUT (notes, active_users, trend) AS risk
|> WHERE risk.outcome = 'ok' AND risk.value.level = 'high'$ actql explain churn-risk.aql --verbose
…
Pushdown
[1] a.status = 'Active'
Eligible: Salesforce
Decision: selected
[2] u.active_users < 20
Eligible: Snowflake
Decision: selected
[5] risk.outcome = 'ok' AND risk.value.level = 'high'
Eligible: after [4] only
Decision: host
Reason: depends on INFER outputそれぞれの条件は、その列を持つ読み取り元に送られます。risk に対する条件はHostに残ります。Eligible(送れるか)とDecision(送ると決めたか)は別物です。送れる条件でもHostに残すことがあります。読み取り元が条件を正確に評価できない場合は、Hostでもう一度評価する Residual も表示されます。
actql catalog show snowflake で、接続のCapabilityを一覧できます。
JOINをどこで実行するか
読み取り元をまたぐ処理は、両方の入力を扱えるSiteで実行します。通常は手元のマシンです。そこでHostが使えるエンジンは、内蔵エンジンとDuckDBの2つです。DuckDBは標準の @actql/node、actql、CLIのパッケージに同梱されています(軽量なSDKにはエンジンが入っていません)。Hostは、意味、権限、リソースの上限を満たす候補を比較します。演算の種類、エンジンのCapability、起動・転送・保持・計算の費用を見ます。Runの中で分かる入力の規模はその材料の1つであって、規則ではありません。必要な情報がそろうまで、explain はその選択を deferred と表示します。explain --verbose は選択肢を表示します。
[3] JOIN [1] + [2]
Kind: inner
Condition: a.id = u.account_id
Site: host:local (fixed)
Engine: deferred
ActQL RelA candidate
DuckDB candidate
Excluded
connector:snowflake moving [1] into Snowflake is not permitted on this Host
Input rows: unknown (no catalog statistics)
Output limit: 50,000 (Host policy)
Transfers: [1] Salesforce → host:local, [2] Snowflake → host:local
Selection: when [1] and [2] report their row countsSnowflakeでのJOINがここで除外されるのは、このHostがSalesforceのレコードをSnowflakeへ移すことを許可していないからです。読み取り元が候補になるのは、両方の入力をそこで扱えるようにでき、転送が許可され、読み取り元がそのCapabilityを宣言している場合です。ローカルのマシンに収まらないプログラムは、リモートのHostやActQL Cloudでそのまま動きます。その場合、Siteにはそのワーカーが表示されます。
見積もり・上限・実績
どのPlanにも、意味のある量ごとに上限があります。読み取り元ごとに読むレコード数、バイト数、LLMに送る異なる項目の数、LLMの呼び出し回数とトークン数、Effectの項目数です。上限の出どころは3つ、Connectorとmodel taskの契約、Hostのポリシー、そしてプログラムの中のガードです。
FROM salesforce.accounts AS a
JOIN snowflake.usage_30d AS u ON a.id = u.account_id
|> WHERE a.industry = 'SaaS'
|> LIMIT 5000
|> INFER `model-task:churn-risk.v1`
INPUT (notes = a.description, active_users = u.active_users, trend = u.trend) AS risk
MAX ITEMS 5000LIMIT 5000 は最大5,000件が欲しいという指定で、そのとおり返ります。MAX ITEMS 5000 はガードです。それより多くの項目を送ることになるなら、何も送らずにステップが失敗します。
explain は3つの数字を区別します。見積もり(Catalogの統計から。出どころと取得日を付け、なければ unknown)、必ず守られる上限、そしてRunの後にだけある実績です。分からない量をゼロとして扱うことはありません。
上限を確定できないPlanは、上限なしに動かすのではなく拒否されます。実行時に上限を超えると、処理はエラーで止まります。途中までの結果を完全なものとして返すことはありません。LLMのステップが件数の上限を超えるなら、LLMは1回も呼ばれません。
Runの後
explain --run は、Runが実際に何をしたかを、記録された証跡から表示します。読み直しも再送もしません。
$ actql explain --run run_01J…
ActQL EXPLAIN · run
Run: run_01J… Status: succeeded
Source: recorded Run evidence; nothing is re-read or re-sent
[1] READ salesforce.accounts
Site: connector:salesforce
Interface: Salesforce API
Rows received: 2
[2] READ snowflake.usage_30d
Site: connector:snowflake
Engine: Snowflake SQL
Rows received: 2
[3] JOIN [1] + [2]
Site: host:local
Engine: ActQL RelA (realized)
Chosen at run time: both inputs were small
Rows: 2 + 2 → 2
[4] INFER churn-risk.v1
Calls: 2 Outcomes: 2 ok
[5] WHERE risk.outcome = 'ok' AND risk.value.level = 'high'
Site: host:local
Engine: ActQL RelA (realized)
Rows: 2 → 2
[6] EFFECT UPDATE salesforce.Account
Checkpoint: ckpt_… approved
Items: 2 Applied: 2
Not measured: transfer bytes for [1]測っていない値は、そうと分かるように表示します。Runの中で選んだエンジンには選んだ理由が付き、Runの途中でHostが選択を見直した場合(入力が見積もりより大きかった、Siteが使えなくなった、など)は、その理由と時刻とともに並びます。静的な計画と実際の実行を並べて読めるようにするためです。見直しが変えるのはまだ始まっていない処理だけで、プログラムの意味、上限、承認は動きません。
Planが思っていたものと違うとき
- 絞り込みが読み取り元に移らなかった。 Connectorがその述語のCapabilityを持っていない(たとえば評価できない関数がある)か、述語が別の読み取り元に依存しています。読み取り元が対応する書き方に直すか、ローカルで動くことを受け入れてください。
- JOINのエンジンが
deferredのまま。 Runの前には統計がありません。エンジンは入力の件数が分かった時点で決まります。どちらを選んでも結果は変わりません。 - EffectがCheckpointの後ろにない。 ポリシーが
allowに振り分けています。プレビューを確認したければ、--stop-before commitを付けてください。 - 上限が不明。
explainは、裏付けのない見積もりを、推測せずに不明と表示します。接続のCatalogの統計があれば、確定します。
次に読む
Source: actql/actql/docs/guides/plans-and-pushdown.md at d904416674e4 · Edit on GitHub