Snowflake
Snowflake Connectorは、Snowflakeのデータベースからテーブル、ビュー、ステージのファイルを読み、レコードとテーブルを書き戻し、ステージにコンテンツをロードし、プロシージャとタスクを実行します。解約リスクの例のDWHがこれです。利用データはここにあり、Connectorは重い部分の処理(絞り込みと集計)をSnowflakeに任せたうえで、レコードをエンジンに届けます。
読み取り元
設定したデータベースとスキーマのすべてのテーブルとビューが読み取り元です。snowflake.usage_30d、snowflake.events、snowflake.accounts_dim のように参照します。列と型はSnowflakeの情報スキーマから取ります。テーブルのキーは、宣言されている PRIMARY KEY か UNIQUE 制約です。Snowflakeは通常のテーブルでこれらの制約を強制しないので、Connectorは事実ではなく検証すべき主張として扱います(Effectと操作を参照)。
内部ステージと外部ステージのファイルも読み取り元です。LIST_FILES(snowflake, glob => r'@exports/*.csv') が resource、version(ファイルのMD5)、path で一覧を返し、resource と version でJOINした snowflake.contents がファイルを content_ref として読みます。
別のスキーマを読むには、プログラムの中で名前を修飾するのではなく、snowflake_raw のような2つ目の接続を設定してください。
型
| Snowflake | ActQL | 備考 |
|---|---|---|
NUMBER(p, s) | decimal(p, s) | 精度とスケールを保つ。NUMBER(38,0) は int64 ではなく、宣言された精度が収まる場合を除いて decimal として読む |
FLOAT | float64 | 有限の値だけ。NaN と無限大は、名前付きの理由とともに読み取りを失敗させる |
VARCHAR、STRING、TEXT | string | |
BOOLEAN | bool | |
DATE | date | |
TIME | time | ナノ秒の精度を保つ |
TIMESTAMP_NTZ | datetime | タイムゾーンなし。セッションのタイムゾーンで解釈し直すことはない |
TIMESTAMP_LTZ | timestamp | UTCで保存される。セッションのタイムゾーンはSnowflakeの表示に影響するだけで、ActQLが読む値には影響しない |
TIMESTAMP_TZ | オフセット付きの timestamp | オフセットは保つ。Snowflakeはゾーン名を保存しないので、ActQLも保存しない |
VARIANT、OBJECT、ARRAY | json | 型付きのJSON。Snowflakeと同じく、NULL とJSONの null は区別する |
BINARY | bytes | |
GEOGRAPHY、GEOMETRY | string(GeoJSON) | 読み取りのみ |
書き込みは同じ規則で逆方向に変換します。対象の列に収まらない値(精度、長さ、型)は、静的に分かるなら check で、そうでなければその項目が失敗します。黙って切り詰めることはありません。
Effectと操作
レコードのEffectはキーで書きます。
- insert —
EFFECT INSERT snowflake.account_scores (col, …)。 - update —
EFFECT UPDATE snowflake.account_scores ID account_id SET (…)。 - upsert —
EFFECT UPSERT snowflake.account_scores ID account_id SET (…)。MERGEとして適用します。 - delete —
EFFECT DELETE snowflake.account_scores ID account_id。
ID で指定するキーは、ちょうど1レコードを識別しなければなりません。どの列がキーになるかは、そのテーブルに対するConnectorのMappingが宣言します。Snowflakeは一意性を強制しないので、Connectorはプレビューを作るときに検証し、複数のレコードに一致するキーの項目は failed になります。単一レコードの変更として適用されることはありません。同じキーに対して元のレコードが2つの異なる値を持つ更新は、check で拒否されます。
テーブルとコンテンツ:
- create table —
|> CREATE [OR REPLACE] TABLE snowflake.active_usageは、レコードをテーブル全体として書きます。 - store —
EFFECT STORE snowflake.stages SET (stage = '@exports', path = …, content = content)は、レコードのcontent(SERIALIZEまたはcontentsの読み取り元から来たもの)をステージに置きます。
操作:
- call —
EFFECT COMMAND snowflake.call ID key INPUT (procedure = 'ANALYTICS.REBUILD', args = [...])は、ストアドプロシージャを呼びます。 - execute_task —
EFFECT COMMAND snowflake.execute_task TARGET task_name ID keyは、タスクを1回実行します。
論理的な項目と物理的な文は別のものです。Connectorは100件の UPSERT の項目を1つの MERGE で適用してもかまいませんが、各項目は自分の識別子、プレビューの行、結果を持ち続けます。
セットアップ
"snowflake": {
"connector": "snowflake",
"account": "acme-eu",
"warehouse": "ANALYTICS",
"database": "PRODUCT",
"schema": "USAGE",
"role": "ACTQL_READER",
"user": "ACTQL",
"credentials": {"private_key": {"env": "SNOWFLAKE_PRIVATE_KEY"}}
}既定はキーペア認証です。パスワードも同じ形で渡せます。ロールには、ウェアハウス、データベース、スキーマへの USAGE と、プログラムが読むテーブルへの SELECT が必要です。書き込みには、同じロールに INSERT、UPDATE、DELETE、CREATE TABLE とステージの権限を与えるか、書き込み用のロールを持つ2つ目の接続(snowflake_write、ロール ACTQL_WRITER)を設定してEffectをそこに向けます。後者なら、ポリシーが2つを区別できます。
ActQL Cloudでも接続の項目は同じです。秘密鍵は環境変数ではなくCloudのシークレットストアに保存します。
Capabilityとプッシュダウン
| 演算 | プッシュダウンされる条件 |
|---|---|
| 列の選択 | 常に。参照される列だけを、宣言された型で選ぶ |
WHERE | 述語がこの読み取り元の列と、SnowflakeでもActQLと同じ意味を持つ演算子(比較、IN、LIKE、IS NULL、日付の演算)だけを使うとき。大文字小文字を区別する述語に、Snowflakeの大文字小文字を区別しない照合順序や ILIKE は使わない。複数の読み取り元にまたがる述語はエンジンに残すか分割し、残りはHostが評価する |
GROUP BY と COUNT、MIN、MAX | 常に |
SUM、AVG | 列が NUMBER(正確なdecimal)のとき、またはプログラムが float64 の演算を宣言したとき。NUMBER に対する AVG はSnowflakeの結果のスケールを保ち、check がそれを報告する |
ORDER BY、LIMIT | 並べ替える列の照合順序がActQL(バイナリ)と一致し、件数が定数かパラメーターのとき |
JOIN | 両方の入力が同じ接続の読み取り元のとき、またはHostが他方の入力をSnowflakeへ移すこと(一時テーブル)を許可しているとき。その場合、Siteは connector:snowflake になる。それ以外ではHostで実行する |
explain は、プッシュダウンされた各ステップをSite connector:snowflake、Engine Snowflake SQL で表示します。ActQLの意味を保ったままSnowflakeで実行できないものはHostで実行し、CloudやリモートのHostはそのためにDuckDBを選ぶことがあります。
プレビューと突き合わせ
プレビュー。 更新、UPSERT、削除は full_diff でプレビューします。Connectorは対象のレコードをキーで読み、現在の値を記録し、プレビューにはレコードごとに現在の値と新しい値が出ます。Snowflakeは READ COMMITTED で動くので、先に読むだけでは足りません。Connectorは各レコードの METADATA$ROW_ID と現在の値のハッシュも記録し、送る MERGE や UPDATE をそれらの条件付きにします。プレビューと適用の間に変わったレコードは更新されません。その項目は failed(conflict)で終わり、続けるよう求められればRunは新しいプレビューを作ります。追加とテーブル作成は targeted(何をどこに書くか)でプレビューし、CREATE OR REPLACE は置き換えられる件数をプレビューします。
突き合わせ。 送信前に、ConnectorはEffectの項目を、実行する文に結び付けます。自分で生成するSQL APIの requestId と、分かった時点で statementHandle とクエリIDです。応答が失われた場合は、その文をSQL APIと QUERY_HISTORY でIDによって探します。履歴にある成功した文でも、Effectの requestId を持たないものは証跡になりません。バッチを明示的なトランザクションで実行する場合は、文の成功とコミットの成功を別々に確認します。コミットされた MERGE は、どの項目に影響したかを報告します。非同期の文は、最終状態になるまでハンドルで追跡します。これらで結果が確定しなければ、項目は unknown のままで、何も再送しません。
上限と費用
- プッシュダウンされたクエリは設定したウェアハウスで動き、そこのほかのクエリと同じように課金されます。
explainはウェアハウスの名前を示します。1回の読み取りが返せる量は、プログラムの件数とバイト数の上限で抑えられます。 - 結果はSQL APIのパーティション単位で取得します。上限に達したConnectorは、途中までの結果を返すのではなく、エラーで止まります。
- 情報スキーマの
QUERY_HISTORYは7日間を対象とし、アカウント利用状況のビューは最大45分遅れます。突き合わせはまず文のハンドルを使い、それらの期間内で履歴に切り替えます。 - 大文字小文字:Snowflakeは引用符のない識別子を大文字にします。リソース名と列名は、Snowflakeに表示されるとおりにプログラムに書いてください。引用はConnectorが行います。
例
直近30日に利用が落ちた取引先を、セグメント別に数えます。
FROM snowflake.usage_30d
|> WHERE trend = 'down'
|> AGGREGATE COUNT(*) AS accounts, AVG(active_users) AS avg_users
GROUP BY segment
|> ORDER BY accounts DESC;LLMの判定でスコアのテーブルを最新に保ちます。
SELECT a.id AS account_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
|> INFER `model-task:churn-risk.v1`
INPUT (notes, active_users, trend) AS risk
|> WHERE risk.outcome = 'ok'
|> SELECT account_id, risk.value.level AS level, risk.value.reason AS reason
|> EFFECT UPSERT snowflake.account_scores ID account_id SET (level = level, reason = reason);DWHの中のデータからレポート用のテーブルを作り直します。
FROM snowflake.events
|> WHERE occurred_at >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
|> AGGREGATE COUNT(*) AS events GROUP BY account_id
|> CREATE OR REPLACE TABLE snowflake.events_30d;Source: actql/actql/docs/connectors/snowflake.md at d904416674e4 · Edit on GitHub