版本
Apache Doris 4.0.2-rc02
可复现性
在以下条件下 100% 可复现:
- 异步 MV 定义中无 WHERE 子句
- 查询含 WHERE 子句(如分区列上的范围谓词)
- CBO 选择了该 MV 进行透明改写
Bug 描述
当带 WHERE 子句的查询被透明改写为使用一个无 WHERE 子句的异步物化视图时,MV 改写逻辑会静默丢弃查询的过滤谓词。改写后的计划中不包含 LogicalFilter 节点,导致:
- MV 扫描读取所有分区而非仅匹配 WHERE 条件的分区
- 分区裁剪失效 — 无谓词可用于裁剪
- 查询返回错误结果 — 包含无关分区数据(如历史日期数据)
这是一个静默数据正确性缺陷:不抛出错误、不记录警告,EXPLAIN 计划看似合法(仅缺失本应存在的 Filter 节点)。
复现步骤
环境搭建(脱敏 Schema)
-- 事实表
CREATE TABLE fact_table_a (
date_key INT,
key_col VARCHAR(32),
measure_a DECIMAL(18,2),
measure_b DECIMAL(18,2)
) DUPLICATE KEY(date_key, key_col)
PARTITION BY (date_key)
DISTRIBUTED BY HASH(key_col) BUCKETS 24
PROPERTIES ("replication_num" = "3");
-- 维度表
CREATE TABLE dim_table_b (
key_col VARCHAR(32),
attr_1 INT,
attr_2 VARCHAR(64),
attr_3 VARCHAR(64),
attr_4 VARCHAR(64),
attr_5 VARCHAR(64)
) DUPLICATE KEY(key_col)
DISTRIBUTED BY HASH(key_col) BUCKETS 24
PROPERTIES ("replication_num" = "3");
-- 异步物化视图(无 WHERE 子句)
CREATE MATERIALIZED VIEW mv_table_ab
BUILD DEFERRED REFRESH AUTO ON MANUAL
PARTITION BY (date_key)
DISTRIBUTED BY RANDOM BUCKETS 24
PROPERTIES ("grace_period" = "0")
AS
SELECT fa.date_key, db.attr_1, db.attr_2, db.attr_3,
count(1) AS cnt,
sum(fa.measure_a) AS total_a,
sum(fa.measure_b) AS total_b
FROM fact_table_a fa
LEFT JOIN dim_table_b db ON fa.key_col = db.key_col
GROUP BY fa.date_key, db.attr_1, db.attr_2, db.attr_3;
-- 刷新 MV
REFRESH MATERIALIZED VIEW mv_table_ab AUTO;
触发 Bug
-- 带 WHERE 子句的查询
SELECT date_key, attr_1, attr_2, count(1)
FROM fact_table_a fa
LEFT JOIN dim_table_b db ON fa.key_col = db.key_col
WHERE date_key > 20260301
GROUP BY date_key, attr_1, attr_2
LIMIT 50;
-- 期望:仅 date_key > 20260301 的行
-- 实际(MV 改写后):包含所有 date_key 值的行(包括 20250101)
通过 EXPLAIN 验证
EXPLAIN SELECT date_key, attr_1, attr_2, count(1)
FROM fact_table_a fa
LEFT JOIN dim_table_b db ON fa.key_col = db.key_col
WHERE date_key > 20260301
GROUP BY date_key, attr_1, attr_2
LIMIT 50;
Bug 在 EXPLAIN 中的表现:
- MV 扫描节点显示
partitions=N/N(全部分区)而非裁剪后的子集 - MV 扫描节点无
PREDICATES:行对应 WHERE 条件 - MV 扫描与聚合之间无
LogicalFilter/VFILTER节点 - VAGGREGATE 显示
sum(cnt)(上卷)但其前方无 Filter
正确行为(禁用 MV 改写)
SET SESSION enable_materialized_view_rewrite = false;
EXPLAIN SELECT ... WHERE date_key > 20260301 ...;
-- 显示:PREDICATES: date_key > 20260301
-- 显示:partitions=裁剪数/N(仅匹配分区)
影响评估
| 场景 | 影响 |
|---|---|
| WHERE 条件作用于分区列 | 结果错误 — 包含历史数据 |
| WHERE 条件作用于非分区列 | 结果错误 — 包含未过滤行 |
| WHERE + GROUP BY = MV GROUP BY | 聚合 + Filter 均丢失(Bug #3 + #4) |
| WHERE + GROUP BY 为 MV GROUP BY 子集 | Filter 丢失(Bug #3),聚合通过上卷保留 |
| 无 WHERE | 正确 — 无需谓词补偿 |
| 大 grace_period + 过期分区 | 额外过期数据风险(Bug #5 + #6) |
以下是glm5.1结合代码诊断后的结论,作为参考
根因分析
Bug 链涉及 Nereids MV 改写引擎中 4 个互相关联的缺陷:
Bug 1:SplitPredicate.equals() 复制粘贴错误(Predicates.java:392)
// Predicates.java 第 391-393 行
SplitPredicate that = (SplitPredicate) o;
return Objects.equals(equalPredicateMap, that.equalPredicateMap)
&& Objects.equals(rangePredicateMap, that.residualPredicateMap) // BUG:应为 that.rangePredicateMap
&& Objects.equals(residualPredicateMap, that.residualPredicateMap);
影响:equals() 将 this.rangePredicateMap 与 that.residualPredicateMap 比较,而非 that.rangePredicateMap。后果:
- 违反
equals()/hashCode()契约(hashCode 正确使用了rangePredicateMap) - 可能混淆范围谓词与残差谓词
- 可导致改写逻辑中
SplitPredicate比较失败
修复:将第 392 行改为 Objects.equals(rangePredicateMap, that.rangePredicateMap)。
Bug 2:compensateRangePredicate() keySet 副作用(Predicates.java:212-213)
// Predicates.java 第 212-213 行
Set<Expression> queryRangeSet = querySplitPredicate.getRangePredicateMap().keySet();
queryRangeSet.remove(BooleanLiteral.TRUE); // BUG:永久修改底层 HashMap!
影响:keySet() 返回 HashMap 键的直接视图。对其调用 .remove() 会永久移除底层 rangePredicateMap 中的 TRUE 键。后续对 getRangePredicateMap() 的调用将缺失此谓词,导致在重复调用时静默丢失谓词。
修复:先使用 new HashSet<>(querySplitPredicate.getRangePredicateMap().keySet()) 创建防御性副本再执行 .remove()。
Bug 3:doRewrite() Filter 丢失短路逻辑(AbstractMaterializedViewRule.java:274-275)
// AbstractMaterializedViewRule.java 第 274-275 行
if (compensatePredicates.isAlwaysTrue()) {
rewrittenPlan = mvScan; // BUG:未创建 LogicalFilter!
}
影响:当 compensatePredicates 判定为"恒真"(三个谓词 Map 均为空)时,改写计划完全跳过创建 LogicalFilter 节点。仅在 MV 的谓词已覆盖查询谓词时此逻辑正确。但 Bug #1/#2 导致补偿 Map 错误地为空时,查询的 WHERE 子句被静默丢弃。
触发条件:MV 无 WHERE 子句,查询有 WHERE 子句 → compensateRangePredicate() 计算差异 → 因 Bug #1/#2 结果为空或不正确 → isAlwaysTrue() 返回 true → 无 Filter。
修复:即使 isAlwaysTrue(),仍需验证查询原始谓词已被 MV 谓词覆盖;若未覆盖,始终用未补偿谓词创建 LogicalFilter。
Bug 4:聚合改写丢弃范围谓词(AbstractMaterializedViewAggregateRule.java:128)
// AbstractMaterializedViewAggregateRule.java 第 128 行
ImmutableMap.of(), // BUG:范围谓词被故意不传入 rewriteExpression
影响:执行 groupBy 相等的聚合改写(查询 GROUP BY 等于 MV GROUP BY)时,rewriteExpression() 以 ImmutableMap.of() 作为范围谓词 Map 被调用。这意味着范围补偿谓词(如 data_date > X)在此改写路径中永远不会被应用,即使它们已被正确计算。
修复:传入实际的 compensatePredicates.getRangePredicateMap() 而非 ImmutableMap.of()。
附加缺陷(P1)
Bug 5:宽限期绕过(MTMVRewriteUtil.java:69-72)
// MTMVRewriteUtil.java 第 69-72 行
long gracePeriodMills = mtmv.getGracePeriod();
for (Partition partition : allPartitions) {
if (gracePeriodMills > 0 && currentTimeMills <= (partition.getVisibleVersionTime()
+ gracePeriodMills) && !forceConsistent) {
res.add(partition); // 绕过:将过期分区视为有效
continue;
}
影响:较大的 grace_period 值(如 360000ms = 6 分钟)使所有分区通过资格检查,绕过 isMTMVPartitionSync。analyzeGracePeriod() 无上限校验——任何正数 Long 值均可被接受。
修复:为 grace_period 添加上限校验,并将 forceConsistent 暴露为 Session 变量。
Bug 6:PartitionCompensator 使用被绕过的分区列表(PartitionCompensator.java:93-94)
// PartitionCompensator.java 第 93-94 行
Collection<Partition> mvValidPartitions = cascadesContext.getStatementContext()
.getMvCanRewritePartitionsMap().get(new BaseTableInfo(mtmv));
影响:mvValidPartitions 来自宽限期绕过逻辑。当所有分区均被视为有效时,无需 union 补偿 → 可能提供过期或不完整数据。
修复建议概要
| Bug | 文件 | 行号 | 修复 |
|---|---|---|---|
| #1 | Predicates.java |
392 | 将 that.residualPredicateMap → that.rangePredicateMap |
| #2 | Predicates.java |
212 | 用 new HashSet<>(...) 包装 keySet() 再执行 .remove() |
| #3 | AbstractMaterializedViewRule.java |
274 | 即使 isAlwaysTrue(),仍需验证查询谓词已被覆盖;未覆盖的谓词应作为 LogicalFilter 补充 |
| #4 | AbstractMaterializedViewAggregateRule.java |
128 | 传入 compensatePredicates.getRangePredicateMap() 而非 ImmutableMap.of() |
| #5 | MTMVRewriteUtil.java |
69-72 | 在 MTMVPropertyUtil.analyzeGracePeriod() 中为 grace_period 添加上限校验;将 forceConsistent 暴露为 Session 变量 |
| #6 | PartitionCompensator.java |
93-94 | 使用严格同步检查的分区列表,而非宽限期绕过后的列表 |
关联问题
- 此 Bug 影响
SplitPredicate类,该类是 MV 改写中所有谓词补偿的核心 keySet()副作用 Bug(Bug #2)具有级联影响:TRUE从rangePredicateMap中移除后,对同一StructInfo的所有后续操作均缺失此谓词- Bug #4(聚合改写中的
ImmutableMap.of())可能是为简化 groupBy 相等路径而刻意为之,但它错误地假设范围谓词已被LogicalFilter处理——而 Bug #3 阻止了该 Filter 的创建
源码引用(Doris 4.0.2-rc02)
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/Predicates.javafe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/AbstractMaterializedViewRule.javafe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/AbstractMaterializedViewAggregateRule.javafe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVRewriteUtil.javafe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensator.javafe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVPropertyUtil.javafe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java