代表的な面接トピック

データエンジニアリング面接:信頼性の高いdbtインクリメンタルモデルをどのように設計しますか?

データ難しい
Offer.cc 編集チーム公開日 更新日

質問

データセットが増加し続け、日次のフル再計算のコストが高すぎる場合、信頼性の高いdbtインクリメンタルモデルをどのように設計しますか?

プロンプトとスコープ

面接官は、継続的な書き込みが発生するイベントテーブルを提示し、日次のファクトテーブルを構築するよう求めます。初回実行では全履歴をスキャンできますが、以降の実行では変更されたデータのみを処理する必要があります。イベントは遅延到着、更新、重複する可能性があり、モデルのカラムが変更されることもあります。フィルター境界、ユニークキー、インクリメンタル戦略、バックフィル計画、および障害後のリカバリについて説明してください。

DWHはSQLをサポートし、ターゲットはdate_dayごとに集約され、各イベントにはevent_atupdated_at、および安定したevent_idが存在すると仮定します。これらの前提条件を最初に述べてください。安定したキーがない場合、更新セマンティクスと重複排除の方法が変わります。

面接官が評価するポイント

面接官は、正確性を犠牲にすることなくスキャンデータ量を削減できるかどうかを見ています。基本的な回答では、is_incremental()とタイムスタンプフィルターに言及します。優れた回答では、フィルターが遅延到着をカバーしなければならない理由、集約にその粒度(grain)に一致するunique_keyが必要な理由、そして--full-refreshが必須となるタイミングについて説明します。

また、遅延イベントの欠落、ビジネス粒度での重複行の書き込み、過去の変換ロジック変更後の新旧ロジックの混在という3つのリスクを区別できているかもテストしています。最近のデータエンジニアリング面接では、インクリメンタルモデル、スナップショット、依存関係グラフが実践的な準備トピックとして扱われています。この質問はSQL、データモデリング、および実行ガバナンスを組み合わせたものです。

回答前の明確化のための質問

ビジネスの粒度(grain)は何ですか?

1行が1日を表す場合、date_dayをユニークキーにできます。1行がユーザーと1日を表す場合、キーは(user_id, date_day)にする必要があります。粒度によってマージ条件、重複チェック、バックフィルのコストが変わります。

データは最大でどれくらい遅れて届きますか?

イベントの遅延が通常2日以内であれば、直近3日間を再計算します。有効な上限がない場合、固定ウィンドウでは不十分です。ウォーターマーク、パーティション修復、または定期的なフル照合を使用してください。ウィンドウは許容される遅延分布をカバーする必要があります。

どのフィールドが上流の更新を表していますか?

event_atはビジネス時刻であり、updated_atは最終更新時刻です。event_atのみでフィルタリングすると、後から修正された古いイベントを見落とします。信頼できるupdated_atまたは変更シーケンスを優先し、ソース側で値が過去に戻らないことを確認してください。

モデルやカラムの変更はどのようにリリースされますか?

カラムの追加、カラムの削除、計算ロジックの変更には、それぞれ異なる対応が必要です。on_schema_changeで構造を同期できるか、古い行をバックフィルする必要があるか、フルリフレッシュをスケジュールできるかを確認してください。

30秒の回答フレームワーク

「まず、ターゲットの粒度と遅延の上限を確認します。初回実行では全履歴からモデルを構築します。以降の実行ではis_incremental()を使用してupdated_atでフィルタリングし、遅延ウィンドウ分を振り返ります。ターゲットにはその粒度に一致するunique_keyを宣言し、直近の日付が重複として追加されるのではなく更新されるようにします。集約前にイベントバージョンによってウィンドウ内の重複を排除し、mergeまたはDWHの同等機能で書き込みます。ウィンドウ、キー、スキーマ変更の挙動をテストします。ロジックの変更により過去の結果に不整合が生じる場合は、制御された--full-refreshを実行し、影響を受ける下流モデルを再構築します。最後に、処理行数、最大更新時刻、重複キー、インクリメンタルとフルの差異を監視します。」

ステップバイステップの詳細解説

1. フルリフレッシュによる正確性のベースラインを定義する

まずフルクエリを作成します。すべてのイベントを読み込み、ターゲットの粒度で集約します。これが正確性のベースラインとなります。インクリメンタル出力は、同じ時間範囲でのフル計算と一致しなければなりません。このベースラインを確立する前に最適化を行うと、レコードの欠落を検出することが困難になります。

2. インクリメンタルフィルターの境界を選択する

インクリメンタルブランチは、ターゲットテーブルが存在し、--full-refreshが指定されておらず、モデルがインクリメンタルとして設定されている場合にのみ適用されます。1つの方法は、ターゲットの最大更新時刻から遅延ウィンドウを差し引くことです:

sql
{{
  config(
    materialized = 'incremental',
    unique_key = ['date_day'],
    incremental_strategy = 'merge'
  )
}}

with source_events as (
  select *
  from {{ ref('app_events') }}
  {% if is_incremental() %}
    where updated_at >= (
      select coalesce(max(updated_at), '1900-01-01') from {{ this }}
    ) - interval '3 day'
  {% endif %}
)
select
  cast(event_at as date) as date_day,
  count(distinct event_id) as events,
  max(updated_at) as max_updated_at
from source_events
group by 1

3日間という値は面接上の仮定であり、普遍的な定数ではありません。遅延、SLA、再計算コストに基づいてウィンドウを選択してください。日付式はDWHに合わせて調整します。

3. ユニークキーをモデルの粒度に一致させる

日次テーブルの場合、date_dayがキーになります。ユーザー×日次テーブルの場合は、['user_id', 'date_day']を使用します。キーカラムにはNULLを含めてはならず、NULLがあるとマージのマッチングに失敗して重複が発生する可能性があります。キーがない場合、多くのアダプターはアペンドオンリーとして動作するため、ウィンドウを再計算すると1つの粒度に対して複数の行が書き込まれる可能性があります。

4. 集約前にウィンドウ内で重複を排除する

リプレイやCDCの更新によって、1つのイベントに対して複数のバージョンが生成されることがあります。event_idと更新時刻でソートし、最新バージョンを保持してから集約します:

sql
with ranked_events as (
  select
    *,
    row_number() over (
      partition by event_id
      order by updated_at desc, ingest_seq desc
    ) as rn
  from source_events
),
deduped_events as (
  select * from ranked_events where rn = 1
)
select
  cast(event_at as date) as date_day,
  count(*) as events,
  max(updated_at) as max_updated_at
from deduped_events
group by 1

ingest_seqは、タイムスタンプが同一である場合の安定したタイブレーカー(同順位の解消手段)としてのみ使用してください。それ以外の場合は、タイルールを未解決のソース契約として扱います。重複排除は集約の前に行う必要があります。そうしないと、1つのイベントの2つのバージョンが両方カウントされてしまいます。

5. マージ、パーティション上書き、アペンドの選択

mergeは、粒度でキー指定された更新・挿入セマンティクスに適しています。パーティション再計算のワークロードでは、行キーの代わりにパーティションに依存するinsert_overwriteを使用できます。上流のイベントが決して変更されない場合は、純粋なアペンドの方がシンプルです。1つの戦略を普遍的なものとして扱うのではなく、更新セマンティクス、スキャンコスト、アダプターのサポート状況から選択してください。

6. スキーマおよびロジックの変更を処理する

カラムを追加しても必ずしも古い行がバックフィルされるわけではありません。削除されたカラムや型の変更は実行時にのみ表面化することがあります。on_schema_changeにはignorefailappend_new_columns、またはsync_all_columnsを設定できますが、これは最上位のカラムのみを追跡し、過去データのバックフィルを代替するものではありません。計算ロジックが変更された場合、過去と新規の履歴が異なるルールに従う可能性があるため、--full-refreshを実行し、影響を受ける下流のインクリメンタルモデルを再構築してください。

7. バックフィルと障害リカバリの設計

実行メタデータに遅延ウィンドウ、ターゲットの最大更新時刻、ソースのウォーターマークを記録します。ウィンドウ実行が失敗した後は、コミットされたターゲットの最後のウォーターマークから再計算します。メモリ上の「処理済み」の値を信頼できる事実として扱わないでください。広範囲の修復を行う場合は、制限された並行性で日付パーティションを処理し、1回のリフレッシュがDWHに過負荷をかけないよう、フルクエリとサンプルを照合してください。

8. 検証ループを完了させる

少なくとも4つのシグナルを検証します。各event_idがウィンドウ内に最大1回しか現れないこと、ターゲットキーが一意であること、直近のインクリメンタル出力がフル再計算との許容差異内に収まっていること、処理行数と最大updated_atが予期せず跳ね上がらないことです。空の入力、重複イベント、古いイベントの更新、遅延到着、境界値と同じタイムスタンプ、フルリフレッシュ後のインクリメンタル実行をテストしてください。

高品質な回答例

「まず、モデルの粒度、遅延の上限、ソースの更新フィールドを確認します。ターゲットは1日あたり1行であり、イベントには安定したevent_idupdated_atがあると仮定します。初回実行は履歴から構築し、以降の実行ではis_incremental()を使用してターゲットの最大更新時刻から3日間振り返ります。そのウィンドウは固定ルールではなく遅延状況から導出されます。

ウィンドウ内ではイベントIDとバージョンで重複を排除し、日ごとに集約します。ターゲットはdate_dayunique_keyとして宣言し、直近の日付が重複するのではなく置換されるようにマージを使用します。ユーザー×日次の粒度には複合キーを使用します。イベント時刻のみでフィルタリングすると古いイベントに対する後からの修正を見落とすため、信頼できる更新タイムスタンプまたは変更シーケンスを優先します。

ウィンドウサイズ、ウォーターマーク、処理行数、重複キー、インクリメンタルとフルの照合を監視します。スキーマ変更設定によって構造の進化に対応できますが、過去の値は入力されません。ロジックが変更された場合や履歴の修復が必要な場合は、制御されたフルリフレッシュまたはパーティションバックフィルを実行し、影響を受ける下流モデルを再構築します。モデルが信頼できると判断する前に、空の入力、遅延・重複イベント、境界タイムスタンプ、およびリトライ動作をテストします。」

よくある間違い

event_atのみでフィルタリングする → 古い更新が見落とされる → updated_atまたは明示的なCDCウォーターマークを使用する

古いイベントが修正されてもビジネス時刻は変わりません。更新が許可されている場合は、更新時刻または変更シーケンスでフィルタリングし、その契約を確認してください。

キーなしでマージする → 行を確実に照合できない → まず粒度と非NULLキーを定義する

キーはターゲットの正確に1行を特定しなければなりません。粒度がユーザー×日次である場合、日付のみを使用すると異なるユーザーが1行にマージされてしまいます。

新しいカラムが自動的にバックフィルされると想定する → 過去の値が空のままになる → バックフィルまたはフルリフレッシュを計画する

スキーマ同期と過去データの投入は別物です。構造の変更は軽量かもしれませんが、値を投入した古い行には明示的な更新または再構築が必要です。

固定の1時間ウィンドウを選択する → 遅延データがウィンドウ外に漏れる → パーセンタイルと照合で調整する

遅延分布、SLA、コストからウィンドウを選択してください。ウィンドウ外への到着を監視し、分布が変化した場合は拡張または修復を行ってください。

フォローアップ質問と回答

イベントの5%が2日遅れて到着する場合、ウィンドウをどのように選択しますか?

まず、許容される鮮度の遅延を確認します。日次レポートを翌日に修正してもよい場合は、2〜3日をカバーして遅延イベントを照合します。初日の結果を確定させる必要がある場合は、SQLウィンドウを拡大するだけでなく、ウォーターマークと修復キューを併用します。割合をそのまま真似るのではなく、遅延曲線とコスト曲線に照らし合わせて選択を検証してください。

ソース内でunique_keyが重複している場合はどうなりますか?

インクリメンタル入力またはターゲットでの重複キーは、アダプターのエラーを引き起こしたり、未定義の結果を生み出したりする可能性があります。両方の場所で一意性をチェックし、重複の発生源を特定し、イベントバージョンで重複を排除するか、真の粒度を表す複合キーを再定義してください。ランダムなIDによって不安定なビジネスキーを隠蔽してはなりません。

モデルのSQLが変更されましたが、7日間分のみを再計算したいと考えています。インクリメンタル実行を継続できますか?

新しいロジックによって過去の結果が影響を受けない場合に限られます。変更がすべての履歴に影響する場合、7日間のインクリメンタル実行を行うと、異なるルールが混在したテーブルが作成されてしまいます。影響を受ける範囲に対して制御されたフルリフレッシュまたはパーティション再計算を実行し、下流モデルを再構築してください。

上流のテーブルがTRUNCATEされました。インクリメンタルモデルはどのようにリカバリしますか?

ウォーターマークの進行を停止し、ソースの再構築を確認して、信頼できるスナップショットまたはCDCポイントからリプレイします。ソースに必要な履歴が含まれなくなった場合、インクリメンタル実行は不完全なソースを完全なベースラインとして扱ってしまうため、スナップショットを復元するか、ターゲットを完全に再構築してください。

公開情報ソース

関連する質問