代表性面试主题

数据工程面试:如何设计可靠的 dbt 增量模型?

数据困难
Offer.cc 编辑团队发布 更新

题干

在数据持续增长且每天不能全量重算时,你会如何设计一个可靠的 dbt 增量模型?

题干与适用场景

面试官给出一张持续写入的事件表,要求你构建按天汇总的事实表。首次运行可以扫描历史数据,但之后每次运行只能处理最近变化的数据;事件可能迟到、更新或重复到达,模型字段也可能变化。请说明筛选边界、唯一键、增量策略、回补方式和失败后的恢复方法。

本文假设仓库支持 SQL、目标模型按 date_day 聚合,事件有 event_atupdated_at 和稳定的 event_id。这些假设应在面试开头说出来;如果没有稳定唯一键,更新语义和去重方案会改变。

面试官考察点

面试官看的是你能否把“少扫描数据”与“结果仍然正确”同时落地。普通回答只说使用 is_incremental() 和时间戳过滤;高质量回答会解释为什么过滤窗口需要覆盖迟到数据、为什么聚合目标必须声明 unique_key,以及什么时候必须 --full-refresh

还会观察你是否区分三种风险:漏掉迟到记录、重复写入同一业务粒度、历史逻辑改变后新旧结果不一致。面试指南通常把增量模型、快照和依赖图放在数据工程或分析工程面试的核心准备范围;这道题适合考察 SQL、数据建模和运行治理的组合能力。

回答前需要澄清的问题

业务粒度是什么?

如果一行代表一天,date_day 可以作为唯一键;如果一行代表用户和天,则键应是 (user_id, date_day)。粒度不同会改变合并条件、重复检查和回补成本。

迟到数据最多晚到多久?

若事件通常只晚到两天,可以每次重算最近三天;若没有可接受上界,就不能只用固定窗口,需要水位线、分区重算或定期全量校准。窗口不是越小越好,它必须覆盖业务可接受的迟到范围。

上游更新由哪个字段表示?

event_at 表示业务发生时间,updated_at 表示记录最后修改时间。只按 event_at 过滤会漏掉“旧事件后来被修改”的情况;优先使用可靠的 updated_at,并验证它在源系统中单调且不会被回拨。

模型逻辑或字段变化如何发布?

新增列、删除列、计算逻辑变化的处理不同。需要确认是否允许 on_schema_change 自动同步,历史列是否要回填,以及发布是否能安排一次全量刷新。

30 秒回答框架

“我先确认目标粒度和迟到上界。首次运行做全量构建,后续用 is_incremental()updated_at 过滤,并向前扩大一个迟到窗口。目标模型声明与粒度一致的 unique_key,这样同一天的新数据会更新原行而不是追加重复行。窗口内按键去重后再聚合,用 merge 或仓库等价策略写入。窗口、唯一键和模式变化都用测试验证;如果逻辑改变导致历史结果不再一致,执行受控的 --full-refresh,同时重跑下游模型。最后监控处理行数、最大事件时间、重复键和新旧结果差异。”

分步骤深入解答

1. 从全量方案定义正确性基线

先写出全量查询:读取所有事件,按目标粒度聚合。这是正确性基线,后续增量结果必须和同一时间范围的全量结果对账。不要一开始就优化,因为没有基线就无法判断“少扫了数据”是否漏算。

2. 选择增量筛选边界

增量模型只在目标表已经存在、未传入 --full-refresh 且模型配置为 incremental 时进入增量分支。可以用目标表的最大更新时间减去迟到窗口:

sql
{{
  config(
    materialized = 'incremental',
    unique_key = ['date_day'],
    incremental_strategy = 'merge'
  )
}}

with source_events as (
  select *
  from {{ ref('app_events') }}
  {% if is_incremental() %}
    where updated_at >= (
      select coalesce(max(updated_at), '1900-01-01') from {{ this }}
    ) - interval '3 day'
  {% endif %}
)
select
  cast(event_at as date) as date_day,
  count(distinct event_id) as events,
  max(updated_at) as max_updated_at
from source_events
group by 1

示例中的三天只是面试假设,不是通用常数。窗口应来自迟到分布、SLA 和重算成本。实际仓库的日期函数语法也需要按适配器调整。

3. 让唯一键匹配模型粒度

如果目标表按天存储,date_day 是唯一键;如果按用户和天存储,则使用 ['user_id', 'date_day']。唯一键列不能含空值,否则 merge 可能无法匹配并产生重复行。没有唯一键时,许多适配器只能 append,窗口重算会把同一粒度写出多行。

4. 在窗口内先去重再聚合

同一事件可能由重放或 CDC 更新产生多条记录。用 event_idupdated_at 排序,保留每个事件的最新版本,再做日聚合:

sql
with ranked_events as (
  select
    *,
    row_number() over (
      partition by event_id
      order by updated_at desc, ingest_seq desc
    ) as rn
  from source_events
),
deduped_events as (
  select * from ranked_events where rn = 1
)
select
  cast(event_at as date) as date_day,
  count(*) as events,
  max(updated_at) as max_updated_at
from deduped_events
group by 1

只有当 ingest_seq 能稳定打破相同更新时间时才使用它;否则应把并列规则说成待确认的源系统契约。去重应发生在聚合前,否则一次事件的两个版本会同时计数。

5. 选择 merge、分区覆盖或 append

merge 适合按唯一键更新与插入;按分区重算的场景可以使用 insert_overwrite,它依赖分区而不是逐行唯一键;纯追加事件且上游永不更新时,append 更简单。选择依据是更新语义、仓库扫描成本和适配器能力,不应把一种策略当成所有仓库的默认答案。

6. 处理模式和逻辑变化

新增列不一定会回填旧行;删除列或类型变化也可能只在运行时暴露。on_schema_change 可配置为 ignorefailappend_new_columnssync_all_columns,但它只跟踪顶层列,不能代替历史数据回补。计算逻辑改变后,新旧历史可能使用不同规则,此时应运行 --full-refresh,并根据依赖关系重建下游增量模型。

7. 设计回补和失败恢复

将迟到窗口、目标最大更新时间、源数据水位写入运行日志。若某次窗口运行失败,下一次仍从已提交的目标水位重新计算,而不是把内存中的“已处理到”当作事实。对大范围历史修复,按日期分片执行并限制并发;完成后用抽样全量查询对账,避免一次性刷新压垮仓库。

8. 建立验证闭环

至少验证四组信号:窗口内每个 event_id 至多一行;目标唯一键没有重复;最近窗口与全量重算结果的差异在允许范围内;每次运行处理行数和最大 updated_at 没有异常跳变。对账要覆盖空输入、重复事件、旧事件更新、迟到事件、窗口边界相等值和全量刷新后再增量运行。

高质量示范回答

“我会先确认模型粒度、迟到上界和源数据的更新字段。假设目标是一行一天,事件有稳定的 event_idupdated_at。第一次运行全量构建;后续用 is_incremental() 从目标表的最大更新时间向前回看三天。这个窗口是根据迟到分布决定的,三天不是固定答案。

窗口内先按 event_id 和更新时间去重,再按天聚合,目标模型把 date_day 设成 unique_key,用 merge 更新最近几天,避免重复行。若目标是用户日粒度,就改成复合键。只按事件发生时间会漏掉旧事件的后续更新,所以我会优先使用可靠的更新时间字段。

我会把窗口大小、最大水位、处理行数、重复键和窗口对账差异作为运行指标。新增列可以按 schema-change 策略处理,但它不会自动填充历史值;如果计算逻辑改变或需要历史回补,就安排分片的 full refresh,并重跑受影响的下游模型。最后用全量查询做抽样对账,验证空输入、迟到、重复、边界时间和失败重试,确保增量优化没有牺牲正确性。”

常见错误

只按 event_at 过滤 → 漏掉旧事件更新 → 使用 updated_at 或明确的 CDC 水位

事件发生时间不会随着后续修正而变化。若业务允许更新,必须按更新时间或变更序列筛选,并验证该字段的可靠性。

没有唯一键就使用 merge → 无法稳定匹配 → 先定义模型粒度和非空键

唯一键不是随便选一列;它必须唯一标识目标的一行。若粒度是用户和天,单独使用日期会把不同用户合并到一起。

以为增量模型会自动回填新列 → 历史值保持空缺 → 设计回补或 full refresh

模式同步和历史数据回填是两件事。新增列只改变结构时可以轻量同步;需要旧记录有值时必须额外更新或重建。

固定使用一小时窗口 → 迟到分布超过窗口时漏算 → 用分位数和对账数据校准

窗口大小应由迟到分布、SLA 和成本共同决定。监控窗口外到达量,发现异常时扩大窗口或执行分片回补。

追问及应对

如果每天有 5% 的事件在两天后到达,你会如何选窗口?

先确认业务允许的准确性延迟。如果日报允许第二天修正,可以覆盖两到三天并把晚到事件计入对账;如果必须在首日稳定,则需要水位线加回补队列,不能只靠更大的 SQL 窗口。窗口选择应由迟到分布和成本曲线验证,而不是直接套用比例。

如果 unique_key 在源数据中重复,会发生什么?

同一次 merge 的新数据或目标数据含重复键时,适配器可能报错,也可能产生不确定结果。先在增量输入和目标表分别执行唯一性检查,找出重复来源;再按事件版本去重,或重新定义能表达真实粒度的复合键。不能用随机 ID 掩盖业务键不稳定。

模型 SQL 改了,但只想重算最近七天,能否继续增量运行?

只有当历史行的计算结果不受新逻辑影响时才安全。若逻辑改变会影响全部历史,最近七天增量会留下新旧规则混合的表,应执行受控 full refresh,或按受影响分区分片重算,并同步重跑下游模型。

上游表被截断后,增量模型如何恢复?

先停止继续推进水位,确认源表重建完成,再从可靠的源快照或 CDC 起点回补。若无法证明源表覆盖了目标所需历史,直接增量运行会把目标当成完整基线,必须恢复快照或执行全量重建。

公开来源

同类题目