从零实现一个分布式数据管道:dbt的核心设计

前言

在数据工程中,数据转换是ETL的核心环节。传统的ETL工具(如Informatica)用图形界面配置,而dbt(data build tool)带来了革命性的变化------用SQL定义转换逻辑,用版本控制管理,让数据工程师像写代码一样开发数据管道。

今天我们从零实现dbt的核心功能:

· 模型定义(Model)

· 依赖管理(ref函数)

· 物化策略(table/view/ephemeral)

· 增量构建(incremental)

· 测试(test)

· 文档生成

· 宏(Macros)

· 变量与配置


一、dbt核心原理

  1. 架构图

```

┌─────────────────────────────────────────────────────────────┐

│ 项目结构 │

│ ├── models/ │

│ │ ├── staging/ (ODS层) │

│ │ ├── marts/ (DWD/DWS层) │

│ │ └── schema.yml (测试/文档) │

│ ├── macros/ (宏定义) │

│ ├── tests/ (自定义测试) │

│ └── dbt_project.yml (项目配置) │

└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐

│ dbt运行流程 │

│ 解析SQL → 解析依赖 → 生成DAG → 编译 → 执行 │

└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐

│ 目标数据仓库 │

│ (Snowflake / BigQuery / Redshift / Postgres) │

└─────────────────────────────────────────────────────────────┘

```

  1. 核心概念

概念 说明

Model 数据模型(SQL文件)

ref 引用另一个模型

source 引用源表

Materialization 物化策略(table/view/ephemeral)

Incremental 增量构建

Test 数据测试(唯一性/非空)

Macro SQL宏(参数化SQL)

Doc 文档(Markdown)


二、完整代码实现

  1. 基础数据结构

```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;

```

  1. 项目初始化

```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);

}

```

  1. 依赖解析

```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;

}

```

  1. 编译与执行

```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");

}

```

  1. 测试执行

```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");

}

```

  1. 测试代码

```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做过什么数据转换场景~

相关推荐
Darkwanderor1 小时前
使用管道实现进程间通信 (IPC, Inter-Process Communitation)
linux·c语言·c++
乱七八糟的屋子1 小时前
【C++数值计算】Armadillo超详细入门教程
c++·线性代数·数值计算·科学计算·矩阵运算·armadillo
Yyyyyy~2 小时前
【C++】vector
c++·c++primer
XH华2 小时前
C++语言第四章:模板初阶
数据结构·c++·算法
我不是懒洋洋2 小时前
从零实现一个分布式流处理:Apache Flink的核心设计
c++
2401_869769592 小时前
C/C++常识
c++
j7~3 小时前
【Linux】二十六.线程篇三《Linux多线程编程:线程控制、线程ID以及进程地址空间分布、线程局部存储__thread以及clone系统调用》---详解
linux·运维·服务器·开发语言·c++·多线程编辑·线程的控制
luj_176813 小时前
星火科技助力边远地区防病攻坚
c语言·开发语言·c++·经验分享·算法
zmzb010314 小时前
C++课后习题训练记录Day175
开发语言·c++