使用版本
Apache Doris 4.1.2-rc01-aec169d2025
Server version: 5.7.99 doris version doris-4.1.2-rc01-aec169d2025
问题类型:Nereids 异步物化视图透明改写 / 查询结果正确性。
影响程度:高。查询可以正常执行且不会报错,但返回的聚合值错误。
问题现象
一条包含年份过滤和分组聚合的查询,在开启异步物化视图透明改写时会命中物化视图,但结果错误;关闭透明改写后,查询基表得到正确结果。
enable_materialized_view_rewrite=true
-> 命中异步物化视图
-> 结果错误
enable_materialized_view_rewrite=false
-> 不使用物化视图
-> 结果正确
错误执行计划中出现了以下关键现象:
- 查询条件中明确存在
dt_year=2026; - 优化后的聚合节点将
dt_year作为常量2026输出,并从GROUP BY中移除了dt_year; - 物化视图 Scan 节点中没有
dt_year=2026谓词; - Scan 节点扫描了物化视图的全部日期分区;
- 最终把其他年份的数据一起聚合,却仍将输出年份标记为
2026。
因此,这是静默错误结果,不只是分区裁剪失效或性能问题。
脱敏后的查询结构
以下库名、表名、字段名及业务值均已脱敏,查询结构与问题 SQL 一致:
SELECT
dt_year,
org_group,
SUM(amount)
FROM (
SELECT
f.amount,
d.dt_year,
d.dt_quarter,
a.org_group,
a.account_type
FROM demo_db.fact_transaction f
LEFT JOIN demo_db.dim_date d
ON d.data_date = f.data_date
LEFT JOIN demo_db.dim_account a
ON a.account_id = f.account_id
-- 其他维表关联省略
) q
WHERE dt_year IN (2026)
AND org_group IN ('GROUP_A', 'GROUP_B', 'GROUP_C')
AND dt_quarter <= 3
AND account_type <> 'EXCLUDED_TYPE'
GROUP BY dt_year, org_group;
异步物化视图已预聚合事实表数据,包含以下查询所需的维度和指标:
dt_year
dt_quarter
org_group
account_type
SUM(amount)
物化视图按事实表的日期字段进行日分区。下文统一使用脱敏名称:
demo_db.mv_fact_transaction_daily
预期行为
透明改写只能改变查询所使用的执行路径,不能改变查询语义。开启或关闭透明改写时,都应该只汇总 dt_year=2026 的数据并返回相同结果。
实际行为
以其中一个分组为例:
关闭透明改写:结果为 X,正确
开启透明改写:结果约为 17.33 * X,错误
开启透明改写后的结果包含了年份范围之外的数据,但查询执行成功且没有任何错误提示。
核心证据一:物化视图开关 A/B
在同一客户端会话中,对同一条业务 SQL 关闭 SQL Cache 后进行对照:
SET enable_sql_cache=false;
SET enable_materialized_view_rewrite=true;
-- 执行业务 SQL:结果错误
SET enable_materialized_view_rewrite=false;
-- 再次执行相同业务 SQL:结果正确
关键执行信息如下:
| 项目 | 开启透明改写 | 关闭透明改写 |
|---|---|---|
chosenMViews |
mv_fact_transaction_daily |
空 |
HitSqlCache |
false |
false |
ScanRows |
164,775,601 | 504,110,671 |
| 返回行数 | 12 | 12 |
| 查询结果 | 错误 | 正确 |
将问题物化视图设置为不参与透明改写后,查询结果同样恢复正确。
这说明错误结果与透明改写到该异步物化视图直接相关。
核心证据二:错误执行计划丢失年份谓词
开启透明改写时,EXPLAIN VERBOSE 显示物化视图改写成功:
MaterializedViewRewriteSuccessAndChose:
CBO.internal.demo_db.mv_fact_transaction_daily chose
脱敏后的核心执行计划如下:
VAGGREGATE (merge finalize)
group by: org_group
final projections: 2026, org_group, sum(amount)
VOlapScanNode
TABLE: demo_db.mv_fact_transaction_daily
PREDICATES:
org_group IN ('GROUP_A', 'GROUP_B', 'GROUP_C')
AND dt_quarter <= 3
AND account_type != 'EXCLUDED_TYPE'
partitions=2863/2863
这里存在明显的不一致:
上层 final projections 把 dt_year 固定输出为 2026;
下层 MV Scan 却没有 dt_year=2026 谓词;
MV Scan 实际扫描了 2863/2863 个日期分区。
原查询的 GROUP BY dt_year, org_group 被优化为只按 org_group 聚合,因此扫描到的其他年份数据会被一起汇总,最终再统一输出为年份 2026。
核心证据三:物化视图数据本身正确
已经对物化视图执行完整刷新,状态为:
State=NORMAL
RefreshState=SUCCESS
SyncWithBaseTables=1
刷新后问题仍然能够稳定复现。
直接查询物化视图,并显式保留年份条件:
SELECT
dt_year,
org_group,
SUM(amount)
FROM demo_db.mv_fact_transaction_daily
WHERE dt_year=2026
AND org_group IN ('GROUP_A', 'GROUP_B', 'GROUP_C')
AND dt_quarter <= 3
AND account_type <> 'EXCLUDED_TYPE'
GROUP BY dt_year, org_group;
该结果与关闭透明改写后的基表查询一致。因此,物化视图中的 2026 年数据本身正确,问题发生在透明改写后的谓词保留或补偿过程中。
辅助证据:顶层 LIMIT 会改变是否出现错误计划
排查过程中还发现,在同一个物理连接、相同 SQL 主体、相同 sql_mode 下,仅改变会话变量 sql_select_limit,就会出现不同结果:
| 项目 | sql_select_limit=200 |
sql_select_limit=DEFAULT |
|---|---|---|
| 命中的 MV | mv_fact_transaction_daily |
mv_fact_transaction_daily |
HitSqlCache |
false |
false |
ScanRows |
26,016,016 | 164,775,601 |
| 返回行数 | 12 | 12 |
| 查询结果 | 正确 | 错误 |
业务查询只返回 12 行,因此上限 200 不会截断本次结果。该现象不是问题本身,也不建议把 sql_select_limit=200 当作正式修复;它只是进一步说明:查询命中同一个物化视图时,不同的逻辑计划形态会进入不同的 MV 扫描或谓词裁剪路径,其中一条路径丢失了年份条件。
已排除因素
1. SQL Cache
所有关键对照查询均显式设置:
SET enable_sql_cache=false;
查询执行信息中的 HitSqlCache 均为 false。
2. 不同客户端或连接
sql_select_limit 的辅助对照是在同一个全新物理连接中串行执行的。透明改写开关 A/B 也可以在同一客户端会话中稳定复现。
3. 物化视图未刷新或数据错误
完整刷新后问题仍然存在;直接查询物化视图并显式添加 dt_year=2026 时结果正确。
4. sql_mode 差异
关键对照查询均使用相同的:
ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES
初步原因判断
根据错误执行计划,可以确认以下事实:
dt_year IN (2026)被识别为单值条件;- 优化器把
dt_year作为固定值列,从聚合键中移除; - 上层通过
final projections输出常量2026; - 透明改写到异步物化视图后,MV Scan 没有保留对应的
dt_year=2026谓词; - 结果是扫描全部年份的数据并只按
org_group聚合。
因此,问题应发生在 Nereids 异步物化视图透明改写过程中:常量传播或聚合键消除之后,原查询的年份过滤条件没有被正确保留为 MV Scan 的补偿谓词。
具体是在哪一条改写规则或候选计划路径中丢失,仍需要社区开发者进一步确认。
与 sql_select_limit 辅助现象相关的源码
Doris 4.1.2 中,sql_select_limit 默认值为 Long.MAX_VALUE:
fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java:1229-1230
fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java:3975-3979
设置有限值时,AddDefaultLimit 会在逻辑计划顶层增加 LogicalLimit:
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/AddDefaultLimit.java:48-55
在 Rewriter.getWholeTreeRewriteJobs() 中,相关执行顺序为:
ADD_DEFAULT_LIMIT
RECORD_PLAN_FOR_MV_PRE_REWRITE
对应代码位置:
fe/fe-core/src/main/java/org/apache/doris/nereids/jobs/executor/Rewriter.java:903-908
这说明顶层 LogicalLimit 会在记录 MV 预改写计划之前加入,从而可能改变后续改写路径。但这一点只是辅助定位信息,透明改写丢失 dt_year=2026 谓词才是需要解决的正确性问题。
希望社区协助确认
- 这是否是 Doris 4.1.2 已知的异步物化视图透明改写正确性问题?
- master、4.1.x 或其他维护分支是否已有对应修复?
- 是否应在常量传播或聚合键消除之后,继续为 MV Scan 保留
dt_year=2026补偿谓词? - 为什么顶层
LogicalLimit的存在会使同一个 MV 走到不同的扫描或谓词裁剪路径? - 除
NO_USE_MV、关闭透明改写或设置use_for_rewrite=false外,是否有粒度更合适的临时规避方式?