dbtベストプラクティスを読む~dbt projects style編~
はい。ベストプラクティスを読む第2回目です。
今回はかなり重要なスタイルガイド編です。
しっかり読み込んで、環境にマッチするベストガイドを策定しよう!
なぜスタイルが必要なのか?
一貫性のある明確なスタイルは可読性を高め、プロジェクトの理解と保守を容易にします。
可読性の高いコードは、明確なメンタル・モデルの構築に役立ち、デバッグやプロジェクトの拡張を容易にします。
同様に重要なことは、他の人があなたのプロジェクトを理解し、貢献するための労力を減らすことができるということです。
スタイルガイドがあれば、書き方の表面的なことよりも、重要なこと、つまりプロジェクトの論理とインパクトに集中することができます。
これにより、チームの仕事に調和とペースが生まれ、レビューがより楽しく価値のあるものになります。
そりゃそうだ。。。
重要な原則
dbtスタイルには次の2つの原則があります
- 明確性
- 一貫性
どのようにスタイルを決めるべきか
プロジェクトのスタイルは、開発メンバーが同意する方法で決めるべきである。
最も重要なことは、スタイルガイドを持ち、それに従うことです。
このガイドは、スタイル・ガイドがどのようなものかを知ってもらうための、単なる提案である。
dbt Labsが提唱するスタイルガイドを参考にチームメイト間で決めることが良いそうですね。
それではスタイルガイドを読んでいきやしょう。
オートメーション
フォーマッターとリンターはできるだけ使いましょう。
私たちは皆人間で、間違いを犯す。
それだけでなく、コードを書いているときの好みや意見も人それぞれです。
自動化は、あなたのプロジェクトが一貫して正しくスタイルされ、完璧に一貫した出力を得ながら、人々が素早く快適な方法で書けるようにするための素晴らしい方法です。
ここめちゃ大事!人間だからミスるの当たり前と思わなきゃ。
スタイルを検討するときはフォーマッターとリンターの選定も一緒にすると良さそう。
How we style our dbt models
Fields and model names
ここはフィールドとモデル名についてのスタイルガイドです。
- モデルは、顧客、注文、製品など、複数形にする。
- 各モデルは主キーを持つ。
- モデルの主キーは、例えば account_id のように、<object>_id という名前にします。
これにより、下流の結合モデルで参照されているidを簡単に知ることができます。 - キーは文字列データ型であるべき
- 一貫性が鍵です!可能であれば、モデル間で同じフィールド名を使用してください。
例えば、customersテーブルのキーは、user_idや'id'ではなく、customer_idという名前にすべきです。 - 略語や別名は使用しない。簡潔さよりも読みやすさを重視する。例えば、customerにcust、ordersにoを使わない。
- 予約語を列名として使用しない。
- ブール値はis_またはhas_を先頭に付ける。
- タイムスタンプ列は<event>_at(例えばcreated_at)という名前にし、UTCにする。異なるタイムゾーンを使用する場合は、接尾辞(created_at_pt)で示す。
- 日付は<event>_dateと命名します。例えば、created_date。
- イベントの日時は、作成、更新、削除の過去形にする。
- 価格/売上フィールドは10進数で指定します
- スキーマ名、テーブル名、カラム名はsnake_caseでなければならない。
- ソース用語ではなく、ビジネス用語に基づいた名前を使用します。
例えば、ソース・データベースがuser_idを使用しているが、ビジネスではcustomer_idと呼んでいる場合、モデルではcustomer_idを使用します。 - モデルのバージョンは、一貫性を保つために接尾辞_v1、_v2などを使用する必要があります(customers_v1とcustomers_v2)。
- データ型の順序を統一し、下の例のように、型ごとに列をグループ化し、ラベルを付けることを検討してください。
こうすることで、結合エラーを最小化し、モデルを読みやすくします。
また、データの下流の消費者がデータ型を理解し、必要な列のためにモデルをスキャンするのを助けます。
id、string、numerics、boolean、dates、timestampsの順で使用するのが望ましいです。
Example model
with
source as (
select * from {{ source('ecom', 'raw_orders') }}
),
renamed as (
select
---------- ids
id as order_id,
store_id as location_id,
customer as customer_id,
---------- strings
status as order_status,
---------- numerics
(order_total / 100.0)::float as order_total,
(tax_paid / 100.0)::float as tax_paid,
---------- booleans
is_fulfilled,
---------- dates
date(order_date) as ordered_date,
---------- timestamps
ordered_at
from source
)
select * from renamed
How we style our SQL
Basic
ここではSQLの標準的なスタイルガイドが紹介されています。
-
SQLFluffを使用すると、これらのスタイルルールを自動的に維持することができます。
-
.sqlfluff設定ファイルを必要に応じてカスタマイズしてください。
-
私たちのプロジェクトで使用しているルールについては、私たちの SQLFluff 設定ファイルを参照してください。
-
標準の .sqlfluffignore ファイルを使用してファイルやディレクトリを除外します。
.sqlfluffignore の構文については .sqlfluffignore syntax docs を参照してください。
-
-
コンパイルしたSQLに含めるべきでないコメントには、Jinjaコメント({# #})を使用してください。
-
末尾にカンマを付ける。
-
インデントは4スペース。
-
SQLの行は80文字以内にする。
-
フィールド名、キーワード、関数名はすべて小文字にします。
-
フィールドやテーブルをエイリアスにする場合は、as キーワードを明示的に使用する。
Fields, aggregations, and grouping
- フィールドは集約とウィンドウ関数の前に記述する。
- 集計は、パフォーマンスを向上させるために、別のテーブルに結合する前に、できるだけ早い段階で(できるだけ小さなデータセットで)実行すべきである。
- 列名を列挙するよりも、数値による順序付けやグループ化(例:group by 1, 2)の方が好ましい(その理由については、この古典的な主張を参照のこと)。
数列以上でグループ化する場合は、モデル設計を見直す価値があるかもしれません。
Joins
- 明示的に重複を削除したい場合を除き、union all を優先する。
- 2つ以上のテーブルを結合する場合は、必ず列名の前にテーブル名を付けてください。1つのテーブルからのみ選択する場合は、接頭辞は不要です。
- 結合タイプを明示する(つまり、結合ではなく内部結合と書く)。
- ジョイン条件におけるテーブルの別名(特に頭文字)は避ける - "customers "と比較して、"c "というテーブルが何であるか理解しにくい。
- 右結合は、どのテーブルから選択し、どのテーブルに結合するかを変更する必要があることを示すことが多い。
'Import' CTEs
- {{ ref('...') }} 文はすべて、ファイルの先頭にある CTE 内に配置する。
- 'インポート' CTE には、参照するテーブルの名前を付ける。
- CTE でスキャンするデータはできるだけ限定してください。可能であれば、実際に使用する列のみを選択し、where 節を使用して不要なデータをフィルタリングします。
- 例えば
with
orders as (
select
order_id,
customer_id,
order_total,
order_date
from {{ ref('orders') }}
where order_date >= '2020-01-01'
)
'Functional' CTEs
- パフォーマンスが許す限り、CTEは単一の論理的な作業単位を実行すべきである。
- 例えば、user_eventsの代わりにevents_joined_to_users(これは良いモデル名かもしれませんが、特定の機能や変換を表すものではありません)。
- モデル間で重複しているCTEは、それぞれの中間モデルに引き出されるべきです。独自のモデルにリファクタリングすべき、繰り返されるロジックの塊に注意。
- モデルの最終行は、最終的な出力CTEからのselect *にします。
こうすることで、開発中にモデル内のさまざまなステップからの出力を簡単に実体化し、監査することができます。
select 文で参照される CTE を変更するだけで、そのステップからの出力が表示されます。
Model configuration
- モデル固有の属性(sort/distキーのような)はモデルで指定する必要があります。
- 特定のコンフィギュレーションがディレクトリ内のすべてのモデルに適用される場合は、dbt_project.yml ファイルで指定する必要があります。
- モデル内コンフィギュレーションは、可読性を最大にするためにこのように指定します:
How we style our Python
Python tooling
おすすめのツールたち
Example Python
import pandas as pd
def model(dbt, session):
# set length of time considered a churn
pd.Timedelta(days=2)
dbt.config(enabled=False, materialized="table", packages=["pandas==1.5.2"])
orders_relation = dbt.ref("stg_orders")
# converting a DuckDB Python Relation into a pandas DataFrame
orders_df = orders_relation.df()
orders_df.sort_values(by="ordered_at", inplace=True)
orders_df["previous_order_at"] = orders_df.groupby("customer_id")[
"ordered_at"
].shift(1)
orders_df["next_order_at"] = orders_df.groupby("customer_id")["ordered_at"].shift(
-1
)
return orders_df
How we style our Jinja
Jinja style guide
ここのスタイルガイドは個人的に気なりますね。まだまだJinja使えてないので、スタイルをマスターして導入してみたい。
- Jinjaのデリミタを使うときは、{{this}}の代わりに{{ this }}のように、デリミタの内側にスペースを使います。
- Jinjaの論理ブロックを視覚的に示すには改行を使います。
- Jinjaのブロックの中でスペースを4つインデントして、中のコードがそのブロックによってラップされていることを視覚的に示します。
- Jinjaの空白制御は(あまり)気にせず、プロジェクトのコードを読みやすく することに集中しましょう。
空白制御を気にしないことで節約できる時間は、コンパイルしたコードに費やす時間をはるかに上回ります。
Examples of Jinja style
{% macro make_cool(uncool_id) %}
do_cool_thing({{ uncool_id }})
{% endmacro %}
select
entity_id,
entity_type,
{% if this %}
{{ that }},
{% else %}
{{ the_other_thing }},
{% endif %}
{{ make_cool('uncool_id') }} as cool_id
How we style our YAML
YAML Style Guide
- インデントは空白2文字で
- リスト項目は字下げする
- 適切な場合、辞書であるリスト項目を区切るために改行を使います。
- YAML の行は 80 文字以下にしてください。
- dbt JSON スキーマを互換性のある IDE と YAML フォーマッター (YAML ファイルを検証し、自動的にフォーマットするために Prettier を推奨します) で使ってください。
Example YAML
version: 2
models:
- name: events
columns:
- name: event_id
description: This is a unique identifier for the event
tests:
- unique
- not_null
- name: event_time
description: "When the event occurred in UTC (eg. 2018-01-01 12:00:00)"
tests:
- not_null
- name: user_id
description: The ID of the user who recorded the event
tests:
- not_null
- relationships:
to: ref('users')
field: id
Discussion