stage_external_sources マクロが外部テーブルを更新しないケース(dbt × Snowflake)
この記事は ナウキャスト Advent Calendar 2025 の13日目の記事です。
はじめに
こんにちは!株式会社ナウキャストでデータエンジニアをしている岡田です。
ナウキャストでは、Amazon S3 に置いているデータを Snowflake に取り込むデータパイプラインで dbt の stage_external_sources マクロを活用することが多いです。
ところが、利用する中で、S3 のファイルは更新されているのに、Snowflake の外部テーブルが更新されず、しかもエラーも出ていない、という事象に遭遇しました。
本記事ではそのメカニズムを解説するとともに、実際にstage_external_sources マクロを運用する際の意思決定のフローについてもまとめます。
外部テーブルそのものの概念については弊社メンバーによる記事がまとまっているのでぜひご覧ください。
stage_external_sources マクロは dbt-external-tables というパッケージで管理されています。
この記事では以下の要件を前提とします。
- dbt-external-tables のバージョン:0.8.2
- 想定する外部ストレージ:Amazon S3
なぜ更新されなかったのか
起きていた問題と原因
外部テーブルに対して max(data_date) を確認したところ、期待値は 2025-10-01 であるにもかかわらず 2025-09-01 が返ってきました。
一方で S3 側のファイルでは同一ファイル名の CSV が差し替えられる形で更新され、2025-10-01 が含まれていました。
このように、S3 の状態と外部テーブルの状態が一致していない、という事象が発生しました。
この原因は、stage_external_sources を次の条件で実行していたことです。
-
ext_full_refresh:false(デフォルト) -
partitions: 未設定 -
auto_refresh: 未設定
この組み合わせでは、dbt-external-tables 0.8.2 の stage_external_sources の仕様上、Snowflake で何の DDL も実行されません。その結果、S3 側でファイルが更新されていても、外部テーブルが更新されませんでした。
マクロの仕組み
dbt プロジェクトの yaml ファイルの external セクションで、外部テーブルに関する定義を一部設定します(詳細は GitHub の README に書かれています)。
version: 2
sources:
- name: source
tables:
- name: transaction
description: ...
columns:
- name: ...
external:
location: # S3 file path or Snowflake stage
...
その上で以下のコマンドを実行すると、Snowflake 上に transaction という名前の外部テーブルが作成されます。
$ dbt run-operation stage_external_sources
挙動を決める3つの変数
外部テーブルを一から作る場合は単純です。
問題は、すでに外部テーブルが存在する状態で再度 stage_external_sources を実行した場合です。このとき、設定によっては Snowflake に対して何の DDL も実行されない ことがあります。この「どの条件でどの挙動になるか」を理解しておくと、後述の表やフローチャートが読みやすくなり、運用時のハマりどころも減らせると思います。
stage_external_sources マクロが Snowflake に対してどの処理を投げるかは、次の3つの変数で決まります。
-
ext_full_refresh- マクロの引数。デフォルトは
false。 - 外部テーブルを完全に作り直すかどうか。
- マクロの引数。デフォルトは
-
partitions- yaml での設定項目
-
sources.yml等のexternalセクション内にパーティション設定があるか。
-
auto_refresh- yaml での設定項目
- S3 イベント通知などを利用してメタデータを自動更新する設定になっているか。
これらの変数の組み合わせと Snowflake 上での挙動は次のように整理できます。
ext_full_refresh |
partitions |
auto_refresh |
Snowflake での実行文 | 解説 |
|---|---|---|---|---|
| true | 任意 | 任意 | CREATE OR REPLACE EXTERNAL TABLE ... |
完全再作成。 テーブル定義ごと作り直し、最新の状態にする。 |
| false | なし | 任意 | Skip | ログに SKIP と出るだけで何もしない。 |
| false | あり | true | Skip | 自動更新 (S3 イベント通知) に任せるため、dbt 側では何もしない。 |
| false | あり | false | ALTER EXTERNAL TABLE ... REFRESH |
メタデータ更新。 テーブル定義はそのまま。 |
この表の2行目について、フルリフレッシュしない(= ext_full_refresh=false)かつ partitions セクションが無いと Skip になるという挙動は、バグではなく仕様です。
マクロが Snowflake に処理を投げないため、S3 側でファイルの内容が更新されても新規のデータが認識されず、外部テーブルに反映されません。
マクロを実行した際の全体像は次のようになります。
挙動の切り替え
実装の全体像
まず、stage_external_sources は内部で以下の順番でマクロを呼び出します。
stage_external_sources
→ get_external_build_plan
→ create_external_table / refresh_external_table
この過程では、「再作成するか」「メタデータ更新するか」「何もしないか」を決めるための判定が行われます。
判定は get_external_build_plan の内部で次の2段階を踏んでいます。
- 外部テーブルを再作成するかどうか
- 作り直さない場合に、メタデータ更新を行うかどうか
1. 外部テーブルを再作成するかどうか
create_or_replace という boolean によって決まります。old_relation は既に外部テーブルが存在するかの boolean です。
old_relation is none |
var('ext_full_refresh', false) |
create_or_replace |
|---|---|---|
| TRUE | TRUE | TRUE |
| TRUE | FALSE | TRUE |
| FALSE | TRUE | TRUE |
| FALSE | FALSE | FALSE |
結果として、以下のような挙動になります。
-
ext_full_refresh=true→ 必ずcreate or replaceが走る -
ext_full_refresh=falseかつ外部テーブルが既に存在 →create or replaceは行わず、メタデータ更新を行うかどうか判断する
2. メタデータ更新を行うかどうか
yaml ファイルで設定する auto_refresh セクションと partitions セクションを設定しているかどうかによって決まります。manual_refresh も auto_refresh と partitions によって決まります。
auto_refresh |
partitions |
manual_refresh |
挙動 |
|---|---|---|---|
| TRUE | TRUE | FALSE | 何も実行しない |
| TRUE | FALSE | FALSE | 何も実行しない |
| FALSE | TRUE | TRUE |
alter ... refresh 文を実行する |
| FALSE | FALSE | FALSE | 何も実行しない |
よって、alter ... refresh 文によってメタデータの更新が行われるのは以下の場合のみとなります。
-
partitionsセクションを設定している -
auto_refreshセクションを設定していない
partitions が無い場合は完全に Skip されます。(だからパーティション設計が必要という意味ではなく、あくまで 0.8.2 時点の実装上の条件分岐によるものです。0.12.0 ではこの条件分岐は廃止されています。)
マクロのコードリーディング
以下では、stage_external_sources の分岐ロジックを実装ベースで追っていきます。
マクロの内部実装まで確認する必要がなければ、次のセクションに進んで問題ありません。
詳細説明
先に示したマクロの実行順序の通りに実装を見ていきます。
1. stage_external_sources
stage_external_sources マクロはこのファイルで定義されています。common というディレクトリの配下にあることから分かるように、DB / DWH 間で共通したエントリーポイントとして機能しています。
この中にある get_external_build_plan というマクロを詳しく見てみましょう。
2. get_external_build_plan
get_external_build_plan が実行されると、アダプタごとにカスタムされたマクロが実行されます。このような仕組みは stage_external_table だけでなく、ほかの dbt コマンドでも共通しています。
Snowflake の場合は以下ファイルで定義されている snowflake__get_external_build_plan が実行されます。
ポイントは以下の部分です。
{% set old_relation = adapter.get_relation(
database = source_node.database,
schema = source_node.schema,
identifier = source_node.identifier
) %}
{% set create_or_replace = (old_relation is none or var('ext_full_refresh', false)) %}
この部分で create or replace 文を実行するかどうかを判別しています。
既に外部テーブルが存在する場合、old_relation is none は false になります。
また、ext_full_refresh は stage_external_sources で引数として指定できますが、何も指定しないと false になることが分かります。
3. create_external_table / refresh_external_table
create_or_replace の値によって後続の挙動がどのように変わるのか見ていきます。
create_or_replace が true の場合 create_external_table マクロが実行される一方で、 create_or_replace が false の場合には refresh_external_table マクロが実行されることが分かります。
それぞれどのような実装になっているのか見ていきます。
create_external_table
snowflake 用の create_external_table マクロは以下のファイルで snowflake__create_external_table として定義されています。
以下の行で create or replace 文が実行されることが分かります。
create or replace external table {{source(source_node.source_name, source_node.name)}}
create or replace external table は、S3 の location や file format などの定義の更新や、ファイルの読み込み(=メタデータの更新)などを行います。
外部テーブルという「箱」自体を作り直すイメージで、箱自体が変わるので中身も最新になります。
refresh_external_table
snowflake 用の refresh_external_table マクロは以下のファイルで snowflake__refresh_external_table として定義されています。
同様に実行されるコマンドが何なのか見てみると、
{% if manual_refresh %}
{% set ddl %}
begin;
alter external table {{source(source_node.source_name, source_node.name)}} refresh;
commit;
{% endset %}
{% do return([ddl]) %}
{% else %}
{% do return([]) %}
{% endif %}
manual_refresh という変数に応じて、alter external table ... refresh 文を実行する場合と何もしない場合に分かれています。
alter external table ... refresh文について
alter external table ... refresh が行うのは、外部ストレージ上のファイル一覧(=メタデータ)の再取得だけで、ファイルの内容の読み直しではありません。
create or replace external table とは異なり、テーブル定義は更新せず、ファイル内容まで読み直すわけではないため、ファイルが増えていない場合は変化が反映されないことに注意が必要です。
テーブルという「箱」そのものは変えず、「箱の中にどんなファイルがあるか」という目録だけを更新するイメージです。
そのため、外部ストレージ上のファイルを上書きして alter external table ... refresh を実行しても外部テーブルの状態は変わりません。
manual_refreshについて
manual_refresh はマクロの中で以下のように決定されます。
{% set external = source_node.external %}
...
{% set auto_refresh = external.get('auto_refresh', false) %}
{% set partitions = external.get('partitions', none) %}
{% set manual_refresh = (partitions and not auto_refresh) %}
source_node.external は yaml の external セクションをそのまま参照しています。
そこから auto_refresh と partitions を取得し、両者の組み合わせで manual_refresh が決まります。
マクロを運用する上での考え方
実装を踏まえた意思決定フロー
バージョン 0.8.2 の stage_external_sources の挙動を踏まえると、運用では主に次の2点で判断します。
- S3 イベント通知を運用できるか
- パーティションを切るか/切るべきか
以下のフローチャートは公式のものではありませんが、実装や仕様を踏まえて整理した判断フローです。
バージョン 0.8.2 の場合の意思決定フロー
-
S3 イベント通知を運用できるか:具体的には以下の観点での判断となります。
- イベント通知を構築するコストを受け入れられるか:S3 イベント通知を使うならインフラ側での準備が必要です。社内・プロジェクト内で S3 イベント通知の運用が一般的であり、かつIaCツールでインフラ管理していれば、既存の設定を参照・活用できるので、準備がしやすいと思います。
- ファイル更新をリアルタイムに DB / DWH に反映させる必要があるか:S3 イベント通知はデータを即時反映できることが大きなメリットですが、即時性が特に求められていないのであれば、無理にそれを使う意味は無いでしょう。
- 既存のワークフロー管理ツールで管理したいか:Airflow などのワークフロー管理ツールで更新タイミングを制御したい場合、イベント通知を採用すると制御が分散するため相性が良くありません。ただしこれは「更新方式そのものの判断基準」ではなく「どのように運用管理したいか」という運用方針の問題です。
-
パーティションを切るか/切るべきか:
- ここで言う「パーティション」は、外部テーブルに対して定義する論理的なパーティションを指します。ここでの判断は、マクロの仕様がどうなっているかではなく、Snowflake の読み込み効率をどう最適化するかという観点になります。
- 外部テーブルでパーティションを設定する主な目的は、クエリ実行やメタデータ更新の際に不要なファイルをスキャン対象から外せるようにすることです。Snowflake の pruning はレコード単位ではなくファイル単位で働くため、効果の大小はデータ量ではなく「除外可能なファイル数」に依存します。
意思決定フローを踏まえた筆者の選択
以上の判断軸を踏まえて、筆者がどのような方法を取ったのか紹介します。
まず、イベント通知は採用しませんでした。
ファイル更新をリアルタイムに DB / DWH に反映させる必要が無いことと、Airflow のタスクとして管理したかったからです。
また、パーティションを切らずにフルリフレッシュを採用しました。
今回の外部テーブルは、構成するファイル数が少ないため、Snowflake が pruning できる候補となるファイル自体がほとんどありませんでした。よって、ファイル単位での分割による恩恵はそれほど期待できず、パフォーマンス改善につながる見込みは薄いと判断しました。
さらに、実際の dbt コマンドの実行時間としては15秒程度、Snowflake 上での DDL は5秒未満で終わるため、既にパフォーマンスとしては十分でした。
このため、本件ではパーティションを切らないフルリフレッシュが最も運用しやすい選択肢となりました。
最新バージョンではどうなるのか
ここまで説明してきた挙動は、筆者が実際に利用していた 0.8.2 の仕様に基づくものですが、現状このバージョンを使っている方は少ないと思います。
執筆時点の最新バージョンであり、かつ多くの方が現時点で使っているであろう 0.12.0 における挙動も併せて紹介します。
挙動の違い
最新バージョンである 0.12.0 ではマクロのロジックが整理され、特に partitions の有無は挙動分岐に影響しなくなっています。
0.12.0 における stage_external_sources の挙動
| ext_full_refresh | auto_refresh | Snowflake での実行 |
|---|---|---|
| true | 任意 | CREATE OR REPLACE EXTERNAL TABLE... |
| false | true | Skip(自動更新に任せる) |
| false | false | ALTER EXTERNAL TABLE … REFRESH |
0.8.2 との差分として、
-
partitionsの設定が挙動の条件分岐と関係なくなった - イベント通知を使っていなければ、フルリフレッシュかどうかに関係なく
alter external table ... refreshを実行するようになった
つまり、0.12.0 では3つではなく2つの変数だけ考えればよいことになります。
意思決定フロー
意思決定フローもかなりシンプルになっています。
バージョン 0.12.0 の場合の意思決定フロー
まとめ
本記事では、dbt-external-tables の stage_external_sources マクロが特定の設定条件下で外部テーブルを更新しないケースがある理由を、バージョン 0.8.2 の実装をもとに整理しました。
特に重要なのは、
ext_full_refresh=false-
partitions未設定
という条件下では Snowflake で何の DDL も実行されないという点です。この挙動はエラーにならないため、実行自体は成功します。その結果、外部ストレージ上のファイルが更新されていても、外部テーブルが古いままになるという状態に陥ります。
一方で、バージョン 0.12.0 ではロジックが簡略化されたおかげで partitions の有無を意識する必要はほぼなくなりました。
ただし、同名ファイルを上書きする運用では、 alter ... refresh だけでは不十分になる点に引き続き注意が必要です。
stage_external_sources は便利ですが、「実行した=更新された」とは限りません。
本記事の整理が、同様の事象を避けるための一助になれば幸いです。
Discussion