前言
在数据工程中,数据转换是ETL的核心环节。传统的ETL工具(如Informatica)用图形界面配置,而dbt(data build tool)带来了革命性的变化------用SQL定义转换逻辑,用版本控制管理,让数据工程师像写代码一样开发数据管道。
今天我们从零实现dbt的核心功能:
· 模型定义(Model)
· 依赖管理(ref函数)
· 物化策略(table/view/ephemeral)
· 增量构建(incremental)
· 测试(test)
· 文档生成
· 宏(Macros)
· 变量与配置
一、dbt核心原理
- 架构图
```
┌─────────────────────────────────────────────────────────────┐
│ 项目结构 │
│ ├── models/ │
│ │ ├── staging/ (ODS层) │
│ │ ├── marts/ (DWD/DWS层) │
│ │ └── schema.yml (测试/文档) │
│ ├── macros/ (宏定义) │
│ ├── tests/ (自定义测试) │
│ └── dbt_project.yml (项目配置) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ dbt运行流程 │
│ 解析SQL → 解析依赖 → 生成DAG → 编译 → 执行 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 目标数据仓库 │
│ (Snowflake / BigQuery / Redshift / Postgres) │
└─────────────────────────────────────────────────────────────┘
```
- 核心概念
概念 说明
Model 数据模型(SQL文件)
ref 引用另一个模型
source 引用源表
Materialization 物化策略(table/view/ephemeral)
Incremental 增量构建
Test 数据测试(唯一性/非空)
Macro SQL宏(参数化SQL)
Doc 文档(Markdown)
二、完整代码实现
- 基础数据结构
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
#include <errno.h>
#include <dirent.h>
#include <sys/stat.h>
#define MAX_MODEL_NAME 128
#define MAX_SQL_LEN 8192
#define MAX_PATH_LEN 512
#define MAX_DEPENDENCIES 32
#define MAX_TESTS 16
#define MAX_MACROS 32
// 物化策略
typedef enum {
MAT_TABLE = 0,
MAT_VIEW,
MAT_EPHEMERAL,
MAT_INCREMENTAL
} materialization_t;
// 模型
typedef struct model {
char nameMAX_MODEL_NAME;
char file_pathMAX_PATH_LEN;
char sqlMAX_SQL_LEN;
materialization_t materialization;
struct model *dependenciesMAX_DEPENDENCIES;
int dep_count;
struct model *dependentsMAX_DEPENDENCIES;
int dep_count_out;
int is_seed;
int is_source;
char schema64;
char tags864;
int tag_count;
struct model *next;
} model_t;
// 测试
typedef struct test {
char nameMAX_MODEL_NAME;
char model_nameMAX_MODEL_NAME;
char test_type32; // unique, not_null, accepted_values, custom
char column64;
char *custom_sql;
int severity; // warn, error
struct test *next;
} test_t;
// 宏
typedef struct macro {
char name64;
char arguments864;
int arg_count;
char sqlMAX_SQL_LEN;
struct macro *next;
} macro_t;
// dbt项目
typedef struct dbt_project {
char name128;
char target_schema64;
char target_database64;
char profile64;
int threads;
model_t *models;
int model_count;
test_t *tests;
int test_count;
macro_t *macros;
int macro_count;
char models_pathMAX_PATH_LEN;
char macros_pathMAX_PATH_LEN;
char tests_pathMAX_PATH_LEN;
pthread_mutex_t mutex;
} dbt_project_t;
// dbt上下文
typedef struct dbt_context {
dbt_project_t *project;
model_t *current_model;
int compiled;
time_t compile_time;
} dbt_context_t;
```
- 项目初始化
```c
// 创建dbt项目
dbt_project_t *dbt_init(const char *name, const char *target_schema) {
dbt_project_t *project = malloc(sizeof(dbt_project_t));
memset(project, 0, sizeof(dbt_project_t));
strcpy(project->name, name);
strcpy(project->target_schema, target_schema);
strcpy(project->target_database, "analytics");
strcpy(project->profile, "default");
project->threads = 4;
project->models = NULL;
project->model_count = 0;
project->tests = NULL;
project->test_count = 0;
project->macros = NULL;
project->macro_count = 0;
pthread_mutex_init(&project->mutex, NULL);
// 创建目录结构
mkdir("models", 0755);
mkdir("models/staging", 0755);
mkdir("models/marts", 0755);
mkdir("macros", 0755);
mkdir("tests", 0755);
printf("dbt 项目 %s 初始化完成,目标schema: %s\n", name, target_schema);
return project;
}
// 创建模型
model_t *dbt_create_model(dbt_project_t *project, const char *name,
materialization_t mat, const char *schema) {
pthread_mutex_lock(&project->mutex);
model_t *model = malloc(sizeof(model_t));
memset(model, 0, sizeof(model_t));
strcpy(model->name, name);
model->materialization = mat;
if (schema) {
strcpy(model->schema, schema);
} else {
strcpy(model->schema, project->target_schema);
}
model->dep_count = 0;
model->dep_count_out = 0;
model->tag_count = 0;
model->is_seed = 0;
model->is_source = 0;
model->next = project->models;
project->models = model;
project->model_count++;
pthread_mutex_unlock(&project->mutex);
printf("dbt 创建模型: %s\n", name);
return model;
}
// 设置模型SQL
void model_set_sql(model_t *model, const char *sql) {
// 解析ref依赖
char bufferMAX_SQL_LEN;
strcpy(buffer, sql);
char *p = buffer;
while ((p = strstr(p, "{{ ref(")) != NULL) {
p += 8;
char *end = strchr(p, ')');
if (end) {
char dep_name128;
int len = end - p;
if (len < 128) {
strncpy(dep_name, p, len);
dep_namelen = '\0';
// 去除引号
char *q_start = strchr(dep_name, '\'');
if (q_start) {
char *q_end = strchr(q_start + 1, '\'');
if (q_end) {
memmove(q_start, q_start + 1, strlen(q_start));
q_end--; *q_end = '\0';
}
}
// 依赖会在解析阶段连接
}
}
}
strcpy(model->sql, sql);
}
// 添加测试
void dbt_add_test(dbt_project_t *project, const char *name,
const char *model_name, const char *test_type,
const char *column, int severity) {
pthread_mutex_lock(&project->mutex);
test_t *test = malloc(sizeof(test_t));
strcpy(test->name, name);
strcpy(test->model_name, model_name);
strcpy(test->test_type, test_type);
if (column) strcpy(test->column, column);
else test->column0 = '\0';
test->severity = severity;
test->custom_sql = NULL;
test->next = project->tests;
project->tests = test;
project->test_count++;
pthread_mutex_unlock(&project->mutex);
printf("dbt 添加测试: %s (模型: %s)\n", name, model_name);
}
```
- 依赖解析
```c
// 查找模型
model_t *dbt_find_model(dbt_project_t *project, const char *name) {
model_t *model = project->models;
while (model) {
if (strcmp(model->name, name) == 0) {
return model;
}
model = model->next;
}
return NULL;
}
// 解析依赖
void dbt_parse_dependencies(dbt_project_t *project) {
model_t *model = project->models;
while (model) {
// 解析SQL中的ref
char bufferMAX_SQL_LEN;
strcpy(buffer, model->sql);
char *p = buffer;
while ((p = strstr(p, "{{ ref(")) != NULL) {
p += 8;
char *end = strchr(p, ')');
if (end) {
char dep_name128;
int len = end - p;
if (len < 128) {
strncpy(dep_name, p, len);
dep_namelen = '\0';
// 去除引号
char *q_start = strchr(dep_name, '\'');
if (q_start) {
char *q_end = strchr(q_start + 1, '\'');
if (q_end) {
memmove(q_start, q_start + 1, strlen(q_start));
q_end--; *q_end = '\0';
}
}
// 查找依赖
model_t *dep = dbt_find_model(project, dep_name);
if (dep) {
model->dependenciesmodel-\>dep_count++ = dep;
dep->dependentsdep-\>dep_count_out++ = model;
printf("dbt 依赖: %s → %s\n", model->name, dep_name);
}
}
}
}
model = model->next;
}
}
// 拓扑排序(执行顺序)
model_t **dbt_topological_sort(dbt_project_t *project, int *count) {
model_t **sorted = malloc(sizeof(model_t*) * project->model_count);
int sorted_count = 0;
int visited = 0;
// 简单DFS(实际需处理循环依赖)
model_t *model = project->models;
while (model) {
if (model->dep_count == 0) {
sortedsorted_count++ = model;
}
model = model->next;
}
// 如果有依赖未满足,再遍历
while (sorted_count < project->model_count) {
model = project->models;
while (model) {
int all_deps_ready = 1;
for (int i = 0; i < model->dep_count; i++) {
int found = 0;
for (int j = 0; j < sorted_count; j++) {
if (sortedj == model->dependenciesi) {
found = 1;
break;
}
}
if (!found) {
all_deps_ready = 0;
break;
}
}
if (all_deps_ready) {
int already_sorted = 0;
for (int i = 0; i < sorted_count; i++) {
if (sortedi == model) {
already_sorted = 1;
break;
}
}
if (!already_sorted) {
sortedsorted_count++ = model;
}
}
model = model->next;
}
}
*count = sorted_count;
return sorted;
}
```
- 编译与执行
```c
// 编译模型(生成SQL)
char *dbt_compile_model(dbt_project_t *project, model_t *model) {
char *compiled = malloc(MAX_SQL_LEN);
strcpy(compiled, model->sql);
// 替换ref为实际表名
char bufferMAX_SQL_LEN;
strcpy(buffer, compiled);
char *p = buffer;
while ((p = strstr(p, "{{ ref(")) != NULL) {
char *end = strchr(p, ')');
if (end) {
char dep_name128;
int len = end - (p + 8);
if (len < 128) {
strncpy(dep_name, p + 8, len);
dep_namelen = '\0';
// 去除引号
char *q_start = strchr(dep_name, '\'');
if (q_start) {
char *q_end = strchr(q_start + 1, '\'');
if (q_end) {
memmove(q_start, q_start + 1, strlen(q_start));
q_end--; *q_end = '\0';
}
}
// 替换为 {{ ref('xxx') }} → analytics.schema.xxx
char replace256;
snprintf(replace, sizeof(replace), "%s.%s",
project->target_database, dep_name);
// 替换
char beforeMAX_SQL_LEN;
char afterMAX_SQL_LEN;
strcpy(before, compiled);
int pos = p - buffer;
strncpy(after, before, pos);
afterpos = '\0';
strcat(after, replace);
strcat(after, end + 1);
strcpy(compiled, after);
}
}
p++;
}
return compiled;
}
// 执行模型
void dbt_run_model(dbt_project_t *project, model_t *model) {
char *compiled = dbt_compile_model(project, model);
printf("dbt 执行模型: %s\n", model->name);
printf(" SQL: %s\n", compiled);
// 实际执行(模拟)
if (model->materialization == MAT_TABLE) {
printf(" → 创建表: %s.%s\n", project->target_schema, model->name);
} else if (model->materialization == MAT_VIEW) {
printf(" → 创建视图: %s.%s\n", project->target_schema, model->name);
} else if (model->materialization == MAT_EPHEMERAL) {
printf(" → CTE: %s\n", model->name);
} else if (model->materialization == MAT_INCREMENTAL) {
printf(" → 增量更新: %s.%s\n", project->target_schema, model->name);
}
free(compiled);
}
// 运行项目
void dbt_run(dbt_project_t *project) {
printf("dbt 开始运行...\n");
// 解析依赖
dbt_parse_dependencies(project);
// 拓扑排序
int count;
model_t **order = dbt_topological_sort(project, &count);
// 按顺序执行
for (int i = 0; i < count; i++) {
dbt_run_model(project, orderi);
}
free(order);
printf("dbt 运行完成\n");
}
```
- 测试执行
```c
// 运行测试
void dbt_run_tests(dbt_project_t *project) {
printf("dbt 运行测试...\n");
test_t *test = project->tests;
while (test) {
printf(" 测试: %s (模型: %s)\n", test->name, test->model_name);
if (strcmp(test->test_type, "unique") == 0) {
printf(" → 唯一性测试: %s.%s\n", test->model_name, test->column);
} else if (strcmp(test->test_type, "not_null") == 0) {
printf(" → 非空测试: %s.%s\n", test->model_name, test->column);
} else if (strcmp(test->test_type, "accepted_values") == 0) {
printf(" → 值域测试: %s.%s\n", test->model_name, test->column);
}
test = test->next;
}
printf("dbt 测试完成\n");
}
```
- 测试代码
```c
void test_dbt() {
printf("=== dbt数据管道测试 ===\n\n");
dbt_project_t *project = dbt_init("analytics", "analytics_dev");
// 创建模型
model_t *orders = dbt_create_model(project, "stg_orders", MAT_VIEW, "staging");
model_set_sql(orders,
"SELECT id, user_id, amount, status, created_at "
"FROM {{ ref('orders_source') }} "
"WHERE status = 'completed'");
model_t *users = dbt_create_model(project, "stg_users", MAT_VIEW, "staging");
model_set_sql(users,
"SELECT id, name, email, created_at "
"FROM {{ ref('users_source') }}");
model_t *order_users = dbt_create_model(project, "order_users", MAT_TABLE, "marts");
model_set_sql(order_users,
"SELECT "
" o.id AS order_id, "
" o.amount, "
" o.created_at, "
" u.name AS user_name, "
" u.email "
"FROM {{ ref('stg_orders') }} o "
"LEFT JOIN {{ ref('stg_users') }} u "
" ON o.user_id = u.id");
// 添加测试
dbt_add_test(project, "unique_order_id", "stg_orders", "unique", "id", 1);
dbt_add_test(project, "not_null_user_id", "stg_orders", "not_null", "user_id", 1);
dbt_add_test(project, "not_null_user_name", "stg_users", "not_null", "name", 1);
// 运行
dbt_run(project);
dbt_run_tests(project);
// 文档生成(简化)
printf("\ndbt 生成文档...\n");
printf(" 模型数: %d\n", project->model_count);
printf(" 测试数: %d\n", project->test_count);
free(project);
}
int main() {
test_dbt();
return 0;
}
```
三、编译和运行
```bash
gcc -o dbt dbt.c -lpthread
./dbt
```
四、dbt vs 本实现
特性 本实现 dbt
模型定义 ✅ ✅
ref函数 ✅ ✅
依赖解析 ✅ ✅
物化策略 ✅ ✅
增量构建 ❌ ✅
测试 ✅ ✅
文档生成 ✅ ✅
宏 ❌ ✅
变量 ❌ ✅
五、总结
通过这篇文章,你学会了:
· dbt的核心架构(项目结构、模型、测试、宏)
· 模型定义与依赖管理(ref函数)
· 拓扑排序与依赖解析
· 物化策略(table/view/ephemeral/incremental)
· 测试(唯一性/非空/值域)
· 文档生成
dbt是现代数据栈的核心工具。掌握它,你就理解了数据转换管道的设计原理。
下一篇预告:《从零实现一个分布式数据集成:Airbyte的核心设计》
评论区分享一下你用dbt做过什么数据转换场景~