题干与适用场景
面试官给出一张持续写入的事件表,要求你构建按天汇总的事实表。首次运行可以扫描历史数据,但之后每次运行只能处理最近变化的数据;事件可能迟到、更新或重复到达,模型字段也可能变化。请说明筛选边界、唯一键、增量策略、回补方式和失败后的恢复方法。
本文假设仓库支持 SQL、目标模型按 dateday 聚合,事件有 eventat、updatedat 和稳定的 eventid。这些假设应在面试开头说出来;如果没有稳定唯一键,更新语义和去重方案会改变。
面试官考察点
面试官看的是你能否把“少扫描数据”与“结果仍然正确”同时落地。普通回答只说使用 isincremental() 和时间戳过滤;高质量回答会解释为什么过滤窗口需要覆盖迟到数据、为什么聚合目标必须声明 uniquekey,以及什么时候必须 --full-refresh。
还会观察你是否区分三种风险:漏掉迟到记录、重复写入同一业务粒度、历史逻辑改变后新旧结果不一致。面试指南通常把增量模型、快照和依赖图放在数据工程或分析工程面试的核心准备范围;这道题适合考察 SQL、数据建模和运行治理的组合能力。
回答前需要澄清的问题
业务粒度是什么?
如果一行代表一天,dateday 可以作为唯一键;如果一行代表用户和天,则键应是 (userid, date_day)。粒度不同会改变合并条件、重复检查和回补成本。
迟到数据最多晚到多久?
若事件通常只晚到两天,可以每次重算最近三天;若没有可接受上界,就不能只用固定窗口,需要水位线、分区重算或定期全量校准。窗口不是越小越好,它必须覆盖业务可接受的迟到范围。
上游更新由哪个字段表示?
eventat 表示业务发生时间,updatedat 表示记录最后修改时间。只按 eventat 过滤会漏掉“旧事件后来被修改”的情况;优先使用可靠的 updatedat,并验证它在源系统中单调且不会被回拨。
模型逻辑或字段变化如何发布?
新增列、删除列、计算逻辑变化的处理不同。需要确认是否允许 onschemachange 自动同步,历史列是否要回填,以及发布是否能安排一次全量刷新。
30 秒回答框架
“我先确认目标粒度和迟到上界。首次运行做全量构建,后续用 isincremental() 按 updatedat 过滤,并向前扩大一个迟到窗口。目标模型声明与粒度一致的 unique_key,这样同一天的新数据会更新原行而不是追加重复行。窗口内按键去重后再聚合,用 merge 或仓库等价策略写入。窗口、唯一键和模式变化都用测试验证;如果逻辑改变导致历史结果不再一致,执行受控的 --full-refresh,同时重跑下游模型。最后监控处理行数、最大事件时间、重复键和新旧结果差异。”
分步骤深入解答
1. 从全量方案定义正确性基线
先写出全量查询:读取所有事件,按目标粒度聚合。这是正确性基线,后续增量结果必须和同一时间范围的全量结果对账。不要一开始就优化,因为没有基线就无法判断“少扫了数据”是否漏算。
2. 选择增量筛选边界
增量模型只在目标表已经存在、未传入 --full-refresh 且模型配置为 incremental 时进入增量分支。可以用目标表的最大更新时间减去迟到窗口:
{{
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. 让唯一键匹配模型粒度
如果目标表按天存储,dateday 是唯一键;如果按用户和天存储,则使用 ['userid', 'date_day']。唯一键列不能含空值,否则 merge 可能无法匹配并产生重复行。没有唯一键时,许多适配器只能 append,窗口重算会把同一粒度写出多行。
4. 在窗口内先去重再聚合
同一事件可能由重放或 CDC 更新产生多条记录。用 eventid 和 updatedat 排序,保留每个事件的最新版本,再做日聚合:
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. 处理模式和逻辑变化
新增列不一定会回填旧行;删除列或类型变化也可能只在运行时暴露。onschemachange 可配置为 ignore、fail、appendnewcolumns 或 syncallcolumns,但它只跟踪顶层列,不能代替历史数据回补。计算逻辑改变后,新旧历史可能使用不同规则,此时应运行 --full-refresh,并根据依赖关系重建下游增量模型。
7. 设计回补和失败恢复
将迟到窗口、目标最大更新时间、源数据水位写入运行日志。若某次窗口运行失败,下一次仍从已提交的目标水位重新计算,而不是把内存中的“已处理到”当作事实。对大范围历史修复,按日期分片执行并限制并发;完成后用抽样全量查询对账,避免一次性刷新压垮仓库。
8. 建立验证闭环
至少验证四组信号:窗口内每个 eventid 至多一行;目标唯一键没有重复;最近窗口与全量重算结果的差异在允许范围内;每次运行处理行数和最大 updatedat 没有异常跳变。对账要覆盖空输入、重复事件、旧事件更新、迟到事件、窗口边界相等值和全量刷新后再增量运行。
高质量示范回答
“我会先确认模型粒度、迟到上界和源数据的更新字段。假设目标是一行一天,事件有稳定的 eventid 和 updatedat。第一次运行全量构建;后续用 is_incremental() 从目标表的最大更新时间向前回看三天。这个窗口是根据迟到分布决定的,三天不是固定答案。
窗口内先按 eventid 和更新时间去重,再按天聚合,目标模型把 dateday 设成 unique_key,用 merge 更新最近几天,避免重复行。若目标是用户日粒度,就改成复合键。只按事件发生时间会漏掉旧事件的后续更新,所以我会优先使用可靠的更新时间字段。
我会把窗口大小、最大水位、处理行数、重复键和窗口对账差异作为运行指标。新增列可以按 schema-change 策略处理,但它不会自动填充历史值;如果计算逻辑改变或需要历史回补,就安排分片的 full refresh,并重跑受影响的下游模型。最后用全量查询做抽样对账,验证空输入、迟到、重复、边界时间和失败重试,确保增量优化没有牺牲正确性。”
常见错误
只按 eventat 过滤 → 漏掉旧事件更新 → 使用 updatedat 或明确的 CDC 水位
事件发生时间不会随着后续修正而变化。若业务允许更新,必须按更新时间或变更序列筛选,并验证该字段的可靠性。
没有唯一键就使用 merge → 无法稳定匹配 → 先定义模型粒度和非空键
唯一键不是随便选一列;它必须唯一标识目标的一行。若粒度是用户和天,单独使用日期会把不同用户合并到一起。
以为增量模型会自动回填新列 → 历史值保持空缺 → 设计回补或 full refresh
模式同步和历史数据回填是两件事。新增列只改变结构时可以轻量同步;需要旧记录有值时必须额外更新或重建。
固定使用一小时窗口 → 迟到分布超过窗口时漏算 → 用分位数和对账数据校准
窗口大小应由迟到分布、SLA 和成本共同决定。监控窗口外到达量,发现异常时扩大窗口或执行分片回补。
追问及应对
如果每天有 5% 的事件在两天后到达,你会如何选窗口?
先确认业务允许的准确性延迟。如果日报允许第二天修正,可以覆盖两到三天并把晚到事件计入对账;如果必须在首日稳定,则需要水位线加回补队列,不能只靠更大的 SQL 窗口。窗口选择应由迟到分布和成本曲线验证,而不是直接套用比例。
如果 unique_key 在源数据中重复,会发生什么?
同一次 merge 的新数据或目标数据含重复键时,适配器可能报错,也可能产生不确定结果。先在增量输入和目标表分别执行唯一性检查,找出重复来源;再按事件版本去重,或重新定义能表达真实粒度的复合键。不能用随机 ID 掩盖业务键不稳定。
模型 SQL 改了,但只想重算最近七天,能否继续增量运行?
只有当历史行的计算结果不受新逻辑影响时才安全。若逻辑改变会影响全部历史,最近七天增量会留下新旧规则混合的表,应执行受控 full refresh,或按受影响分区分片重算,并同步重跑下游模型。
上游表被截断后,增量模型如何恢复?
先停止继续推进水位,确认源表重建完成,再从可靠的源快照或 CDC 起点回补。若无法证明源表覆盖了目标所需历史,直接增量运行会把目标当成完整基线,必须恢复快照或执行全量重建。