🍣

Data Engineering Design Patterns 補助輪シリーズ -- 2章: Data Loading

に公開

はじめに

こんにちは!bare64 メンバーの surimi (X: @surimi_22) です。

私たちは2026年2月現在、O’Reilly から 2025年に出版された Data Engineering Design Patterns という本の輪読会をしています。

輪読会を通じて、本書は著者が長年培ったデータ基盤構築のプラクティスをパターン化したもので、各フェーズの設計手法を構造的に学べる良書であると分かりました。しかし、概念・事例説明の抽象度が高いからか、内容理解に時間が掛かる・自信がないという課題が散見されました。

そんな中、「初学者や、1 人で読み進めている方の助けになる記事を書いてみては」という声があり、本記事の執筆に至っています。本記事以外にも、主要な章やパターンを取り上げていくつか執筆予定です。

今回は2章 "Data Ingestion Patterns" のうち、Data Loading に分類される 「Full Loader」「Incremental Loader」「Change Data Capture」 の 3パターンを見ていきます。参考までに、2章で取り上げられるパターンとその分類を下図に示します。

Full Loader パターン

Full Loader パターンは、毎回すべてのデータを取り込み先に入れ直すパターンです。SQL でいうと、ジョブのたびに TRUNCATE + INSERTCREATE OR REPLACE TABLE を実行するイメージです。

こんなときにおすすめ

  • データ量が少なく、今後も大きく増えないデータソース
  • 例:業種コード、都道府県マスタなど

気をつけること

実装が比較的シンプルな点が Full Loader パターンのメリットですが、元のデータ量が多い・データの増加が速いデータソース(例:メッセージ履歴、ログデータ)は、最初は問題なくてもいずれタイムアウトやコスト増加につながるため避けた方が良いです。そういったデータを扱う場合は、次節以降の Incremental Loader または Change Data Capture を検討します。

Full Loader のもう1つの注意点としては、処理中にデータが不完全な状態になるため、その間に下流のジョブがデータを参照しないようにする必要があります。

たとえば、Full Loader が 6:00〜7:00 に実行されるとして、ダッシュボードの更新ジョブが 6:30 に走ると、途中までしかデータが入っていない状態で表示されてしまいます。
対策としては、下図のような構成が考えられます。

  • Full Loader は毎回別のテーブルに書き込む
  • 最新のテーブルから SELECT する view を用意
  • 下流のシステムは view の方を参照する

こうすれば、Full Loader の完了タイミングでデータを安全に切り替えられます。

補足: 実装例

私たちの関わった Snowflake プロジェクトでは、取り込み元のデータを Amazon S3 にエクスポートし、 Snowflake の外部テーブル機能を使うことで社内の各種データベース → Snowflake の Full Loader パターンを実装しました。

外部テーブルは通常のテーブルと比べてパフォーマンスが悪くなりますが、それが問題にならない場合は次のようなメリットがあります。

  • データ自体は S3 にあるため、 Snowflake 以外からも利用できる (ベンダーロックインされにくい)
  • 外部テーブルを一度設定すれば S3 の更新 = データの更新になるため、データパイプラインの実装をデータソース側のチームに任せやすい

前節の不完全なデータの問題に対する解決策として、Snowflakeではこの他に以下の方法が考えられそうです。

Incremental Loader パターン

Full Loader パターンでは毎回すべてのデータを処理していましたが、Incremental Loader では新しく追加・更新されたデータのみを対象に処理します。

どのデータが「新しい」かを判定するためには、 ingestion_timeupdated_at のような時間を表すカラムを使います。たとえば Web サイトへの訪問イベントを取り込む場合は、実行ごとに max(ingestion_time) を記録し、次の実行では ingestion_time がその値より大きいものを取り込むことで新しいデータだけを対象にできます。また、ECサイトの商品テーブルであれば、 updated_at カラムを使って同じように前回からの増分 (increment) を取得できます。

このような、どの行が増分かを判定するために使われるカラムを本書では「delta column」と呼んでいます。なお、delta column 以外の Incremental Loader の実装方法として、本書では時間ベースのパーティションを使う実装も紹介されていますので、気になった方はご参照ください。

コラム: 意外と身近なincremental loader

たとえば SaaS の API からデータを取り込むとき、?after=2025-01-01 のようなパラメータで直近の更新分だけ取得することがあります。これも Incremental Loader の考え方です。また、厳密にはデータ取り込みジョブではありませんが、dbt の incremental モデルも同じ考え方にもとづいています。

こんなときにおすすめ

  • データ量が多い、または増加が速いデータソース
  • 例:メッセージ履歴、ログデータなど

気をつけるところ

Incremental Loader では、取り込み元で削除されたレコードを検知できません。

以下の SQL で Incremental Loader の抽出対象を取得するとします。

select * from source_table
where date(updated_at) >= date_sub(current_date(), interval 1 day);

下図左側のように、取り込み元のテーブルで 2025-12-31 に id = 101, 102, 103 のレコードが作成されたとします。

2026-01-01 のジョブでは、前日(2025-12-31)以降に更新されたデータが対象になるため、3行すべてが取り込まれます (下図右側)。ここでは、 updated_at を基準に更新対象を特定しています。

ジョブ完了後、同日に取り込み元で以下の変更があったとします。

  • id = 101 が削除される
  • id = 102 が更新される
  • id = 103 はそのまま

翌日 2026-01-02 のジョブでは、where date(updated_at) >= '2026-01-01' に該当する id = 102 のみが処理対象になります。

取り込み元では、 id = 101 は削除されましたが、削除されたレコードは where 句に引っかからないため、取り込み先には残ったままになります。

履歴テーブルのように INSERT のみの場合は問題になりませんが、物理削除があるデータソースでは以下の対策を検討してください。

  • 取り込み元で物理削除をやめて論理削除に変更する
  • Change Data Capture パターンを使う(次節で紹介)

論理削除の場合、削除されたレコードにも updated_at が更新されるため、Incremental Loader で検知できるようになります。

補足: Incremental Loader とリトライ

本書に明記されていない Incremental Loader の考慮事項として、私は「リトライの複雑さ」があると考えています。

たとえば前述の例と同様、以下の SQL で「前日以降に更新されたデータ」を取り込む Incremental Loader があるとします。

select * from source_table
where date(updated_at) >= date_sub(current_date(), interval 1 day);

ここで、2026-01-01 のジョブが途中で失敗し、最初の10件は書き込まれたが、11件目で不正なデータがありジョブが止まった、という状況を考えます。イメージのため、取り込み元・取り込み先のデータを下に図示します。

取り込み元でデータを修正した後、そのまま Incremental Loader のジョブを再実行すると、1回目で書き込まれた行が再度書き込まれ、重複データができるという問題が発生します。

重複データをつくらずにリトライするためには、DELETE 文を発行してからジョブを再実行する必要がありますが、本番環境で DELETE を実行するのは神経を使いますし、そもそも人間に書き込み権限がないことも多いです。

リトライ時の手動オペレーションを避ける方法としては、ジョブ内で事前に DELETE を発行したり、 INSERT の代わりに MERGE を使う方法もありますが、いずれも実装の複雑さが増します。

このように、Incremental Loader は Full Loader と比べてリトライ時の考慮事項が多く、設計段階で「失敗したらどうリカバリするか」を意識しておくことが重要です。

Change Data Capture パターン

Full Loader と Incremental Loader は、一定間隔でまとめてデータを処理する「バッチ処理」です。データの反映をもっと速くしたい場合や、物理削除も検知したい場合は、データが発生次第すぐに処理する Change Data Capture(CDC)パターンを検討します。

CDC のデータパイプラインは以下のパーツから構成されます。

  • コミットログ: データベースの変更 (INSERT, UPDATE, DELETE) を記録するログ
  • CDC プロセス: コミットログを監視し、変更を「ストリーミング」するプロセス
  • ストリーミングブローカー: 取り出した変更イベントを一時的に保持し、下流のシステムに流す中継地点

こんなときにおすすめ

  • 低レイテンシが求められる (e.g. ニアリアルタイムでデータがほしい)
  • 物理削除も反映したい

気をつけること

  • CDC は「変更」を取り込む処理のため、CDC 開始前のデータは別途取得する必要があります (バックフィル)。Full Loader などを組み合わせて対応することが多いです。
  • 低レイテンシでデータを取得できるのは CDC の強みですが、バッチ処理よりシステム構成が複雑になります。たとえば、取り込み元データベースでコミットログを有効化するためにインフラチームとの調整が必要になるなど、設定の工数が増えることがあります

補足: 実装例

Google Cloud の Datastream は CDC のマネージドサービスで、BigQuery への取り込みに対応しています。バックフィル機能もあるため、 Datastream でストリーム開始前後のデータを取得できます。

参考: https://docs.cloud.google.com/datastream/docs/manage-backfill-for-the-objects-of-a-stream

補足: Data Loading の各パターンの使い分け

データパイプラインは、バグや当初想定しなかったデータ、サービス側の障害などさまざまな理由で実行が失敗することがあります。自動リトライで復旧しない場合は人間の介入が必要ですが、こうした手動オペレーションが増えると、データエンジニアが日々の対応に追われてしまいがちです。

そのため、私がデータパイプラインを作るときは、リトライのしやすさを考えてまず Full Loader パターンを検討します。Incremental Loader の方が処理するデータ量は少なくなりますが、バッチ処理は処理時間が長くても問題ないことが多いです。したがって、 Incremental Loader の物理削除が検知できない・リトライが難しいなどのデメリットを踏まえて、なるべく Full Loader で実装するようにします。

ただし、サードパーティの API からデータを取得する場合は、以下のような理由から Incremental Loader を採用することもあります。

  • 1リクエストごとに取れる件数が取得対象のデータ量に対して少ない
  • レートリミットに引っかかりやすい
  • 従量課金

上記の要素があると、データ取得ができなかったり、処理時間が長くなりすぎるたりするため、「直近1週間に変更された分」のような Incremental Loader を検討します。

以上を踏まえて、私が新しくデータパイプラインを立てる際は次のような流れで実装パターンを検討しています。

  • まず Full Loader でできないか考える
  • どうしても Full Loader でできない場合は Incremental Loader を使う
  • リアルタイム同期など特殊な要件があれば Change Data Capture を検討する

このほか、本書の内容からは外れますが、そもそも内製せず Fivetran などのマネージドサービスを使うことも、長期的に運用しやすいデータ基盤につながると考えています。

おわりに

本記事では、Data Engineering Design Patterns の第2章のうち、Data Loading に関わるデザインパターンを3つ紹介しました。

Full Loader は、最もシンプルでリトライしやすいですが、データ量が多いと処理時間・コストで問題が出てきます。一方で、 Incremental Loader は、差分のみを更新してこれを解決しましたが、物理削除やリトライで考慮が必要です。 CDC は、よりリアルタイム性を持って差分を更新できますが、実装が複雑になります。

この章を読んでみて、一見シンプルなデータ取込みにも色々な考慮点があって、個々の状況に応じたパターンの採用が重要だと改めて感じました。個人的には、リトライのしやすさを優先して、はじめに Full Loader から検討するようにしています。

このように、bare64 の本記事シリーズでは、 Data Engineering Design Patterns を読む際の補助輪となるような内容を発信していきます。次回の記事も、お楽しみに!

bare64

Discussion