mcpField
D
dataform-engineering-fundamentals

dataform-engineering-fundamentals

github.com/ihistand数据与数据库240 次安装3 周前更新
代码仓库

关于 dataform-engineering-fundamentals

面向使用 BigQuery 的 Dataform 开发与维护团队,涵盖 SQLX 架构、依赖与数据质量实践,以及优先在开发环境执行的测试驱动流程。

主要功能

  1. 01分层 SQLX 项目结构
  2. 02安全的开发运行与 dry run
  3. 03使用 ref() 跟踪依赖
  4. 04增量表模式
  5. 05断言与列文档
  6. 06测试驱动开发流程

适用场景

  1. 01构建 SQLX 转换
  2. 02排查流水线故障
  3. 03创建报表表和数据源
  4. 04部署前验证数据质量

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/ihistand/claude-skills --skill dataform-engineering-fundamentals

安装后在 Claude Code 中输入/dataform-engineering-fundamentals确认已加载。

详细介绍

Dataform 工程基础知识

概述

核心原则:在 Dataform 开发中,安全实践和恰当的架构永远不可或缺,无论时间压力或业务紧迫性如何。通过走捷径“节省”的时间,最终会变成数小时的调试、断裂的依赖关系和生产问题。

**必备基础:**此技能基于 superpowers:test-driven-development。该技能中的所有 TDD 原则在此同样适用;本技能针对 BigQuery Dataform SQLX 文件对这些原则进行了调整。

**对于 PostgreSQL / Supabase:**请改用 sqlanvil-engineering-fundamentals(也可同时使用)——下文中的架构、${ref()}、TDD 和 columns:{} 规则均可直接沿用;该技能涵盖 Postgres/Supabase 的差异(配置块、凭据、DDL、命名连接)。

**官方文档:**语法/配置/API:https://cloud.google.com/dataform/docs · 仓库结构与命名:https://cloud.google.com/dataform/docs/best-practices-repositories

适用场景

任何 Dataform 工作都应使用此技能——新的 SQLX 转换、修改表、添加数据源、排查流水线问题,以及“快速”报表或临时分析。

尤其是在时间紧迫、相关方正在等待、你加班到很晚且疲惫不堪,或你正打算“先让它跑起来”时——越是在容易跳过规范的时候,纪律就越重要。

相关技能:

  • superpowers:brainstorming — 编写代码之前,将粗略需求梳理成清晰的设计
  • superpowers:systematic-debugging — 结构化地解决流水线故障
  • superpowers:root-cause-tracing — 将连锁错误追溯到源头
  • elements-of-style:writing-clearly-and-concisely — 用于列描述、提交消息及任何文字内容

不可妥协的安全实践

始终必须遵守。截止日期、紧急情况或“简单”任务都不能成为例外。

1. 测试时始终使用 --schema-suffix dev
# WRONG: testing in production
dataform run --actions my_table

# CORRECT: test in dev first
dataform run --schema-suffix dev --actions my_table

写入 schema_dev.my_table,而不是生产环境,因此你可以在不触及真实数据或仪表板的情况下进行测试。

2. 执行前始终使用 --dry-run
dataform compile                                              # check compilation
dataform run --schema-suffix dev --dry-run --actions my_table # validate SQL, estimate cost
dataform run --schema-suffix dev --actions my_table           # only then execute

在使用 BigQuery 槽之前,捕获 SQL 错误、缺失的依赖关系及意外成本。

3. 在使用 ref() 之前声明数据源

查询外部表之前先声明它们,这样依赖关系跟踪才能正常工作。

-- definitions/sources/external_system/table_name.sqlx
config {
  type: "declaration",
  database: "project_id",
  schema: "external_schema",
  name: "table_name"
}

然后使用 FROM ${ref("table_name")} 引用——绝不要使用 FROM \project.external_schema.table_name``。

4. 始终使用 ${ref()}——绝不硬编码表路径
-- WRONG
FROM `project.reporting_schema.customer_metrics`
SELECT * FROM project.source_schema.customers

-- CORRECT (after declaring the source)
FROM ${ref("customer_metrics")}
SELECT * FROM ${ref("customers")}

${ref()} 会自动构建依赖关系图、使 --schema-suffix 生效,并确保重构安全。硬编码路径会破坏这三项功能。**例外:无。**在 SQLX 文件中,没有任何正当理由可以硬编码表路径。

5. ref() 语法:默认使用单参数形式

优先使用单参数 ref()——Dataform 会根据数据源声明解析完整路径:

FROM ${ref("sales_order")}

当某个表名在多个架构中不唯一,或需要显式控制架构/数据源时(例如,多个数据集的 ODS 层中存在重名表),请使用双参数 ref():

FROM ${ref("external_schema", "sales_order")}

两种形式都有效。优先使用单参数形式;必须指定架构时,双参数形式才是正确选择,并非错误。

6. 基本验证查询

始终验证输出——及时发现静默失败(空表、空值、错误的联接):

bq query --use_legacy_sql=false \
  "SELECT COUNT(*) FROM \`project.schema_dev.my_table\`"
bq query --use_legacy_sql=false \
  "SELECT COUNT(*) FROM \`project.schema_dev.my_table\` WHERE key_field IS NULL"

架构模式(不可省略)

即使是“快速”工作也不例外。(仓库结构与命名:https://cloud.google.com/dataform/docs/best-practices-repositories)

分层结构
definitions/
  sources/          # external data declarations
  intermediate/     # transformations and business logic
  output/
    reports/        # reporting tables
    marts/          # data marts for specific use cases

不要在输出层构建单体查询——应将其拆分成可复用、可测试的中间步骤。每张表都应清晰地完成一项转换。

增量刷新与全量刷新
config {
  type: "incremental",
  uniqueKey: ["order_id"],
  bigquery: {
    partitionBy: "DATE(order_date)",
    clusterBy: ["customer_id", "product_id"]
  }
}

uniqueKey 是一个数组(它会合并行,而不是追加行)。对于每日增长的表(事件、交易、日志),使用增量刷新;对于较小的维度表,以及带回看窗口的聚合,使用全量刷新。

增量内部机制

使用 incremental() 上下文函数,仅在增量运行时写入新行,并使用 ${self()} 引用正在构建的表:

config {
  type: "incremental",
  uniqueKey: ["order_id"],
  bigquery: { partitionBy: "DATE(order_date)" }
}

SELECT order_id, order_date, amount
FROM ${ref("orders")}
${when(incremental(), `WHERE order_date > (SELECT MAX(order_date) FROM ${self()})`)}
  • incremental() 仅在增量运行时为 true(首次构建和 --full-refresh 时为 false)。
  • ${self()} 会解析为此表——在增量 WHERE 子句中使用它来读取高水位标记。
  • pre_operations { ... } / post_operations { ... } 会在构建前后运行 DDL(例如授权)。将一次性 DDL 包装在 ${when(!incremental(), ...)} 中,使其仅在创建表或完全刷新时运行,而不是在每次追加时运行。
Dataform 断言
config {
  type: "table",
  assertions: {
    uniqueKey: ["call_id"],
    nonNull: ["customer_phone_number", "start_time"],
    rowConditions: ["duration >= 0"]
  }
}

在管道运行期间自动捕获数据质量问题。

源声明:优先使用 .sqlx 文件

所有新增的源声明都必须使用 .sqlx 文件(它支持列文档、更易维护,并可集成依赖关系跟踪):

-- definitions/sources/external_system/table_name.sqlx
config {
  type: "declaration",
  database: "project_id",
  schema: "external_schema",
  name: "table_name",
  columns: { id: "Unique identifier for records" }
}

现有的 .js declare() 文件可以保留,但下次修改时请迁移到 .sqlx。

架构配置规则

definitions/operations/ 和 definitions/test/ 中的文件不得包含 schema: 配置。操作位于默认的 dataform 架构中,断言位于 dataform_assertions(在 workflow_settings.yaml 中设置);显式指定架构会导致冲突。

-- CORRECT
config { type: "operations", tags: ["daily"] }

-- WRONG
config { type: "operations", schema: "dataform", tags: ["daily"] }

文档规范(不可妥协)

每个 type: "table" 表都必须包含完整的 columns: {} 块。(描述请遵循 elements-of-style:writing-clearly-and-concisely。)

-- WRONG: no column documentation
config { type: "table", schema: "reporting" }

-- CORRECT
config {
  type: "table",
  schema: "reporting",
  columns: {
    customer_id: "Unique customer identifier from source system",
    total_revenue: "Sum of all order amounts in USD, excluding refunds"
  }
}

描述的来源: 上游源声明、第三方/API 文档(CRM、ERP、分析)、计算字段的业务逻辑,以及分析师需要的 BI 工具上下文。

源声明也应记录列的文档——下游表会继承并扩展这些描述,从而确保整个管道的文档保持一致:

config {
  type: "declaration",
  database: "project_id",
  schema: "external_api",
  name: "events",
  description: "Event records from external API",
  columns: {
    event_id: "Unique event identifier from API",
    user_id: "User who triggered the event",
    event_type: "Type of event (click, view, purchase, etc.)",
    timestamp: "UTC timestamp when the event occurred"
  }
}

测试驱动开发(TDD)工作流

必需的背景知识: 遵循 superpowers:test-driven-development。对于需求不明确的新功能,先使用 superpowers:brainstorming 完善设计,然后开始 TDD。

Dataform 表的周期:

  1. RED — 先编写数据质量断言 / 单元测试;运行它们并观察其失败(表尚不存在)。
  2. GREEN — 编写最精简的 SQLX,使测试通过。
  3. REFACTOR — 在保持测试通过的同时进行清理。

先编写测试定义了应该发生什么;之后再写测试只是检查实际发生了什么——而所谓的“之后”通常意味着永远不会写。

示例

步骤 1 — 先写断言(definitions/assertions/assert_customer_metrics.sqlx):

config { type: "assertion", description: "Customer metrics must have valid data" }

SELECT 'Duplicate customer_id' AS test
FROM ${ref("customer_metrics")}
GROUP BY customer_id HAVING COUNT(*) > 1
UNION ALL
SELECT 'Negative lifetime value' AS test
FROM ${ref("customer_metrics")}
WHERE lifetime_value < 0

步骤 2 — 运行并观察其失败:

dataform run --schema-suffix dev --run-tests --actions assert_customer_metrics
# ERROR: Table customer_metrics does not exist ✓ EXPECTED

步骤 3 — 最精简的实现(definitions/output/reports/customer_metrics.sqlx):

config {
  type: "table",
  schema: "reporting",
  columns: {
    customer_id: "Unique customer identifier",
    lifetime_value: "Total revenue from customer in USD"
  }
}

SELECT customer_id, SUM(order_total) AS lifetime_value
FROM ${ref("orders")}
GROUP BY customer_id

步骤 4 — 运行并观察其通过:

dataform run --schema-suffix dev --actions customer_metrics
dataform run --schema-suffix dev --run-tests --actions assert_customer_metrics
# No rows returned ✓ TESTS PASS

快速参考

任务命令
仅编译dataform compile
试运行dataform run --schema-suffix dev --dry-run --actions table_name
在开发环境中测试dataform run --schema-suffix dev --actions table_name
连同依赖项一起运行dataform run --schema-suffix dev --include-deps --actions table_name
按标签运行dataform run --schema-suffix dev --tags looker
生产部署dataform run --actions table_name(仅在开发环境测试成功后执行)

常见借口(以及为何不成立)

借口事实解决方法
“太紧急,没时间在开发环境中测试”生产环境故障比开发测试浪费更多时间花 3 分钟测试,可省下 60 分钟调试
“这只是一个快速报告”“快速”报告会变成永久表从一开始就采用恰当的架构
“业务部门在等”错误的输出会浪费相关方的时间10 分钟后拿到正确结果 > 现在拿到错误结果
“硬编码路径比 ${ref()} 更快”会破坏依赖跟踪和 --schema-suffix声明数据源,使用 ${ref()}(30 秒)
“我之后会重构 / 添加测试 / 补充文档”之后 = 永远不会;技术债务很少会得到修复一开始就做好(先写测试,立即添加 columns:{})
“之后再写测试也一样”事后测试检查行为;先写测试则定义行为TDD 能及早发现设计缺陷
“内部表的列文档可有可无”内部表最终也会变成外部表始终为所有内容编写文档
“我在加班,只想让它能跑起来”精疲力竭容易导致失误越是疲惫,纪律就越重要
“这次情况不同,因为[业务原因]”这些规则的存在,正是为了应对例外情况遵循不可妥协的实践

危险信号 — 立即停止

如果发现自己有以下任何想法,请停下来并遵循此技能:

  • “这次我就跳过 --schema-suffix dev / --dry-run”
  • “我直接硬编码路径 / 用反引号代替 ${ref()}”
  • “我不创建中间层,直接写一个大文件”
  • “这里的测试可有可无 / 我之后再写”
  • “这个表不需要 columns: {} 代码块 / 我之后再补充文档”
  • “我会为这个新声明使用 .js 文件”
  • “我会在这个操作 / 测试文件中添加 schema:”
  • “我之后再解决技术债务”

时间紧迫时的处理流程

在极端紧急的情况下(2 小时后召开董事会会议,生产环境宕机),以上不可妥协的要求仍然适用 — 在开发环境中测试、使用 --dry-run、声明数据源、添加 columns:{}、先写测试和执行基本验证,都只需几分钟,却能节省数小时。

可以跳过(且只能通过添加带有 technical_debt 标签的 TODO 来记录后续工作)的事项:详尽的独立文档文件、同行评审和性能优化。除此之外,其他事项都不能跳过。

故障排查

对于任何 Bug、测试失败或意外行为,在尝试修复前,请使用 superpowers:systematic-debugging;对于级联故障,请使用 superpowers:root-cause-tracing。Dataform 特定错误:https://cloud.google.com/dataform/docs

  • “找不到表” — 检查源声明是否存在,验证 ref() 语法(如果已声明数据源,则使用单参数形式),确认架构 / 数据库是否匹配,运行 dataform compile 查看解析后的 SQL。
  • 依赖循环 — 使用 ${ref()}(而非直接引用),查找循环链(A → B → A),在 Dataform 界面中检查依赖关系图。
  • 超时 — 添加分区 / 集群,改为增量处理而非全量刷新,将大型转换拆分为更小的中间表。

遵循纪律比走捷径更快。

评论

支持 Markdown