dbtモデルの手動作成は、データエンジニアにとって何時間も取られる単調な作業です。特に大きなビジネス変換がない場合、エンジニアのデータ業務の中でも最もやりがいを感じにくい部分と言えるでしょう。
しかし、このプロセス全体を自動化できるとしたらどうでしょうか?本ブログでは、GitLab Duo Agent Platformを使用して、適切な構造、テスト、ドキュメントを含む包括的なdbtモデルをわずか数分で生成する方法を詳しく解説します。
何を構築するのか
マーケティングチームは、広告投資を効果的に管理・最適化したいと考えています。広告プラットフォームの一つがRedditであるため、Reddit Ads APIからエンタープライズデータプラットフォームSnowflakeにデータを抽出しています。GitLabでは、3つのストレージレイヤーがあります:
-
raw
レイヤー - 外部ソースからの未処理データの最初の着陸地点。ビジネス用途に準備されていない -
prep
レイヤー - ソースモデルを含む最初の変換レイヤー。まだ一般的なビジネス用途には準備されていない -
prod
レイヤー - ビジネス用途とTableauレポート用に準備された最終変換データ
この実践ガイドでは、データは既に抽出ツールFivetranによってrawレイヤーに配置されており、prep
レイヤーからprod
レイヤーまでデータを処理するdbtモデルを生成します。
一行のdbtコードを自分で書くことなく、このガイドを最後まで進めると以下が完成します:
-
prepレイヤーのソースモデル
-
prodレイヤーのワークスペースモデル
-
Reddit Adsデータセットのすべての13テーブル(112列を含む)の完全なdbt構成
-
結果を検証するテストクエリ
プロセス全体は10分以内で完了します。手動では通常数時間かかる作業です。以下の手順に従ってください:
1. データ構造の準備
GitLab Duoがモデルを生成する前に、完全なテーブル構造を理解する必要があります。そこで重要になるのが、Snowflakeの情報スキーマに対してクエリを実行することです。現在、GitLab DuoをModel Context Protocol(MCP)経由でSnowflakeインスタンスに接続する方法を検討中です:
SELECT
table_name,
column_name,
data_type,
is_nullable,
CASE
WHEN is_nullable = 'NO' THEN 'PRIMARY_KEY'
ELSE NULL
END as key_type
FROM raw.information_schema.columns
WHERE table_schema = 'REDDIT_ADS'
ORDER BY table_name, ordinal_position;
このクエリは以下を取得します:
-
すべてのテーブル名と列名
-
適切なモデル構造のためのデータ型
-
NULL制約
-
主キーの識別(このデータセットでは非NULL列)
ポイント: Reddit Adsデータセットでは、すべての非NULL列が主キーとして機能するパターンになっています。実際にad_group
などのテーブルを確認したところ、このテーブルには2つの非NULL列(account_id
とid
)があり、両方とも主キーとしてマークされています。このクエリを実行すると112行のメタデータが返されたため、それらをCSVファイルとしてエクスポートしてモデル生成に使用しました。この手動ステップは現在は問題なく機能していますが、MCPを介してデータプラットフォームとGitLab Duoの直接統合を調査して、このプロセスの完全自動化を目指して取り組んでいます。
2. GitLab Duoのセットアップ
GitLab Duoと対話する方法は2つあります:
-
Web UIチャット機能
-
Visual Studio Codeプラグイン
dbtモデルをローカルで実行してテストできるため、VS Codeプラグインを選択しました。
3. 「効果的な」プロンプトの入力
すべてのdbtコードを生成するために使用した実際のプロンプトは以下の通りです:
Create dbt models for all the tables in the file structure.csv.(structure.csvファイル内のすべてのテーブルに対してdbtモデルを作成してください)
I want to have the source models created, with a filter that dedupes the data based on the primary key. Create these in a new folder reddit_ads.(主キーに基づいてデータの重複を除去するフィルターを使用してソースモデルを作成したいです。これらを新しいフォルダーreddit_adsに作成してください。)
I want to have workspace models created and store these in the workspace_marketing schema.(ワークスペースモデルを作成し、これらをworkspace_marketingスキーマに保存してください。)
Take this MR as example: [I've referenced to previous source implementation]. Here is the same done for Source A, but now it needs to be done for Reddit Ads.
(このMRを例として参考にしてください: [以前のソース実装を参照]。これはSource Aで実施した内容ですが、同様の処理をReddit Adsに対して行う必要があります。)
Please check the dbt style guide when creating the code: https://handbook.gitlab.com/handbook/enterprise-data/platform/dbt-guide/(コードを作成する際は、dbtスタイルガイドをチェックしてください)
このプロンプトが効果的だった理由:
-
ソースモデルとワークスペースモデル両方の明確な仕様
-
以前の類似マージリクエストからの参考例
-
コードの品質と一貫性を確保するためのスタイルガイド参照
-
適切な構成管理のための特定のスキーマ指定
4. GitLab Duoのプロセス
プロンプトを送信した後、GitLab Duoが作業を開始しました。生成プロセス全体は数分かかり、その間にGitLab Duoは以下を行いました:
-
CSV入力ファイルを読み取り分析
-
メタデータからテーブル構造を調査
-
コーディング標準についてdbtスタイルガイドを参照
-
適切な構造化のために類似のマージリクエストを考慮
-
すべての13テーブルのソースモデルを生成
-
すべての13テーブルのワークスペースモデルを作成
-
dbtサポートファイルを生成:
sources.yml
構成- テストとドキュメントを含む
schema.yml
ファイル - スキーマ参照で
dbt_project.yml
を更新
結果
出力は素晴らしいものでした:
-
変更ファイル1個: dbt_project.yml(reddit_adsスキーマ構成を追加)
-
新しいファイル29個:
- dbtモデル26個(ソース13個 + ワークスペース13個)
- YAMLファイル3個
-
900行近くのコードが自動生成
-
主キー列の一意制約を含む組み込みデータテスト
-
すべてのモデルと列の汎用説明
-
ソースモデルの適切な重複除去ロジック
-
GitLab dbtスタイルガイドに従ったクリーンで一貫したコード構造
transform/snowflake-dbt/
├── dbt_project.yml [MODIFIED](変更)
└── models/
├── sources/
│ └── reddit_ads/
│ ├── reddit_ads_ad_group_source.sql [NEW](新規)
│ ├── reddit_ads_ad_source.sql [NEW]
│ ├── reddit_ads_business_account_source.sql [NEW]
│ ├── reddit_ads_campaign_source.sql [NEW]
│ ├── reddit_ads_custom_audience_history_source.sql [NEW]
│ ├── reddit_ads_geolocation_source.sql [NEW]
│ ├── reddit_ads_interest_source.sql [NEW]
│ ├── reddit_ads_targeting_community_source.sql [NEW]
│ ├── reddit_ads_targeting_custom_audience_source.sql [NEW]
│ ├── reddit_ads_targeting_device_source.sql [NEW]
│ ├── reddit_ads_targeting_geolocation_source.sql [NEW]
│ ├── reddit_ads_targeting_interest_source.sql [NEW]
│ ├── reddit_ads_time_zone_source.sql [NEW]
│ ├── schema.yml [NEW]
│ └── sources.yml [NEW]
└── workspaces/
└── workspace_marketing/
└── reddit_ads/
├── schema.yml [NEW]
├── wk_reddit_ads_ad.sql [NEW]
├── wk_reddit_ads_ad_group.sql [NEW]
├── wk_reddit_ads_business_account.sql [NEW]
├── wk_reddit_ads_campaign.sql [NEW]
├── wk_reddit_ads_custom_audience_history.sql [NEW]
├── wk_reddit_ads_geolocation.sql [NEW]
├── wk_reddit_ads_interest.sql [NEW]
├── wk_reddit_ads_targeting_community.sql [NEW]
├── wk_reddit_ads_targeting_custom_audience.sql [NEW]
├── wk_reddit_ads_targeting_device.sql [NEW]
├── wk_reddit_ads_targeting_geolocation.sql [NEW]
├── wk_reddit_ads_targeting_interest.sql [NEW]
└── wk_reddit_ads_time_zone.sql [NEW]
生成されたコードのサンプル
生成されたコード品質の例をご紹介します。time_zone
テーブルについて、GitLab Duoは以下を作成しました:
Prepレイヤーソースモデル
WITH source AS (
SELECT *
FROM {{ source('reddit_ads','time_zone') }}
QUALIFY ROW_NUMBER() OVER (PARTITION BY id ORDER BY _fivetran_synced DESC) = 1
),
renamed AS (
SELECT
id::VARCHAR AS time_zone_id,
code::VARCHAR AS time_zone_code,
dst_offset::NUMBER AS time_zone_dst_offset,
is_dst_active::BOOLEAN AS is_time_zone_dst_active,
name::VARCHAR AS time_zone_name,
offset::NUMBER AS time_zone_offset,
_fivetran_synced::TIMESTAMP AS fivetran_synced_at
FROM source
)
SELECT * FROM renamed
Schema.yml
models:
- name: reddit_ads_time_zone_source
description: Time zone data from Reddit Ads system(Reddit Adsシステムからのタイムゾーンデータ)
columns:
- name: time_zone_id
description: Time zone data from Reddit Ads system(タイムゾーンレコードの一意識別子)
data_tests:
- unique
- not_null
- name: time_zone_code
description: Code for the time zone(タイムゾーンのコード)
- name: time_zone_dst_offset
description: Daylight saving time offset for the time zone(タイムゾーンの夏時間オフセット)
- name: is_time_zone_dst_active
description: Flag indicating if daylight saving time is active(夏時間がアクティブかどうかを示すフラグ)
- name: time_zone_name
description: Name of the time zone(タイムゾーンの名前)
- name: time_zone_offset
description: Offset for the time zone(タイムゾーンのオフセット)
- name: fivetran_synced_at
description: Timestamp when the record was last synced by Fivetran(レコードがFivetranによって最後に同期されたタイムスタンプ)
Source.yml
sources:
- name: reddit_ads
database: RAW
schema: reddit_ads
loaded_at_field: _fivetran_synced
loader: fivetran
description: Reddit Ads data
quoting:
database: true
schema: false
identifier: false
tables:
- name: time_zone
ワークスペースモデル
WITH source AS (
SELECT *
FROM {{ ref('reddit_ads_time_zone_source') }}
)
SELECT * FROM source
5. 品質検証
コードに問題がないことを確認し、MRにプッシュしてCIテストパイプラインを実行し、コードのテストと結果検証を行いました。GitLab Duoに検証クエリの作成を依頼しました:
Create a test query to test the row counts between the raw layer and the workspace layer. Keep in mind that we do deduplication, so we can compare both using distinct on the primary keys.(rawレイヤーとworkspaceレイヤー間の行数をテストするクエリを作成してください。重複除去を実施しているため、主キーに対してdistinctを使用して比較できます。)
AIが生成した包括的な検証クエリには、以下が含まれています:
-
rawレイヤーとワークスペースレイヤー間の行数を比較
-
重複除去ロジックに対応
-
全13テーブルをテスト
-
データ保持率を算出
生成されたSQLテストクエリ
-- Reddit Ads Row Count Validation Test(Reddit Ads行数検証テスト)
-- Compares distinct counts between RAW layer and WORKSPACE_MARKETING layer(RAWレイヤーとWORKSPACE_MARKETINGレイヤー間の一意カウントを比較)
-- Accounts for deduplication logic in source models(ソースモデルの重複除去ロジックを考慮)
WITH raw_counts AS (
-- Single primary key tables(単一主キーテーブル)
SELECT 'ad' AS table_name, COUNT(DISTINCT id) AS raw_count
FROM RAW.REDDIT_ADS.AD
UNION ALL
SELECT 'business_account' AS table_name, COUNT(DISTINCT id) AS raw_count
FROM RAW.REDDIT_ADS.BUSINESS_ACCOUNT
UNION ALL
SELECT 'campaign' AS table_name, COUNT(DISTINCT id) AS raw_count
FROM RAW.REDDIT_ADS.CAMPAIGN
UNION ALL
SELECT 'custom_audience_history' AS table_name, COUNT(DISTINCT id) AS raw_count
FROM RAW.REDDIT_ADS.CUSTOM_AUDIENCE_HISTORY
UNION ALL
SELECT 'geolocation' AS table_name, COUNT(DISTINCT id) AS raw_count
FROM RAW.REDDIT_ADS.GEOLOCATION
UNION ALL
SELECT 'interest' AS table_name, COUNT(DISTINCT id) AS raw_count
FROM RAW.REDDIT_ADS.INTEREST
UNION ALL
SELECT 'time_zone' AS table_name, COUNT(DISTINCT id) AS raw_count
FROM RAW.REDDIT_ADS.TIME_ZONE
-- Composite primary key tables(複合主キーテーブル)
UNION ALL
SELECT 'ad_group' AS table_name, COUNT(DISTINCT CONCAT(account_id, '|', id)) AS raw_count
FROM RAW.REDDIT_ADS.AD_GROUP
UNION ALL
SELECT 'targeting_community' AS table_name, COUNT(DISTINCT CONCAT(ad_group_id, '|', community_id)) AS raw_count
FROM RAW.REDDIT_ADS.TARGETING_COMMUNITY
UNION ALL
SELECT 'targeting_custom_audience' AS table_name, COUNT(DISTINCT CONCAT(ad_group_id, '|', custom_audience_id)) AS raw_count
FROM RAW.REDDIT_ADS.TARGETING_CUSTOM_AUDIENCE
UNION ALL
SELECT 'targeting_device' AS table_name, COUNT(DISTINCT _fivetran_id) AS raw_count
FROM RAW.REDDIT_ADS.TARGETING_DEVICE
UNION ALL
SELECT 'targeting_geolocation' AS table_name, COUNT(DISTINCT CONCAT(ad_group_id, '|', geolocation_id)) AS raw_count
FROM RAW.REDDIT_ADS.TARGETING_GEOLOCATION
UNION ALL
SELECT 'targeting_interest' AS table_name, COUNT(DISTINCT CONCAT(ad_group_id, '|', interest_id)) AS raw_count
FROM RAW.REDDIT_ADS.TARGETING_INTEREST
),
workspace_counts AS (
-- Workspace layer counts using primary keys from schema.yml(schema.ymlの主キーを使用したワークスペースレイヤーカウント)
SELECT 'ad' AS table_name, COUNT(DISTINCT ad_id) AS workspace_count
FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_AD
UNION ALL
SELECT 'business_account' AS table_name, COUNT(DISTINCT business_account_id) AS workspace_count
FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_BUSINESS_ACCOUNT
UNION ALL
SELECT 'campaign' AS table_name, COUNT(DISTINCT campaign_id) AS workspace_count
FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_CAMPAIGN
UNION ALL
SELECT 'custom_audience_history' AS table_name, COUNT(DISTINCT custom_audience_id) AS workspace_count
FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_CUSTOM_AUDIENCE_HISTORY
UNION ALL
SELECT 'geolocation' AS table_name, COUNT(DISTINCT geolocation_id) AS workspace_count
FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_GEOLOCATION
UNION ALL
SELECT 'interest' AS table_name, COUNT(DISTINCT interest_id) AS workspace_count
FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_INTEREST
UNION ALL
SELECT 'time_zone' AS table_name, COUNT(DISTINCT time_zone_id) AS workspace_count
FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_TIME_ZONE
-- Composite primary key tables(複合主キーテーブル)
UNION ALL
SELECT 'ad_group' AS table_name, COUNT(DISTINCT CONCAT(ad_group_account_id, '|', ad_group_id)) AS workspace_count
FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_AD_GROUP
UNION ALL
SELECT 'targeting_community' AS table_name, COUNT(DISTINCT CONCAT(targeting_community_ad_group_id, '|', targeting_community_id)) AS workspace_count
FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_TARGETING_COMMUNITY
UNION ALL
SELECT 'targeting_custom_audience' AS table_name, COUNT(DISTINCT CONCAT(targeting_custom_audience_ad_group_id, '|', targeting_custom_audience_id)) AS workspace_count
FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_TARGETING_CUSTOM_AUDIENCE
UNION ALL
SELECT 'targeting_device' AS table_name, COUNT(DISTINCT targeting_device_fivetran_id) AS workspace_count
FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_TARGETING_DEVICE
UNION ALL
SELECT 'targeting_geolocation' AS table_name, COUNT(DISTINCT CONCAT(targeting_geolocation_ad_group_id, '|', targeting_geolocation_id)) AS workspace_count
FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_TARGETING_GEOLOCATION
UNION ALL
SELECT 'targeting_interest' AS table_name, COUNT(DISTINCT CONCAT(targeting_interest_ad_group_id, '|', targeting_interest_id)) AS workspace_count
FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_TARGETING_INTEREST
)
-- Final comparison with validation results(検証結果との最終比較)
SELECT
r.table_name,
r.raw_count,
w.workspace_count,
r.raw_count - w.workspace_count AS count_difference,
CASE
WHEN r.raw_count = w.workspace_count THEN '✅ PASS(成功)'
WHEN r.raw_count > w.workspace_count THEN '⚠️ RAW > WORKSPACE (Expected due to deduplication 重複除去のため正常)'
ELSE '❌ FAIL(失敗) - WORKSPACE > RAW (Unexpected 異常)'
END AS validation_status,
ROUND((w.workspace_count::FLOAT / r.raw_count::FLOAT) * 100, 2) AS data_retention_percentage
FROM raw_counts r
JOIN workspace_counts w ON r.table_name = w.table_name
ORDER BY r.table_name;
このクエリを実行した結果:
-
重複除去後の行数に差異なし
-
全テーブルで100%のデータ保持
-
すべてのテストが正常に完了
結論:大幅な時間短縮
-
従来のアプローチ: 手動コーディング、テスト、デバッグで6-8時間
-
GitLab Duoを活用: 生成 + レビュー時間で6-8分
開発効率が60倍に向上(6-8時間から6-8分へ)し、高品質なコードを確保できます。
成功のためのベストプラクティス
本事例から得られた主要な推奨事項:
メタデータの準備
-
データ型や制約を含む完全なテーブル構造を抽出する
-
主キーと関係を事前に特定する
-
クリーンで適切にフォーマットされたCSV入力ファイルをエクスポートする
注: GitLab DuoをMCP経由で(メタ)データに接続することで、この手動ステップを省略できる可能性があります。
明確なコンテキストの提供
-
可能な限り既存のMR例を参照例として示す
-
コーディング標準とスタイルガイドを明示する
-
フォルダー構造と命名規則について明確に指定する
徹底的な検証
-
データ整合性を確認する検証クエリを必ず作成する
-
マージ前にローカルでテストする
-
CI/CDパイプラインを実行して問題を検出する
AIを活用したフォローアップ作業
-
テストクエリを自動生成する
-
ドキュメントテンプレートを作成する
-
検証スクリプトを構築する
今後の展開
本ガイドでは、GitLab DuoをはじめとするAI搭載開発ツールがデータエンジニアリングワークフローをいかに変革しているかを示しました。テスト、ドキュメント、適切な構造を含む数百行の本番対応コードを数分で生成できる能力は、反復的な開発タスクへのアプローチの根本的な転換を表しています。
AIにdbtモデル作成の反復作業を委ねることで、データエンジニアはデータモデリング戦略、パフォーマンス最適化、ビジネスロジック実装などといった、より価値の高い業務に専念できます。
ぜひお試しください 小さなデータセットから着手し、メタデータを慎重に準備することで、GitLab Duoが何時間の作業をわずか数分の自動生成に変える様子を実感できます。