关于 dataform-engineering-fundamentals
面向使用 BigQuery 的 Dataform 开发与维护团队,涵盖 SQLX 架构、依赖与数据质量实践,以及优先在开发环境执行的测试驱动流程。
主要功能
- 01分层 SQLX 项目结构
- 02安全的开发运行与 dry run
- 03使用 ref() 跟踪依赖
- 04增量表模式
- 05断言与列文档
- 06测试驱动开发流程
适用场景
- 01构建 SQLX 转换
- 02排查流水线故障
- 03创建报表表和数据源
- 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 表的周期:
- RED — 先编写数据质量断言 / 单元测试;运行它们并观察其失败(表尚不存在)。
- GREEN — 编写最精简的 SQLX,使测试通过。
- 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 界面中检查依赖关系图。 - 超时 — 添加分区 / 集群,改为增量处理而非全量刷新,将大型转换拆分为更小的中间表。
遵循纪律比走捷径更快。