yujun777 opened a new pull request, #68861:
URL: https://github.com/apache/doris/pull/68861

   ### What problem does this PR solve?
   
   Issue Number: N/A
   
   Problem Summary:
   
   A scalar aggregate query can be answered by a **grouped** aggregate 
materialized view.
   `AbstractMaterializedViewAggregateRule#isGroupByEquals` treats the query and 
the view grouping as
   equal when a uniform function dependency proves the filtered view holds at 
most one group, and then
   eliminates the aggregate entirely, leaving a bare view scan. With
   
   ```sql
   select count(*) from base where k = 9999;   -- k = 9999 matches no row
   ```
   
   the rewritten plan was `filter -> OlapScan[mv]` with no aggregate node at 
all, so the query returned
   an **empty result set** instead of one row of `0`. "At most one group" does 
not imply "one group
   exists", while a scalar aggregate must always emit exactly one row.
   
   COUNT is the function that hits this: its argument is not a real column, so 
it is the only aggregate
   whose output the fast path can substitute directly with a view slot. 
`SUM`/`MIN`/`MAX` over the same
   view already fall through to the roll up path and return the correct single 
row.
   
   The roll up path has a second, related gap: `Count.constructRollUp` produces 
`SUM`, and `SUM` returns
   NULL for an empty input whereas `COUNT` guarantees 0. Keeping the aggregate 
alone would only turn the
   empty result set into a wrong `NULL`.
   
   ### What is changed and how does it work?
   
   1. `isGroupByEquals`: reject the aggregate-eliminating fast path when the 
query is a scalar aggregate
      and the view is grouped, so the rewrite goes through the roll up path and 
keeps the aggregate node.
      The `query scalar + view scalar` combination is untouched — there the 
view always holds exactly one
      row, so eliminating the aggregate is safe.
   2. roll up: when the query function guarantees a non-null empty-input result 
and its roll up does not,
      wrap the aggregate output with `Nvl(slot, resultForEmptyInput())` in the 
project above the aggregate.
      This reuses `NotNullableAggregateFunction#resultForEmptyInput`, the same 
mechanism `SubqueryToApply`
      uses to force one row of 0 for a scalar subquery. A roll up that keeps 
the guarantee (e.g.
      `count distinct` -> `bitmap_union_count`) is left alone, so no redundant 
`ifnull` is introduced.
   
   Before / after for the query above:
   
   ```
   empty set
   ```
   
   ```
   +----------+
   | count(*) |
   +----------+
   |        0 |
   +----------+
   ```
   
   and the plan now keeps the aggregate over the view:
   
   ```
   PhysicalResultSink
   --PhysicalProject
   ----hashAgg[GLOBAL]
   ------PhysicalProject
   --------filter((mv.k = 9999))
   ----------PhysicalOlapScan[mv]
   ```
   
   ### Release note
   
   A scalar `COUNT` rewritten by a grouped aggregate materialized view now 
returns `0` instead of an empty
   result set when the query predicate matches no row.
   
   ### Check List (For Author)
   
   - Test
       - [x] Regression test
       - [ ] Unit Test
       - [ ] Manual test (add detailed scripts or steps below)
       - [ ] No need to test or manual test. Explain why:
   
     New suite `nereids_rules_p0/mv/agg_scalar_empty_input` compares scalar 
`COUNT`/`SUM`/`MIN`/`MAX`
     rewritten by a grouped MV against the same queries with the rewrite 
disabled, covering the empty-input
     row, the non-empty roll up values, `count(*) + 1` over an empty input, a 
`count(distinct)` that the MV
     must not answer, and the plan shape that keeps the aggregate. It also pins 
the `query grouped + view
     grouped` uniform case that must still eliminate the aggregate. The suite 
fails on an unpatched FE with
     `Check tag 'count_on_empty' failed ... real line is empty, but expect is 
0`.
     No FE unit test is added: whether the MV is picked at all is decided by 
the cost model over table
     statistics, so a mocked-catalog assertion on the rewritten plan is not 
stable, while the regression
     suite covers the same path end to end.
     Existing aggregate-elimination and roll up suites under 
`nereids_rules_p0/mv`
     (`agg_optimize_when_uniform`, `aggregate_with_roll_up`, 
`aggregate_without_roll_up`, `agg_on_none_agg`,
     `agg_variety`, `any_value_roll_up`, `single_table_without_aggregate`) and 
the aggregate-related suites
     under `mv_p0` (`count_star`, `sum_count`, `agg_no_group`, `dis_26495`, 
`test_28741`,
     `multi_agg_with_same_slot`, `test_approx_count_distinct`, 
`agg_have_dup_base`, `sum_devide_count`,
     `agg_table_mv`, `agg_use_key_direct`) all pass.
   
   - Behavior changed:
       - [ ] No.
       - [x] Yes. A scalar aggregate rewritten by a grouped aggregate MV now 
keeps its aggregate node and
         rolls the view function up, so an empty input yields the function's 
guaranteed result (`COUNT` -> 0)
         instead of no row. Non-empty results are unchanged; the rewritten plan 
gains one aggregate node for
         the affected queries.
   
   - Does this need documentation?
       - [x] No.
       - [ ] Yes.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to