Doris 4.1.2 异步物化视图透明改写丢失过滤谓词,导致聚合查询返回错误结果

Viewed 7

使用版本

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
  -> 不使用物化视图
  -> 结果正确

错误执行计划中出现了以下关键现象:

  1. 查询条件中明确存在 dt_year=2026;
  2. 优化后的聚合节点将 dt_year 作为常量 2026 输出,并从 GROUP BY 中移除了 dt_year;
  3. 物化视图 Scan 节点中没有 dt_year=2026 谓词;
  4. Scan 节点扫描了物化视图的全部日期分区;
  5. 最终把其他年份的数据一起聚合,却仍将输出年份标记为 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

初步原因判断

根据错误执行计划,可以确认以下事实:

  1. dt_year IN (2026) 被识别为单值条件;
  2. 优化器把 dt_year 作为固定值列,从聚合键中移除;
  3. 上层通过 final projections 输出常量 2026;
  4. 透明改写到异步物化视图后,MV Scan 没有保留对应的 dt_year=2026 谓词;
  5. 结果是扫描全部年份的数据并只按 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 谓词才是需要解决的正确性问题。

希望社区协助确认

  1. 这是否是 Doris 4.1.2 已知的异步物化视图透明改写正确性问题?
  2. master、4.1.x 或其他维护分支是否已有对应修复?
  3. 是否应在常量传播或聚合键消除之后,继续为 MV Scan 保留 dt_year=2026 补偿谓词?
  4. 为什么顶层 LogicalLimit 的存在会使同一个 MV 走到不同的扫描或谓词裁剪路径?
  5. 除 NO_USE_MV、关闭透明改写或设置 use_for_rewrite=false 外,是否有粒度更合适的临时规避方式?
1 Answers

-- 重跑一下原 SQL 和 EXPLAIN VERBOSE,对照关闭 MV 改写的结果
SET enable_sql_cache=false;
SET enable_materialized_view_rewrite=true;
SET disable_nereids_rules='CONSTANT_PROPAGATION';

方便的话加我主页微信,给我同步一下原SQL 和 DDL 语句之类的