尧图精选

Doris预聚合实战:从Aggregate模型到物化视图的查询优化指南

🕒 发布时间:2026/9/10 1:41:44 📁 来源:尧图网络
这两年我帮好几个团队调过 Doris 集群接手时几乎都会听到同一句话Doris 查询还是慢几千万行的明细表按时间按渠道聚合一下就要十几秒。听完这句话我基本能猜到他们只把 Doris 当成一个能存几千万行的 MySQL在用完全没用上 Doris 在多维分析里最核心的预聚合能力。今天这篇不聊安装部署也不聊运维监控专门把 Doris 多维分析里最容易被忽略、但价值最大的预聚合机制讲透。我会从存储模型讲起接着拆 Rollup、物化视图的原理最后用一个真实的销售分析场景复盘完整的优化链路顺带把高基数维度、查询改写失败、实时性冲突这些坑也一并说清楚。无论你是刚接触 Doris 的 BI 工程师还是正在做技术选型的数据架构师这篇都能给你一些能直接落地的思路。1. 一个多维查询卡顿背后的问题本质1.1 明细表为什么撑不住多维分析先看一个最常见的场景。业务库里有张订单明细表每天新增几百万行累积下来几千万甚至上亿行。业务方想要的报表是按时间、渠道、品类、区域任意组合看订单数、GMV、客单价、件单价。这种查询翻译成 SQL 很直接SELECT channel, category, SUM(amount) AS gmv, COUNT(order_id) AS order_cnt FROM order_detail WHERE dt 2024-01-01 AND dt 2024-01-31 GROUP BY channel, category;问题在于这条 SQL 要跑得快数据库必须在一秒内扫完几千万行、做完 GROUP BY 和 SUM。传统 OLTP 数据库扛不住大部分 OLAP 引擎虽然能扛但每次查询都是全量扫描加实时聚合算力成本高查询延迟随数据量线性增长。很多人第一反应是加索引。但多维分析不同于单点查询它有十几个维度的任意组合你不可能为每种组合都建一棵 B 树索引。而且你还需要实时计算 SUM、COUNT、AVG索引在这种情况下只能加速定位不能替代聚合。所以大部分团队做来做去最后还是回到把常用的汇总结果提前算好这条路这就是预聚合的核心思想。1.2 预聚合的思路把算过的账存起来而不是每次重新算预聚合这个词听起来高大上道理其实特别朴素。你记录每笔花销月底想知道这个月在餐饮上花了多少钱正常做法是打开记账 App把每顿饭的钱加起来。但如果每天都顺手记一个当日餐饮小计月底直接看小计汇总就行根本不用翻每一笔流水。Doris 里的预聚合就是这个逻辑。在数据导入阶段就把明细按某些维度组合聚合好存成一份缩小版数据。查询时如果能命中这份聚合结果直接读取汇总值扫描的数据量可能从千万行降到几千行查询自然就快了。但把某几个维度组合聚合好听着简单做起来有几道坎。第一维度组合是笛卡尔积式的时间、渠道、品类、区域、用户、商品……几十个维度全组合是不可能的必须选最常用的几个组合。第二数据不是一成不变的今天导入的数据明天可能要修正预聚合结果怎么跟着更新第三查询的过滤条件、分组条件不可能每次都和预聚合的维度完全一致什么时候能命中预聚合什么时候必须回明细算这些问题Doris 的存储模型、Rollup、物化视图就是在回答它们。接下来一个一个拆。2. Aggregate模型Doris预聚合的地基2.1 Doris 三种数据模型怎么选要用好预聚合先要理解 Doris 的数据模型。Doris 有 Aggregate、Unique、Duplicate 三种表模型区别集中在Key 列重复时Value 列怎么处理。模型相同 Key 时 Value 的处理适用场景典型用途Aggregate按指定的聚合函数合并明细更新少、对汇总查询性能要求高订单指标、流量统计、财务汇总Unique新数据直接覆盖旧数据需要按主键更新的场景用户表、商品表、状态表Duplicate全部保留不合并需要查原始明细、不需要更新日志明细、交易流水审计你可能会问Unique 模型是不是完全和预聚合无关其实不是。Unique 模型本质上是 Aggregate 模型的一个特例Value 列用了 REPLACE 聚合函数。但它的强项是主键更新预聚合不是它的核心能力。真正把预聚合发挥到极致的是 Aggregate 模型。做多维分析报表时只要业务上不需要频繁修改历史明细优先考虑 Aggregate 模型。订单、流量、点击、曝光这类事实数据天然适合进来之后直接聚合。2.2 聚合的两种触发时机导入时与查询时Doris 的聚合不是一个孤立动作它分两个阶段。第一个阶段是数据导入时Doris 会在写入过程中对相同 Key 的批次数据做一次合并这个叫部分聚合。第二个阶段是查询时如果内存中还有未合并的相同 Key 数据Doris 会在 Scan 之后再做一次最终聚合。举个例子。你有一张按天存储的订单汇总表维度是日期、渠道、品类指标是订单数和 GMV。业务方每 10 分钟同步一批订单数据这批数据里可能有 10 条订单属于同一天、同一个渠道、同一个品类。导入时 Doris 会把这 10 条先加一遍落盘时只剩一行。等查询时再把这 10 分钟内多个批次合并后的结果再汇总一次。这两个阶段合起来才是 Doris 聚合模型的完整语义。理解这一点很重要因为它决定了你建表时聚合函数选错了查询结果就会出错。比如要统计去重用户数选了 SUM出来的结果就可能是重复统计的。2.3 一个订单事实表的建表实战直接给一个建表例子这是我在项目里常用的一种订单汇总表结构CREATE TABLE dwd_order_agg ( dt DATE, channel VARCHAR(32), category VARCHAR(64), province VARCHAR(32), order_cnt BIGINT SUM DEFAULT 0, amount DECIMAL(20, 2) SUM DEFAULT 0 ) AGGREGATE KEY(dt, channel, category, province) DISTRIBUTED BY HASH(channel, category) BUCKETS 16 PROPERTIES ( replication_num 1 );这里 AGGREGATE KEY 列是维度列order_cnt 和 amount 是 Value 列后面必须跟聚合函数类型。DISTRIBUTED BY HASH 决定了分桶键实际生产环境可以用任意列分桶但有个经验分桶键尽量选择分布均匀、且经常出现在等值过滤条件里的列避免数据倾斜和 Scan 范围过大。建完表后你导入两批有相同 Key 的数据用 SELECT 查一下会发现相同 Key 的行被合并了。这个过程中 Doris 还会触发底层 compaction把小文件合并成大文件进一步优化扫描效率。3. Rollup手工打造的多维索引3.1 Rollup 到底解决了什么问题Aggregate 模型解决的是同一份维度组合的自动合并但它有个明显局限表一旦建好聚合维度就固定了。比如你建了一张按 (dt, channel, category, province) 聚合的订单表业务方觉得查某天某渠道的 GMV很快但突然想按某天某省所有渠道的 GMV汇总这张表也能查但没法跳过 channel 维度必须把所有 channel 的数据扫出来再汇总。数据量一大还是会慢。Rollup 解决的就是这个问题。它是基于 Base 表额外建立的一份按更少维度聚合的索引你可以理解为手动为常用查询组合提前建好的预聚合结果。Base 表还是 (dt, channel, category, province) 粒度Rollup 可以做到只有 (dt, channel) 粒度这样按天渠道的查询直接扫描 Rollup数据量少了好几倍。这句话很重要Rollup 不是新的表而是对 Base 表数据按不同维度组合做的预聚合索引。3.2 建 Rollup 的正确姿势在 Aggregate 模型上建 Rollup语法如下ALTER TABLE dwd_order_agg ADD ROLLUP r_dt_channel (dt, channel, SUM(order_cnt), SUM(amount));Rollup 列必须从 Base 表列中选择不能新增列也不能把 Base 表的 Key 列改成 Value 列。关键限制是Rollup 的 Key 列顺序必须遵循 Base 表 Key 列的前缀关系。比如 Base 的 Key 是 (dt, channel, category, province)你可以建 (dt, channel) 的 Rollup也能建 (dt, channel, category) 的但不能建 (dt, category, channel)因为顺序破坏了前缀约束。为什么要有这个限制因为 Doris 底层数据按 Key 列有序排列前缀列相同的数据在物理存储上连续查询时能快速定位。如果 Rollup 顺序和 Base 不一致就没法借用排序前缀优势了。实际生产里我建 Rollup 前会先看两个东西一是 BI 报表里最常出现的 GROUP BY 组合二是过滤条件最常用的等值列。优先把高频维度组合 高频过滤列纳入 Rollup 设计。常见的周报月报、渠道日报、区域日报基本都是这个套路。3.3 查询怎么命中 Rollup规划器帮你选路Rollup 建好后不需要改 SQLDoris 的查询规划器会自动决定是否走 Rollup。判定规则大致有三条查询的 GROUP BY 维度是 Rollup 维度列的子集查询的过滤条件涉及的列Rollup 里有查询的聚合函数能基于 Rollup 的聚合结果推导出来比如 Rollup 是r_dt_channel(dt, channel, SUM(order_cnt), SUM(amount))那么这条查询就能命中SELECT dt, channel, SUM(amount) FROM dwd_order_agg WHERE dt 2024-06-01 GROUP BY dt, channel;但如果你加了一个 Rollup 没有的维度比如GROUP BY dt, channel, category就走不了 Rollup得回 Base 表。想知道 Rollup 有没有生效最简单的方法是用 EXPLAINEXPLAIN SELECT dt, channel, SUM(amount) FROM dwd_order_agg WHERE dt 2024-06-01 GROUP BY dt, channel;查看执行计划里表名后面跟的是不是 Rollup 名字比如TABLE: dwd_order_agg(r_dt_channel)如果还是 Base 表名说明没命中。这个排查技巧我会在后面统一讲坑的时候再展开。3.4 用 EXPLAIN 验证一次真实命中我在测试环境跑过一次完整验证。Base 表数据量大约 5000 万行Base 粒度 (dt, channel, category)Rollup 粒度 (dt, channel)。查询按天渠道聚合 GMV命中 Rollup 的查询扫描数据量约 120 万行耗时 260ms强制走 Base 的查询扫描 5000 万行耗时 8.3s差 30 多倍。这就是预聚合在真实场景下的价值同一个 SQL只是 Doris 自动帮你省掉了大部分扫描和聚合计算。4. 物化视图让预聚合自动化的进阶玩法4.1 物化视图和 Rollup 是什么关系Rollup 虽然好用但限制也不少只能做简单聚合不支持过滤条件也不支持表达式。比如你想预聚合金额大于 100 的订单数Rollup 就很难表达。Doris 的物化视图可以理解为 Rollup 的增强版。它在语法层面更接近建一张自动维护的汇总表支持在 SELECT 里写 WHERE 过滤、表达式、聚合函数定义完成后由 Doris 自动维护查询时同样自动改写。需要特别注意Doris 的物化视图目前是同步物化视图也就是数据导入时同步更新不是传统数仓里那种异步定时刷新的物化视图。这对实时性要求高的场景很友好但也意味着导入路径上会多一份计算开销。4.2 从明细表直接建预聚合物化视图假设我们的订单事实表是 Duplicate 模型保留全量明细CREATE TABLE order_detail ( dt DATE, channel VARCHAR(32), category VARCHAR(64), province VARCHAR(32), order_id VARCHAR(64), amount DECIMAL(20, 2) ) DUPLICATE KEY(dt, channel, category, province) DISTRIBUTED BY HASH(order_id) BUCKETS 32;业务方频繁查按渠道按品类看 GMV 和订单数我们可以建一个物化视图CREATE MATERIALIZED VIEW mv_channel_category AS SELECT dt, channel, category, SUM(amount) AS total_amount, COUNT(order_id) AS order_cnt FROM order_detail GROUP BY dt, channel, category;这条物化视图建好后Doris 会在后台为它建立一份预聚合数据。查询时如果 SQL 粒度和它匹配会自动改写直接读这份结果。与 Rollup 相比物化视图有两个核心优势一是可以使用 WHERE比如只聚合状态为已支付的订单二是可以使用表达式比如把金额做范围分桶后再聚合。这种灵活性在实际业务里非常值钱因为报表口径很少是整表全量聚合大多数都带着各种过滤条件。4.3 物化视图的维护成本天上不会掉免费的午餐物化视图虽好但维护成本必须心里有数。首先是存储成本。物化视图本质是 Base 表的一份冗余存储维度组合越多冗余越大。如果建了五六个物化视图存储可能变成 Base 表的三到四倍。其次是导入开销。同步物化视图在每次数据导入时都会触发计算相当于每个批次的数据要计算好几套聚合结果。物化视图数量太多导入吞吐会明显下降。最后是查询改写的不确定性。物化视图和 Rollup 一样不会 100% 自动命中所有 SQL。遇到很复杂的表达式、多表 JOIN查询规划器可能不会用物化视图而是老老实实扫明细。所以我在生产里定了一条规矩物化视图不是越多越好最多给一张明细表配 3 到 5 个核心汇总视图每个视图必须能覆盖一类高频查询宁缺毋滥。5. 实战复盘一个销售分析场景的完整优化链路5.1 场景介绍与原始慢查询某电商业务方有一个经营驾驶舱看板底层数据是一张订单明细表每天约 800 万行保留 180 天累计 1.4 亿行左右。报表常见的查询有四种按天看全渠道 GMV、订单数按天渠道看 GMV、订单数、客单价按天品类看 GMV、退款金额按天区域看 GMV、订单数、件数优化前这些查询直接在明细表上 GROUP BY耗时大概 6 到 15 秒。对于一个每次打开看板都要等半天的产品来说这个体验显然不合格。而且每到月底全量导数据时查询会更慢甚至会触发 Doris OOM。我接手后的第一个判断是这个场景不需要一上来就上物化视图而是要把预聚合 明细分层的思路理清楚。5.2 优化动作先分层再预聚合第一步把明细保留在 Duplicate 模型的原始表中仅用于审计、排查和特殊口径的下钻查询。第二步在明细表上直接用物化视图覆盖高频查询组合这是最省事的方式。我最终建了三个物化视图-- 日 渠道 CREATE MATERIALIZED VIEW mv_daily_channel AS SELECT dt, channel, SUM(amount) AS gmv, COUNT(order_id) AS order_cnt FROM order_detail GROUP BY dt, channel; -- 日 品类 CREATE MATERIALIZED VIEW mv_daily_category AS SELECT dt, category, SUM(amount) AS gmv, COUNT(order_id) AS order_cnt FROM order_detail GROUP BY dt, category; -- 日 区域 CREATE MATERIALIZED VIEW mv_daily_province AS SELECT dt, province, SUM(amount) AS gmv, COUNT(order_id) AS order_cnt FROM order_detail GROUP BY dt, province;第三步对跨天、跨多个维度的复杂报表在 Doris 外部用定时任务把聚合结果写到一张独立的 Aggregate 汇总表里供看板读取。5.3 优化前后对比调整之后我在测试环境压了一轮结果大概是这样的查询场景优化前耗时优化后耗时数据扫描量变化按天看全渠道 GMV8.5s180ms1.4 亿行 - 约 20 万行按天渠道看 GMV9.2s220ms1.4 亿行 - 约 5 万行按天品类看退款12.6s350ms1.4 亿行 - 约 8 万行按天区域看件数10.1s290ms1.4 亿行 - 约 7 万行这个提升幅度任何没有做预聚合的查询都很难达到。更重要的是查询压力大幅下降之后集群的整体 CPU 使用率也降下来了之前偶尔出现的 Doris OOM 也不再发生了。5.4 数据更新与预聚合的冲突处理预聚合方案最怕的是什么历史数据被修改。订单一旦发生退款、取消明细表里的金额变了物化视图里的汇总值就旧了。Doris 的物化视图在数据导入时会增量更新但如果你用UPDATE直接修改历史明细行情况会复杂很多。我的处理经验是对需要频繁更新的业务数据不要反复 UPDATE 明细。方案是把变更生成一条补偿记录导出到 Doris比如退单就写入一条负数的 amount 记录。这样汇总时 SUM 会自动把退款减掉物化视图能正常增量更新。这个口径需要在数仓上层控制好但换来的收益是预聚合结果始终是正确的。6. 预聚合的边界与常见坑6.1 高基数维度预聚合最容易失效的地方预聚合的本质是用维度组合换数据量减少。如果维度组合的基数非常高比如按 (user_id, 日期) 聚合每个用户每天基本只有一两笔订单聚合前后数据量几乎不变预聚合就失去了意义白白浪费存储和导入开销。判断一个预聚合方案有没有价值可以粗略算一下预聚合后的行数 / 明细行数。如果比值小于 0.1效果明显如果大于 0.5基本别做了直接查明细更快。高基数场景的替代方案通常是改用 Bitmap、HLL 这类近似/精确去重数据结构或者只对高维度做低粒度预聚合比如按天用户聚合而不是按天用户商品聚合。6.2 预聚合和实时性之间的博弈Doris 的导入链路本身是为批量和小批量设计的。每导入一批数据聚合模型和物化视图都要计算一次。如果业务要求秒级实时导入每秒都有几万条明细进来物化视图的计算压力会非常大。我在实时大屏类项目里的经验是不要把实时明细直接灌进带多个物化视图的表。可以先用一个简单的 Duplicate 明细表承接实时数据然后以分钟级或小时级任务做二次聚合喂给预聚合汇总表。这样既能满足准实时需求又不会让预聚合层被拖垮。6.3 查询为什么没命中 Rollup 或物化视图这个问题我遇到太多次了。明明建了物化视图查询还是扫全表。常见原因无非这几种COUNT(*)无法直接复用SUM的聚合结果查询里对维度列做了函数处理比如DATE_FORMAT(dt, %Y-%m)物化视图里没有这个表达式GROUP BY 的维度不是物化视图维度列的子集查询里有物化视图无法表达的计算比如 HAVING 里嵌套了聚合维度顺序和物化视图定义不一致排查方法核心就一条用EXPLAIN看执行计划确认实际扫描的是哪个表/物化视图再反向核对 SQL 的维度组合与物化视图定义。不要凭感觉猜执行计划不会说谎。6.4 Doris 和 ClickHouse 的预聚合选型最近几年总有人问我Doris 和 ClickHouse 到底怎么选。我个人的看法是两者在预聚合思路上有本质差异。ClickHouse 的物化视图本质是导入时触发的异步写入它把聚合结果写到一张独立的物化表里查询端需要知道这张表的存在否则还是查原表本质上是两条链路。Doris 的物化视图和 Rollup 则更接近智能索引查询规划器自动完成改写业务侧无感知。如果团队是给 BI 报表、 Ad-hoc 查询、口径频繁变化的场景用Doris 这种透明改写的模式更省心。如果团队已经把查询 SQL 固化且对流式数据实时聚合有强需求ClickHouse 的物化视图链路也很适合。最怕的是选型之前没想清楚自己到底要的是预聚合索引还是异步汇总任务。从我的实践看Doris 的预聚合最适合的场景就是交互式多维分析这也是它的标签。它把预聚合的复杂度封装在底层让上层写 SQL 的人不需要关心数据是怎么被算好的这是 Doris 区别于其他很多 OLAP 引擎的核心优势。最后再分享一个我个人的经验做预聚合优化永远先看业务访问模式再看技术实现。建物化视图之前先拉一个月的报表查询日志统计出 Top 10 的维度组合和过滤条件。把这 10 个组合覆盖好性能问题基本解决八成。剩下两成才是靠 Rollup、物化视图这些工具去抠细节。技术方案千万条业务场景第一条。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →