数仓 APP 层存储设计:Hive 与 OLAP 引擎如何分工 为什么 APP 层既要落 Hive又要同步到 ClickHouse / Doris前段时间和一位之前一起做数据的同学聊到了一个问题数仓的 APP 层为什么要先在 Hive 建表之后还要在 ClickHouse 或 Doris 中再建一张表既然最终都是为了提供查询为什么不直接把 Spark 的计算结果写入 ClickHouse博主平时比较喜欢站在面试官的视角思考问题。一个看似简单的问题如果继续追问“为什么”往往会牵扯出技术选型、系统职责、数据恢复、存储成本和性能优化等一系列问题。这些问题中有些我能够说出大概思路但真要完整地回答又会发现自己的理解并没有想象中那么扎实还有一些问题虽然能够问出来却一时给不出足够全面的答案。但我逐渐意识到能够发现问题、提出问题本身就是学习过程中非常重要的一步。很多时候真正推动我们进步的并不是记住了多少现成结论而是愿不愿意继续追问一句为什么要这样设计有没有其他方案换一个业务场景结论还成立吗带着这些疑问我重新梳理了 Hive APP、ClickHouse 和 Doris 在数仓架构中的定位也尝试把这条思考链完整地整理下来。这既是一个实际开发中经常遇到的架构问题也是一条很典型的面试追问链。APP/ADS 层应该放在哪里 ↓ 为什么选择 ClickHouse 或 Doris ↓ 为什么不直接把 Spark 结果写入 OLAP 引擎 ↓ Hive APP 表还有没有保留价值 ↓ ClickHouse / Doris 为什么查询快 ↓ 线上可能遇到哪些问题又该如何调优一、APP/ADS 层究竟应该放在哪里首先要明确一个容易混淆的概念APP/ADS 是逻辑分层不代表某一种具体的数据库。APP 层也常被叫作 ADS 层主要存放面向具体应用、报表、接口和业务场景的最终结果数据。它既可以放在 Hive也可以放在其他存储系统中例如ClickHouse、DorisMySQL、PostgreSQLIceberg、 Paimon所以不能简单地认为APP 层 Hive 表更准确的理解应该是APP/ADS 层 面向业务应用的数据结果层至于这批结果最终存在哪里要根据实际场景判断包括查询延迟要求、查询并发量数据规模、数据保留周期、数据恢复能力下游使用方式、运维和存储成本有些公司的 APP 层全部在 Hive 中有些放在 ClickHouse 或 Doris 中还有一些公司会同时保留两份。这些方案本身没有绝对的对错关键要看每一份数据到底承担什么职责或者说为什么要这么做二、为什么很多公司同时建设 Hive APP 和 ClickHouse/Doris 表一种常见的离线数仓链路是DWD / DWS ↓ Spark 计算 ↓ Hive APP ↓ ClickHouse / Doris ↓ 报表、接口、数据大屏从数据内容上看Hive APP 和 ClickHouse/Doris 中的数据可能几乎完全相同似乎只是重复存储了一份。但我理解从系统职责上看两者扮演的角色并不一样。1. Hive APP负责结果沉淀Hive APP 表通常用于保存已经计算完成的业务结果保留较长周期的历史数据支持数据回溯和问题排查支持下游数据重新同步降低复杂任务的重复计算成本为多个下游提供统一的数据来源它更像一个稳定、成本相对较低、可以重新恢复的数据底座。这里的重点不是 Hive 查询有多快而是这份结果能不能可靠地保存下来。2. ClickHouse/Doris负责查询服务ClickHouse 和 Doris 通常用于BI 报表查询数据大屏展示 、接口查询多维分析、高并发访问秒级甚至亚秒级聚合它们更接近 Serving 层也就是面向用户或业务系统的数据服务层。因此我理解这套架构的本质并不是简单地“复制一份数据”而是让不同系统各自负责自己擅长的事情Hive APP 负责数据沉淀、历史保留和恢复 ClickHouse / Doris 负责低延迟、高并发的数据查询三、为什么不直接把 Spark 结果写入 ClickHouse先说结论技术上当然可以。链路完全可以设计成Hive DWS ↓ Spark 计算 ↓ ClickHouse / Doris APP这种架构并没有问题在实时数仓、轻量级数据平台或者链路比较简单的场景中也很常见。但对于一些复杂的离线任务先落 Hive再同步到 ClickHouse/Doris通常会更稳。1. 降低失败重试成本假设某个 APP 指标需要经过多张大表 Join、开窗计算、去重用户归因、多级聚合、复杂业务规则判断完整链路可能是DWD 明细数据 ↓ 大表 Join ↓ 窗口计算 ↓ 业务规则处理 ↓ 结果聚合 ↓ 写入 ClickHouse如果 Spark 直接写 ClickHouse前面的计算全部完成了但在最后写入阶段发生失败例如网络抖动、ClickHouse 节点异常写入超时、分片不可用磁盘空间不足、后台 Merge 压力过大这时如果任务没有设计好断点恢复就可能需要重新执行整套计算。而先落 Hive 后链路会被拆成两个阶段阶段一业务计算 DWD / DWS ↓ Spark ↓ Hive APP阶段二结果同步 Hive APP ↓ ClickHouse / Doris如果同步 ClickHouse 失败只需要重新执行第二阶段不必重新扫描大量明细数据也不需要再跑一遍复杂的 Join 和聚合。先落 Hive 的一个重要价值就是把昂贵的计算结果保存下来避免因为下游写入失败而重复计算。2. 实现计算系统和查询系统解耦如果 Spark 直接写 ClickHouse那么 Spark 任务的成功与否会受到 ClickHouse 集群状态的影响。例如ClickHouse 正在扩容或维护、集群负载过高网络出现波动、表结构正在调整某个分片节点异常、后台 Merge 堆积查询流量占用了大量资源这些本来属于查询系统的问题却可能直接拖垮上游的离线计算任务。先落 Hive 后同样可以分别定义两个任务的成功状态业务计算成功 Hive APP 数据产出成功数据服务成功 Hive APP 数据同步到 ClickHouse 成功这样即使 ClickHouse 暂时不可用业务计算结果也已经保存在 Hive 中。等 ClickHouse 恢复后再重新同步即可。这也是解耦的实际意义不是为了让架构看起来更复杂而是为了让一个系统的故障不要无限向上游传播。3. 支持 ClickHouse/Doris 数据恢复线上可能出现很多情况某个分区数据写错数据重复导入集群迁移、表结构重建历史数据需要重新灌入ClickHouse 数据与离线口径不一致如果 Hive APP 保存了完整结果就可以直接从 Hive 重新导入Hive APP 历史分区 ↓ 重新同步 ↓ ClickHouse / Doris这通常比重新运行整条 DWD、DWS、APP 链路成本更低。不过这个说法有一个重要前提数据团队确实把 Hive APP 当作权威数据源并且建立了从 Hive 重建 ClickHouse/Doris 数据的流程。如果 Hive APP 平时没人维护出现问题时也无法从它恢复那么这张表只是理论上的“备份”并没有真正产生价值。4. 更容易控制数据发布过程还有一个很容易被忽略的问题数据什么时候对外可见假设 Spark 直接向 ClickHouse 写入一个分区的数据写入过程中报表已经可以查到部分结果就可能出现数据只写了一半、新旧数据同时存在某些分片已经更新某些分片还没更新重试后产生重复数据用户在任务运行过程中查到不完整结果而先落 Hive 后可以先完成离线结果校验再进行统一发布Spark 计算 ↓ 写入 Hive 临时分区 ↓ 数据量、空值、指标波动校验 ↓ 切换正式分区 ↓ 同步 ClickHouse同步到 ClickHouse 时也可以配合临时表、分区替换、版本字段或幂等写入机制尽量避免用户看到中间状态。所以 Hive APP 除了保存结果也可以作为数据发布前的缓冲区。5. 降低长期存储成本Hive 表的数据一般存储在HDFS、对象存储、数据湖这些系统更适合保存大规模、长周期的历史数据。而 ClickHouse/Doris 为了提供高性能查询通常需要承担额外成本例如多副本存储排序和索引结构后台数据合并更高性能的磁盘更多 CPU 和内存资源因此同样一份数据长期保存在 ClickHouse/Doris 中综合成本往往更高。一种常见设计是Hive APP保存完整历史 ClickHouse/Doris只保存近期热点数据例如Hive APP保留最近两年 ClickHouse保留最近三个月三个月以前的数据如果偶尔需要查询可以走离线查询日常报表只查询最近三个月则直接访问 ClickHouse。不过数据生命周期不能只凭经验决定。ClickHouse 也可以保存多年数据Hive APP 也可能只保留较短时间。实际要结合以下配置和成本判断Hive 分区清理策略ClickHouse TTL、 Doris 动态分区策略冷热数据分层磁盘容量、对象存储费用、查询频率四、Hive APP 表是不是一定要保留不一定。数仓分层是一种建模和管理思想不代表每一层都必须在 Hive 中建一张实体表。判断 Hive APP 是否需要保留关键是看它有没有解决真实问题。如果存在以下情况保留 Hive APP 通常比较合理APP 计算逻辑复杂重新计算成本较高需要保留长期历史OLAP 引擎只保存热点数据APP 结果还有其他下游需要支持 ClickHouse/Doris 快速重建希望计算系统与查询系统解耦需要在发布前进行数据质量校验需要保存某个时间点的确定性结果链路可以设计成DWD / DWS ↓ Spark ↓ Hive APP ├──→ ClickHouse / Doris ├──→ MySQL ├──→ 文件导出 └──→ 其他离线任务这种情况下Hive APP 不只是中间表而是一份可以复用的数据资产。如果满足以下条件则可以考虑直接写 OLAP 系统计算逻辑比较简单数据重新生成成本低结果只供一个查询系统使用ClickHouse/Doris 保存完整历史已有可靠的备份和容灾机制没有其他任务依赖 Hive APP写入过程能够保证幂等数据发布过程可控希望降低链路复杂度和数据冗余链路可以简化为Hive DWS ↓ Spark 计算 ↓ ClickHouse / Doris APP甚至在实时链路中还可能是Kafka ↓ Flink ↓ ClickHouse / Doris所以不能机械地认为只要做数仓分层APP 层就必须在 Hive 建表。真正应该问的是这张 Hive APP 表是一份有恢复、复用和沉淀价值的数据资产还是一张没人使用的重复副本如果一张表既不用于恢复也没有其他下游还增加了任务延迟和维护成本那它很可能应该被删除。五、为什么选择 ClickHouse 或 Doris面试中经常会被问你们为什么把 APP/ADS 层放在 ClickHouse 或 Doris只回答“因为查询快”显然不够。更完整的回答需要结合业务场景我们的 APP 数据主要服务于报表、数据大屏和接口查询 查询以按时间、地区、渠道等维度进行聚合为主 数据量较大同时存在一定并发 Hive 无法满足秒级响应要求 所以使用 ClickHouse/Doris 承担查询服务。这里面至少包含了几个信息查询对象是谁、查询模式是什么数据量有多大、延迟要求是多少并发情况怎么样、为什么原来的系统不适合我理解技术选型一定要从业务需求出发而不是只背数据库特性。面试官想听到的回答想必也是你从自身业务特性出发同时结合技术特性。六、ClickHouse 和 Doris 为什么查询快ClickHouse 和 Doris 的实现方式并不完全相同但它们在分析型查询上有一些共同特点。1. 列式存储分析查询通常只会读取少量字段。例如一张订单表有 100 个字段但某个报表只需要SELECTprovince,SUM(pay_amount)FROMorder_detailWHEREdt2026-08-01GROUPBYprovince;这条 SQL 只关心provincepay_amountdt列式存储可以只读取这些列而不需要把整行的 100 个字段全部加载出来。读取的数据量更少磁盘 IO 和网络传输自然也会下降。列式数据通常还具有更好的压缩效果因为同一列的数据类型和取值分布比较接近。2. 向量化执行传统执行引擎可能一行一行处理数据。向量化执行则会一次处理一批数据减少函数调用和解释执行的开销并更好地利用 CPU 缓存和 SIMD 指令。可以简单理解为逐行处理 取一行 → 计算 → 取下一行 → 再计算 向量化处理 一次取一批数据 → 批量计算对于扫描、过滤和聚合类查询批量处理的效率通常更高。3. 分区裁剪和数据跳过如果表按照日期分区查询中又包含日期条件WHEREdt2026-08-01系统就可以只读取对应日期的分区而不是扫描整张表。此外合理的排序键、前缀索引、Zone Map、MinMax 等结构也可以帮助引擎跳过大量不符合条件的数据块。例如数据按照下面的字段排序dt, tenant_id, province查询条件如果经常包含dt和tenant_id就可以减少很多无效扫描。这也是为什么表结构设计会直接影响查询性能。不是把数据导入 ClickHouse 或 Doris查询就一定会自动变快。4. 并行和分布式执行ClickHouse 和 Doris 都可以把查询拆分到多个节点执行。例如一个聚合查询可以由多个节点分别扫描和计算局部结果最后再进行汇总。节点 1扫描一部分数据并聚合 节点 2扫描一部分数据并聚合 节点 3扫描一部分数据并聚合 ↓ 汇总结果这种并行执行方式比较适合大规模数据分析但分布式并不意味着节点越多越好。如果数据分布不均、分片键不合理或者大量数据需要跨节点 Shuffle增加节点也可能无法解决问题。5. 预聚合和物化视图很多报表查询的维度和指标相对固定例如按天、渠道统计订单金额 按天、地区统计活跃用户 按小时统计接口调用量如果每次查询都扫描明细表重新计算成本会比较高。这时可以提前生成聚合结果订单明细 ↓ 按天、渠道预聚合 ↓ 报表直接查询聚合表ClickHouse 可以使用物化视图、AggregatingMergeTree 等能力Doris 可以使用物化视图、聚合模型等方式减少查询时的计算量。不过预聚合也有代价增加存储空间增加数据写入和维护成本维度发生变化时可能需要重建口径管理会更加复杂所以不能看到慢查询就直接建物化视图还是要先分析真正的瓶颈。七、线上使用 ClickHouse/Doris常见问题有哪些理论上“列存、向量化、分布式”听起来都很快但线上是否稳定往往取决于数据模型和使用方式。下面是一些比较常见的问题。1. 小批量、高频写入ClickHouse 不适合持续进行大量单条写入。如果上游每来一条数据就执行一次 Insert可能会产生大量小 Part增加后台 Merge 压力。常见表现包括Part 数量持续增加、Merge 队列堆积磁盘 IO 变高查询性能抖动更合适的方式通常是批量写入错误方式 每条数据执行一次 Insert 更合适的方式 攒够一批数据后批量 Insert批次大小需要结合单行数据量、写入延迟和集群配置进行测试不能直接照搬固定数值。Doris 同样需要关注导入批次、并发导入任务以及 Compaction 压力。2. 分区过细分区不是越多越好。如果按照小时甚至分钟建立大量分区可能导致元数据数量增加、小文件或小 Part 增多分区管理复杂、查询计划和后台任务压力增大分区字段通常应该选择查询中经常使用、数据生命周期需要依赖、基数相对可控日期是最常见的分区字段但具体按天还是按月要根据每日数据量和查询范围判断。3. 排序键设计不合理在 ClickHouse 中ORDER BY不只是控制数据显示顺序它决定了数据在磁盘上的组织方式对查询性能影响很大。假设常见查询条件是WHEREdt?ANDtenant_id?但排序键只设置成ORDER BY user_id那么系统很难利用数据顺序跳过无关数据。排序键应该结合高频过滤条件设计一般会优先考虑使用频率高、过滤选择性较好能够帮助数据裁剪、不会造成明显写入热点同时也不能把所有字段都塞进排序键。排序键过长会增加存储、写入和维护成本。4. 高基数字段使用不当用户 ID、设备 ID、订单 ID 等字段通常具有很高的基数。如果经常对高基数字段进行精确去重、大范围 Group By、大规模 Join、全量排序内存和 CPU 消耗可能会非常高。例如COUNT(DISTINCTuser_id)在数据量很大时精确去重可能比较昂贵。需要根据业务要求判断是否可以使用近似去重、预聚合Bitmap、HLL可参考博主HyperLogLog详解分层计算提前生成用户粒度结果优化之前首先要确认业务到底需要“绝对精确”还是允许少量误差比如之前面试时遇到的需要实时计算uv的场景跟面试官确认一下去重的细节则会有不同的设计方案。5. 大表 JoinClickHouse 和 Doris 都支持 Join但不能因为支持就把离线数仓中的所有复杂 Join 原样搬到查询层。如果用户每次打开报表都要实时关联几张数十亿级别的大表查询很容易出现问题。更合理的方式通常是在离线层提前完成复杂 Join使用宽表或轻度汇总表对高频查询进行预计算查询引擎应该承担交互式分析而不是替代所有离线计算。6. 数据倾斜和热点如果数据分片键选择不合理可能导致某些节点数据特别多另一些节点数据很少。例如按照一个取值非常集中的字段分片channel app 的数据占 90%那么包含该值的节点可能承受大量写入和查询压力。数据倾斜会导致节点磁盘使用不均查询由最慢节点决定部分节点 CPU 和内存持续偏高扩容后效果不明显分片键需要同时考虑数据分布是否均匀、常见查询条件Join 方式、数据扩容方式八、Hive APP 和 ClickHouse/Doris 之间如何保证一致性双份存储带来的一个现实问题是两边数据可能不一致。这也是面试中常会遇到的问题。常见原因包括同步任务漏跑、同步过程中部分失败重跑产生重复数据Hive 分区已经更新但 OLAP 数据未更新业务口径变更后只修改了一侧、历史数据回刷不完整因此链路中至少要考虑以下机制。1. 保证写入幂等同一个业务日期重复执行时结果应该保持一致而不是越写越多。常见思路先删除目标分区再重新导入或者写入临时表 ↓ 校验完成 ↓ 替换正式分区也可以通过业务主键、版本字段或数据模型实现覆盖更新。具体方案取决于使用的是 ClickHouse 还是 Doris以及表引擎和数据模型。2. 增加数据校验同步完成后可以校验总行数、核心指标总量分区最大最小时间、主键去重数量空值比例、与前一天相比的波动范围例如Hive APP 当日订单金额1000 万 ClickHouse 当日订单金额1000 万总量一致不代表数据一定完全正确但至少可以快速发现明显异常。3. 记录数据版本可以在数据中保留业务日期、任务执行时间数据版本号、批次号这样出现问题时能够确认当前查询到的到底是哪一次产出的数据。十、面试官视角如果面试官问为什么 APP 层既在 Hive 建表又同步到 ClickHouse可以这样回答APP/ADS 是逻辑分层不一定必须建在 Hive。我们同时保留 Hive APP 和 ClickHouse 表是因为两边承担的职责不同。Hive APP 主要负责沉淀离线计算结果、保留历史数据并作为下游同步和数据恢复的来源ClickHouse 主要负责报表、接口和多维分析等低延迟查询。先落 Hive 可以把复杂业务计算和 OLAP 写入解耦。如果 ClickHouse 写入失败只需要重新同步不必重新执行前面的大表 Join 和聚合。同时在 ClickHouse 表误删、数据写错或集群迁移时也可以从 Hive 快速恢复。但 Hive APP 并不是一定要保留。如果计算逻辑简单、结果只供 ClickHouse 使用、写入可以保证幂等并且已经有完整的备份和恢复机制也可以直接把 Spark 结果写入 ClickHouse减少链路和重复存储。如果面试官继续问ClickHouse 为什么查询快主要是因为列式存储、数据压缩、向量化执行、分区裁剪、排序索引以及并行计算。对于按少量维度进行过滤和聚合的分析型查询它只需要读取相关列和相关数据块不需要扫描完整行数据。但性能也依赖表设计。如果分区、排序键、分片键设计不合理或者存在大量小批量写入、高基数聚合和大表 Join查询一样可能很慢。这样的回答既说明了架构原因也保留了边界条件不会显得只是背概念。十一、写在最后回到最开始的问题为什么先在 Hive 建 APP 表再在 ClickHouse 或 Doris 中建一张表核心原因可以概括为一句话Hive APP 解决的是数据沉淀和可恢复问题 ClickHouse/Doris 解决的是数据查询和服务问题。先落 Hive 的价值主要包括保存复杂计算结果降低失败重试成本解耦计算系统和查询系统支持 OLAP 数据恢复支持多个下游复用控制数据发布过程降低长期历史数据的存储成本但这并不意味着 Hive APP 永远不能删除。判断一张表是否应该存在不能只看公司以前怎么做也不能只看数仓分层规范怎么写。我理解最终还是要回答几个问题这份结果重新计算贵不贵 是否有多个下游使用 是否需要长期保存 OLAP 数据损坏后如何恢复 写入失败后如何重试 两份数据如何保证一致如果 Hive APP 能解决这些问题它就是有价值的数据资产。如果它既不能用于恢复也没有其他下游只是机械地复制一份数据那么它可能只是在增加链路复杂度。我想数仓架构没有固定答案。真正重要的不是“表到底建在哪”而是每个系统的职责是否清楚每一份数据是否真的有存在的意义。最后分享一下博主七月的歌单