この記事は、dbt Advent Calendar 2025の2日目の記事です。
こんにちは、六本木アナリティクスエンジニアのTaku(@aelabdata )です。
dbtを用いてデータ分析開発する上で、スキーマの適切な管理はプロジェクトの秩序と保守性を保つ重要な要素です。具体的にdbtプロジェクトでスキーマを整理したいという動機は、多くの場合、以下のようなユースケースから生まれます。
- ビジネスユニットごとの分離: プロジェクトが複数の事業領域をカバーする場合、
core(全社共通)、marketing(マーケティング部門)、finance(財務部門)のように、ビジネスドメインに基づいてモデルをグループ化することで、関心事の分離を実現します。これにより、各部門の担当者は自身に関連するデータセットに集中できます。 - レイヤーごとの分離: データ変換のパイプラインを階層的に整理するアプローチです。例えば、データソースからロードした直後の中間モデルを
stagingスキーマに集約し、ビジネスユーザーやBIツールが直接参照する最終的な分析用モデルのみをanalyticsスキーマに配置します。これにより、エンドユーザーにはクリーンで理解しやすいデータ環境を提供できます。
「dbt_project.ymlで特定のモデル群に対して+schema: marketingと設定すれば、当然marketingスキーマにテーブルやビューが作成されるだろう」
しかし、すべてのモデルがターゲットとして設定されたデフォルトスキーマに作成されてしまったのです。
この問題の根本原因を特定し、最終的に「隠れたUI設定」への依存から脱却し、透明性の高いコードベースの構成(Configuration as Code)へと移行するまでの道のりを詳述します。
※この記事のコード例やプロジェクト構成は、私の実際の業務経験に基づき作成しています。しかし、機密保持の観点から、具体的なビジネスロジック、テーブル名、スキーマ名などは、分かりやすい抽象的な名称や構造に変更しています。
1. 根本原因の特定:カスタムマクロという名の伏兵
私たちは、モデル整理のため、レイヤーごとの分離をデータベースで分け、ビジネスユニットごとの分離をスキーマで分けるために、カスタムスキーマを導入しました。
早速、dbt_project.ymlファイルに以下のような設定を加えました。
models:
my_project:
marts:
marketing:
+schema: marketing # marketingのモデルはこのスキーマへ
finance:
+schema: finance # financeのモデルはこのスキーマへ
この設定により、models/marts/marketing配下のモデルはmarketingスキーマに、models/marts/finance配下のモデルはfinanceスキーマに出力されるはずでした。しかし、dbt Cloudでjobを実行しても、全てのモデルは無情にもdbt Cloudでプロジェクトに設定するデフォルトスキーマに吐き出され続けるのです。
「dbt_project.ymlでの設定が、なぜ期待通りに反映されないのか?」
調査の結果、今回の問題の直接的な原因は、generate_schema_nameというカスタムマクロにあることが判明しました。このマクロがdbt_project.ymlの設定を解釈するデフォルトのロジックを上書きし、独自のルールを適用していたのです。
以下が、問題を引き起こしていたマクロのコードです。
変更前コード (generate_custom_schema.sql)
{% macro generate_schema_name(custom_schema_name, node) %}
{% set default_schema = target.schema %}
{% if target.name == 'prod' and node.resource_type == 'seed' and custom_schema_name is not none %}
{{ custom_schema_name | trim }}
{% elif target.name != 'prod' and node.resource_type == 'seed' and custom_schema_name is not none %}
{{ default_schema }}_{{ custom_schema_name | trim }}
{% elif target.name == 'prod' and custom_schema_name is not none %}
{{ custom_schema_name | trim }}
{% elif custom_schema_name is none %}
{{ default_schema }}
{% else %}
{{ default_schema }}
{% endif %}
{% endmacro %}
このコードを分析すると、問題の核心がif target.name == 'prod'という条件分岐にあることがわかります。このマクロは「本番環境(target.nameがprod)の場合に限り、dbt_project.ymlで指定されたカスタムスキーマをそのまま使用する」というロジックを持っていました。また、target.name == 'prod'のチェックがseedとmodel(暗黙的に)で重複しており、ロジックが不必要に複雑であるという「コードの匂い」も感じられます。
開発時に実行していたdbt CloudのJobでは、このtarget.name変数がどこで設定されるのか不明だったため、この条件がtrueになることはありませんでした。その結果、マクロは常に最後のelse句、すなわち{{ default_schema }}にフォールバックし、すべてのモデルをデフォルトスキーマに出力していたのです。dbt_project.ymlの設定は、このカスタムマクロによって意図的に無視されていたわけです。
この発見により、次の疑問が浮かび上がります。「では、dbt Cloudにおいてtarget.nameは一体どこで設定するのか?」この隠れた設定の発見が、次なる課題への扉を開くことになります。
2. 最初の解決策と新たな課題:UIの奥深くに潜む「罠」
技術的な問題の根本原因を特定できても、その解決方法自体が属人的であったり、直感的でなかったりする場合、それは新たな組織的・運用的な問題を生み出す可能性があります。優れたエンジニアリングとは、単に問題を解決するだけでなく、その解決策がチーム全体にとって透明で、再現可能で、スケーラブルであることを保証することです。
target.name変数を設定する場所をdbt CloudのUI上で探しましたが、自力での発見は困難を極めました。最終的にdbt Cloudのサポートに問い合わせた結果、驚くべき事実が判明します。
この設定は、各Jobの実行設定画面にある 「Advanced settings」という折りたたまれたセクションの中に、target.name変数を設定するためのフィールドとして存在していたのです。


このフィールドにprodと入力し、Jobを再実行したところ、ついに魔法がかかりました。マクロのtarget.name == 'prod'という条件分岐がtrueと評価され、長らく無視され続けてきたdbt_project.ymlの+schema設定がようやく反映されたのです。問題は解決したかに見えました。
しかし、この解決策は深刻な副作用を内包していました。これは「脆い構成(brittle configuration)」の典型例です。UIの奥深くにある設定で、バージョン管理もされず、チーム全体に可視化されていないものは、知識のサイロ化を招き、将来のバグの温床となります。つまり、極めて属人性の高い「隠れた罠」 となっていたのです。
実際に、この「罠」の存在を知らない他のチームメンバーは、スキーマが正しく出力されない問題に直面し、その都度デバッグに時間を浪費していました。特定の担当者の知識に依存するこの解決策は、チームのスケーラビリティを著しく阻害します。この属人性という名の新たな壁を乗り越え、より恒久的で透明性の高い解決策を模索する必要に迫られたのです。
3. 最終的な解決策:環境変数によるマクロの最適化
優れたデータエンジニアリングの実践とは、特定のUI設定への依存を最小限に抑え、構成をコードとして管理(Configuration as Code)することにあります。このアプローチは、構成をバージョン管理下に置き、ピアレビューを可能にし、変更の監査可能な履歴を作成します。これらは成熟したエンジニアリングプラクティスの証です。
最終的な解決策の目標は、以下の3点に集約されます。
- 目標1: UIの分かりにくい「Advanced settings」への依存を完全になくす。
- 目標2: Jobごとのtarget.name設定よりも、より広範で管理しやすい環境変数(Environment Variables)で環境を定義する。
- 目標3: dbt_project.ymlというGitで管理されたコードを編集するだけで、誰でも直感的にスキーマを制御できるようにする。
この目標を達成するため、私たちはgenerate_custom_schemaマクロの条件分岐を、target.nameからdbtの組み込み関数であるenv_varに切り替えました。env_var("DBT_ENV_NAME", "dev")は、dbt Cloudの環境で設定されたDBT_ENV_NAMEという環境変数を読み込み、設定されていない場合はデフォルト値としてdevを返す関数です。

変更後コード (generate_custom_schema.sql)
{% macro generate_schema_name(custom_schema_name, node) %}
{%- set default_schema = target.schema -%}
{%- set env=env_var("DBT_ENV_NAME", "dev") -%}
{%- if env == 'prod' and custom_schema_name is not none -%}
{{ custom_schema_name | trim }}
{%- else -%}
{{ default_schema | trim }}
{% endif %}
{% endmacro %}
この変更により、マクロのロジックは劇的にシンプルかつ明確になりました。平易な言葉で表現すると、「環境が ‘prod’ で、かつカスタムスキーマが定義されている場合、そのカスタムスキーマ名を使用する。それ以外のすべての場合は、デフォルトのターゲットスキーマを使用する」 というルールです。
この修正によってもたらされたメリットは絶大です。dbt Cloudの「Environments」設定で一度DBT_ENV_NAME=prodという環境変数を設定しておけば、あとはすべてGitで管理されているdbt_project.ymlを編集するだけでスキーマを制御できます。
Jobごとの隠れた設定は不要となり、特定の担当者の知識に依存しない、スケーラブルで透明性の高い運用フローが確立されました。この最適化されたアプローチは、dbtプロジェクトの長期的な健全性を保つための確かな一歩となったのです。
4. まとめと教訓
dbt Cloudでカスタムスキーマが反映されないという一見単純な問題は、dbtのカスタマイズ機能、UI仕様、そしてチームの運用プラクティスが複雑に絡み合った、示唆に富む課題でした。この問題解決のプロセスは、一個人の悪戦苦闘から、属人化していた知識をチームの仕組みに変えるプロセスと言えるでしょう。
この経験から得られた、dbtを実践するすべてのエンジニアにとって価値のある教訓を3つにまとめます。
generate_schema_nameマクロを疑う: スキーマが意図通りに出力されない場合、まず最初にプロジェクトのmacrosディレクトリにgenerate_schema_nameというカスタムマクロが存在しないか、そのロジックがどうなっているかを確認することが最も重要です。これが根本原因であるケースは非常に多いです。- dbt Cloudの
target.nameはJob設定にあり: もしカスタムマクロでtarget.name変数が使われている場合、その値はdbt Cloudの各Jobに紐づく「Advanced settings」内のフィールドによって制御されることを認識しておく必要があります。これは非常に見つけにくく、属人化の温床となり得ます。 - UI設定より環境変数を優先する: スケーラブルで保守性の高いプロジェクトを構築するためには、常にコードとして管理される構成(
env_varなど)を、特定のUI画面に隠れた設定よりも優先すべきです。これこそが「Configuration as Code」の本質であり、チームの成長を支える基盤となります。
今回の悪戦苦闘を通じて得られた知見が、皆さんのdbt Cloudプロジェクトをより堅牢で保守性の高いものにする一助となることを願っています。
