30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

target や config を理解して dbt model の出力先を制御できるようになる

dbt を使おうとすると、profile や target、config、property など、様々な概念が出てくる。
それらをあまり理解できていなくても、何となく動かすことはできるかもしれない。
しかし、これらの概念を理解していないと、意図した通りに動かしたり他者が記述した設定内容を理解したりするのは難しい。

この記事では、最初は理解が難しいこれらの概念について、「model の出力先の設定」を題材にして説明していく。
dbt runした際にどこにテーブルやビューが出力されるのか、設定を読み解いて理解できるようになることを目指す。

この記事の内容は dbt のバージョン1.8.5で動作確認している。
データウェアハウスは BigQuery を使用する。

概要

どのデータウェアハウスのどのテーブルにあるデータを使うのか、データの出力先はどこになるのか。
そういった設定には「profile による基本設定」と「config による個別設定」の 2 つがあり、個別設定によって基本設定を上書きすることができる。
この仕組みによって、特定の model についてだけ出力先を変える、といったことが可能になる。

まずは基本設定から見ていく。

基本設定

dbt では profile という概念によって、データウェアハウスとの接続に関する情報を扱う。
そしてその profile について記述するためのファイルが、profiles.yml。このファイルに、どのデータウェアハウスを使うのか、どのデータベースを使うのか、といったことを記述していく。

以下は、データウェアハウスとして BigQuery を使い、dbt-project-a-432612というプロジェクトのdestデータセットに model を出力する場合のprofiles.yml

my_profile_1: # profile の名前
  target: dev # デフォルトで使う target の名前
  outputs:
    dev: # target の名前
      type: bigquery # 接続先のデータウェアハウスの種類
      method: oauth # 認証に使う方法
      project: dbt-project-a-432612 # 使用するプロジェクト
      dataset: dest # 使用するデータセット

ひとつの profile は、ひとつ以上の target を持つ。データウェアハウスについての具体的な情報は target に書く。target はprofiles.yml<profile-name>.outputsに書いていく。
上記の例では、devという target を定義している。
詳細は後述するが、ひとつの profile のなかで複数の target を定義できるため、デフォルトで使う target を指定しておくことができる。profiles.yml<profile-name>.targetで指定する。
上記の例ではtarget: devがそれにあたる。

上記の例では target で 4 つの項目を設定している。
このうちmethodは認証に関するものであり、この記事の内容との関係は薄いので割愛する。
詳細は、公式ドキュメントの以下のページで確認できる。
https://docs.getdbt.com/docs/core/connect-data-platform/bigquery-setup

重要なのはprojectdatasetである。これで、接続先を指定している。
項目名はデータウェアハウスによって異なるので注意する。例えば Snowflake の場合はdatabaseschemaを使って設定を行う。
https://docs.getdbt.com/reference/dbt-jinja-functions/target

projectは、データの出力先として使われるだけでなく、source の参照先としても使われる。
例えば以下のように source を定義してfrom {{ source('src', 'user') }}とする場合、dbt-project-a-432612プロジェクトのsrcデータセットのuserテーブルが使われる。

version: 2

sources:
  - name: src # dataset を省略した場合は name が dataset として使われる
    tables:
      - name: user
        columns:
          - name: id
          - name: name

dbt-project-a-432612.src.userを用意し、それを参照しているfooという model を書いて$ dbt run -s fooを実行してみると……。
エラーになる。

$ dbt run -s foo
15:48:52  Running with dbt=1.8.5
15:48:52  Encountered an error:
Runtime Error
  No dbt_project.yml found at expected path <カレントディレクトリのパス>/dbt_project.yml
  Verify that each entry within packages.yml (and their transitive dependencies) contains a file named dbt_project.yml

dbt runを実行するためには、dbt_project.ymlを用意する必要がある。そして、dbt_project.ymlで、どの profile を使うか指定しなければならない。

name: 'my_dbt_project'
config-version: 2
version: '1.0.0'

profile: 'my_profile_1' # profiles.yml で定義した my_profile_1 を指定

nameversionに、この dbt プロジェクトの名前とバージョンを指定する。config-version2にする決まり。
そしてprofileキーに、使用する profile を指定する。

この状態で$ dbt run -s fooすると今度は上手くいき、dbt-project-a-432612.dest.fooというビューが作られる。

target の使い分け

ひとつの profile に対して、複数の target を定義できる。
以下の例では、先ほど定義したdevに加えて、prodという target を定義した。そしてprodではdbt-project-b-432615プロジェクトを使うと定義している。

my_profile_1: # profile の名前
  target: dev # デフォルトで使う target の名前
  outputs:
    dev: # target の名前
      type: bigquery
      method: oauth
      project: dbt-project-a-432612
      dataset: dest
    prod: # target の名前
      type: bigquery
      method: oauth
      project: dbt-project-b-432615
      dataset: dest

dbt runする際に--targetフラグを使うことで、使用する target を指定できる。
例えば$ dbt run -s foo --target prodを実行すると、dbt-project-b-432615.dest.fooビューが作られる。その際はもちろん、dbt-project-b-432615.src.userが source として使われる。
--targetフラグを使わなかった場合は、<profile-name>.targetで指定した target (上記の例だとdev)が使われる。
--targetフラグを使わず<profile-name>.targetも定義されていなかった場合はエラーになる。

$ dbt run -s foo
16:57:30  Running with dbt=1.8.5
16:57:30  target not specified in profile 'my_profile_1', using 'default'
16:57:30  Encountered an error:
Runtime Error
  The profile 'my_profile_1' does not have a target named 'default'. The valid target names for this profile are:
   - dev
   - prod

config

model 毎に、その出力先を設定することができる。
その仕組みを理解するためにはまず、config という概念を理解する必要がある。

model に対しては property を設定できる(model だけでなく seed や test などにも property を設定できるが、この記事では割愛する)。
そして property の中には、 config と呼ばれる特殊な property がある。

config は以下の 3 つの方法で設定できる。

  1. config()関数
  2. 個別の model について記述した.ymlファイル
  3. dbt_project.yml

上記の順番で優先度が高く、ひとつの config に対して複数の方法で設定されていた場合、優先度が高い方法で設定した内容が採用される。
詳細はこれから具体例を用いて説明していくので、config には複数の設定方法がある、優先順位が決まっている、ということをまずは覚えておけばよい。

model に設定できる config のうち、model の出力先に関係するのがdatabaseschema
BigQuery の場合はprojectdatasetという config も設定できる。それぞれ、databaseschemaと互換性がある。
BigQuery の用語(プロジェクトとデータセット)と合わせたほうが分かりやすいので、この記事ではprojectdatasetを使うことにする。

dbt_project.yml で config を設定する

まずはdbt_project.ymlproject config を使ってみる。

name: 'my_dbt_project'
config-version: 2
version: '1.0.0'

profile: 'my_profile_1'

models:
  my_dbt_project: # name で指定した project の名前を書く
    marts: # marts ディレクトリ以下にある model は……
      +project: dbt-project-x # dbt-project-x に出力される

model に対する config はmodels:の下に書いていく。まずは project の名前を書き、その下に具体的な設定を書いていく。
今回はmartsディレクトリ以下の model に対して config を設定したいので、まずはmartsと書く。そして、+<config名>: 設定内容を書く。上記の例ではproject config にdbt-project-xを設定している。

こうすると、martsディレクトリ以下にある model はdbt-project-xに出力されるようになる。

例えばmodels/の中身が以下のときに$ dbt run --target devすると、dbt-project-a-432612.dest.foodbt-project-x.dest.barが作られる。

models
├── foo.sql
├── marts
│   └── bar.sql
└── sources.yml

target としてdevを指定したので、profiles.ymlに記した内容に基づき、接続先はdbt-project-a-432612.destになる。
しかしmarts以下のディレクトリについてはdbt-project-xに出力するようにdbt_project.ymlに記したので、その設定が優先される。datasetについてはdbt_project.ymlで特に設定していないので、devで設定したdestがそのまま使われる。
そのため、marts/bar.sqlの実行結果がdbt-project-x.dest.barに書き込まれるのである。

次はdatasetも使ってみる。
dbt_project.yml+dataset: my_suffix_1を書き加える。

name: 'my_dbt_project'
config-version: 2
version: '1.0.0'

profile: 'my_profile_1'

models:
  my_dbt_project: # name で指定した project の名前を書く
    marts: # marts ディレクトリ以下にある model は……
      +project: dbt-project-x # dbt-project-x に出力される
      +dataset: my_suffix_1 # target.dataset に _my_suffix_1 という接尾辞をつけたデータセットに出力される

この状態で$ dbt run --target devすると、dbt-project-a-432612.dest.foodbt-project-x.dest_my_suffix_1.barが出力される。
my_suffix_1というデータセットに出力されるのではなく、target.dataset(今回の場合dest)とmy_suffix_1_でつないだデータセットに出力されるので、注意が必要。

model のprojectdataset config で設定しているのはあくまでも、「model の出力先の設定」である。「source としてどのデータベースを参照するか」には影響を与えない。
そのためのfoobarも、from {{ source('src', 'user') }}している場合はdbt-project-a-432612.src.userを参照するので注意する。

config を入れ子にする

以下のように config をネストさせていくことも可能。

name: 'my_dbt_project'
config-version: 2
version: '1.0.0'

profile: 'my_profile_1'

models:
  my_dbt_project: # name で指定した project の名前を書く
    marts: # marts ディレクトリ以下にある model は……
      +project: dbt-project-x # dbt-project-x に出力される
      +dataset: my_suffix_1 # target.dataset に _my_suffix_1 という接尾辞をつけたデータセットに出力される
      special: # marts/special ディレクトリ以下にある model は……
        +dataset: my_suffix_2 # target.dataset に _my_suffix_2 という接尾辞をつけたデータセットに出力される

このように書くと、marts/specialディレクトリ以下にある model はdest_my_suffix_2データセットに出力されるようになる。

marts/special/baz.sqlを用意して確認してみる。

models
├── foo.sql
├── marts
│   ├── bar.sql
│   └── special
│       └── baz.sql
└── sources.yml

この状態で$ dbt run --target devすると、以下のビューが作られる。

  • dbt-project-a-432612.dest.foo
  • dbt-project-x.dest_my_suffix_1.bar
  • dbt-project-x.dest_my_suffix_2.baz

specialで設定したのはdatasetのみなので、projectmartsで設定したdbt-project-xmarts/special/baz.sqlにも適用される。

個別の model について記述した .yml ファイルで config を設定する

model と同じディレクトリに.ymlファイルを用意し、そこに個別の model に対する property を記述することができる。
config も property の一部なので、projectdatasetもそのファイルに書くことができる。

例として、models/marts/special/qux.sqlという model を用意し、その model について記述したmodels/marts/special/qux.ymlを用意する。
models/marts/special/qux.ymlには以下の内容を書く。

version: 2

models:
  - name: qux
    config:
      project: dbt-project-y

quxprojectdbt-project-yにしている。
datasetは特に上書きしていないので、dbt_project.ymlの内容がそのまま使われる。qux.sqlmarts/special以下にあるので、今回の例だとmy_suffix_2になる。
その結果、models/marts/special/qux.sqlの内容はdbt-project-y.dest_my_suffix_2.quxに出力される。

このように、個別の model について記述した.ymlファイルで設定した内容は、dbt_project.ymlの内容よりも優先される。

config() 関数で config を設定する

最後に、最も優先順位が高いconfig()関数を使った方法について述べる。

先ほど用意したmodels/marts/special/qux.sqlの内容を以下のようにする。

{{ config(
  project='dbt-project-z'
) }}
select name from {{ source('src', 'user') }}

そうすると、出力先はdbt-project-z.dest_my_suffix_2.quxになる。
このように、.ymlで定義した内容(今回の例だとproject: dbt-project-y)よりも、config()関数の内容(今回の例だとproject='dbt-project-z')が優先される。

generate_database_name と generate_schema_name

ここまで、model の出力先がどのように決まるのか見てきたが、その挙動は実は macro によって制御されている。
dbt には予めgenerate_database_nameという macro とgenerate_schema_nameという marco が用意されている。
そしてこれらの macro はdbt runなどを実行したときに暗黙的に呼び出され、そこに書かれているロジックによって macro の出力先が決まる。

そしてどちらの macro も、自分で定義して既存の振る舞いを上書きすることができる。

generate_database_name

generate_database_nameは、出力先の database を決めるための macro 。projectdatabaseと互換性があるため、databaseはそのままprojectに読み替えてしまって問題ない。
デフォルトの実装は以下のようになっている。

{% macro generate_database_name(custom_database_name=none, node=none) -%}

    {%- set default_database = target.database -%}
    {%- if custom_database_name is none -%}

        {{ default_database }}

    {%- else -%}

        {{ custom_database_name | trim }}

    {%- endif -%}

{%- endmacro %}

引数のcustom_database_nameは、 model に設定されたdatabase(もしくはproject) config 。
これが設定されない場合はdefault_database、つまりtarget.database(BigQuery の場合はtarget.project)が使われる。
custom_database_nameが設定されている場合は、それがそのまま(trimだけして)使われる。

このように、この記事でここまで説明してきた挙動は、この macro によって定義されているのである。

generate_schema_name

generate_schema_nameも同様に、出力先の schema を決定するロジックを定義している。datasetschemaと互換性があるため、schemaはそのままdatasetに読み替えてしまって問題ない。
デフォルトの実装は以下。

{% macro generate_schema_name(custom_schema_name, node) -%}

    {%- set default_schema = target.schema -%}
    {%- if custom_schema_name is none -%}

        {{ default_schema }}

    {%- else -%}

        {{ default_schema }}_{{ custom_schema_name | trim }}

    {%- endif -%}

{%- endmacro %}

独自定義によるオーバーライド

macros/generate_database_name.sqlあるいはmacros/generate_schema_name.sqlを定義することで、挙動を上書きすることができる。

例として、以下の内容でmacros/generate_database_name.sqlを作成して、出力先の database(もしくは project)を決めるロジックを自分で定義してみる。

{% macro generate_database_name(custom_database_name=none, node=none) -%}

    {{ 'dbt-project-y' }}

{%- endmacro %}

このように定義してdbt runすると、target や config でprojectをどのように設定していても、その設定は使われず、全ての model がdbt-project-yに出力されるようになる。

source の接続先

projectdatasetは source にも設定できる。
しかし source に対してはgenerate_database_namegenerate_schema_nameが実行されることはなく、projectdatasetに設定した値がそのまま使われる。
そのため、以下のように設定した状態でfrom {{ source('src', 'user') }}とすると、dbt-project-b-432615.src.userが参照される。

version: 2

sources:
  - name: src # dataset を省略した場合は name が dataset として使われる
    project: dbt-project-b-432615
    tables:
      - name: user
        columns:
          - name: id
          - name: name

target によって参照先を変えるということも可能。
以下のようにすると、devのときはdbt-project-a-432612を、prodのときはdbt-project-b-432615を参照するようになる。

version: 2

sources:
  - name: src
    project: |
      {%- if  target.name == "dev" -%} dbt-project-a-432612
      {%- elif target.name == "prod" -%} dbt-project-b-432615
      {%- else -%} invalid_database
      {%- endif -%}
    tables:
      - name: user
        columns:
          - name: id
          - name: name

そして例えばprofiles.ymlを以下のようにしておくと、--target devdbt runしたときはdbt-project-a-432612.src.userを参照してdbt-project-xに model を出力し、--target prodのときはdbt-project-b-432615.src.userを参照してdbt-project-yに model を出力するようになる。
config で model の出力先を設定している場合は、もちろんそれが採用される。

my_profile_1: # profile の名前
  target: dev # デフォルトで使う target の名前
  outputs:
    dev: # target の名前
      type: bigquery
      method: oauth
      project: dbt-project-x
      dataset: dest
    prod: # target の名前
      type: bigquery
      method: oauth
      project: dbt-project-y
      dataset: dest

参考資料