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]