Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Lakeflow - Spark Declarative Pipelines って知ってますか?

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

Lakeflow - Spark Declarative Pipelines って知ってますか?

JEDAIでお話したスライドです!

Avatar for Akihiro Kuwano

Akihiro Kuwano

February 24, 2026
Tweet

More Decks by Akihiro Kuwano

Other Decks in Technology

Transcript

  1. スピーカー Akihiro Kuwano / 桑野 章弘 経歴 ▪ 道玄坂 緑

    会社B2C企業で インフラエンジニアとして キャリア や、目黒 オレンジ 会社パブリッククラウドベンダーでソリューション アーキテクトとしてキャリアを重 、現在 京橋 ブロック 会社 でソ リューションアーキテクトをしています! ▪ B2C企業担当 ソリューションアーキテクトとして様々な案件において技術 支援を実施しております! Databricks Japan 株式会社 Solutions Architect
  2. アジェンダ • データエンジニアリング 課題 • これってもっと簡単にならん でしたっけ? • Lakeflow Spark

    Declarative Pipeline「できらぁ!」 • で なにが、できらぁ!なんだ ? • 今日から始めるSDP
  3. 何がつらい? • 依存関係 • Aテーブルが更新されたらBを動かす、という設定をワークフローや、Jobスケジュールで手動 管理する が辛い • バッチ?ストリーミング? •

    結局同じ様な処理をバッチとストリーミングで分けて書かないといけない が辛い • やりたい事までが遠い • リトライ処理、チェックポイント管理、スキーマ進化 対応など、本来やりたいデータ変換以外 (でも大事) コードが大半を占めている が辛い
  4. クエリから 本番環境まで 苦労 SQLクエリを信頼性 高いETLパイプラインに変換するために必要な作業 CREATE TABLE raw_data as SELECT

    * FROM json.`…` CREATE TABLE clean_data as SELECT … FROM raw_data 依存性管理 日々 パーテ ション計算 チェックポイント &再試行 クオリティチェック ガバナンス データディスカバリ バックフィル ハンドリング バージョン管理 インフラ環境 デプロイ
  5. 運用 複雑さが支配的 時間が変換で なくツール作り に費やされている CREATE TABLE raw_data as SELECT

    * FROM json.`…` CREATE TABLE clean_data as SELECT … FROM raw_data 依存性管理 日々 パーテ ション計算 チェックポイント &再試行 クオリティチェック ガバナンス データディスカバリ バックフィル ハンドリング バージョン管理 インフラ環境 デプロイ
  6. どこに時間を集中させるべき ? データから価値を引き出すに どうしたらいい? Dependency Management Daily Partition Computation Checkpointin

    g & Retries Quality Checks Governance Data Discovery Backfill Handling Version Control Deployment Infrastructure CREATE TABLE raw_data as SELECT * FROM json.`…` CREATE TABLE clean_data as SELECT … FROM raw_data
  7. Unity Catalog Repos Databricks Workflows Spark Declarative Pipelines 紹介 SDPを使用するだけで、クエリから本番パイプラインまでを一気通貫に処理が可能になる

    CREATE STREAMING TABLE raw_data as SELECT * FROM cloud_files(…) CREATE MATERIALIZED VIEW clean_data as SELECT … FROM LIVE.raw_data 依存性管理 インクリメンタ ル計算 チェックポイント &再試行 Expectations フル更新 Lakeflow Spark Declarative Pipelines
  8. Lakeflow Spark宣言型パイプラインと ? ETL処理 ため 「手順」を書く をやめ、「理想」を定義する Lakeflow SDP 、シンプルな宣言型アプローチ

    で信頼性 高いデータパイプラインを構築する、ETLフ レームワークです。Lakeflow SDP インフラストラクチャを自動管理 し、データアナリストやエンジニア ツールに費やす時間を削減、データから価値を引き出すこと に集中できらぁ! ETL開発を加 インフラストラクチャを 自動管理 データへ 信頼を確保 バッチと ストリーミングを 簡素化 https://www.databricks.com/jp/product/data-engineering/spark-declarative-pipelines
  9. ストリーミングテーブル (ST) 技術的な特徴 • Append-onlyなデータ取り込み / 変換専用 テーブル • 各入力レコード

    1回だけ処理 (Exactly-Once) • 上流データが既に取り込み済み レコード 再処理 行わ れない(ただし、Full Refreshを行うと全レコードを再処理) Append-onlyなデータ取り込み / 変換 基本的な作成方法 <SQL 場合> CREATE OR REFRESH STREAMING TABLE basic_st AS SELECT * FROM STREAM samples.nyctaxi.trips; <Python 場合> from pyspark import pipelines as dp @dp.table(name = "trips_st") def basic_st(): return spark.readStream .table("samples.nyctaxi.trips")
  10. マテリアライズドビュー (MV) 技術的な特徴 • 結果を事前計算、キャッシュするビュー • 上流データ 変更/削除に追随して結果が最新化される • 全データを再処理する

    と同じ最新 結果を担保しつつ、 内部で 可能な限り増分的に処理する • 指定した間隔で上流データと同期 変更や削除も含め、常に最新 結果を反映 基本的な作成方法 <SQL 場合> CREATE OR REFRESH MATERIALIZED VIEW basic_mv AS SELECT * FROM samples.nyctaxi.trips; <Python 場合> from pyspark import pipelines as dp @dp.materialized_view(name = "trips_mv") def basic_mv(): return spark.read .table("samples.nyctaxi.trips")
  11. Append フロー データソースに追加された新しいデータ(ファイル、レコード) みを処理し、 ターゲットに追記(Append)するフロー ストリーミングテーブルやマテリアライズドビューにデータを取り込む 基本的な作成方法 (明示的に作成する場合 ) <SQL

    場合> CREATE FLOW customers_silver AS INSERT INTO customers_silver BY NAME SELECT * FROM STREAM(customers_bronze); <Python 場合> from pyspark import pipelines as dp @dp.append_flow(target = "customers_silver") def customer_silver(): return spark.readStream.table("customers_bronze") ※ 上記 例で 、customer_silverというストリーミングテーブルが予め作成されてい るも とする • マテリアライズドビューやストリーミングテーブルを データ取得クエリを含める形で作成した場合、 暗黙的 にAppendフローが作成されている • 明示的に作成したフロー 、ストリーミングテーブルま た シンク(後述) みをターゲットとすることができる • 複数 フローからターゲットを、同一 ストリーミング テーブルに指定することもできる
  12. Auto CDC フロー データソース Change Data Feed (CDF)を使用して、ターゲットテーブルを差分更新する。追記だけで く 更新、削除を含む。

    Change Data Feed (CDF)を使用した差分更新 基本的な作成方法 <SQL 場合> CREATE FLOW target_flow AS AUTO CDC INTO target FROM stream(cdc_data.users) KEYS (userId) APPLY AS DELETE WHEN operation = "DELETE" SEQUENCE BY sequenceNum COLUMNS * EXCEPT (operation, sequenceNum) STORED AS SCD TYPE 2; <Python 場合> dp.create_auto_cdc_flow ( target = "target", source = "users", keys = ["userId"], sequence_by = col("sequenceNum"), apply_as_deletes = expr("operation = 'DELETE'"), except_column_list = ["operation", "sequenceNum"], stored_as_scd_type = "2" ) ※ 上記 例で 、targetというストリーミングテーブルが予め作成されおり、usersに ソース CDF情報が格納されているいるも とする。 • Append フローと異なり、Auto CDC フロー 必ず明 示的に作成する必要がある • Auto CDC フロー ターゲットとして指定できる 、 ストリーミングテーブル み • Append フロー同様、複数 フローから ターゲット を同一 ストリーミングテーブルに指定することもでき る • 同一キーを持つレコードに対して上流から複数レコー ドが到着した場合 順序判断や、 SCD Type 1 / Type 2 どちらで更新するか等、差分更新 動作を コントロールするパラメータが用意されている
  13. Auto CDC による SCD Type 2 容易な実現 SCD Type 2

    実装上 煩雑さを排除し、シンプルなAPIで実現 userId name city operation sequence 123 Isabel Monterrey INSERT 2025/1/10 5:00 123 null null DELETE 2025/3/1 2:00 125 Mercedes Guadalajara UPDATE 2025/3/1 2:00 ・・・ ・・・ ・・・ ・・・ ・・・ Sourceから CDF (Change Data Feed) userId name city 123 Isabel Chihuahua 124 Raul Oaxaca 125 Mercedes Guadalajara 126 Lily Cancun Sourceから スナップショット userId name city __START_AT __END_AT 123 Isabel Monterrey 2025/1/10 5:00 2025/2/20 9:00 123 Isabel Chihuahua 2025/2/20 9:00 2025/3/1 2:00 124 Raul Oaxaca 2025/1/10 5:00 null 125 Mercedes Tijuana 2025/1/25 8:00 2025/2/20 9:00 125 Mercedes Mexicali 2025/2/20 9:00 2025/3/1 2:00 125 Mercedes Guadalajara 2025/3/1 2:00 null 126 Lily Cancun 2025/1/25 8:00 null OR SDPが提供する Python/SQL API create_auto_cdc_flow() また create_auto_cdc_from_sn apshot_flow() 前ページの例のような 様々なケースに対応する ための処理を実装済み • ソートキーの指定 • NULL列の扱い • DELETEの挙動指定 • TRUNCATEの挙動指定 • 追跡対象の列指定 • SCD Typeの指定
  14. シンク ST/ MV以外 形式でデータを書き出す LDPで加工したデータをDatabricks外 様々なシステムで使用可能にするため、ST / MV以外 形式で データを書き出す。

    シンクで対応可能な書き出し先 / 形式 • Delta テーブル • Apache Kafka • Azure Event Hubs • Python カスタムデータソース • 任意 書き込み先/形式をカスタム実装可能 基本的な作成方法 (Kafkaをシンクにする例) <Python> credential_name = "<service-credential>" eh_namespace_name = "dp-eventhub" bootstrap_servers = f"{eh_namespace_name}.servicebus.windows.net:9093" topic_name = "dp-sink" dp.create_sink ( name = "eh_sink", format = "kafka", options = { "databricks.serviceCredential": credential_name, "kafka.bootstrap.servers": bootstrap_servers, "topic": topic_name } ) @dp.append_flow(name = "kafka_sink_flow", target = "eh_sink") def kafka_sink_flow(): return ( spark.readStream .table("spark_referrers") .selectExpr("cast(current_page_id as string) as key", "to_json(struct(referrer, current_page_title, click_count)) AS value") )
  15. リアルタイム性が必要 な場合、MVで なく通 常 ビューも選択肢 パイプライン 中で ST / MV

    使い分け 上流データソース 種類/更新 性質と、変換処理 内容によって選択 • クラウドストレージ:新規ファイル追加 み • メッセージバス (Kafka / Kinesis / Event Hubs / etc.) • Append-only テーブル (Federation 接続した外部テーブル、 Databricksマ ネージドテーブル ) データソース Bronze Silver Gold • ストリーミングテーブル • CDFがあるデータソース • クラウドストレージ:既存ファイル 上 書きあり • マテリアライズドビュー • 更新/削除があるテーブル (Federation接続した外部テーブル、 Databricksマネージドテーブル ) ストリーミング テーブル マテリアライズド ビュー マテリアライズド ビュー ストリーミング テーブル マテリアライズド ビュー Append フロー Auto CDC フロー Append フロー Append フロー(暗 黙) Append フロー(暗 黙) Append フロー(暗 黙) Append フロー(暗 黙) フィルタリング、カラム追 加等レコード単位 変換 み 場合 上流がMV 場合 Auto CDC フロー 集計処理 パイプライン外にある上流データ 生データ 保存 整形、結合等を行ったデータ (集計無し) 個々 分析用途に合わせて 集計された結果 シンク 外部システム向けに ST/MV以外 任意 形式で書き出したい場合
  16. 実現できること • バッチとストリーミングを同じ構文で統一的に記述 • CDC(Change Data Capture) 順序保証つきUPSERT・DELETEを宣言的に処理 • データ品質チェック(NULLチェック・バリデーション)をコードに宣言として埋め込む

    • 1つ ソースから複数テーブルへ 分岐(ファンアウト)をチェックポイント 手動管理なしに実現 • 遅延データ ウォーターマーク処理とウィンドウ集計を1つ 定義で書ける • パス・リソース設定など 環境依存部分をConfigurationとして外出しし、コードを環境非依存に • パイプライン全体 依存関係を自動解決・管理 • 障害時 リカバリやリトライを自動で処理
  17. バッチとストリーミングを同じ構文で • SDP で 、spark.read(バッチ)か spark.readStream(ストリーミング)かを 切り替えるだけで、デコレータ 同じ @dp.table() /

    @dp.materialized_view() を使う • 「どこに書くか」「どう管理するか」 自動 readかreadStreamか選ぶだけ # ストリーミング取り込み( Streaming Table) @dp.table() def bronze_events(): return spark.readStream.format("cloudFiles") \ .option("cloudFiles.format", "json") \ .load(spark.conf.get("source_path")) # バッチ集計( Materialized View) @dp.materialized_view() def silver_summary(): return spark.read.table("bronze_events") \ .groupBy("user_id") \ .sum("amount")
  18. データ品質チェックを宣言的に amount がNullじゃないか 0 以上 データだけを対象にしたい # 手動バリデーション df =

    spark.read.table("bronze_events") invalid = df.filter("amount IS NULL OR amount < 0") if invalid.count() > 0: raise Exception(f"Invalid records found: {invalid.count()}") df.filter("amount IS NOT NULL AND amount >= 0").write.saveAsTable("silver_events") # expectで宣言的に書ける (Python) @dp.table() @dp.expect_or_drop("valid_amount", "amount IS NOT NULL AND amount >= 0") def silver_events(): return spark.readStream.table("bronze_events") SDPだと? # expectで宣言的に書ける (SQL) CREATE OR REFRESH STREAMING TABLE silver_events CONSTRAINT valid_amount EXPECT (amount IS NOT NULL AND amount >= 0) ON VIOLATION DROP ROW AS SELECT * FROM STREAM(bronze_events);
  19. ファンアウト (1ソースから複数テーブルへ 分岐 ) 細かいストリームごと 管理など 必要なし/もちろん細かくもできる # ソースを複数回読む or

    キャッシュして分岐 df = spark.readStream.format("cloudFiles") \ .option("cloudFiles.format", "json") \ .load("s3://bucket/events/") df.cache() # 各テーブルごとに個別 writeStreamを管理 df.filter("event_type = 'purchase'") \ .writeStream \ .option("checkpointLocation", "/checkpoints/purchases") \ .toTable("silver_purchases") df.filter("event_type = 'click'") \ .writeStream \ .option("checkpointLocation", "/checkpoints/clicks") \ .toTable("silver_clicks") df.filter("event_type = 'error'") \ .writeStream \ .option("checkpointLocation", "/checkpoints/errors") \ .toTable("silver_errors") @dp.table() def silver_purchases(): return spark.readStream.table("bronze_events").filter("event_type = 'purchase'") @dp.table() def silver_clicks(): return spark.readStream.table("bronze_events").filter("event_type = 'click'") @dp.table() def silver_errors(): return spark.readStream.table("bronze_events").filter("event_type = 'error'") SDPだと?
  20. 遅延データ ウォーターマーク処理 めんどくさいウォーターマーク 処理もシンプルに # ウォーターマーク設定 df = spark.readStream \

    .table("bronze_events") \ .withWatermark("event_time", "30 minutes") # ウィンドウ集計 windowed = df.groupBy( window(col("event_time"), "10 minutes"), col("user_id") ).agg(sum("amount").alias("total")) def write_with_merge(batch_df, batch_id): batch_df.createOrReplaceTempView("updates") spark.sql(""" MERGE INTO gold_summary t USING updates s ON t.user_id = s.user_id AND t.window = s.window WHEN MATCHED THEN UPDATE SET total = s.total WHEN NOT MATCHED THEN INSERT * """) windowed.writeStream \ .foreachBatch(write_with_merge) \ .option("checkpointLocation", "/checkpoints/windowed") \ .outputMode("update") \ .start() CREATE OR REFRESH MATERIALIZED VIEW gold_summary AS SELECT window(event_time, '10 minutes') AS window, user_id, SUM(amount) AS total FROM STREAM(bronze_events) WITH WATERMARK ON event_time DELAY OF 30 MINUTES GROUP BY window(event_time, '10 minutes'), user_id; SDPだと?
  21. CDCで順序保証つき UPSERTを行いたい CDC用 関数が用意されています def upsertToDelta(microBatchOutputDF: DataFrame, batchId: Long) {

    microBatchOutputDF .groupBy("key") .agg(max_by("ts", struct("*")).alias("row")) .select("row.*") .createOrReplaceTempView("updates") microBatchOutputDF.sparkSession.sql(s""" MERGE INTO cdc_data_raw t USING updates s ON s.key = t.key WHEN MATCHED AND s.is_delete THEN UPDATE SET DELETED_AT=now() WHEN MATCHED THEN UPDATE SET A=CASE WHEN s.ts > t.ts THEN s.a ELSE t.a, B=CASE WHEN s.ts > t.ts THEN s.b ELSE t.b, ... for every column ... WHEN NOT MATCHED THEN INSERT * """) } cdcData.writeStream \ .foreachBatch(upsertToDelta) \ .outputMode("update") \ .start() CREATE OR REFRESH STREAMING TABLE cdc_data; CREATE FLOW cdc_flow AS AUTO CDC INTO cdc_data FROM STREAM(source_data) KEYS (id) SEQUENCE BY ts APPLY AS DELETE WHEN is_deleted; SDPだと?
  22. Lakeflow Pipelines Editor Lakeflowを使ったデータエンジニアリング専用 ”IDE” 複数ファイル タブ切替 コードファイル 管理 パイプライン

    設定 & 実行 テーブル 自動可視化 テーブル プレビュー パフォーマンス /メトリクス エラー調査 効率的な開発 /デバッグ ため 部分的実行