default eye-catch image.

ソースについて考えてみた話

dbt Fundamentalsの「Sources」のセクションについて理解した内容を言葉にしてみた。 かなりの部分で感想を織り交ぜてみた。この記事は前回の続き。 [clink url=\"https://ikuty.com/2023/07/15/dbt_models/\"] dbtはModularityのコンセプトに基づいてクエリをバラし上流から下流へと続くデータフローを 構築するフレームワーク。上流から下流までどのようにバラしても自由ではあるが、 何度か似たような仕組みを作っていくと、共通して見られる特徴に気づくことがある。 例えば、データを取り込んで、加工して綺麗にして、中間処理をして、BIやMLに渡す、といったように、 機能単位で処理をレイヤ化しておくと見通しが良くなることに気づく。 dbtはそのバラし方についてある程度規定していて以下のようにレイヤリングすることが示されている。 Source Staging Intermediate Fact Dimension この記事はSourceレイヤについての記事。 [arst_toc tag=\"h4\"] 外部インターフェースとDRY原則 データ分析基盤の構築に限らず、システムを設計する際に外部データとのインターフェースを 独立して検討が必要な設計項目とすることが多い気がしている。 それは何故かというと「自分たちの管理外にあるものであって変わり続けるもの」だからだと思う。 インターフェース仕様を定義し変化を制御しようとする。制御下にある変化を受け入れる必要がある。 変化する対象をハードコードした場合、変化が起きたときに全てを変更する必要がある。 もし対象を1箇所に記述しそれを論理的に参照する仕組みがあれば、変化に対する変更は1回で済む。 実際には、想定する粒度や概念の範囲を逸脱した場合は書き直しが必要だろうと思うので、 それは見通しが甘かったことに対する罰として受け入れないといけない。 インターフェース仕様の想定の甘さはシステム内部のそれと比べて影響が大きい。 モダンな言語やフレームワークでは、同じ変更を何度も行わせるようなタルいことをさせないように 作られている。これはDRY(Don\'t Repeat Yourself)原則と言い一般的な設計論として話される。 dbtはELTを前提としていて、外部データはそのままテーブルにロードすべし、としている。 dbtにとって、外部データをロードした一番最初のテーブルが「生」であり変化し得るが、 DRY原則に従って、1箇所で定義し抽象化したものを使いまわせるようになっている。 Sourceの定義と利用 さて、dbtのSourceによってどのようにDRYが実現できるか見てみる。まずは生クエリ。 CTEにより記述され、1個目のsourceを2個目のstagedで使用している。 ikuty_raw.jaffle_shop.customers が生データを格納したテーブル。 ハードコードされた状態。 with source as ( select * from ikuty_raw.jaffle_shop.customers ), staged as ( select id as customer_id, first_name, last_name from source ) select * from staged ; 次にdbt。model-pathにsource.ymlというファイルを配置する。 生クエリのsourceがYMLで定義されている。jaffle_shopという名前のSourceを指定している。 スキーマやテーブルが変更されたとしても、このファイルを変更すれば良い。 version: 2 sources: - name: jaffle_shop database: ikuty_raw schema: jaffle_shop tables: - name: customers - name: orders 生クエリをモデル上で以下のように書き直す。 この際、上で定義したjaffle_shop Sourceを {{source()}} により参照している。 select id as customer_id, first_name, last_name from {{ source(\'jaffle_shop\', \'customers\') }} 古いデータに基づく分析から得られた意思決定はゴミ? データ分析界隈では「データの新しさ」がしばしば重要なトピックとなる。 しかし、それはなぜなのか上手い説明を聞いたことがなかった。 dbt公式が用意するドキュメントの中に、dbtが生まれた経緯が説明されているものがあり、 その中で、風が吹けば桶屋が儲かる的なノリでこう書いてある。 explicitにデータ鮮度の重要さを示すものではないが、 「古いデータに基づく分析から得られた意思決定はゴミ」ぐらいの気持ちになった。 The dbt Viewpoint https://docs.getdbt.com/community/resources/viewpoint Quality Assurance Bad data can lead to bad analyses, and bad analyses can lead to bad decisions. データ鮮度の定義と実行 データ分析基盤には、データを定期的に取り込む類のタスクがある。 dbtは取り込みの度に取り込んだデータの鮮度を確認できる機能を備えている。 「定期的」とは、serviceやdaemon的な何かが動いてデータソースを見にいく訳ではなく、 dbtを実行して取り込む度に毎度値を参照するという意味。 物理的には、「新鮮であると見做すことができるレコードと現在との間の許容可能な時間」 を定義し、許容できない時間差を検知することでデータが古いのか新しいのかを判断させる。 この辺り、Declarativeであり、新しい何か的な印象を持つ。 sourceの定義ファイルにfreshnessブロックを定義する。 公式による仕様の説明は以下の通り。 version: 2 sources: - name: freshness: warn_after: count: period: minute | hour | day error_after: count: period: minute | hour | day filter: loaded_at_field: tables: - name: freshness: warn_after: count: period: minute | hour | day error_after: count: period: minute | hour | day filter: loaded_at_field: ... loaded_at_fieldにデータ鮮度の判定に利用するカラムを指定する。 loaded_at_fieldと現在時刻の差がcount、periodに定義した時間を超えていた場合にレポートする。 レポートには警告とエラーの2種類が存在し、warn_afterに警告、error_afterにエラーの場合を書く。 count,periodの単語選びのセンスがちょっと分からない。periodが単位、countが数値である。 例えばcount=2、period=dayなら「2日」。Declarativeに読めば良いのか? freshnessブロックは継承関係を持たせることができる。 つまりsources直下に書いたものでデフォルトを定義し、tables配下に書いたもので上書きできる。 loaded_at_fieldカラムはtimestamp型かつUTCである必要があり、変換例が公式に載っている。 # If using a date field, you may have to cast it to a timestamp: loaded_at_field: \"completed_date::timestamp\" # Or, depending on your SQL variant: loaded_at_field: \"CAST(completed_date AS TIMESTAMP)\" # If using a non-UTC timestamp, cast it to UTC first: loaded_at_field: \"convert_timezone(\'UTC\', \'Australia/Sydney\', created_at_local)\" ここでは以下のような定義とする。 version: 2 sources: - name: jaffle_shop database: ikuty_raw schema: jaffle_shop tables: - name: customers - name: orders loaded_at_field: _etl_loaded_at freshness: warn_after: {count: 12, period: hour} error_after: {count: 24, period: hour} dbt source freshnessコマンドによりデータ鮮度が評価される。 _etl_loaded_atの最大値が現在よりも12時間以上前だったのでWARNが出た。 $ dbt source freshness 15:10:34 Running with dbt=1.5.0 15:10:34 Found 1 model, 0 tests, 0 snapshots, 0 analyses, 321 macros, 0 operations, 0 seed files, 2 sources, 0 exposures, 0 metrics, 0 groups 15:10:34 15:10:35 Concurrency: 1 threads (target=\'dev\') 15:10:35 15:10:35 1 of 1 START freshness of jaffle_shop.orders ................................... [RUN] 15:10:37 1 of 1 WARN freshness of jaffle_shop.orders .................................... [WARN in 1.72s] 15:10:37 Done. 宣言的記述について ソフトウエアパラダイムの1つとして市民権を得ている宣言的(Declarative)記述について書いてみる。 開発者人口が多いVue.jsかReact.jsで爆発的に認知され(ひと昔前に)一気に当たり前になった印象がある。 歴史的な経緯からフロントコードは非同期かつイベント駆動であって、酷い可読性だった覚えがある。 onClickの中にonClickを書いて、その中にonClickを書いて...みたいなことが起こり得た。 JSの言語仕様でasync awaitパターンがサポートされステートマシンを合理的に記述できるようになった。 「テキストボックスにバインドする変数はX」と書くだけで、関係する処理が省略できてむっちゃ楽。 個人的には構成管理ツールのAnsibleで宣言的記述の良さを実感できた気がする。 構成管理の界隈では、冪等性が重要で例えば「nginxは192.168.1.64:8080でlistenすること」 のように定義しさえすれば、何度実行しても設定が定義値であることが保証される仕組みが欲しい。 もし状態を宣言的に記述できなければ、細かい処理を自力で実装しなければならなくなる。 「listen: 192.168.1.64:8080」と書いて実行しさえすれば良いならむっちゃ楽。 ミドルウェアとデータの違いはあるが、実体があるものを抽象化し振る舞いを状態定義する様から、 dbtはAnsibleやTerraformに近い気がする。(データの構成管理をしているような...) freshnessを確認するコードをJavaで書けとか言われたらタル過ぎるので、 dbtで当たり前のように宣言的に記述できるのはありがたい。 まとめ dbt Fundamentalsの「Sources」セクションを聴いて、内容を文書化してみた。 Sourceを抽象化することでDRYを実現できること、 抽象化した先でデータ鮮度の確認ができることについて定義や実行例を追って確認してみた。

default eye-catch image.

モデルについて考えてみた話

dbt Fundamentalsの「Models」のセクションについて、理解した内容を言葉にしてみた。 かなりの部分で感想を織り交ぜてみた。この記事は前回の記事(以下)の続き。 [clink url=\"https://ikuty.com/2023/07/10/dbt-word/\"] 実践的なコンテンツというよりは、概念の理解を優先した入門用のコンテンツであって、 これでモノを作れることはないと思うし、資格対策には不足していると思う。 動画の講師の方は自己紹介で教師のバックグラウンドがあると話されている。 個人的にはUdemyの類似品より構成と英語の発音がわかりやすいと感じる。 [arst_toc tag=\"h4\"] クエリをバラして理解しやすいようにしたい 関数型言語は、関数が再起的に評価されることで最終的な応答が確定する。 手続き型言語と異なり、再帰の評価の途中で一時停止して内容を観察することが難しい。 SQLは関数型言語ではないが、内包するSQLの評価を一時停止して観察することが難しい、 という点で近い。制御がない一筆書きである点が複雑さから逃れる理由になっていそう。 ビジネスロジックが乗った巨大なクエリは確かに一筆書きではあるが、理解しづらい。 クエリをバラしたら理解しやすいんじゃね? みたいな動機があるのだと思う。 ただバラして制御するとそれはもう手続きなので、バラすけど再利用するだけに留める。 バラして、途中経過を観察したり動きを制御したり、そういう仕組みをdbtが提供する。 Modularity クエリをバラすことを、dbtでは「Modularity(モジュール性)」という用語で説明している。 動画では「車の製造」が例えとして挙げられている。パーツとパーツを繋げて車を製造すると。 Modularityは構造化されていて、上流(Upstream)から下流(Downstream)へと繋がる。 Upstreamで作ったパーツをDownstreamで再利用する、という仕組み。 1個1個のパーツは、CTE(Common Table Expression)を利用して表現される。 CTEは標準SQLに定義されていて、クエリの再利用性を高める表現手段。 要は、dbtはCTEを使ってクエリの再利用性を強制するF/Wなんだろうと思う。 CTEと論理レイヤとモデル 実際に動作する生クエリを書くのではなく、論理的な記述レイヤを設けている。 論理レイヤの記述フォーマットとして、jinjaテンプレートが採用されている。 jinjaテンプレートにマクロを記述することで論理レイヤの表現能力を獲得している様子。 Modularityの観点でCTEで各パーツを定義し、Up->Downのフローを論理的に定義する。 dbtが論理レイヤをコンパイルして物理SQLを吐くという仕様になっている。 このパーツのことをdbtでは「モデル」と呼んでいる様子。 1個のSELECT文が1個のモデルと対応する制約が与えられる。 これにより、Up側モデルをDown側モデルで再利用することと、クエリ結果の再利用を紐付けている。 Materialization パーツは個別に具体的なSQLにコンパイルされる。これには「Materialize」という用語が付いている。 Upstream側を実体化する際、その実体化の手段を選択できる。 View モデルをViewとして実体化する。実データを作らないので作るのが速いしストレージを食わない。 Viewに乗ったロジックが複雑だと所要時間が増えるので、その場合はTableの採用を考える。 CREATE VIEW AS ... が使われる。 Table モデルをTableとして実体化する。実データを作るので作るのに時間がかかるしストレージを食う。 ロジックが複雑であっても所要時間が増えない。BIから直接参照するとかならTableの採用を考える。 Up側にあるデータに追加や変更があっても反映されない。 CREATE (TRANSIENT) TABLE AS ... が使われる。 Incremental パイプラインを構築する際、「置き換え」なのか「増分」なのかは重要な観点となる。 初回生成時は通常のTableとして、2回目移行は増分だけを生成する。なので速い。 「増分」とする場合、どこが既存と増分の境界なのかを定義する必要がある。 その定義をモデルファイルに書く必要があり、若干複雑になる。 Ephemeral 中間テーブルをいちいちViewやTableに実体化し続けると、DWが再利用する可能性が低い何かで 埋め尽くされてしまうことがある。そういったテーブルはModularityの中で論理的に再利用したいが、 物理的に存在して欲しくない。それを実現できる。 Downstream側で1個か2個使うだけで、直接クエリを実行する必要がない場合に使うと良い。 モデルの例 動画では以下のクエリをdbt流に書く例が示されている。 以下は生クエリで、with句により3個のクエリを用意し4個目のクエリで結合している。 with customers as ( select id as customer_id, first_name, last_name from raw.jaffle_shop.customers ), orders as ( select id as order_id, user_id as customer_id, order_date, status from raw.jaffle_shop.orders ), customer_orders as ( select customer_id, min(order_date) as first_order_date, max(order_date) as most_recent_order_date, count(order_id) as number_of_orders from orders group by 1 ), final as ( select customers.customer_id, customers.first_name, customers.last_name, customer_orders.first_order_date, customer_orders.most_recent_order_date, coalesce(customer_orders.number_of_orders, 0) as number_of_orders from customers left join customer_orders using (customer_id) ) select * from final さて、次はdbtの論理レイヤの話。まずcustomersとordersをを別のファイルに記述する。 with customers as ( select id as customer_id, first_name, last_name from raw.jaffle_shop.customers ) select * from customers with orders as ( select id as order_id, user_id as customer_id, order_date, status from raw.jaffle_shop.orders ) select * from orders customersとordersを結合する。その際、customersとordersをref関数により参照する。 言い換えると、以下が評価される時点で、既にcustomersとordersは実体が存在する。 そのため、以下を評価する前に、customersとordersの中にあるデータを確認できる。 with customers as ( select * from {{ ref(\'stg_customers\')}} ), orders as ( select * from {{ ref(\'stg_orders\') }} ), customer_orders as ( select customer_id, min(order_date) as first_order_date, max(order_date) as most_recent_order_date, count(order_id) as number_of_orders from orders group by 1 ), final as ( select customers.customer_id, customers.first_name, customers.last_name, customer_orders.first_order_date, customer_orders.most_recent_order_date, coalesce(customer_orders.number_of_orders, 0) as number_of_orders from customers left join customer_orders using (customer_id) ) select * from final dbt runコマンドにより、全てのモデルをコンパイルする。 また --selectをつけると、、特定のモデルだけコンパイルできる。 dbtには下図のようにモデル間の依存関係をグラフィカルに表示する機能がある。 モデリングの方法論とdbtの仮定 dbtは単なるクエリの変換ツールであって、現実世界のモデルする方法論を規定するものではない。 しかし、動画ではモデルのセクションでチラッとこの話に触れられている。 Fact TableとDimensional Tableに整理してDimension毎のFactを提供しましょう、 という流儀や、Kimball,Data Vaultのような流儀など、諸々存在する。 動画では、これらをTraditionalと分類し正規化がポイントであると言っている。 コンピューティングとストレージが安くなり、ELTが可能になったおかげで、 \"正規化しない(Denormalized)\"モデリング手法が実現可能になりましたよ、と言っている。 むむ.. 相当深そうだが入り口だけ。 いったん、ELTを前提としてモデルを組み上げてください、ぐらいの話として聞き取る。 ELTを前提として、モデルをこうレイヤリングし名前を付けますよと話されている。 Source ELTであるが故に、外部のデータを無変換でDWに格納したところからスタートする。 そのレイヤを「Source」と名付ける。 Staging Sourceレイヤのモデルのクレンジングと標準化を行うレイヤを「Staging」と名づける。 StagingモデルはSourceモデルと1:1対応させる。 Intermediate Stagingモデルに対してビジネスロジックを適用して最終的なテーブルを生成する。 ビジネスロジックはダラダラと汚く大きくなることが常で、このレイヤで吸収する。 Fact 起きていること、または、既に起きたこと。履歴があるもの。 なんたらイベント、Webサイトにおけるクリック事象、投票など。 Dimension 時間の経過で発生するものではない履歴がない事象。 人、場所、コト、ユーザ、会社、製品、、顧客など。 以下は、動画で話されている実行例。 dbt-labs/jaffle_shop https://github.com/dbt-labs/jaffle_shop 一番左の緑色のモデルがSource。外部から引っ張ってきたRawデータ。 Sourceと1対1対応する形で、「stg_*」という名前のStagingモデルが存在する。 Sourceに対してクレンジング、標準化を適用したデータ。 「fact_*」は、時間経過により発生する履歴データ、そして、「dim_*」は属性データ。 Down側からUp側へ、Source->Staging->Fact/Dimension という流れになっている。 よもやま話(動画と関係なし) 例えばLaravel/EloquentやRails/ActiveRecordにはQuery Builderが付いていて、 手続き型言語によってクエリのModularityを得るアプローチには既視感があると勝手に思っている。 もちろんクエリ結果をDWに統合できないから、DWに統合できるF/Wが必要なので、 そのアプローチはSnowflakeで言えばSnowparkによって扱われるのかなと思う。 手続き型言語ではソフトウエア開発の複雑さから逃れられないことになっている。 ちなみにORMにおける物理テーブルの抽象化において「モデル」という用語が充てられている。 少なくとも1990年代後半にはORMの文脈で物理テーブルを抽象化する概念は実用化されていた気がする。 2000年代には、オブジェクト指向分析設計の文脈で「現実世界のモデル化」がイキりたおされていた。 2010年代、Laravel/Eloquent, Rails/ActiveRecordでORMのモダニズムが言われてた。 手続きに持ち込まず、SQLの世界だけでモデル化が話されるのは新しい気がする。 ここは最初の「Analytics Engineers」の話に由来している気がする。 まとめ dbt Fundamentalsの「Models」セクションを聴いて、内容を文書化してみた。 dbtはModularityと考え方に基づいてCTEによりクエリをバラすツールであることについて、 Modelの定義や実行例を追って確認してみた。

default eye-catch image.

ETLとELTの違いが説明されていたので聞いてみた話

サーバーサイドエンジニアのデータベースエンジニア寄りの枠ぐらいの認識しかないと、 データエンジニアリングの尖った部分に永遠に近づけないという感触がある。 サーバーサイドエンジニアの入口から入った場合はちゃんとチェンジしないといけないな。 dbtが公式で「データエンジニアリングの用語と概念」を長い尺で説明していて学んでみた。 サーバーサイドエンジニア的にはETLは普通のジョブやバッチ処理だと思うが、 ELT化することで何を改善しようとしていて何が達成されたのか説明できるようになった。 dbt Fundamentals https://courses.getdbt.com/courses/fundamentals [arst_toc tag=\"h4\"] ETLとELTの違い 旧来から分析基盤を作るためにデータを抽出してどこかに蓄え加工してDBに入れてきた。 これは、巨大なデータをダイレクトに入れる箱や資源がなくそれ以外の選択肢がなかったため。 これはもうサーバーサイド開発であってバッチ処理で必要なデータを揃えているのにあたる。 構成を実現するためのインフラレベルの観点、分析用途に加工するビジネスロジックの観点の2つ。 この2つをゴチャっと処理する巨大なソフトウエアを書かないといけなかった。 クラウドベースのDWが登場し、巨大なデータを入れる箱や資源が安く手に入るようになった。 そしてdbtのように「生データをうまいこと加工する」ツールが手に入るようになった。 抽出したままの生データを突っ込むのは簡単だし、ツールでうまいこと加工できるようになった。 もちろんSQLだけで加工するので出来ること出来ないことはあるが、 うまくいけば、誰も開発やメンテがしづらい巨大なソフトウエアを書かなくてもよくなった。 分析用ビジネスロジックの分離 dbt公式のdbt Fundamentalsの初っ端で、 抽出したデータをDWに投入するインフラレベルの人たちを「Data Engineers」、 生データをBIで必要なレベルまで加工する人たちを「Analytics Engineers」、 分析する人たちを「Data Analysts」と分類している。 実際、エンジニアが分析対象を理解しようとすると、キャリアの壁にぶち当たる。 エンジニアは技術力が必要だし分析者は要件定義能力が必要。 要件定義的なことができるエンジニアは圧倒的に不足しているのが世の常で、 エンジニアに対する要件から要件定義能力を分離したいというのは切実な思い。 dbt Fundamentalsで紹介されている下の表はよく出来ていると思う。 (フロントにETLを前提としたBIツールがあってTとLをUIでやらせる世界観だと 組み合わせたときにELTLになってしまう。トータルで考えないと上手くいかないと思う) Modern Data Stackとdbt dbtは「T」をやるツール。dbtの有名な図は以下。「E」と「L」は外。BIやMLは外。 ほぼSQLとymlだけで構成できるところが特徴。 SQLを使ってモデルを開発して、SQLを使ってテストして、ドキュメンテーションを構造化する。 WHがあって初めて存在するツールであって、WHの資源を使って動く。 オーケストレーションとDAGの管理が内包される。 まとめ インフラ的な観点とビジネスロジック的な観点の2つがゴチャった何かを作るのは大変という世界がある。 まぁそれでも書けよ、とも思うけど単にデータを抜いて格納するだけの人と、 要件定義してビジネスロジックを考えてデータを加工する人を分けられれば、人を集めやすい。 強力なDWが出来たことでデカいデータを生でぶち込めるようになったし、dbtみたいなツールができて ビジネスロジックに基づいて生データを加工するのが簡単になった。 トータルで複雑なソフトウエアを作らなくてもよくなるからELT美味しいよ、という話でした。

default eye-catch image.

Azure環境において最小権限でストレージ統合を構成する

Azure側を最小権限で構成する方法が書かれた記事がありそうで全然見つからない。 そこで、本記事ではAzure側を最小権限で構成する方法を調べてまとめてみた。 2023年7月現在、公式はSASを使う方法ではなくストレージ統合を使う方法を推奨している。 ストレージ統合によりAzureADへ認証を委譲することで認証情報を持ち回る必要がなくなる。 本記事ではストレージ統合を使用する。 ストレージアカウントにはURL(認証)・N/Wの2系統のパブリックアクセスの経路が存在するが、 URL(認証)によるパブリックアクセスは遮断し、Snowflake VNetからのアクセスのみ許可する。 Azure Portal/CLIからのアクセスを固定IPで制限する。 [arst_toc tag=\"h4\"] ストレージアカウント ストレージアカウントは、Blob(Object)、Files(SMB)、Queue、Table(NoSQL)の4種類が存在する。 Snowflakeの外部ステージとして使用するのはAWS S3でありAzure Blobである。 Blobは最上位階層にない。ストレージアカウント内にはコンテナがあり、コンテナ内にBlobがある。 Blobストレージ内のオブジェクトに対してのみ、外部からアクセス(パブリックアクセス)できる。 パブリックアクセスには、URL・ネットワークの2経路が存在し、それぞれ独立して存在する。 他方を制限したからといってもう一方が自動的に制限されたりはしない。 パブリックURLアクセス ストレージアカウントレベルで、パブリックアクセスを以下から選択できる。 Enabled Disabled また、コンテナレベルで、パブリックアクセスを以下から選択できる。 Private (no anonymous access) Blob (anonymous read access for blobs only) Container (anonymous read access for containers and blobs) 上位階層であるストレージアカウントレベルの設定が優先される。上位で不許可にすれば不許可。 もしストレージアカウントレベルで許可した場合、コンテナレベルの設定が効く。 その組み合わせは以下の通り。確かにMECEなのだが絶望的に冗長に思える。 コンテナ自体はURLアクセス不許可だがBlobはURLアクセス可とか設定できてしまう。 パブリックネットワークアクセス パブリックネットワークアクセスとして以下から選択できる。 Enabled from all network Enabled from selected virtual networks and IP addresses Disabled 分かりづらいが、ストレージアカウント/Blobへのアクセスに使われるN/Wインターフェースを選ぶ。 Disabledにすると、仮想ネットワークのプライベートIPアドレスを使用するN/Wインターフェースとなる。 Enabledには2つあり、全てのN/Wからアクセス可能なN/Wインターフェースか、 選択した仮想N/WまたはIPアドレスからアクセス可能なN/Wインターフェースを選ぶ もちろんDisabledにした場合は、仮想プライベートエンドポイントが必要となる。 ストレージ統合の際のストレージアカウントの最小権限構成 以下のようにして最小権限構成を行なった。 パブリックURLアクセスをストレージアカウントレベルでDisabled パブリックネットワークアクセスを固定IP、及びSnowflake VNet IDsに設定 Snowflakeのストレージ統合機能によりサービスプリンシパルを作成 ストレージコンテナのIAMでサービスプリンシパルにBlob読み書きロール設定 ストレージアカウントに対するSnowflakeのVNetサブネットIDsからのアクセス許可 ストレージアカウントへのパブリックアクセスを「選択したVNetまたはIPアドレスのみ」とした場合、 SnowflakeのVNetまたはIPアドレスが分かる必要がある。IPアドレスは不定なのでVNet IDsを指定する。 公式にドンピシャの説明があるので、それを参考にVNet IDsを設定する。 VNet サブネット IDs の許可 Snowflake側で以下のSQLを実行すると、属するVNet IDがJSONで複数出力される。 それを記録しておく。 USE ROLE ACCOUNTADMIN; SELECT SYSTEM$GET_SNOWFLAKE_PLATFORM_INFO() ; { \"snowflake-vnet-subnet-id\":[ \"/subscriptions/hogehoge\", \"/subscriptions/fugafuga\" ] } 続いて、ストレージアカウントに上の2個のVNet IDからのアクセスを許可する。 以下のように、Azure CLIでログインしてVNet IDの個数だけコマンドを実行する。 $ az storage account network-rule add --account-name --resource-group --subnet \"/subscriptions/hogehoge\" ... $ az storage account network-rule add --account-name --resource-group --subnet \"/subscriptions/fugafuga\" ... ストレージ統合を作成する AWS環境と同様に、CREATE STORAGE INTEGRATION によりストレージ統合を作成する。 Azure CLIやAzure PortalでテナントIDを取得しておいてAZURE_TENANT_IDに渡す。 STORAGE_ALLOWED_LOCATIONSにはBLOBの位置を表す文字列を渡す。 書式は azure://ストレージアカウント名.blob.core.windows.net/コンテナ名/パス 。 BLOBの位置は複数渡せる。 CREATE STORAGE INTEGRATION storage_integration_azure TYPE = EXTERNAL_STAGE STORAGE_PROVIDER = \'AZURE\' ENABLED = TRUE AZURE_TENANT_ID = \'\' STORAGE_ALLOWED_LOCATIONS = (\'azure://storageikuty1.blob.core.windows.net/container1/files/\') ; 続いて、同意URL(AZURE_CONSENT_URL)を取得する。 DESC STORAGE INTEGRATION storage_integration_azure ; property property_type property_value property_default ENABLED Boolean true false STORAGE_PROVIDER String AZURE STORAGE_ALLOWED_LOCATIONS List azure://storageikuty1.blob.core.windows.net/container1/files/ [] STORAGE_BLOCKED_LOCATIONS List [] AZURE_TENANT_ID String AZURE_CONSENT_URL String https://login.microsoftonline.com/hogehoge&response_type=code AZURE_MULTI_TENANT_APP_NAME String COMMENT String URLを開き、表示された画面で「承認」ボタンを押下する。 この際、権限がなければ権限不足である旨表示されてここで終了する。(権限は後述) このタイミングでAzure環境にSnowflakeと対応するサービスプリンシパルが作られる。 上手くいくとSnowflakeの企業サイトに遷移する。 Azure Portalにログインし、Snowflakeと共有したいストレージアカウントを選択する。 IAMから、サービスプリンシパルに対して以下のロールを割り当てる。 サービスプリンシパルIDは、上で得られたマルチテナントアプリ名のハイフンの前の部分。 この辺り、むちゃくちゃ煩雑で、AWSよりも大変。 Storage Blob Data Reader(ストレージBlobデータ閲覧者) Storage Blob Data Contributor(ストレージBlobデータ共同作成者) 次にDBにフォーマットと(外部)ステージを作成する。 USE ROLE SYSADMIN; USE DATABASE HOGE; CREATE OR REPLACE FILE FORMAT MY_CSV_FORMAT TYPE = CSV COMMENT = \'接続検証\' ; CREATE OR REPLACE STAGE my_azure_stage STORAGE_INTEGRATION = storage_integration_azure URL = \'azure://storageikuty1.blob.core.windows.net/container1/files/\' FILE_FORMAT = my_csv_format; ; 該当のBlobにファイルをアップロードする。(パスを指定した場合忘れがち) そして疎通確認。 $ show storage_integration_azure ... $ list @storage_integration_azure ... まとめ Azure環境でストレージ統合によりBlobアクセスを設定してみた。 その際、Azure環境側でパブリックアクセスの開き方が最小限になるように構成した。 Azure側、Snowflake側共に大きな権限が必要だが、公式推奨だし可能ならこの方法が良さそう。

default eye-catch image.

Azure Data Factoryに入門する

Azureクラウド。データと特にAIの世界で頭ひとつ抜け出しそうだ。 有限な時間を有効に使うには、\"ベストソリューション\"に全振りすることが適切だと信じているが、 一方で、それだと認知の奥行きのようなものが少ない気がしている。 考え方を広げるには色々知るべきだとも思う。 知識を糧にするために必要なことは要約と文書化だと信じている。 データ・パイプラインを構築する数多の技術の1つとして、Azure Data Factoryがある。 今回、Azure Data Factoryに入門してみようと思う。 Azureクラウドは公式の説明に癖がある。 分かるまでとっつきづらい印象がある。知識を糧にするため要約と文書化を続けてみたい。 [arst_toc tag=\"h4\"] パイプライン、アクティビティ、データセット、リンクされたサービス データが物理的に格納されている場所をデータセットとリンクする。「リンクサービス」などと言う。 対応するサービスは後述する。 アクティビティには入力データセットと出力データセットを設定できる。 アクティビティは入力データセットのデータを消費し、出力データセットにデータを生成する。 アクティビティは大きく「データのコピー」と「データの変換」と「制御」の責務を持たせられる。 複数のアクティビティを束ねてパイプラインを構成する。これらの関係は下図の通り。 このうち「制御」アクティビティは、変数の追加、別のパイプラインの実行、Assert、ForEach、 If Condition、ルックアップ、変数の設定、Until、検証、Wait、Web、Webhookなどができる。 ユーザは、UI上でポチポチしてパイプラインを組み上げられる。 下図のように、アクティビティの出力を別のアクティビティの入力とすることで機能を作り込んでいく。 前のアクティビティが正常終了しなかった場合、次のアクティビティは実行されない。 もちろん並列実行させることもできる。 サポートするデータストア・コネクタ 移動アクティビティは、ソースからシンクへデータをコピーする。 サポートされているデータストアは公式に記述があるが、一部記述に矛盾があり信用できない。 Azure Data Factory と Azure Synapse Analytics のコネクタの概要 さすがに仕様上は凄まじいサポート具合だと思う。[汎用]により事実上無限に繋げられる。 [Azure] Blob, Cognitive Search Index, CosmosDB, Data Explore, ADSL Gen1/Gen2, Azure Database (MariaDB/PostgreSQL/MySQL), Databricks Delta Lake, Files, SQL Database, SQL Managed Instance, Synapse Analytics, Table Storage [Database] AWS RDS(Oracle, SQL Server), AWS Redshift, DB2, Drill, GCP BigQuery, GreenPlum, HBase, Apache Impala, Informix, MariaDB, Microsoft Access, MySQL, Netezza, Phenix, PostgreSQL, SAP Business Warehouse, SAP HANA, Snowflake, Spark, SQL Server, Sybase, Terradata, Vertica [NoSQL] Cassandra, MongoDB [Files] S3, FileSystem, FTP, GCP Cloud Storage, HDFs, Oracle Storage, SFTP [サービスとアプリ] Amazon Marketplace WebService, Appfingures, Asana, Concur,data world, Dataverse, Dynamics365, Dynamics AX, Dynamics CRM, GitHub, Google AdWords, Google Spredsheet, HubSpot, Jira, Magento, Microsoft365, Oracle Eloqua, Oracle Responsys, Oracle Service Cloud, Paypal, Quickbase,Quick Books,Salesforce, Salesforce Service Cloud, Sales Marketing Cloud, C4C, SAP ECC, Service Now, SharePoint Online, Shopify, SmartSheet, Square, TeamDesk, Twillo, Web(HTML), Xero, Zen Desk, Zoho [汎用] HTTP, OData, ODBC, RESTfulAPI ファイル形式は以下。 Arvo,バイナリ,Common Data Model形式,区切りテキスト, 差分形式, Excel, JSON, ORC, Parquet, XML スケジュール設定と実行 「スケジュール」と名前が付くものがパイプライン、アクティビティ、データセットに存在するが、 パイプラインの実行時刻・タイミングを定義するのは「出力データセット」であるようだ。 アクティビティの実行タイミングは、入力ではなく出力のデータセットのスケジュールで決まる。 このため、入力データセットは省略できるが、出力データセットは必ず1個作る必要がある。 アクティビティと入出力データセットの関係が下図に示されている。 出力データセットが、「8-9AM」、「9-10AM」、「10-11AM」の3回の枠でスケジュールされている。 それに合わせてアクティビティと入力データセットのスケジュールが決まる。 下図はさらに、入力が準備され、アクティビティと出力がこれから実行される様を表している。 アクティビティにスケジュールをオプションで設定できるが、 その場合は出力データセットのものと合わせる必要がある。(何の意味があるのか...) パイプラインにパイプラインがアクティブな期間を設定できる。 非アクティブな時間に出力データセットが発火しても無視される。 統合ランタイム(IR) Azure内のフルマネージドなサーバーレスコンピューティング。 仮想化されたコンピューティングリソースをData Factoryでは統合ランタイムと呼ぶ。 悪いセンスの極みみたいな名前だが、Windowsで開発した経験があるとピンと来ると思う。 アクティビティや入出力データセットで定義された内容を処理する際に消費される計算資源。 各処理がどこで実行されるのか、というのはパイプラインの実行で気になるポイント。 可能な限りデータの移動やコピーは省略して欲しいし、各サービスが得意な機能を使って欲しい。 この辺りを最大限考慮して必要な場所で処理するよ、と公式には書かれている。 3種類の統合ランタイムが存在する。 Azure統合ランタイム Azureでデータフローを実行する。クラウドストア間でコピーアクティビティを実行する。 コピーは負荷が高い操作であるので、オートスケールが考慮されている。 他に、アクティビティの負荷をコンピューティングに割り当てる(ディスパッチ)機能を有する。 Self-Hosted統合ランタイム オンプレミスかAzure上のプライベートネットワークにインストールするコンピューティングリソース。 Windowsマシンを自前で用意(Self-Host)して、Azureサービスに提供するというもの。 まさにMicrosoftのWindowsをAzureと繋ぐコンピューティング仮想化の姿とも言える。 オンプレミスに大量のコンピュータを用意してAzureに提供したりすることができる。 Azure-SSIS統合ランタイム SSIS(SQL Server Integration Service)パッケージを実行する専用のAzure上の フルマネージドクラスタ。 SSISプロジェクト用に独自のAzureSQL Database、SQL Managed Instanceを持ち込める。 ノードサイズのスケールアップ、クラスターのスケールアウトを実行できる。 SSIS統合ランタイムを手動で停止することでコストを節約することもできる。 SSDTやSSMS等、SQLServer用クライアントツールをSSIS統合ランタイムに繋ぐことができる。 統合ランタイムの場所 仮想マシンがAzureクラウド上のどこに物理的に配置されるのかは気になるところ。 一般に性能観点だけであればデータに近いところに仮想マシンを配置しておくと良い結果となる。 が、データコンプライアンスの観点で、仮想マシンを配置する場所を固定しないといけない場合がある。 Azure IR Azure IRについて、デフォルトでは自動的に配置場所が決まる。 つまり、シンクデータストアと\"同じリージョン\"または\"同じ地理的な場所\"に配置される。 クラウドサービスを使っていると、そのサービスのリージョン、地理的な場所が不定となる場合がある。 シンクデータストアの場所がわからず、世界中の好き勝手なところにAzure IRが作られてしまう。 そんな時は、自動的なコンフィグレーションを無視して配置場所を固定することができる。 また、データが特定のリージョンから離れないように、明示的にVMの場所を固定できる。 Self-hosted IR Self-hostするものなので、そもそも場所は自分で決めるもの。 Azure-SSIS IR 箱としてのデータベースと、そのデータベースを駆動するコンピューティングリソースが分離している。 普通に考えて、箱の場所とVMの場所は同じにしないとパフォーマンスが悪いだろう。(公式に記載あり) ただし、Data FactoryとSSIS IRの場所は必ずしも同じでなくても良いようだ。 データ統合単位(DIU) なんか説明なくいきなり出てくるワード。Data Integration Unitの略だろうか。 Azure Data Factoryにおいて、1つの単位の能力を表す尺度。 CPU、メモリ、ネットワークリソース割り当てを組み合わせたもの。 DIUはAzure IRランタイムにのみ適用される。Self-hostedランタイムには適用されない。 要はAzure IRランタイムの戦闘力を数値化したもの。課金に影響する。 データ統合単位 シナリオにあった統合ランタイムの選択 Azure統合ランタイムからパブリックにアクセスできないデータストアにアクセスする場合、 データストアのファイアウォールなどにAzure統合ランタイムのパブリックIPを設定して抜け穴を作る。 しかし、Azureを含むクラウドアーキテクチャの設計においてあまり望ましくない。 よりベターなのは、PrivateLinkや、Load Balancerの追加セットアップを行う。 また、オンプレミスにある場合、Express Route、S2S VPN経由でアクセスを行う。 この追加セットアップは必要以上に面倒を増やすし、最初からSelf-Hostedにした方が良い。 Enterpriseの現場において、データ転送の経路に対するセキュリティ要件は厳しい。 転送経路が全てプライベートになっている状態を保証できることが理想となる。 つまり、プライベートエンドポイント間のPrivateLinkを設定することが理想である。 Azure統合ランタイムは、デフォでプライベートエンドポイントとPrivateLinkはサポートされない。 マネージド仮想ネットワークを使用すると、データストアに対してプライベートエンドポイントを設定し、 統合ランタイムをPrivateLinkを設定できる。 また、Self-hosted統合ランタイムにおいて、仮想ネットワークにプライベートエンドポイントと PrivateLinkを設定できる。 クラウドインフラストラクチャの責任共有モデルについての議論もある。 Azure統合ランタイムのパッチ、バージョン更新等のメンテナンスはAzure側が自動的に行う。 対して、Self-hosted統合ランタイムは、ユーザが責任を持つ必要がある。 より楽をしたいならAzure統合ランタイムがより良い選択肢となる。 まとめ ドキュメントを調べてAzure Data Factoryの用語をまとめてみた。 Azureクラウドの公式ドキュメントは癖があり、ドキュメントを読むだけで一苦労だなという印象。 やはり、AWSと比較してEnterpriseを全面的に推している印象がある。 後続の記事でガンガン知識を文書化していく。

default eye-catch image.

dbt Analytics Engineering 認定試験の学習要領を読んでみる

Moden Data Stackの重要な技術として有名なdbtを理解したい。 と言いつつも使う機会がなかなか無いな、という点と、認定試験があるようなので、 まずは認定試験に合格することを目的に、知識を獲得していきたい。 SnowPro Certificationsと同様に参考書も問題集も無いが、 公式に学習要領のようなものが無茶苦茶丁寧に書かれている。 もちろん試験に向けた資料なので偏りだったり不足はあるのだろうけれど、 何も知らない素人がステップバイステップで物事を理解していくには大分心強い。 dbt Analytics Engineering Certification Exam Study Guide この記事では、この学習要領を上から順に読んでいく。 後続の記事で(ブログドリブンで)頭に入れた知識をアウトプットしていく。 [arst_toc tag=\"h4\"] トピック一覧 何を理解したら 「dbtのことを理解した」 と言えるのか。 dbtが備えている機能とその詳細が試験要項に一覧化されている。 初心者には本当に助かる。これらが頭の中で整理されて当たり前になったときが次のステップ。 何も知らない状況では分からない言葉や概念が多い。 振り返らず5分程度で訳を付けて終了。何となく全体像が見えてくる。 Topic 1: Developing dbt models Identifying and verifying any raw object dependencies Understanding core dbt materializations Conceptualizing modularity and how to incorporate DRY principles Converting business logic into performant SQL queries Using commands such as run, test, docs and seed Creating a logical flow of models and building clean DAGs Defining configurations in dbt_project.yml Configuring sources in dbt Using dbt Packages dbtモデルの実装方法と理論について。 dbtは物理的に存在するDBオブジェクトを抽象化するフレームワークであって論理と物理のマップがある。 抽象的な構造から見た具体化方式について理解する。 dbtを構成する概念の理解とDRY(Don\'t Repeat Yourself)法則をどう実現するか。 ビジネスロジックを高性能なクエリに変換する仕組みを理解する。 run,test,docs,seedなどのコマンドの使い方を理解する。 モデルのフローとDAGs、dbt_project.ymlの定義方法、sourceの構成方法を理解する。 Topic 2: Debugging data modeling errors Understanding logged error messages Troubleshooting using compiled code Troubleshooting .yml compilation errors Distinguishing between a pure SQL and a dbt issue that presents itself as a SQL issue Developing and implementing a fix and testing it prior to merging dbtモデルのデバッグ方法について。 ログとして記録されたエラーメッセージの解釈方法を理解する。 dbtによりコンパイル/生成されたコードを使ったトラブルシュート方法を理解する。 .ymlコンパイルエラーに対するトラブルシュート方法を理解する。 SQL自身の問題なのかdbtに纏わる問題なのかを区別する方法を理解する。 (git branchに?)mergeする前に修正/テストする方法を理解する。 Topic 3: Monitoring data pipelines Understanding and testing the warehouse-level implications of a model run failing at different points in the DAG Understanding the general landscape of tooling 作ったパイプラインの監視。 DAG のさまざまなポイントでモデル実行が失敗した場合のウェアハウスレベルの影響の理解+テスト方法。 (ウェアハウスレベルってどこのレベル??) Topic 4: Implementing dbt tests Using generic, singular and custom tests on a wide variety of models and sources Understanding assumptions specific to the datasets being generated in models and to the raw data in the warehouse Implementing various testing steps in the workflow Ensuring data is being piped into the warehouse and validating accuracy against baselines dbtテストの実装。 様々な種類のモデル・ソースに対する generic, singular, customテストの使用方法。 モデルで生成されるデータセットとウェアハウス内の生データに特有の仮定を理解する。(意味不..) ワークフロー内にテストステップを実装する方法。 データがウェアハウスにパイプされていることを確認し、ベースラインに対する精度を検証する。 Topic 5: Deploying dbt jobs Understanding the differences between deployment and development environments Configuring development and deployment environments Configuring the appropriate tasks, settings and triggers for the job Understanding how a dbt job utilizes an environment in order to build database objects and artifacts Using dbt commands to execute specific models デプロイ環境と開発環境の違いを理解する。 コンフィグによってデプロイ環境と開発環境を作る。 ジョブの適切なタスク、設定、トリガーの構成。 dbtジョブがDBオブジェクト・生成物をビルドするために環境をどのように使うかを理解する。 特定のdbtモデルを実行するためのdbtコマンドの使用方法。 Topic 6: Creating and Maintaining dbt documentation Updating dbt docs Implementing source, table, and column descriptions in .yml files Using dbt commands to generate a documentation site Using macros to show model and data lineage on the DAG dbt documentationの作成・維持。 dbt docsの更新、.ymlファイル内に source,table,列定義を書く方法。 documentationサイトを生成するためのdbtコマンドの使用方法。 DAGにおけるモデルとリネージを表示するマクロの使用方法。 Topic 7: Promoting code through version control Understanding concepts and working with Git branches and functionalities Creating clean commits and pull requests Merging code to the main branch gitとのコラボw。版管理。 版管理の概念。Git branchとの連携とその機能。 cleanコミットとプルリク。mainブランチへのマージ。 Topic 8: Establishing environments in data warehouse for dbt Understanding environment’s connections Understanding the differences between production data, development data, and raw data データウェアハウスの各環境とdbtの接続を確立する。 環境との接続確立方法を理解する。本番環境データ、開発環境データ、生データの違いを理解する。 学び方 素人が脱初心者するためのステップ。なんだかやたら丁寧。 各ステップで参照すべきリソースがまとまっている。順番を無視する理由は特にないので最初から見ていく。 Checkpoint 0 - 前提条件 SQLの理解が必要。結合、集計、CTEsの書き方、ウィンドウ関数に詳しくないといけない。 Gitの理解が必要。ブランチ戦略や基本的なgitコマンド、プルリクの方法など。 Checkpoint 1 - Build a Foundation やたら丁寧だな... オンライン動画。 dbt Fundamentals 読み物 dbt viewpoint ドキュメント Source properties Node selection syntax dbt_project.yml General resource properties やってみる Creating a dbt project from scratch to deployment Debugging errors コマンド達 dbt compile dbt run dbt source freshness dbt test dbt docs generate dbt build dbt run-operation Checkpoint 2 - Modularity and Refactoring オンライン動画 Refactoring SQL for Modularity Readings How we structure our dbt projects Your Essential dbt Project Checklist ドキュメント Refactoring legacy SQL to dbt やってみる - Refactoring SQL for performance and clarity Checkpoint 3 - Doing More with dbt オンライン動画 Jinja, Macros, and Packages Advanced Materializations Analyses and Seeds ドキュメント Exposures Env_var Target Schema Database やってみる - Utilizing packages and macros in a dbt project - Implementing all core materializations into a dbt project - Implementing seeds コマンド達 - dbt snapshot - dbt seed Checkpoint 4 - Deployment and Testing オンライン動画 Advanced Testing Advanced Deployment 読み物 The exact grant statements we use in a dbt project The exact GitHub pull request template we use at dbt Labs How to review an analytics pull request How we configure Snowflake ドキュメント Tags Hooks & Operations Custom Schema Threads やってみる - Defining environments in your data platform - Defining environments in dbt - Promoting code through git including use of multiple branches, pull requests - Troubleshooting errors in production runs - Defining dbt jobs for optimal performance まとめ 本来探し回らないと見つからないドキュメントが集まっていて良さそう。 このレベルでまとめるだけで何となく全体像が見えてきたような。 後続の記事で(ブログドリブンで)インプットした内容をアウトプットしていく。

default eye-catch image.

検索最適化サービス(Search Optimization Service)の使い方

ドキュメントを読んで使ったことが無い機能に詳しくなるシリーズ。 今回は検索最適化サービス(Search Optimization Service)。 他のドキュメントと比較して英語・日本語ともに文体が異なる気がする。 日本語訳が変なのではなく、元の英文の文体が違う。 まず、\"クラスタリング\"とか\"Materialized View\"が構造の名称である一方で、 \"検索最適化\"が目的の名称となっている点に違和感を感じる。 似て非なる索引(index)とは違うのだよ、という何かなのだろうか... [arst_toc tag=\"h4\"] 検索最適化サービスの概要 検索最適化サービスは、1つまたは少数の異なる行のみを返す「ポイントルックアップクエリ」。むむ?。 要は、巨大なテーブルに対するSELECT文の結果が数行となるようなケースを高速化する。 例えば、あるテーブルのageというカラムに対する等価条件を設定したクエリを速くしたい場合、 あらかじめ高速化用のデータ構造を構築し、ageに対する等価条件を使ったクエリを高速化する。 等価条件検索、範囲条件検索、部分文字列検索に加え、 プレビュー機能として、正規表現検索、半構造化型値検索、地理空間関数検索 が用意される。 他の高速化技術との比較 高速化機能として、他にクラスタリング、Materialized Viewがあるが、それぞれ機序が異なる。 クラスタリングは、マイクロパーティションの分散と枝刈りを利用した方法であって、 ナチュラルキーよりも高いプルーニング効率が得られるケースに適用する。 Materialized Viewは、同一テーブルに対して複数のクラスタリングキーを透過的に適用する 用途で用いられる。もちろん直アクセスでは遅い場所の事実上のキャッシュとしても使う。 検索最適化サービスは、ある意味、RDBMSの索引チックな使い方ができる。 良く使うカラムと検索述語を「アクセスキー」として登録し、高速検索用の索引を構築する。 検索最適化サービスの構成に必要な権限 以下の権限が必要。 テーブルの所有権 テーブルを含むスキーマに対する ADD SEARCH OPTIMIZATION 権限 検索最適化サービスを適用したテーブルにアクセスするには普通にSELECTできれば良い。 検索最適化サービスで未サポートのもの 以下に対して検索最適化サービスを適用することはできない。 (逆に以下以外なら適用できる。) 外部テーブル マテリアライズドビュー COLLATE 句 で定義された列 列の連結 分析表現 テーブル列にキャスト(文字列にキャストされる固定小数点数を除く) Materialized Viewは外部テーブルアクセスを高速化できるが、 検索最適化は外部テーブル、Materialized Viewを対象に出来ない。 何かの事情でCOLLATE句を指定した場合、その列は使用できない。 述語を指定する際に、定数側の明示的キャストはサポートされるが、 列側をキャストすると効かなくなる。例えば以下のような違いがある。 ただし、NUMBERからVARCHARへのキャストだけは機能する。 -- 機能する SELECT hoge FROM table as t1 WHERE timestamp1 = \'2020-01-01\'::date; -- 機能する SELECT hoge FROM table as t1 WHERE cast(numeric_column as varchar) = \'2\' -- 機能しない SELECT hoge FROM table as t1 WHERE to_date(varchar_column) = \'2020-01-01\'; (分析表現って何!?) 検索最適化サービスの構成方法 ALTER TABLE と ADD SEARCH OPTIMIZATION を使用する。 テーブル全体(つまり全カラム)に対して適用できるか、カラムを指定することもできる。 カラムを指定する場合、ON句に最適化したい検索述語等を指定する。 例えば以下であればc1,c2列を使用した等価述語が最適化される。 c1列、c2列に対して等価述語(=)を使ったSQLが高速化される。 -- t1テーブルの c1,c2 の等価述語に対して検索最適化を適用 ALTER TABLE t1 ADD SEARCH OPTIMIZATION ON EQUALITY(c1,c2); -- クエリ SELECT hoge FROM t1 where c1 = 100 and c2 = 200; 検索最適化サービスが適用済みか確認する方法 DESCRIBE により該当テーブルへの検索最適化サービスの適用状況を確認できる。 定義した「検索アクセスパス」の詳細と、有効無効が表示される。 DESCRIBE SEARCH OPTIMIZATION ON t1; +---------------+----------+--------+------------------+--------+ | expression_id | method | target | target_data_type | active | +---------------+----------+--------+------------------+--------+ | 1 | EQUALITY | C1 | NUMBER(38,0) | true | +---------------+----------+--------+------------------+--------+ テーブル全体に対して検索最適化サービスを適用していた場合、 列の追加により、追加した列に対して自動的に検索最適化サービスが適用される。 逆に列の削除により、削除した列が検索最適化サービスから除外される。 列のデフォルト値を無効にすると、検索最適化サービスが「無効」になる。 一度「無効」になってしまったら、SEARCH OPTIMIZATION を一度 DROP し、 再度 ADD しなければならない。 検索最適化サービスのコスト 以下のコストがかかる。 ストレージリソース コンピューティングリソース ストレージリソース 検索アクセスパスのためのデータ構造を構築するために使われる。 通常、元テーブルの1/4ぐらいのサイズになるらしい。 ただし、テーブル内の個別の値の数が多ければ多いほどストレージを消費する。 例えば、全カラムを対象としている場合、全カラムの値が全て異なるという状況は最悪で、 元テーブルと同じだけのストレージを消費してしまう。 コンピューティングリソース 検索最適化を維持するためにコンピューティングリソースが使われる。 テーブル内で大量のデータが変更される場合、リソースの消費量が激しくなる。 追加・変更に比例して大きくなる。削除は若干使用する。 ざっくり以下に比例してコストが増加する。 この機能が有効になっているテーブルの数、およびそれらのテーブル内の個別の値の数。 これらのテーブルで変更されるデータの量 SYSTEM$ESTIMATE_SEARCH_OPTIMIZATION_COSTS 関数によりコストを見積もれる。 -- テーブルに検索最適化を追加する場合の推定コスト select SYSTEM$ESTIMATE_SEARCH_OPTIMIZATION_COSTS(\'TABLE_WITHOUT_SEARCH_OPT\'); +---------------------------------------------------------------------------+ | SYSTEM$ESTIMATE_SEARCH_OPTIMIZATION_COSTS(\'TABLE_WITHOUT_SEARCH_OPT\') | |---------------------------------------------------------------------------| | { | | \"tableName\" : \"TABLE_WITHOUT_SEARCH_OPT\", | | \"searchOptimizationEnabled\" : false, | | \"costPositions\" : [ { | | \"name\" : \"BuildCosts\", | | \"costs\" : { | | \"value\" : 11.279, | | \"unit\" : \"Credits\" | | }, | | \"computationMethod\" : \"Estimated\", | | \"comment\" : \"estimated via sampling\" | | }, { | | \"name\" : \"StorageCosts\", | | \"costs\" : { | | \"value\" : 0.070493, | | \"unit\" : \"TB\" | | }, | | \"computationMethod\" : \"Estimated\", | | \"comment\" : \"estimated via sampling\" | | }, { | | \"name\" : \"MaintenanceCosts\", | | \"costs\" : { | | \"value\" : 30.296, | | \"unit\" : \"Credits\", | | \"perTimeUnit\" : \"MONTH\" | | }, | | \"computationMethod\" : \"Estimated\", | | \"comment\" : \"Estimated from historic change rate over last ~11 days.\" | | } ] | | } | +---------------------------------------------------------------------------+ -- すでに検索最適化が行われているテーブルに対するこの関数の出力 select SYSTEM$ESTIMATE_SEARCH_OPTIMIZATION_COSTS(\'TABLE_WITH_SEARCH_OPT\'); +---------------------------------------------------------------------------+ | SYSTEM$ESTIMATE_SEARCH_OPTIMIZATION_COSTS(\'TABLE_WITH_SEARCH_OPT\') | |---------------------------------------------------------------------------| | { | | \"tableName\" : \"TABLE_WITH_SEARCH_OPT\", | | \"searchOptimizationEnabled\" : true, | | \"costPositions\" : [ { | | \"name\" : \"BuildCosts\", | | \"computationMethod\" : \"NotAvailable\", | | \"comment\" : \"Search optimization is already enabled.\" | | }, { | | \"name\" : \"StorageCosts\", | | \"costs\" : { | | \"value\" : 0.052048, | | \"unit\" : \"TB\" | | }, | | \"computationMethod\" : \"Measured\" | | }, { | | \"name\" : \"Benefit\", | | \"computationMethod\" : \"NotAvailable\", | | \"comment\" : \"Currently not supported.\" | | }, { | | \"name\" : \"MaintenanceCosts\", | | \"costs\" : { | | \"value\" : 30.248, | | \"unit\" : \"Credits\", | | \"perTimeUnit\" : \"MONTH\" | | }, | | \"computationMethod\" : \"EstimatedUpperBound\", | | \"comment\" : \"Estimated from historic change rate over last ~11 days.\" | | } ] | | } | +---------------------------------------------------------------------------+ まとめ 検索最適化サービスについてドキュメントを読んでいくつか試してみた。 ドキュメントにある「ポイントルックアップクエリ」という言葉が機能を最も適切に表していると思う。 高速化したいクエリが明確な時に使えるのかもしれない。

default eye-catch image.

外部関数とUDFs/UDTFs

使ったことがない機能のドキュメントを読んで詳しくなるシリーズ。 今回は外部関数(External Function)。 [arst_toc tag=\"h4\"] ユーザ定義関数 まずユーザ定義関数(User Defined Function)について。 UDFは、組み込み関数と同様にSQLから呼び出すことができる。組み込み関数にない機能を自作できる。 コードを共通化して2個以上の場所で発生するコードクローンを除去できる。 ユーザ定義関数は単一の値を返すパターンと、表を返すパターンの2パターンを定義できる。 単一の値を返すパターンはスカラー関数(UDFs)。 表、つまり0個,1個,2個以上の値を返すパターンは表関数(UDTFs)。 サポートされている言語はJava,JavaScript,Python,SQL。 スカラー関数(UDFs) スカラー関数は入力行に対して1つの出力行を返す。 公式のサンプルを読むのが早い。 -- UDFの定義 create or replace function addone(i int) returns int language python runtime_version = \'3.8\' handler = \'addone_py\' as $$ def addone_py(i): return i+1 $$; -- UDFの実行 select addone(1); 2 こんな調子で、create function の中に書き下した処理をインラインハンドラと言う。 処理を修正する際は、alter function を実行して対応する。 確かにお手軽だが、行数が増えてきたり、処理を共通化したりしたい場合は微妙。 あらかじめコードをコンパイルしておき再利用することもできる(ステージングハンドラ)。 ステージングハンドラであれば、より多くの行数に対応できるし、処理の共通化も用意。 ステージングハンドラの実行例は以下の通り。 CREATE FUNCTION my_udf(i NUMBER) RETURNS NUMBER LANGUAGE JAVA IMPORTS = (\'@mystage/handlers/my_handler.jar\') HANDLER = \'MyClass.myFunction\' インラインハンドラは全ての言語で対応できるが、 ステージングハンドラは、JavaまたはPythonが必要。 表関数(UDTFs) 表関数は入力行に対して表、つまり0行,1行,2行,...行 を返す。 実行例は以下の通り。なるほど。 戻り値が表(table)になるため、SQLだと処理と戻り値のギャップが少ないが、 SQLでは手続き的な処理が書けないため自由度は低い。 -- サンプルテーブル・データの作成 create or replace table orders ( product_id varchar, quantity_sold numeric(11, 2) ); insert into orders (product_id, quantity_sold) values (\'compostable bags\', 2000), (\'re-usable cups\', 1000); -- UDTFの定義 create or replace function orders_for_product(PROD_ID varchar) returns table (Product_ID varchar, Quantity_Sold numeric(11, 2)) as $$ select product_ID, quantity_sold from orders where product_ID = PROD_ID $$ ; -- UDTFの実行 select product_id, quantity_sold from table(orders_for_product(\'compostable bags\')) order by product_id; +------------------+---------------+ | PRODUCT_ID | QUANTITY_SOLD | |------------------+---------------| | compostable bags | 2000.00 | +------------------+---------------+ JavaScriptで書くと以下のようになる。手続き的に処理を書いている。 これであればある程度の自由度を獲得できる。 -- UDTFの定義 -- IP アドレスの「範囲」を IP アドレスの完全なリストに変換 CREATE OR REPLACE FUNCTION range_to_values(PREFIX VARCHAR, RANGE_START FLOAT, RANGE_END FLOAT) RETURNS TABLE (IP_ADDRESS VARCHAR) LANGUAGE JAVASCRIPT AS $$ { processRow: function f(row, rowWriter, context) { var suffix = row.RANGE_START; while (suffix <= row.RANGE_END) { rowWriter.writeRow( {IP_ADDRESS: row.PREFIX + "." + suffix} ); suffix = suffix + 1; } } } $$; -- UDTFの実行 SELECT * FROM TABLE(range_to_values('192.168.1', 42::FLOAT, 45::FLOAT)); +--------------+ | IP_ADDRESS | +==============+ | 192.168.1.42 | +--------------+ | 192.168.1.43 | +--------------+ | 192.168.1.44 | +--------------+ | 192.168.1.45 | +--------------+ 外部関数 ユーザ定義関数のロジック(ハンドラ)を外部のコンピューティングリソースに配置することができる。 概念図は以下の通り。Snowflake側から見て、外部関数、API統合、プロキシサービス、リモートサービスから成る。 外部関数(External Function)はスカラー関数(UDFs)だが、自前でコードを書かない。 その実体は、プロキシサービスを介してリモートサービスのコードを実行するためのオブジェクトで、 CREATE EXTERNAL FUNCTION により定義する。 リモートサービスは、外部のコンピューティングリソース。 AWS Lambda、Azure Functionsが分かりやすいが、 HTTPSでJSONを受けてJSONを返せれば、一般的なWebサーバでも良い様子。 プロキシサービスは、要はリモートサービスを抽象化するレイヤの機能を持つ。 AWS API Gateway、Azure API Managementサービスと言うと分かりやすい。 外部関数が直接リモートサービスを叩かない理由は、プロキシサービスに認証機能を持たせるため。 API統合は、外部関数をSnowflakeの世界に持ち込むための抽象化レイヤ。 同様の機能として、S3,Blobへのアクセス手段を抽象化するストレージ統合がある。 使い場合はUDFと同様。 select my_external_function_2(column_1, column_2) from table_1; select col1 from table_1 where my_external_function_3(col2) < 0; create view view1 (col1) as select my_external_function_5(col1) from table9; select upper(zipcode_to_city_external_function(zipcode)) from address_table; リモートサービスの入出力定義 前述のように、リモートサービスはHTTPSでJSONを受けてJSONを返す必要がある。 そのインターフェースは公式に記述がある。 リモートサービスの入力および出力データ形式 基本的に、POSTを受けて同期的に応答するようにする。 ただし、非同期処理もサポートしており、その際はGETでポーリングリクエストを受ける必要がある。 関数 ext_func(n integer) returns varchar のケースにおいて、リクエストヘッダは以下。 いわゆるRPC(Remote Procedure Call)を定義する何かに準じている。 ヘッダ文字列値 sf-external-function-formatjson固定 sf-external-function-format-version1.0固定 sf-external-function-query-batch-idリクエスト固有の識別子。この値を使って処理の成否を確認する。 sf-external-function-name関数名 ext_func sf-external-function-name-base64関数名(base64) base64(ext_func) sf-external-function-signature関数名署名 (N NUMBER) sf-external-function-signature-base64関数名署名(base64) base64(N NUMBER) sf-external-function-return-type戻り値型 VARCHAR(16777216) sf-external-function-return-type-base64戻り値型(base64) base64(VARCHAR(16777216)) リクエスト本文はJSON。 重要なことは、外部関数はスカラー関数であり、1個の入力に対して1個の応答を返す必要があるが、 一度に複数の入力を受けることができ、それぞれに対して応答を返す必要がある、ということ。 複数の入力のセットを公式ではバッチ、と読んでいる様子。 署名 f(integer, varchar, timestamp) を持つ外部関数にシリアル化したJSONを送る。例は以下。 先頭の数字が1個多い気がするが、これは行番号を表す。 { \"data\": [ [0, 10, \"Alex\", \"2014-01-01 16:00:00\"], [1, 20, \"Steve\", \"2015-01-01 16:00:00\"], [2, 30, \"Alice\", \"2016-01-01 16:00:00\"], [3, 40, \"Adrian\", \"2017-01-01 16:00:00\"] ] } リモートサービスの応答定義 レスポンス本文はJSON。リクエスト本文と同様にコレクションの先頭は行番号を設定する必要がある。 公式には、都市名を引数として、その緯度軽度を返す関数の例が書いてある。 このように、入力と対応する応答値を、入力と同じ順番にコレクションに詰めてシリアライズして返す。 この外部関数は、VARIANT型を戻している。このように、スカラー関数ではあるが、 1回の処理が(半)構造を持てるので、事実上複数の入力値に対して複数の応答値を返せる。 -- 呼び出し例 select val:city, val:latitude, val:longitude from (select ext_func_city_lat_long(city_name) as val from table_of_city_names); -- レスポンス { \"data\": [ [ 0, { \"City\" : \"Warsaw\", \"latitude\" : 52.23, \"longitude\" : 21.01 } ], [ 1, { \"City\" : \"Toronto\", \"latitude\" : 43.65, \"longitude\" : -79.38 } ] ] } StatusCode=200で、同期応答完了。 202を返すと、非同期応答となり処理を継続できる様子だが今回は省略。 その他はエラー応答として扱われる。 まとめ 外部関数に関するドキュメントと、その理解に必要なUDFs、UDTSsのドキュメントを読んでみた。 実体はHTTPS上のWebAPIであり、事実上無限の自由度を獲得できると思う。 (これを持ち出す頃には、本末転倒な何かに陥っていないか注意は必要だと思う)

default eye-catch image.

外部テーブルと使い方

使ったことがない機能のドキュメントを読んで詳しくなるシリーズ。 ステージに配置したファイルに対してロードせずに直接クエリを実行できる仕組み。 [arst_toc tag=\"h4\"] 外部テーブルの基本 ファイルの構造を自動的に解釈してテーブルにしてくれる訳ではなく、 いったんレコードがVARIANT型のVALUEカラムに書かれる。 外部テーブルに対するSELECT文の中で、VALUEカラム内の値を取得して新規に列を作る。 この列を仮想列と言ったりする。 外部テーブルは、クエリ実行のたびにファイルにアクセスすることになるため遅い。 Materialized Viewを作成することで、高速に外部テーブルにアクセスできる。 複数ファイルとパーティション化 外部テーブルの実体は外部ステージ上のファイルであって、通常、複数のファイルから構成される。 複数のファイルが1つの外部テーブルとして扱われるところがポイント。 stackoverflowにドンピシャの記事があってとても参考になった。 Snowflake External Table Partition - Granular Path /appname/lob/ という論理ディレクトリの下に Granular にファイルが配置されている例。 例えば、/appname/lob/2020/07/24/hoge.txt のように論理ディレクトリ以下に日付を使って ファイルが配置されることを想定している。 CREATE EXTERNAL TABLEする際、LOCATION を /appname/lob/ とすることで、 /appname/lob/以下に配置された複数のファイルが1つの外部テーブルで扱われる。 その際、それぞれのファイル名が metadata$filename に渡される。 公式によると、外部テーブルのパーティショニングを行うことが推奨されている。 パーティション化された外部テーブル 下記の例では、ファイル名からYYYY/MM/DDを抽出して日付化して、パーティションキーとしている。 CREATE OR REPLACE EXTERNAL TABLE Database.Schema.ExternalTableName( date_part date as to_date(substr(metadata$filename, 14, 10), \'YYYY/MM/DD\'), col1 varchar AS (value:col1::varchar)), col2 varchar AS (value:col2::varchar)) PARTITION BY (date_part) INTEGRATION = \'YourIntegration\' LOCATION=@SFSTG/appname/lob/ AUTO_REFRESH = true FILE_FORMAT = (TYPE = JSON); パーティショニングとは、名前が似ているがマイクロパーティションの事ではないと思う。 概念的に似ていて、データを別々の塊として格納し、検索時にプルーニング的な何かを期待する。 ガバナンス系オブジェクトとの関係 以前、列レベルセキュリティのマスキングポリシーをまとめたように、 テーブルと独立してマスキングポリシーを定義し、カラムに適用(Apply)することで列を保護する。 [clink url=\"https://ikuty.com/2023/03/31/column-level-security/\"] 外部テーブルは上記のように、VALUE列にKey-Valueで列値が入り、 その後、SELECT文内で仮想列を定義する、という仕様で、外部テーブルに列がある訳ではない。 このため、マスキングポリシーを適用する先がない、という問題が起こる。 ただ、VALUE列全体にApplyすることは出来る様子。 外部テーブル-セキュリティカラムイントロ Masking Policyはビューのカラムに対して適用することができるが、 これは、外部テーブルに設定したMaterialized Viewにも設定できる。 本記事の図のように、Materialized Viewを用意した上であれば、 Masking Policyを適用することができる。

default eye-catch image.

replicationとshareとcloneの違い

データをコピーしてしまうと、データをコピーする際の計算資源とストレージが必要になる。 そして、「どちらのデータが正しいか問題」が発生してしまい、由々しき事態になる。 SSOT(Single Source Of True)という考え方があり、なるべくデータは1箇所に集めたい。 Zero Copy Cloning Zero Copy Cloning を使うことにより、1つのデータをコピーせずに参照できる。 Cloneだけで意味が通じるが、「コピーしてないよ」を強調するために\"Zero Copy\"を付けて呼ぶ。 Clone は同一アカウント内のオブジェクト操作レベルで使用する。 つまり、Database、Schema、Table単位で使用する。 Share Share は 同一クラウドプロバイダ内の複数のアカウントで参照するために使用する。 一度 Shareというオブジェクトを作成し、Share から Database オブジェクトを作る。 Share から Databaseオブジェクトを作る際にコピーは行われない。 Cross-Cloud,Cross-Region してしまうと、「コピーせずに」が出来なくなる。 なので、Shareは同一Cloud,同一Regionまで。 Replication Cross-Cloud, Cross-Region でデータをコピーするための仕組み。 DR対応などで異なるリージョンにバックアップを取りたい場合や、 異なるクラウドプラットフォームに別機能を実装する場合などに用いる。 どうしてもコピーが発生してしまう。 一度 Cross-Cloud, Cross-Region で Replication を行い、 Replication先で Share, Clone を行うことで、Cross する回数が最小化される。 Whitepaper Shareについて以下が参考になった。 Snowflake Data Sharing EXTENDING THE BUILT-FOR-THE CLOUD DATA WAREHOUSE BEYOND ORGANIZATIONAL AND APPLICATION BOUNDARIES