题干与适用场景
一张 Apache Iceberg 事件表约有 1 PiB,每天新增 6 TiB,保留 180 天。所有查询都带 eventtime 范围,通常查询 1 到 7 天;其中 35% 还会等值筛选 12,000 个 tenantid 之一。事件最多迟到 48 小时,也可能执行历史回填。当前未分区表扫描数据过多。
请选择初始分区规则,说明常见替代方案为什么会失败,并解释如何证明裁剪生效。回答要覆盖谓词写法、数据倾斜、文件大小、迟到数据、分区演进、灰度和回滚。
所有容量、比例、基数和保留期都是面试假设。本题适用于数据工程师、分析平台工程师和湖仓工程师。核心能力是物理数据布局与查询规划,因此 category 为 data。
面试官考察点
第一项信号是从工作负载出发。常见谓词能够让引擎排除物理数据时,分区列才有价值;列的基数高不等于适合做分区键。
第二项信号是粒度控制。分区太粗会多读数据,分区太细或直接使用高基数列会制造元数据、小文件和写入开销。强回答会先估算每个分区的字节数和分区数量。
第三项信号是拿出证据。SQL 中出现日期条件不代表一定裁剪。候选人应检查物理执行计划或 dry run、计划读取的文件和分区、扫描字节与结果一致性。
第四项信号是生命周期意识。事件时间和摄取时间服务不同问题;迟到数据会改写旧的事件时间分区。分区演进后,旧文件在重写前仍保留旧规则。
回答前需要澄清的问题
- 哪些谓词是必带且具有选择性的? 如果多数查询不带时间,时间分区无法限制这些扫描;如果几乎每条查询都选一个租户,按租户分桶的价值会上升。
- 业务按事件时间还是到达时间筛选? 事件时间匹配报表但要接受迟到写;摄取时间便于追加,却可能把同一业务日散落到多个到达分区。
- 租户分布是否倾斜? 固定哈希桶能控制高基数,但一个超大租户仍会形成热点;只有测量后才考虑为它提供独立路径。
- 引擎对文件和元数据有什么约束? 分区数量应受控,而且每个活跃分区必须获得足够字节,才能产出合理大小的文件。
- 表格式能否隐藏变换并演进规则? 如果不能,读写方可能需要显式派生分区列和迁移方案。
30 秒回答框架
“我会先分析查询谓词。每条查询都筛选 eventtime,所以先灰度 day(eventtime)。每天 6 TiB 很大;针对 35% 带租户等值条件的查询,我会实测增加固定的 bucket(64, tenant_id) 变换。它每天最多形成 64 个租户物理组,不会生成 12,000 个租户分区。
查询使用半开事件时间范围;我会检查执行计划和计划文件数,并与未分区对照组比较扫描字节和结果。tenant_id/hour 每天可能形成数十万个组,会饿死写入器,所以直接排除。迟到事件写入对应事件日。只有裁剪收益、文件大小、写入成本和元数据都达标,才演进 Iceberg 规则并按工作负载扩大。”
分步骤深入解答
第一步:把查询日志整理成谓词矩阵
选择布局前先抽样真实查询。按工作负载记录频率、延迟或成本权重、时间跨度、租户选择性、连接方式,以及谓词是否能在扫描事实表前确定。只按频率排序可能会优化便宜的仪表盘,却忽略每天一次但读取大部分数据的任务。
题干给出一个强不变量:每条查询都有事件时间范围,因此事件时间是第一裁剪维度。租户等值条件只出现在 35% 的查询中,不能单独承担分区维度。自由文本、指标值或高基数事件 ID 既不匹配主访问路径,也无法形成稳定物理组。
第二步:估算候选分区的大小和数量
按事件日分区时,每个分区约 6 TiB,七天查询在文件和行组裁剪之前最多先落到约 42 TiB。按小时分区时,平均每个分区约 256 GiB:
6 TiB / 24 = 0.25 TiB = 256 GiB/小时两种粒度都足以填满常规列式文件。按日约有 180 个活跃时间组,按小时约有 4,320 个。只有大量查询集中在几小时内,而且实测收益超过元数据和写入扇出,才选更细粒度。
直接按 tenant_id 和小时分区有危险的上界:
12,000 个租户 * 24 小时 = 每天 288,000 个租户小时组稀疏数据和倾斜会让实际数量低于上界,但这个估算已经暴露失败方式:每批写入触碰许多组并关闭小文件。固定的 bucket(64, tenant_id) 把每个时间分区的租户维度限制为 64 组。若数据均匀,每个日桶约 96 GiB;真实倾斜必须另测。
第三步:选择满足工作负载的最简单规则
先用 day(event_time)。它服务全部查询,元数据面最小。再针对租户密集型工作负载实测以下候选:
PARTITIONED BY (day(event_time), bucket(64, tenant_id))桶数只是题干中的候选,不是通用默认值。使用真实租户分布、目标文件大小、写入并行度和查询并发,比较 16、32、64、128。只有引擎能从租户等值条件推导桶,而且减少的计划文件足以抵消元数据和写入扇出,增加桶才有价值。
表格式支持隐藏变换时,不要让业务 SQL 感知物理布局。查询只筛选逻辑列 eventtime 和 tenantid,表元数据负责把谓词映射到物理分区。这样可以避免派生列不一致,也能在不重写所有消费查询的情况下演进规则。
第四步:让谓词可被规划器裁剪
把业务时区边界转换为 UTC 后,使用明确的半开范围:
SELECT tenant_id, event_type, COUNT(*)
FROM analytics.events
WHERE event_time >= TIMESTAMP '2026-07-01 00:00:00 UTC'
AND event_time < TIMESTAMP '2026-07-08 00:00:00 UTC'
AND tenant_id = 'tenant-42'
GROUP BY tenant_id, event_type;半开区间避免相邻窗口在边界重复计数。不受支持的函数包裹分区源、与另一行相关列比较、隐藏在 OR 后或使用不一致类型转换,都可能阻止静态排除。不同引擎支持的变换不同,因此应验证计划,不能背一套优化器规则。
本地自然日报表应先把指定时区的当天起点和次日起点换算成 UTC。跨夏令时直接减固定 24 小时会出错。存储的事件时间、分区变换和报表边界还必须采用有文档的时间戳约定。
第五步:证明裁剪和结果正确
测试矩阵至少包含一小时、一天、七天、带租户、不带租户、空范围、迟到事件和夏令时边界。每条查询记录:
- 逻辑与物理计划,包括分区或文件过滤条件;
- 计划读取的分区数和文件数;
- 估算与实际扫描字节;
- 规划时间、执行 p50/p95 和任务数;
- 与对照组一致的行数、聚合值和校验和。
未分区快照作为对照。若 180 天的数据大小近似均匀,一天查询在文件统计生效前应只考虑约 1/180 的活跃字节。这个值只用于数量级校验,不是承诺比例。执行计划写着过滤条件但仍读取全部文件,不能判定通过。
还要设置负对照:去掉时间谓词、把它改成不受支持的表达式,再把租户等值改成范围。扫描范围应按可解释方式扩大。负对照证明测量方法能够发现裁剪缺失。
第六步:处理写入、迟到数据和维护
分区会改变写入路径。监控每个任务打开的写入器、每次提交产生的文件数、文件 p50/p90、提交延迟、元数据增长和冲突重试。写入前按分区变换分布数据,并按每组字节数决定写入器数量。分区裁剪无法抵消数百万小文件。
报表按 event_time 查询,因此迟到 48 小时的事件应写入历史事件日。写入和压实都要允许这种更新。明确分区何时转冷,并等迟到窗口关闭后再做激进重写。回填必须限定分区范围、使用幂等写,并采用与流式摄取相同的文件大小检查。
第七步:避免一次性大迁移地演进
Iceberg 隐藏分区演进后,新规则只作用于新文件,旧文件仍保留原规则。规划器可以同时读取两者,但在热数据或常查历史被重写前,收益会混合。记录基线快照,灰度最近范围,并确认跨新旧规则的查询都正确后再扩大。
回滚包括停止用新规则写入,并在支持时恢复旧写入配置或表快照;它不会自动恢复已物理删除的文件。快照保留和物理清理应放在灰度窗口之外。只有实测节省足以覆盖 I/O 与冲突风险,才重写历史数据。
验收条件包括业务结果一致、各工作负载按预期排除分区、扫描字节或延迟下降、文件大小健康、规划时间受控、摄取 SLO 无回退,以及元数据增长稳定。
高质量示范回答
“我会先提取真实谓词分布。这里每条查询都带事件时间,只有 35% 选择单个租户,所以时间是主维度。日分区约 6 TiB,共 180 个活跃时间组;小时分区约 256 GiB,但会产生 4,320 个活跃时间组。只有窄小时查询的收益超过额外元数据,我才会选小时。
基线灰度是 day(eventtime)。针对租户密集查询,我会测试 day(eventtime), bucket(64, tenant_id)。64 只是候选:它限制租户扇出,在数据均匀时每个日桶约 96 GiB。我会排除直接租户小时分区,因为上界是每天 288,000 个组,容易造成小文件和写入扇出。
查询使用 UTC 半开范围与租户等值条件。我会查看物理计划、计划文件和扫描字节,再与未分区快照比较结果和校验和。测试覆盖一小时、一天、七天、空范围、迟到事件、租户与非租户、夏令时;还要通过去掉或遮蔽时间谓词做负对照。
迟到事件写回事件日,因此压实要等 48 小时窗口后。灰度期间监控文件分位数、任务写入器数、规划 p95、元数据、写入延迟和冲突。Iceberg 支持分区演进时,新文件采用新规则,旧文件继续可读。只有查询收益超过重写成本才处理历史。任何结果差异、全文件扫描、小文件激增或摄取 SLO 回退都会停止扩大。”
常见错误
- 选择基数最高的列 → 稀疏组和小文件增加,却不服务主谓词 → 从加权查询谓词出发,并限制物理组数量。
- 直接按租户和小时分区 → 题干允许每天 288,000 个组 → 先按时间,并实测固定桶变换。
- 看到日期条件就认定裁剪 → 不受支持的表达式仍可能读取全部文件 → 检查物理计划、计划文件和扫描字节。
- 只验证延迟 → 缓存、并发和计算资源可能伪造收益 → 同时使用扫描字节、计划证据、负对照和等量资源。
- 未分析就用摄取时间服务事件时间报表 → 同一业务日的迟到事件会散落到多个到达分区 → 让物理维度匹配查询时间语义。
- 忽略文件大小 → 细分区饿死写入器,把扫描成本转为规划成本 → 跟踪每组字节、写入扇出和文件分位数。
- 立即重写全部 1 PiB → 成本、冲突和回滚暴露过大 → 灰度近期范围,只重写收益明确的历史。
- 认为演进会自动重写旧文件 → 多套规则会共存 → 测试混合规则规划并选择性重写。
追问及应对
追问一:如果 95% 的查询都会筛选一个租户呢?
租户分桶的权重会显著上升。按真实租户分布和查询并发重新计算桶数,比较仅时间、时间加桶,以及为超大租户隔离的方案。直接创建 12,000 个分区仍需证明每组能形成健康文件且元数据可控。
追问二:每小时也有 256 GiB,为什么不直接按小时?
如果多数查询只跨几分钟或几小时,小时分区可能正确,但它会把时间组数量和写入扇出扩大 24 倍。必须按真实范围分布比较规划、扫描字节、文件大小和摄取成本。
追问三:查询筛选本地自然日时怎么办?
把指定时区当天起点和次日起点转换成 UTC instant,再使用半开范围。这样能处理夏令时导致的 23 或 25 小时日。还要验证分区变换与存储时间戳约定能让引擎推导相关物理日。
追问四:计划显示已裁剪,但扫描字节没有下降怎么办?
检查选中分区是否因倾斜包含大部分数据、旧文件是否使用其他规则、残余谓词是否无法利用文件统计,以及字节指标是否包含无关阶段。对比计划文件 ID 与未分区对照组,不能只相信计划中的标签。
追问五:如何安全地从日分区改成小时分区?
先让新文件灰度使用新规则,保留旧快照,并查询跨越新旧布局的范围。验证正确性、规划、文件大小和写入成本。只重写收益足以覆盖 I/O 的历史范围,物理清理要等回滚和保留窗口结束。