目次
ガイド

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)
realizedRunが実際に使ったもの(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つがすべて成り立つときだけです。

  1. ConnectorがそのCapabilityを宣言している。
  2. 結果が、プログラムの意味が求めるものと正確に一致する。同じレコード、順序が意味を持つなら同じ順序、同じ失敗時の振る舞い、同じ権限の境界、そして観測するデータの範囲が広がらないこと。
  3. その演算を、依存関係と意味を保ったまま読み取り元で評価できる。プログラムに書いた順番で決まるわけではない。

だから、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 counts

Snowflakeでの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 5000

LIMIT 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