Lightdash Jaffle Shop 演示 dbt 项目解析:跨仓库兼容的测试模型、Lightdash 元数据与常见故障排查
【免费下载链接】lightdashAgentic BI. Analytics at the speed of code ⚡️项目地址: https://gitcode.com/GitHub_Trending/li/lightdash
本文基于 Lightdash 仓库中examples/full-jaffle-shop-demo/dbt/目录下的项目说明文档展开,讲解这个用于全面测试 Lightdash 功能的 dbt 演示项目:从 CSV 种子数据、模型建模规范、Lightdash 参数/Spotlight 集成,到通过 Jinja 宏实现 PostgreSQL / Snowflake / BigQuery / Trino / Athena 多仓库兼容的完整方案。读完后你可以直接复用该项目的模型编写模式与类型转换宏,在自己的多仓库 dbt 项目中避免 SQL 方言差异问题。
项目定位:一个面向 Lightdash 功能测试的 dbt 项目
该项目的官方定位(见 CLAUDE.md)是:基于经典 "jaffle shop" 演示数据,并额外增加了一批模型,用于全面测试 Lightdash 功能。它不是一个通用电商教程,而是一个自包含的"测试游乐场"(playground)——Lightdash 的维度、指标、参数、Spotlight、跨表关联、跨仓库 SQL 方言等能力,都在这个项目的模型上得到验证。
与 dbt/README.md 中说明的经典 jaffle_shop 不同,这个增强版项目刻意包含了一些真实测试场景需要的内容:
- 种子(seeds)代替 sources,让项目自包含、可离线运行;
- 覆盖多图表类型、多时间类型、多参数组合的模型;
- 通过 dbt_project.yml 中的
meta.group_label把模型分组到 Lightdash Catalog 的分组标签(fanouts、campaigns、healthcare、funnels、maps 等),便于在 Lightdash 中按业务域组织。
项目目录结构如下:
examples/full-jaffle-shop-demo/ ├── dbt/ # dbt 项目根目录 │ ├── data/ # 所有原始 CSV 种子数据 │ │ ├── raw_customers.csv │ │ ├── raw_orders.csv │ │ ├── raw_payments.csv │ │ ├── raw_subscriptions.csv │ │ ├── raw_plan.csv │ │ └── seeds.yml # 种子列类型声明(跨仓库兼容关键) │ ├── macros/ │ │ └── casts.sql # 跨仓库类型转换/日期宏 │ ├── models/ # .sql 模型 + .yml 模式定义 │ ├── dbt_project.yml │ └── lightdash.config.yml # 全局参数与 Spotlight 配置 ├── profiles/profiles.yml # dbt 连接配置 ├── docker-compose.yml # 本地 Postgres 环境 └── entrypoint.sh数据源:CSV 种子与 git add -f 注意事项
所有原始数据来自data/目录下的 CSV 文件,文档明确列出的核心种子包括:
| 文件 | 内容 |
|---|---|
raw_customers.csv | 客户信息 |
raw_orders.csv | 订单交易 |
raw_payments.csv | 支付记录 |
raw_subscriptions.csv | 订阅数据(1000 条记录) |
raw_plan.csv | 订阅套餐定义 |
重要约定:向该目录新增数据文件时,必须使用git add -f <filename>,因为这些文件通常被 gitignore 忽略。这一点在文档的 Troubleshooting 一节中再次强调——任何data/目录下的新数据文件都要用git add -f提交,否则克隆仓库的人拿不到原始数据,模型无法运行。
实际的data/目录远不止这 5 个文件:从目录清单可见还有raw_events.csv、raw_product_events.csv、raw_timezone_test.csv、raw_geo.csv以及fanout_data/子目录等数十个 CSV,对应文档所述"额外模型用于全面测试"。
种子加载时列类型并非自动推断,而是由 data/seeds.yml 与 dbt_project.yml 共同声明。例如dbt_project.yml中为raw_orders声明了order_date: date、shipping_cost: numeric等类型;而seeds.yml则用 Jinja 条件表达式处理仓库差异,典型写法如下:
- name: raw_product_events config: column_types: event_properties: >- {%- if target.type == 'snowflake' -%} variant {%- elif target.type in ['athena', 'trino', 'duckdb', 'clickhouse'] -%} varchar {%- else -%} jsonb {%- endif -%}raw_timezone_test种子更进一步,为同一逻辑列声明了时区感知/非时区感知的多种类型映射(Snowflake 用timestamp_tz/timestamp_ntz,ClickHouse 用DateTime64(3, 'UTC'),Trino/Athena 用timestamp with time zone,其余默认timestamptz),可见该项目同时承担了 Lightdash 时间类型处理功能的测试载体角色。
模型模式:CTE、别名、描述与测试
文档"Model Patterns"一节规定了该项目遵循的建模规范:
.sql文件包含模型逻辑(带 CTE 的 SELECT 语句);.yml文件包含模式定义、测试与 Lightdash 元数据;- 使用 CTE(
with语句)编写可读 SQL; - 多表连接时使用规范的表别名;
- 同时包含原始维度与计算字段;
- 为业务用户编写完整的字段描述。
以 models/customers.sql 为例,可以看到标准写法:customers、orders、payments三个 CTE 分别引用 staging 层模型,再用customer_orders、customer_payments聚合出终身指标(首单/最近订单日期、订单数、累计支付额)。值得一提的是,该文件内还内嵌了一处仓库方言分支——注释写明 "Athena/Trino: USING joins create unqualified columns, can't reference as table.col",并通过{% if target.type == 'trino' or target.type == 'athena' %}切换列引用方式,这正是后文"Athena 限制 5"在真实模型中的落地。
文档点名的四个核心模型:
customers.sql/.yml— 客户维度表,含终身指标(lifetime metrics);orders.sql/.yml— 订单事实表,含订单级计算;subscriptions.sql/.yml— 订阅模型,含真实感 SaaS 指标与参数;plan.sql/.yml— 订阅套餐维度表。
测试约定包括:主键上的unique与not_null测试、业务逻辑自定义测试、事实表与维度表之间的relationships测试。subscriptions.yml 中即可看到subscription_id列同时挂了unique和not_null测试。
Lightdash 集成:参数、Spotlight 与模型元数据
Lightdash 与 dbt 项目的集成点分两层(见 lightdash.config.yml):
第一层:全局配置lightdash.config.yml,包含三块:
- spotlight:定义 Spotlight 分类及其颜色。示例项目中定义了
core(Core Metrics, blue)、experimental(Experimental Metrics, orange)、sales(Sales, green)、revenue_growth(Revenue Growth, violet)四个分类,default_visibility: show控制默认可见性。 - parameters:声明项目级参数,被维度/指标的 SQL 以
${ld.parameters.<name>}引用。该文件覆盖了多种参数形态,是理解 Lightdash 参数系统的良好样本:subscription_status:options_from_dimension从subscriptions模型的subscription_status维度动态取选项;min_duration_months:带allow_custom_values: true和显式选项列表的数值参数;plan_type:multiple: true的多选参数,选项来自plan模型的plan_name维度;time_zoom/metric_type:控制 MRR 计算周期与"计数 vs 收入"切换;date_dim_parameter/date_custom_parameter:type: date的日期参数,前者选项来自orders.order_date维度,后者允许自定义输入;date_granularity:驱动 Liquid 模板演示的动态粒度参数。
- custom_granularities:自定义时间粒度,示例包含
fiscal_quarter(财务季度,用EXTRACT+INTERVAL '3 months'拼接出FYxxxx-Qn字符串)与biweekly(双周,用FLOOR(EXTRACT(EPOCH FROM ...) / 1209600)按 14 天对齐)。
第二层:模型 YML 的config.meta,维度、指标与连接全部声明在 YAML 中。以subscriptions.yml为例,config.meta下的joins定义了三种连接:
config: meta: primary_key: subscription_id joins: - join: customers sql_on: ${customers.customer_id} = ${subscriptions.customer_id} relationship: many-to-one - join: plan sql_on: ${plan.id} = ${subscriptions.plan_id} relationship: many-to-one - join: orders sql_on: ${customers.customer_id} = ${orders.customer_id} relationship: one-to-many各列的meta.dimension(类型、time_intervals、colors、format)与meta.metrics(count_distinct、sum、average等聚合类型及format如"$#,##0.00"、percent)共同构成 Lightdash Catalog 的可视化元数据。subscriptions模型还演示了参数驱动的动态 SQL,例如按time_zoom参数在weekly_mrr/monthly_mrr/quarterly_mrr之间切换的conditional_mrr_total指标,以及按状态/时长/套餐组合过滤的combined_filter_count指标。
plan.yml 则展示了additional_dimensions:从metadataJSON 列派生created_by维度,再级联派生created_by_first_name/created_by_last_name(SPLIT_PART(${created_by}, ' ', 1)),说明维度可以引用同模型内其他维度的派生列。
数据生成与 MRR 口径
文档"Data Generation"一节说明订阅数据的构造方式:
- 订阅套餐分布采用加权抽样:40% free、37% silver、14% gold、8% platinum、2% diamond;
- 订阅时长采用现实感模式(多样的订阅长度);
- MRR 按套餐层级计算。
从 models/subscriptions.sql 源码可以看到落地实现:plan_id1–5 分别映射到 9.99 / 19.99 / 39.99 / 79.99 / 149.99 美元的monthly_mrr,并按* 12 / 52与* 3推导weekly_mrr、quarterly_mrr;同时计算duration_days、duration_months(天数除以 30 的近似月数)、subscription_status(Current / Expired / Cancelled 三态)与months_remaining。模型本身只依赖raw_subscriptions种子与plan模型的左连接,全部时间运算都走下文的跨仓库宏。
常用命令
文档给出的标准开发命令(注意--profiles-dir ../profiles/指向 demo 根目录下的 profiles/profiles.yml,其中连接信息通过PGHOST、PGPORT、PGUSER、PGPASSWORD、PGDATABASE等环境变量注入,目标 schema 默认为jaffle):
# 运行全部模型 dbt run --profiles-dir ../profiles/ # 运行指定模型 dbt run --select subscriptions --profiles-dir ../profiles/ # 编译查看生成的 SQL dbt compile --select subscriptions --profiles-dir ../profiles/ # 运行测试 dbt test --profiles-dir ../profiles/其中dbt compile是排查 Lightdash 维度不显示问题的第一手段:编译产物直接体现宏展开后的目标方言 SQL。
跨仓库兼容:Jinja 类型转换宏
项目通过 Jinja 宏屏蔽 PostgreSQL 的::type语法与其他仓库CAST(col AS TYPE)语法的差异。macros/casts.sql 中每个宏都按target.type分两支:Trino/Athena 走标准CAST,其余仓库(PostgreSQL、Snowflake、BigQuery 等)走::缩写。
类型转换宏:
| 宏 | PostgreSQL | Athena/Trino | 备注 |
|---|---|---|---|
cast_numeric(col) | col::numeric | CAST(col AS DOUBLE) | |
cast_float(col) | col::float | CAST(col AS DOUBLE) | |
cast_decimal(col) | col::decimal | CAST(col AS DOUBLE) | |
cast_integer(col) | col::integer | CAST(col AS INTEGER) | |
cast_boolean(col) | col::boolean | CAST(col AS BOOLEAN) | |
cast_date(col) | col::date | CAST(col AS DATE) | |
cast_timestamp(col) | col::timestamp | CAST(col AS TIMESTAMP) | |
cast_time(col) | col::time | CAST(col AS VARCHAR) | Athena 不支持 TIME 类型 |
cast_json(col) | col::json | CAST(col AS JSON) |
日期/时间辅助宏:
| 宏 | PostgreSQL | Athena/Trino | 备注 |
|---|---|---|---|
date_diff_days(d1, d2) | d1::date - d2::date | DATE_DIFF('day', d2, d1) | 两日期相差天数 |
timestamp_diff_days(t1, t2) | EXTRACT(day FROM t1 - t2) | DATE_DIFF('day', t2, t1) | |
age_years(date_col) | date_part('year', age(...)) | DATE_DIFF('year', col, CURRENT_DATE) | 年龄(年) |
JSON 提取:
| 宏 | PostgreSQL | Athena/Trino |
|---|---|---|
json_extract_string(col, key) | col::json->>'key' | JSON_EXTRACT_SCALAR(col, '$."key"') |
对照 casts.sql 源码可以看到两处细节与文档表格略有差异,值得注意:
json_extract_string的 PostgreSQL 分支实际生成(col->>'key')(源码注释说明该写法对json与jsonb均有效,且额外加了括号以保证对结果做类型转换时运算符优先级正确);- 文件末尾还有一个
unsupported(feature_name)宏:在 Trino/Athena 下直接渲染为NULL并保留特性名注释,其他仓库下通过caller()渲染调用方传入的 SQL 片段——这是为"目标方言完全不支持某特性"预留的降级占位符。
种子列类型与 Athena 兼容性
文档强调部分种子必须显式声明列类型以兼容 Athena,核心规则:
- jsonb 列:Athena 不支持 jsonb,声明为
varchar; - time 列:Hive metastore 不支持 TIME,声明为
varchar; - numeric 列:声明为
double,避免对小数值的 INT 转换错误; - date 列:Athena 只接受 ISO 格式(YYYY-MM-DD),不能是 "Jan 1, 2024" 这类人类可读格式。
seeds.yml中的实际例子与文档一致,例如raw_generated的time列写作"{{ 'varchar' if target.type in ['athena', 'duckdb'] else 'time' }}",并附注释说明 DuckDB 同样无法自动解析 "7:01 AM" 格式。
Athena 专项限制清单
文档整理了 8 条 Athena 限制及对应替代方案,是多仓库 dbt 开发中复用价值最高的部分:
- 不支持
::转换语法— 通过宏使用CAST(col AS TYPE); - 无 TIME 类型— 用 VARCHAR 存时间字符串;
- 无 jsonb 类型— 用 VARCHAR 存储,再用 JSON 函数解析;
- 无
numeric类型— 使用 DOUBLE 或 DECIMAL(p,s); - USING 连接后不能使用带表限定的列—
JOIN ... USING (col)之后必须写不带限定的col,不能写table.col(customers.sql中即为此做了分支); - 无
DISTINCT ON— 改用子查询中ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)+WHERE rn = 1; - 无
string_agg— 改用array_join(array_agg(distinct col), ', '); - 日期/时间行为差异,逐项替代:
- 日期只接受 ISO 格式(YYYY-MM-DD),不接受 "Jan 1, 2024";
- 无
age()函数 — 用DATE_DIFF('year', date_col, CURRENT_DATE); - 日期相减不返回整数(返回 INTERVAL)— 用
DATE_DIFF('day', start_date, end_date); - 不支持
date_col + 30整数加减 — 用DATE_ADD('day', 30, date_col); - 不支持 timestamp 与 varchar 隐式比较 — 用
TIMESTAMP '2024-12-31 11:52:45'字面量; - 区间语法不同,
interval '90 days'不可用 — 用DATE_ADD('day', 90, date_col); - 无
EXTRACT(epoch FROM ...)— 用DATE_DIFF('second', start, end)取秒数; - 无
to_char— 用date_format(date, '%W')取星期名、date_format(date, '%Y-%m-%d')做格式化。
故障排查
文档 Troubleshooting 一节给出三条排查路径:
- Lightdash 维度不显示— 先检查该列是否存在于编译后的 SQL 中(
dbt compile --select <model> --profiles-dir ../profiles/),宏分支或种子类型声明错误都会让列在目标仓库中缺失; - 跨表引用失败— 确保连接存在于 SQL 模型中,而不仅仅是 YAML 声明(YAML 的
joins需要底层模型 SQL 真的产出了相应连接关系); - 新增数据文件不生效— 对
data/下的新文件执行git add -f后再提交。
小结
这个演示项目把"如何用 dbt 项目全面支撑一个 BI 产品"浓缩在一个可运行样本里:CSV 种子保证自包含可复现;dbt_project.yml与seeds.yml的column_types处理类型声明与方言差异;macros/casts.sql用统一的宏接口抹平 PostgreSQL 与其他仓库的语法鸿沟;lightdash.config.yml加模型 YML 的config.meta完成维度/指标/参数/Spotlight 的声明式集成;dbt compile与三条排查原则构成调试闭环。若要扩展该项目,文档给出的边界也很清楚:新增 CSV 数据必须git add -f,SQL 方言差异必须经由宏而非直接内联方言语法解决。
【免费下载链接】lightdashAgentic BI. Analytics at the speed of code ⚡️项目地址: https://gitcode.com/GitHub_Trending/li/lightdash
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考