This is an automated email from the ASF dual-hosted git repository.
yujun777 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 1d03523339f [feature](ivm) Track MIN/MAX extreme counts for
incremental refresh (#69286)
1d03523339f is described below
commit 1d03523339fbd6ec27b70e58b6d45e06bc196c1e
Author: yujun <[email protected]>
AuthorDate: Tue Oct 20 16:13:13 2026 +0800
[feature](ivm) Track MIN/MAX extreme counts for incremental refresh (#69286)
Incremental refresh of a MIN/MAX materialized view currently falls back
to a
complete refresh whenever a deleted row could be the current extreme.
Deleting
one of several rows that share the minimum value is a common case, yet
the MV
does not remember how many rows hold that extreme, so the refresh cannot
tell
that the extreme survives.
This PR adds `COUNT_AT_MIN` / `COUNT_AT_MAX` and stores that count as a
hidden
IVM state column. The delta plan now emits the count at the insert-side
and
delete-side extreme. Apply keeps the old extreme while its surviving
count is
positive; only deleting the last row at the extreme still falls back to
a
complete refresh.
Trace issue: https://github.com/apache/doris/issues/65418
### What changed?
- Add public `COUNT_AT_MIN(expr)` / `COUNT_AT_MAX(expr)` aggregate
functions.
They return the number of input rows equal to the group's
minimum/maximum,
ignore NULLs, return 0 for empty or all-NULL input, and reject DISTINCT.
They are equivalent to Oracle's
`COUNT(*) KEEP (DENSE_RANK FIRST/LAST ORDER BY expr)`.
- Add `MIN_COUNT` / `MAX_COUNT` IVM hidden state keys and hidden columns
named
in the existing `__DORIS_IVM_AGG_{ordinal}_{STATE}_COL__` format.
- Emit insert/delete extreme counts in the delta aggregate.
- Use the surviving extreme count in the apply guard and update the
hidden
count state.
- Add BE/FE unit coverage and a regression suite.
---
.../aggregate_function_count_at_min_max.cpp | 178 +++++++++++++++++++
.../aggregate_function_count_at_min_max.h | 167 ++++++++++++++++++
.../aggregate_function_simple_factory.cpp | 2 +
be/src/exprs/aggregate/single_value_data.h | 10 ++
be/src/exprs/function/function_dict_get_many.cpp | 1 +
.../exprs/aggregate/agg_count_at_min_max_test.cpp | 163 ++++++++++++++++++
.../doris/catalog/BuiltinAggregateFunctions.java | 4 +
.../mtmv/ivm/agg/IvmAggExpressionBuilder.java | 2 +
.../mtmv/ivm/agg/IvmAggExtremalProcessor.java | 191 +++++++++++++++++----
.../doris/mtmv/ivm/agg/IvmAggFunctionKind.java | 2 +
.../mtmv/ivm/agg/IvmAggFunctionProcessor.java | 6 +
.../doris/mtmv/ivm/agg/IvmAggMaxProcessor.java | 31 ++--
.../doris/mtmv/ivm/agg/IvmAggMinProcessor.java | 31 ++--
.../apache/doris/mtmv/ivm/agg/IvmAggStateKey.java | 4 +-
.../expressions/functions/agg/CountAtMax.java | 113 ++++++++++++
.../expressions/functions/agg/CountAtMin.java | 113 ++++++++++++
.../visitor/AggregateFunctionVisitor.java | 10 ++
.../doris/mtmv/ivm/IvmAggDeltaHandlerTest.java | 10 +-
.../mtmv/ivm/IvmNormalizeUniqKeysMTMVTest.java | 14 +-
.../doris/mtmv/ivm/agg/IvmAggMaxProcessorTest.java | 30 +++-
.../doris/mtmv/ivm/agg/IvmAggMinProcessorTest.java | 30 +++-
.../mtmv/ivm/agg/IvmAggProcessorTestBase.java | 23 +++
.../mtmv_p0/ivm/test_ivm_agg_expr_over_agg_1.out | 2 +
.../mtmv_p0/ivm/test_ivm_agg_expr_over_agg_2.out | 2 +
.../data/mtmv_p0/ivm/test_ivm_minmax_count.out | 25 +++
.../mtmv_p0/ivm/test_ivm_minmax_count_join.out | 13 ++
.../data/query_p0/aggregate/count_at_min_max.out | 36 ++++
.../mtmv_p0/ivm/test_ivm_minmax_count.groovy | 162 +++++++++++++++++
.../mtmv_p0/ivm/test_ivm_minmax_count_join.groovy | 148 ++++++++++++++++
.../query_p0/aggregate/count_at_min_max.groovy | 126 ++++++++++++++
30 files changed, 1564 insertions(+), 85 deletions(-)
diff --git a/be/src/exprs/aggregate/aggregate_function_count_at_min_max.cpp
b/be/src/exprs/aggregate/aggregate_function_count_at_min_max.cpp
new file mode 100644
index 00000000000..df67ac115a3
--- /dev/null
+++ b/be/src/exprs/aggregate/aggregate_function_count_at_min_max.cpp
@@ -0,0 +1,178 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "exprs/aggregate/aggregate_function_count_at_min_max.h"
+
+#include "common/exception.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/data_type/define_primitive_type.h"
+#include "core/data_type/primitive_type.h"
+#include "exprs/aggregate/aggregate_function_simple_factory.h"
+#include "exprs/aggregate/factory_helpers.h"
+#include "exprs/aggregate/helpers.h"
+
+namespace doris {
+
+namespace {
+
+template <template <typename> class ExtremeData>
+AggregateFunctionPtr create_aggregate_function_count_at_extreme(const String&
name,
+ const
DataTypes& argument_types,
+ const
DataTypePtr& result_type,
+ const bool
result_is_nullable,
+ const
AggregateFunctionAttr& attr) {
+ assert_arity_range(name, argument_types, 1, 1);
+ switch (argument_types[0]->get_primitive_type()) {
+ case PrimitiveType::TYPE_STRING:
+ case PrimitiveType::TYPE_CHAR:
+ case PrimitiveType::TYPE_VARCHAR:
+ case PrimitiveType::TYPE_JSONB:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+
AggregateFunctionsCountAtExtreme<ExtremeData<SingleValueDataString>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_DATE:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+
AggregateFunctionsCountAtExtreme<ExtremeData<SingleValueDataFixed<TYPE_DATE>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_DATETIME:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+
AggregateFunctionsCountAtExtreme<ExtremeData<SingleValueDataFixed<TYPE_DATETIME>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_DATEV2:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+
AggregateFunctionsCountAtExtreme<ExtremeData<SingleValueDataFixed<TYPE_DATEV2>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_DATETIMEV2:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+ AggregateFunctionsCountAtExtreme<
+ ExtremeData<SingleValueDataFixed<TYPE_DATETIMEV2>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_TIMESTAMP_NS:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+ AggregateFunctionsCountAtExtreme<
+ ExtremeData<SingleValueDataFixed<TYPE_TIMESTAMP_NS>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_TIMESTAMPTZ:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+ AggregateFunctionsCountAtExtreme<
+ ExtremeData<SingleValueDataFixed<TYPE_TIMESTAMPTZ>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_TIMEV2:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+
AggregateFunctionsCountAtExtreme<ExtremeData<SingleValueDataFixed<TYPE_TIMEV2>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_IPV4:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+
AggregateFunctionsCountAtExtreme<ExtremeData<SingleValueDataFixed<TYPE_IPV4>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_IPV6:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+
AggregateFunctionsCountAtExtreme<ExtremeData<SingleValueDataFixed<TYPE_IPV6>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_UUID:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+
AggregateFunctionsCountAtExtreme<ExtremeData<SingleValueDataFixed<TYPE_UUID>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_BOOLEAN:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+
AggregateFunctionsCountAtExtreme<ExtremeData<SingleValueDataFixed<TYPE_BOOLEAN>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_TINYINT:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+
AggregateFunctionsCountAtExtreme<ExtremeData<SingleValueDataFixed<TYPE_TINYINT>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_SMALLINT:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+
AggregateFunctionsCountAtExtreme<ExtremeData<SingleValueDataFixed<TYPE_SMALLINT>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_INT:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+
AggregateFunctionsCountAtExtreme<ExtremeData<SingleValueDataFixed<TYPE_INT>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_BIGINT:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+
AggregateFunctionsCountAtExtreme<ExtremeData<SingleValueDataFixed<TYPE_BIGINT>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_LARGEINT:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+
AggregateFunctionsCountAtExtreme<ExtremeData<SingleValueDataFixed<TYPE_LARGEINT>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_FLOAT:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+
AggregateFunctionsCountAtExtreme<ExtremeData<SingleValueDataFixed<TYPE_FLOAT>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_DOUBLE:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+
AggregateFunctionsCountAtExtreme<ExtremeData<SingleValueDataFixed<TYPE_DOUBLE>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_DECIMAL32:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+ AggregateFunctionsCountAtExtreme<
+ ExtremeData<SingleValueDataFixed<TYPE_DECIMAL32>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_DECIMAL64:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+ AggregateFunctionsCountAtExtreme<
+ ExtremeData<SingleValueDataFixed<TYPE_DECIMAL64>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_DECIMALV2:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+ AggregateFunctionsCountAtExtreme<
+ ExtremeData<SingleValueDataFixed<TYPE_DECIMALV2>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_DECIMAL128I:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+ AggregateFunctionsCountAtExtreme<
+ ExtremeData<SingleValueDataFixed<TYPE_DECIMAL128I>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_DECIMAL256:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+ AggregateFunctionsCountAtExtreme<
+ ExtremeData<SingleValueDataFixed<TYPE_DECIMAL256>>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_ARRAY:
+ case PrimitiveType::TYPE_MAP:
+ case PrimitiveType::TYPE_STRUCT:
+ case PrimitiveType::TYPE_AGG_STATE:
+ case PrimitiveType::TYPE_BITMAP:
+ case PrimitiveType::TYPE_HLL:
+ case PrimitiveType::TYPE_QUANTILE_STATE:
+ return
creator_without_type::create_unary_arguments_return_not_nullable<
+
AggregateFunctionsCountAtExtreme<ExtremeData<SingleValueDataColumn>>>(
+ argument_types, result_is_nullable, attr);
+ case PrimitiveType::TYPE_VARBINARY:
+ throw Exception(ErrorCode::NOT_IMPLEMENTED_ERROR,
+ "Aggregate function {} does not support "
+ "VARBINARY arguments",
+ name);
+ default:
+ return nullptr;
+ }
+}
+
+} // namespace
+
+void
register_aggregate_function_count_at_min_max(AggregateFunctionSimpleFactory&
factory) {
+ factory.register_function_both(
+ "count_at_min",
+
create_aggregate_function_count_at_extreme<AggregateFunctionCountAtMinData>);
+ factory.register_function_both(
+ "count_at_max",
+
create_aggregate_function_count_at_extreme<AggregateFunctionCountAtMaxData>);
+}
+
+} // namespace doris
diff --git a/be/src/exprs/aggregate/aggregate_function_count_at_min_max.h
b/be/src/exprs/aggregate/aggregate_function_count_at_min_max.h
new file mode 100644
index 00000000000..91d327947f9
--- /dev/null
+++ b/be/src/exprs/aggregate/aggregate_function_count_at_min_max.h
@@ -0,0 +1,167 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#pragma once
+
+#include <cstdint>
+#include <memory>
+#include <string>
+
+#include "core/column/column_vector.h"
+#include "core/data_type/data_type.h"
+#include "core/data_type/data_type_number.h"
+#include "core/types.h"
+#include "exprs/aggregate/aggregate_function.h"
+#include "exprs/aggregate/single_value_data.h"
+
+namespace doris {
+
+/**
+ * State shared by COUNT_AT_MIN and COUNT_AT_MAX: the current extreme value
and the
+ * number of input rows that are equal to it. The wrapped Data type owns the
value and
+ * decides whether a new value is better (smaller for MIN, greater for MAX).
+ */
+template <typename Data>
+struct AggregateFunctionCountAtMinData : Data {
+ using Self = AggregateFunctionCountAtMinData;
+ int64_t count = 0;
+
+ AggregateFunctionCountAtMinData() { reset(); }
+
+ void change_if_better(const IColumn& column, size_t row_num, Arena& arena)
{
+ if (!this->has()) {
+ this->set(column, row_num, arena);
+ count = 1;
+ } else if (this->is_equal_to(column, row_num)) {
+ ++count;
+ } else if (this->set_if_smaller(column, row_num, arena)) {
+ count = 1;
+ }
+ }
+
+ void change_if_better(const Self& to, Arena& arena) {
+ if (!to.has()) {
+ return;
+ }
+ if (!this->has()) {
+ this->set(to, arena);
+ count = to.count;
+ } else if (this->is_equal_to(to)) {
+ count += to.count;
+ } else if (this->set_if_smaller(to, arena)) {
+ count = to.count;
+ }
+ }
+
+ void reset() {
+ Data::reset();
+ count = 0;
+ }
+
+ static const char* name() { return "count_at_min"; }
+};
+
+template <typename Data>
+struct AggregateFunctionCountAtMaxData : Data {
+ using Self = AggregateFunctionCountAtMaxData;
+ int64_t count = 0;
+
+ AggregateFunctionCountAtMaxData() { reset(); }
+
+ void change_if_better(const IColumn& column, size_t row_num, Arena& arena)
{
+ if (!this->has()) {
+ this->set(column, row_num, arena);
+ count = 1;
+ } else if (this->is_equal_to(column, row_num)) {
+ ++count;
+ } else if (this->set_if_greater(column, row_num, arena)) {
+ count = 1;
+ }
+ }
+
+ void change_if_better(const Self& to, Arena& arena) {
+ if (!to.has()) {
+ return;
+ }
+ if (!this->has()) {
+ this->set(to, arena);
+ count = to.count;
+ } else if (this->is_equal_to(to)) {
+ count += to.count;
+ } else if (this->set_if_greater(to, arena)) {
+ count = to.count;
+ }
+ }
+
+ void reset() {
+ Data::reset();
+ count = 0;
+ }
+
+ static const char* name() { return "count_at_max"; }
+};
+
+/**
+ * Aggregate function returning the number of rows equal to the minimum (or
maximum)
+ * value in the group. NULL inputs are ignored. An empty group, or a group
whose inputs
+ * are all NULL, returns 0.
+ */
+template <typename Data>
+class AggregateFunctionsCountAtExtreme final
+ : public IAggregateFunctionDataHelper<Data,
AggregateFunctionsCountAtExtreme<Data>> {
+private:
+ using Base = IAggregateFunctionDataHelper<Data,
AggregateFunctionsCountAtExtreme<Data>>;
+
+public:
+ explicit AggregateFunctionsCountAtExtreme(const DataTypes& arguments) :
Base(arguments) {}
+
+ String get_name() const override { return Data::name(); }
+
+ DataTypePtr get_return_type() const override { return
std::make_shared<DataTypeInt64>(); }
+
+ void create(AggregateDataPtr __restrict place) const override { new
(place) Data; }
+
+ void add(AggregateDataPtr __restrict place, const IColumn** columns,
ssize_t row_num,
+ Arena& arena) const override {
+ this->data(place).change_if_better(*columns[0], row_num, arena);
+ }
+
+ void reset(AggregateDataPtr place) const override {
this->data(place).reset(); }
+
+ void merge(AggregateDataPtr __restrict place, ConstAggregateDataPtr rhs,
+ Arena& arena) const override {
+ this->data(place).change_if_better(this->data(rhs), arena);
+ }
+
+ void serialize(ConstAggregateDataPtr __restrict place, BufferWritable&
buf) const override {
+ this->data(place).write(buf, this->argument_types[0],
IAggregateFunction::version);
+ buf.write_binary(this->data(place).count);
+ }
+
+ void deserialize(AggregateDataPtr __restrict place, BufferReadable& buf,
+ Arena& arena) const override {
+ this->data(place).read(buf, this->argument_types[0],
IAggregateFunction::version, arena);
+ buf.read_binary(this->data(place).count);
+ }
+
+ void insert_result_into(ConstAggregateDataPtr __restrict place, IColumn&
to) const override {
+ assert_cast<ColumnInt64&,
TypeCheckOnRelease::DISABLE>(to).insert_value(
+ this->data(place).count);
+ }
+};
+
+} // namespace doris
diff --git a/be/src/exprs/aggregate/aggregate_function_simple_factory.cpp
b/be/src/exprs/aggregate/aggregate_function_simple_factory.cpp
index e0e48cef8c4..49f3da1f981 100644
--- a/be/src/exprs/aggregate/aggregate_function_simple_factory.cpp
+++ b/be/src/exprs/aggregate/aggregate_function_simple_factory.cpp
@@ -32,6 +32,7 @@ void
register_aggregate_function_combinator_foreachv2(AggregateFunctionSimpleFac
void register_aggregate_function_sum(AggregateFunctionSimpleFactory& factory);
void register_aggregate_function_minmax(AggregateFunctionSimpleFactory&
factory);
+void
register_aggregate_function_count_at_min_max(AggregateFunctionSimpleFactory&
factory);
void register_aggregate_function_max_min_by(AggregateFunctionSimpleFactory&
factory);
void register_aggregate_function_avg(AggregateFunctionSimpleFactory& factory);
void register_aggregate_function_count(AggregateFunctionSimpleFactory&
factory);
@@ -89,6 +90,7 @@ AggregateFunctionSimpleFactory&
AggregateFunctionSimpleFactory::instance() {
std::call_once(oc, [&]() {
register_aggregate_function_sum(instance);
register_aggregate_function_minmax(instance);
+ register_aggregate_function_count_at_min_max(instance);
register_aggregate_function_max_min_by(instance);
register_aggregate_function_avg(instance);
register_aggregate_function_count(instance);
diff --git a/be/src/exprs/aggregate/single_value_data.h
b/be/src/exprs/aggregate/single_value_data.h
index ff947577d5d..798725414f9 100644
--- a/be/src/exprs/aggregate/single_value_data.h
+++ b/be/src/exprs/aggregate/single_value_data.h
@@ -137,6 +137,10 @@ public:
return Compare::equal(_value_at(column, row_num), value);
}
+ bool is_equal_to(const Self& to) const {
+ return has() && to.has() && Compare::equal(value, to.value);
+ }
+
bool set_if_greater(const Self& to, Arena& arena) {
if (to.has() && (!has() || Compare::greater(to.value, value))) {
set(to, arena);
@@ -301,6 +305,8 @@ public:
}
return _value_at(column, row_num) == _value();
}
+
+ bool is_equal_to(const Self& to) const { return has() && to.has() &&
_value() == to._value(); }
};
static_assert(sizeof(SingleValueDataString) ==
SingleValueDataString::AUTOMATIC_STORAGE_SIZE);
@@ -412,6 +418,10 @@ public:
bool is_equal_to(const IColumn& column, size_t row_num) const {
return has() && column.compare_at(row_num, 0, *_value, 1) == 0;
}
+
+ bool is_equal_to(const Self& to) const {
+ return has() && to.has() && _value->compare_at(0, 0, *to._value, 1) ==
0;
+ }
};
} // namespace doris
diff --git a/be/src/exprs/function/function_dict_get_many.cpp
b/be/src/exprs/function/function_dict_get_many.cpp
index e077041b98c..a2cb5a4d1f7 100644
--- a/be/src/exprs/function/function_dict_get_many.cpp
+++ b/be/src/exprs/function/function_dict_get_many.cpp
@@ -32,6 +32,7 @@
#include "exprs/function/dictionary_factory.h"
#include "exprs/function/function.h"
#include "exprs/function/simple_function_factory.h"
+#include "runtime/exec_env.h"
namespace doris {
diff --git a/be/test/exprs/aggregate/agg_count_at_min_max_test.cpp
b/be/test/exprs/aggregate/agg_count_at_min_max_test.cpp
new file mode 100644
index 00000000000..db0bc9eabf3
--- /dev/null
+++ b/be/test/exprs/aggregate/agg_count_at_min_max_test.cpp
@@ -0,0 +1,163 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <gtest/gtest.h>
+
+#include <cstring>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "core/column/column.h"
+#include "core/column/column_string.h"
+#include "core/column/column_vector.h"
+#include "core/data_type/data_type_number.h"
+#include "core/data_type/data_type_string.h"
+#include "core/types.h"
+#include "exprs/aggregate/aggregate_function.h"
+#include "exprs/aggregate/aggregate_function_simple_factory.h"
+#include "exprs/aggregate/single_value_data.h"
+
+namespace doris {
+
+void
register_aggregate_function_count_at_min_max(AggregateFunctionSimpleFactory&
factory);
+
+namespace {
+
+AggregateFunctionPtr create_function(AggregateFunctionSimpleFactory& factory,
+ const std::string& name, const DataTypes&
argument_types) {
+ auto function = factory.get(name, argument_types, nullptr, false, -1);
+ EXPECT_NE(function, nullptr);
+ return function;
+}
+
+void aggregate_int32(IAggregateFunction* function, Arena& arena, const
IColumn& column,
+ int64_t expected) {
+ std::unique_ptr<char[]> memory(new char[function->size_of_data()]);
+ AggregateDataPtr place = memory.get();
+ function->create(place);
+ const IColumn* columns[1] = {&column};
+ for (size_t row = 0; row < column.size(); ++row) {
+ function->add(place, columns, row, arena);
+ }
+ ColumnInt64 result;
+ function->insert_result_into(place, result);
+ EXPECT_EQ(expected, result.get_element(0));
+ function->destroy(place);
+}
+
+} // namespace
+
+TEST(CountAtMinMaxTest, Int32) {
+ AggregateFunctionSimpleFactory factory;
+ register_aggregate_function_count_at_min_max(factory);
+ DataTypes data_types = {std::make_shared<DataTypeInt32>()};
+
+ auto column = ColumnInt32::create();
+ for (int value : {1, 2, 1, 3, 1, 2}) {
+ column->insert_value(value);
+ }
+
+ Arena arena;
+ auto count_at_min = create_function(factory, "count_at_min", data_types);
+ aggregate_int32(count_at_min.get(), arena, *column, 3);
+
+ auto count_at_max = create_function(factory, "count_at_max", data_types);
+ aggregate_int32(count_at_max.get(), arena, *column, 1);
+}
+
+TEST(CountAtMinMaxTest, EmptyInput) {
+ AggregateFunctionSimpleFactory factory;
+ register_aggregate_function_count_at_min_max(factory);
+ DataTypes data_types = {std::make_shared<DataTypeInt32>()};
+ Arena arena;
+
+ auto column = ColumnInt32::create();
+ for (const auto& name : {"count_at_min", "count_at_max"}) {
+ auto function = create_function(factory, name, data_types);
+ aggregate_int32(function.get(), arena, *column, 0);
+ }
+}
+
+TEST(CountAtMinMaxTest, Merge) {
+ AggregateFunctionSimpleFactory factory;
+ register_aggregate_function_count_at_min_max(factory);
+ DataTypes data_types = {std::make_shared<DataTypeInt32>()};
+ Arena arena;
+
+ auto first = ColumnInt32::create();
+ auto second = ColumnInt32::create();
+ for (int value : {5, 1, 5}) {
+ first->insert_value(value);
+ }
+ for (int value : {5, 1, 9}) {
+ second->insert_value(value);
+ }
+
+ auto function = create_function(factory, "count_at_min", data_types);
+ std::unique_ptr<char[]> memory(new char[function->size_of_data() * 2]);
+ AggregateDataPtr lhs = memory.get();
+ AggregateDataPtr rhs = memory.get() + function->size_of_data();
+ function->create(lhs);
+ function->create(rhs);
+
+ const IColumn* first_columns[1] = {first.get()};
+ const IColumn* second_columns[1] = {second.get()};
+ for (size_t row = 0; row < first->size(); ++row) {
+ function->add(lhs, first_columns, row, arena);
+ }
+ for (size_t row = 0; row < second->size(); ++row) {
+ function->add(rhs, second_columns, row, arena);
+ }
+ function->merge(lhs, rhs, arena);
+
+ ColumnInt64 result;
+ function->insert_result_into(lhs, result);
+ EXPECT_EQ(2, result.get_element(0));
+ function->destroy(lhs);
+ function->destroy(rhs);
+}
+
+TEST(CountAtMinMaxTest, String) {
+ AggregateFunctionSimpleFactory factory;
+ register_aggregate_function_count_at_min_max(factory);
+ DataTypes data_types = {std::make_shared<DataTypeString>()};
+ Arena arena;
+
+ auto column = ColumnString::create();
+ for (const auto& value : {"b", "a", "b", "a", "c", "a"}) {
+ column->insert_data(value, strlen(value));
+ }
+
+ for (const auto& [name, expected] : {std::pair {"count_at_min", 3},
{"count_at_max", 1}}) {
+ auto function = create_function(factory, name, data_types);
+ std::unique_ptr<char[]> memory(new char[function->size_of_data()]);
+ AggregateDataPtr place = memory.get();
+ function->create(place);
+ const IColumn* columns[1] = {column.get()};
+ for (size_t row = 0; row < column->size(); ++row) {
+ function->add(place, columns, row, arena);
+ }
+ ColumnInt64 result;
+ function->insert_result_into(place, result);
+ EXPECT_EQ(expected, result.get_element(0));
+ function->destroy(place);
+ }
+}
+
+} // namespace doris
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinAggregateFunctions.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinAggregateFunctions.java
index e20a45baf03..4aefb09b1e1 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinAggregateFunctions.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinAggregateFunctions.java
@@ -36,6 +36,8 @@ import
org.apache.doris.nereids.trees.expressions.functions.agg.CollectSet;
import org.apache.doris.nereids.trees.expressions.functions.agg.Corr;
import org.apache.doris.nereids.trees.expressions.functions.agg.CorrWelford;
import org.apache.doris.nereids.trees.expressions.functions.agg.Count;
+import org.apache.doris.nereids.trees.expressions.functions.agg.CountAtMax;
+import org.apache.doris.nereids.trees.expressions.functions.agg.CountAtMin;
import org.apache.doris.nereids.trees.expressions.functions.agg.CountByEnum;
import org.apache.doris.nereids.trees.expressions.functions.agg.CountMap;
import org.apache.doris.nereids.trees.expressions.functions.agg.Covar;
@@ -149,6 +151,8 @@ public class BuiltinAggregateFunctions implements
FunctionHelper {
agg(CorrWelford.class, "corr_welford"),
agg(ExponentialMovingAverage.class,
"exponential_moving_average"),
agg(Count.class, "count"),
+ agg(CountAtMin.class, "count_at_min"),
+ agg(CountAtMax.class, "count_at_max"),
agg(CountByEnum.class, "count_by_enum"),
agg(CountMap.class, "count_map"),
agg(Covar.class, "covar", "covar_pop"),
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggExpressionBuilder.java
b/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggExpressionBuilder.java
index 24990b7a72b..7e7e8f19c36 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggExpressionBuilder.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggExpressionBuilder.java
@@ -143,6 +143,8 @@ public class IvmAggExpressionBuilder {
return target.stateColumnName(IvmAggStateKey.SUM);
case MIN:
case MAX:
+ case MIN_COUNT:
+ case MAX_COUNT:
case BITMAP_UNION:
return IvmUtil.ivmAggHiddenColumnName(target.getOrdinal(),
slotKind.name());
default:
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggExtremalProcessor.java
b/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggExtremalProcessor.java
index 3954792fd1e..446466252a3 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggExtremalProcessor.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggExtremalProcessor.java
@@ -19,14 +19,19 @@ package org.apache.doris.mtmv.ivm.agg;
import org.apache.doris.mtmv.ivm.IvmException;
import org.apache.doris.mtmv.ivm.IvmFailureReason;
+import org.apache.doris.nereids.trees.expressions.Add;
import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.And;
import org.apache.doris.nereids.trees.expressions.CaseWhen;
import org.apache.doris.nereids.trees.expressions.EqualTo;
import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.GreaterThan;
import org.apache.doris.nereids.trees.expressions.IsNull;
import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.Not;
import org.apache.doris.nereids.trees.expressions.Or;
import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.Subtract;
import org.apache.doris.nereids.trees.expressions.WhenClause;
import
org.apache.doris.nereids.trees.expressions.functions.agg.AggregateFunction;
import org.apache.doris.nereids.trees.expressions.functions.agg.Sum;
@@ -35,26 +40,50 @@ import
org.apache.doris.nereids.trees.expressions.functions.scalar.If;
import org.apache.doris.nereids.trees.expressions.literal.BigIntLiteral;
import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
+import org.apache.doris.nereids.types.BigIntType;
import org.apache.doris.nereids.util.TypeCoercionUtils;
import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
/**
- * Shared logic for MIN/MAX.
+ * Shared delta/apply logic for MIN/MAX.
*
- * <p>The delta aggregate produces an insert-only extreme, a delete-only
extreme, and a non-NULL count delta. Apply can
- * merge inserts directly, but if a deleted row may have been the old visible
extreme, the assert guard forces fallback
- * to complete refresh because the new extreme cannot be derived from current
MV state plus delta alone.
+ * <p>Besides the current extreme value, the MV stores the number of rows
equal to that
+ * extreme ({@code MIN_COUNT}/{@code MAX_COUNT}). The delta aggregate produces
the extreme
+ * on the insert and delete sides together with the count of rows equal to
each side's
+ * extreme. Apply uses those counts to decide whether the old extreme survived:
+ *
+ * <ul>
+ * <li>An inserted value better than the old extreme becomes the new
extreme.</li>
+ * <li>A group whose old extreme is NULL starts from the insert-side
extreme, and its
+ * count is first reduced by deletes that cancel the same value (a join
can emit
+ * such a delete for a row that the same refresh inserted
elsewhere).</li>
+ * <li>Otherwise the old extreme survives while
+ * {@code oldCount + equal inserts - equal deletes > 0}.</li>
+ * <li>If the old extreme is exhausted and no inserted value is better, the
new extreme
+ * cannot be derived from the current state and delta; the assert guard
keeps the
+ * existing fallback to a complete refresh.</li>
+ * </ul>
+ *
+ * <p>The count state is what makes deleting a duplicate extreme incremental:
deleting one
+ * of several rows equal to the minimum no longer trips the fallback.
*/
abstract class IvmAggExtremalProcessor extends IvmAggFunctionProcessor {
+ private static final String INSERT_COUNT_SLOT = "EXTREME_INS_COUNT";
+ private static final String DELETE_COUNT_SLOT = "EXTREME_DEL_COUNT";
+
private final IvmAggFunctionKind functionKind;
+ private final IvmAggStateKey countStateKey;
private final String deleteSlotName;
- IvmAggExtremalProcessor(IvmAggFunctionKind functionKind, String
deleteSlotName) {
+ IvmAggExtremalProcessor(IvmAggFunctionKind functionKind, IvmAggStateKey
countStateKey,
+ String deleteSlotName) {
this.functionKind = functionKind;
+ this.countStateKey = countStateKey;
this.deleteSlotName = deleteSlotName;
}
@@ -65,7 +94,7 @@ abstract class IvmAggExtremalProcessor extends
IvmAggFunctionProcessor {
@Override
protected List<IvmAggStateKey> hiddenStateKeys(AggregateFunction function)
{
- return ImmutableList.of(IvmAggStateKey.COUNT);
+ return ImmutableList.of(IvmAggStateKey.COUNT, countStateKey);
}
@Override
@@ -78,6 +107,10 @@ abstract class IvmAggExtremalProcessor extends
IvmAggFunctionProcessor {
deleteDeltaColumnName(target, ctx)));
outputs.add(new Alias(new Sum(ctx.nonNullDeltaCountValue(arg,
dmlFactorSlot)),
ctx.deltaColumnName(target, IvmAggFunctionKind.COUNT)));
+ outputs.add(new
Alias(buildExtremeCountAggregate(ctx.insertOnlyValue(arg, dmlFactorSlot)),
+ ctx.transientDeltaColumnName(target, INSERT_COUNT_SLOT)));
+ outputs.add(new
Alias(buildExtremeCountAggregate(ctx.deleteOnlyValue(arg, dmlFactorSlot)),
+ ctx.transientDeltaColumnName(target, DELETE_COUNT_SLOT)));
}
@Override
@@ -87,16 +120,12 @@ abstract class IvmAggExtremalProcessor extends
IvmAggFunctionProcessor {
@Override
void mapApplyDeltaSlots(IvmAggTarget target, Map<String, Slot>
outputByName,
- Map<IvmAggDeltaSlotRef, Slot> applyDeltaSlots, Slot
deltaGroupCountSlot, IvmAggExpressionBuilder ctx) {
+ Map<IvmAggDeltaSlotRef, Slot> applyDeltaSlots, Slot
deltaGroupCountSlot,
+ IvmAggExpressionBuilder ctx) {
super.mapApplyDeltaSlots(target, outputByName, applyDeltaSlots,
deltaGroupCountSlot, ctx);
- String columnName = deleteDeltaColumnName(target, ctx);
- Slot slot = outputByName.get(columnName);
- if (slot == null) {
- throw new IvmException(IvmFailureReason.PLAN_REWRITE_FAILED,
- "IVM agg delta rewrite failed to resolve delta output
slot: "
- + columnName + " for target " + target);
- }
- applyDeltaSlots.put(deleteSlotRef(target), slot);
+ resolveDeltaSlot(target, deleteSlotName, outputByName,
applyDeltaSlots, ctx);
+ resolveDeltaSlot(target, INSERT_COUNT_SLOT, outputByName,
applyDeltaSlots, ctx);
+ resolveDeltaSlot(target, DELETE_COUNT_SLOT, outputByName,
applyDeltaSlots, ctx);
}
@Override
@@ -109,48 +138,126 @@ abstract class IvmAggExtremalProcessor extends
IvmAggFunctionProcessor {
IvmAggExpressionBuilder ctx = applyContext.expressions();
Slot oldExtreme =
applyContext.rawMvSlot(target.getValueStateColumnName());
Expression deltaInsert = applyContext.deltaSlotValue(target,
functionKind);
- Expression deltaDel = applyContext.deltaSlotValue(target,
deleteSlotRef(target));
- Expression newCount = applyContext.buildNewHiddenCount(target);
+ Expression deltaDelete = applyContext.deltaSlotValue(target,
deleteSlotRef(target));
+ Expression deltaInsertCount = applyContext.deltaSlotValue(target,
insertCountSlotRef(target));
+ Expression deltaDeleteCount = applyContext.deltaSlotValue(target,
deleteCountSlotRef(target));
+ Slot oldCountSlot =
Objects.requireNonNull(target.getHiddenStateSlot(countStateKey),
+ "missing extreme count state for " + target);
+ Expression oldCount =
ctx.zeroIfNull(applyContext.rawMvSlot(oldCountSlot.getName()));
+ Expression newTotalCount = applyContext.buildNewHiddenCount(target);
+ Expression zero = new BigIntLiteral(0L);
+
+ // Keep every predicate non-NULL: an empty insert/delete side must
read as false, not
+ // as unknown, so the guard below is a plain boolean.
+ Expression insertedPresent = new Not(new IsNull(deltaInsert));
+ Expression deletedPresent = new Not(new IsNull(deltaDelete));
+ Expression oldExtremeNull = new IsNull(oldExtreme);
+ Expression oldExtremePresent = new Not(oldExtremeNull);
+ Expression insertedBetter = new And(insertedPresent,
insertedIsBetter(deltaInsert, oldExtreme));
+ Expression insertedEqual = new And(insertedPresent, new
EqualTo(deltaInsert, oldExtreme));
+ Expression deletedEqual = new And(deletedPresent, new
EqualTo(deltaDelete, oldExtreme));
+ Expression survivingOldCount = new Subtract(
+ new Add(oldCount, new If(insertedEqual, deltaInsertCount,
zero)),
+ new If(deletedEqual, deltaDeleteCount, zero));
+
+ // When the old extreme is NULL the new extreme comes from the insert
side. Its count
+ // is the insert-side count minus the deletes that cancel the same
value. That
+ // subtraction is only possible when the delete-side extreme is
exactly that value,
+ // or is farther from the extreme (so no delete touches it). A delete
closer to the
+ // extreme, or deletes that cancel every inserted row, leaves the
count unprovable
+ // and keeps the complete-refresh fallback.
+ Expression deleteEqualsInsert = new And(ImmutableList.of(
+ deletedPresent, insertedPresent, new EqualTo(deltaDelete,
deltaInsert)));
+ Expression insertExtremeNetCount = new Subtract(deltaInsertCount,
deltaDeleteCount);
+ Expression insertExtremeProvable = new And(insertedPresent, new
Or(ImmutableList.of(
+ new IsNull(deltaDelete),
+ new And(deletedPresent,
+ deletedExtremeIsBeyondInsert(deltaDelete,
deltaInsert)),
+ new And(deleteEqualsInsert, new
GreaterThan(insertExtremeNetCount, zero)))));
+ Expression oldNullGuard = new And(oldExtremeNull, new
Or(ImmutableList.of(
+ new EqualTo(newTotalCount, zero),
+ insertExtremeProvable)));
+
+ // The old extreme survives when no delete is below it and its
remaining count is
+ // positive. An inserted value that beats the old extreme also makes
the merge
+ // derivable without a complete refresh.
+ Expression oldPresentNoDeletes = new And(oldExtremePresent, new
IsNull(deltaDelete));
+ Expression oldPresentBoundary = new And(ImmutableList.of(
+ oldExtremePresent,
+ deletedPresent,
+ deletedExtremeIsAtLeastOld(deltaDelete, oldExtreme),
+ new Or(ImmutableList.of(
+ insertedBetter,
+ new GreaterThan(survivingOldCount, zero)))));
Expression guardCond = new Or(ImmutableList.of(
- new EqualTo(newCount, new BigIntLiteral(0L)),
- new IsNull(deltaDel),
- new IsNull(oldExtreme),
- deletedExtremeKeepsOldValueValid(deltaDel, oldExtreme,
deltaInsert)
- ));
+ new EqualTo(newTotalCount, zero),
+ oldNullGuard,
+ oldPresentNoDeletes,
+ oldPresentBoundary));
Expression guard = new AssertTrue(guardCond, new
StringLiteral(fallbackMessage()));
+
Expression newExtremeRaw = new CaseWhen(
ImmutableList.of(
- new WhenClause(new EqualTo(newCount, new
BigIntLiteral(0L)),
+ new WhenClause(new EqualTo(newTotalCount, zero),
new NullLiteral(oldExtreme.getDataType())),
new WhenClause(new IsNull(oldExtreme), deltaInsert),
- new WhenClause(new IsNull(deltaInsert), oldExtreme)
- ),
- mergeOldAndInsertedExtreme(oldExtreme, deltaInsert)
- );
+ new WhenClause(insertedBetter, deltaInsert)),
+ oldExtreme);
Expression newExtremeGuarded = new If(guard, newExtremeRaw,
new NullLiteral(newExtremeRaw.getDataType()));
- applyContext.putFinalExpression(target,
target.getHiddenStateSlot(IvmAggStateKey.COUNT).getName(), newCount);
+ Expression insertExtremeCount = new If(
+ deleteEqualsInsert, insertExtremeNetCount, deltaInsertCount);
+ Expression newCountRaw = new CaseWhen(
+ ImmutableList.of(
+ new WhenClause(new EqualTo(newTotalCount, zero), zero),
+ new WhenClause(oldExtremeNull, insertExtremeCount),
+ new WhenClause(insertedBetter, deltaInsertCount)),
+ survivingOldCount);
+ Expression newCountGuarded = new If(guard, newCountRaw,
+ new NullLiteral(BigIntType.INSTANCE));
+
+ applyContext.putFinalExpression(target, oldCountSlot.getName(),
newCountGuarded);
+ applyContext.putFinalExpression(target,
target.getHiddenStateSlot(IvmAggStateKey.COUNT).getName(),
+ newTotalCount);
applyContext.putFinalExpression(target,
target.getVisibleSlot().getName(),
- new If(ctx.isPositive(newCount),
-
TypeCoercionUtils.castIfNotMatchType(newExtremeGuarded,
target.getVisibleSlot().getDataType()),
+ new If(ctx.isPositive(newTotalCount),
+ TypeCoercionUtils.castIfNotMatchType(newExtremeGuarded,
+ target.getVisibleSlot().getDataType()),
new
NullLiteral(target.getVisibleSlot().getDataType())));
}
+ private void resolveDeltaSlot(IvmAggTarget target, String slotName,
Map<String, Slot> outputByName,
+ Map<IvmAggDeltaSlotRef, Slot> applyDeltaSlots,
IvmAggExpressionBuilder ctx) {
+ String columnName = ctx.transientDeltaColumnName(target, slotName);
+ Slot slot = outputByName.get(columnName);
+ if (slot == null) {
+ throw new IvmException(IvmFailureReason.PLAN_REWRITE_FAILED,
+ "IVM agg delta rewrite failed to resolve delta output
slot: "
+ + columnName + " for target " + target);
+ }
+ applyDeltaSlots.put(new IvmAggDeltaSlotRef(target.getOrdinal(),
slotName), slot);
+ }
+
/** Builds MIN or MAX over a filtered insert/delete expression. */
protected abstract Expression buildExtremeAggregate(Expression input);
+ /** Builds COUNT_AT_MIN or COUNT_AT_MAX over a filtered insert/delete
expression. */
+ protected abstract Expression buildExtremeCountAggregate(Expression input);
+
+ /** True when an inserted value becomes the new extreme. */
+ protected abstract Expression insertedIsBetter(Expression deltaInsert,
Slot oldExtreme);
+
+ /** True when the deleted extreme does not fall below/above the old
extreme. */
+ protected abstract Expression deletedExtremeIsAtLeastOld(Expression
deltaDelete, Slot oldExtreme);
+
/**
- * Returns true when the old visible extreme is still deterministically
valid after deletes,
- * either because no delete hit the boundary point, or because the insert
side provides a
- * value that equals or improves the old extreme position.
+ * True when the delete-side extreme is farther from the aggregate extreme
than the
+ * insert-side extreme, so no deleted row can cancel the insert-side
extreme.
*/
- protected abstract Expression deletedExtremeKeepsOldValueValid(Expression
deltaDeletedExtreme,
- Slot oldExtreme, Expression deltaInsertExtreme);
-
- /** Merges the old visible extreme with the insert-only delta extreme. */
- protected abstract Expression mergeOldAndInsertedExtreme(Slot oldExtreme,
Expression deltaInsertExtreme);
+ protected abstract Expression deletedExtremeIsBeyondInsert(
+ Expression deltaDelete, Expression deltaInsert);
/** Assertion message used when the delta is not sufficient and complete
refresh is required. */
protected abstract String fallbackMessage();
@@ -159,6 +266,14 @@ abstract class IvmAggExtremalProcessor extends
IvmAggFunctionProcessor {
return new IvmAggDeltaSlotRef(target.getOrdinal(), deleteSlotName);
}
+ private IvmAggDeltaSlotRef insertCountSlotRef(IvmAggTarget target) {
+ return new IvmAggDeltaSlotRef(target.getOrdinal(), INSERT_COUNT_SLOT);
+ }
+
+ private IvmAggDeltaSlotRef deleteCountSlotRef(IvmAggTarget target) {
+ return new IvmAggDeltaSlotRef(target.getOrdinal(), DELETE_COUNT_SLOT);
+ }
+
private String deleteDeltaColumnName(IvmAggTarget target,
IvmAggExpressionBuilder ctx) {
return ctx.transientDeltaColumnName(target, deleteSlotName);
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggFunctionKind.java
b/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggFunctionKind.java
index b7f216b2a32..801d6c9abd2 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggFunctionKind.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggFunctionKind.java
@@ -29,6 +29,8 @@ public enum IvmAggFunctionKind {
AVG,
MIN,
MAX,
+ MIN_COUNT,
+ MAX_COUNT,
BITMAP_UNION,
BITMAP_UNION_COUNT,
ARRAY_AGG,
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggFunctionProcessor.java
b/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggFunctionProcessor.java
index 2466f90b8a9..18ab268cdc0 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggFunctionProcessor.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggFunctionProcessor.java
@@ -27,6 +27,8 @@ import org.apache.doris.nereids.trees.expressions.Slot;
import
org.apache.doris.nereids.trees.expressions.functions.agg.AggregateFunction;
import org.apache.doris.nereids.trees.expressions.functions.agg.BitmapUnion;
import org.apache.doris.nereids.trees.expressions.functions.agg.Count;
+import org.apache.doris.nereids.trees.expressions.functions.agg.CountAtMax;
+import org.apache.doris.nereids.trees.expressions.functions.agg.CountAtMin;
import org.apache.doris.nereids.trees.expressions.functions.agg.Sum;
import com.google.common.collect.ImmutableList;
@@ -155,6 +157,10 @@ public abstract class IvmAggFunctionProcessor {
return new Sum(function.child(0));
case BITMAP_UNION:
return new BitmapUnion(function.child(0));
+ case MIN_COUNT:
+ return new CountAtMin(function.child(0));
+ case MAX_COUNT:
+ return new CountAtMax(function.child(0));
default:
throw new IllegalArgumentException("Unsupported IVM aggregate
state: " + stateKey);
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggMaxProcessor.java
b/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggMaxProcessor.java
index 9d1edf7ea16..dad7d15430a 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggMaxProcessor.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggMaxProcessor.java
@@ -19,18 +19,18 @@ package org.apache.doris.mtmv.ivm.agg;
import org.apache.doris.mtmv.ivm.IvmFailureClassifier;
import org.apache.doris.nereids.trees.expressions.Expression;
-import org.apache.doris.nereids.trees.expressions.GreaterThanEqual;
+import org.apache.doris.nereids.trees.expressions.GreaterThan;
import org.apache.doris.nereids.trees.expressions.LessThan;
-import org.apache.doris.nereids.trees.expressions.Or;
+import org.apache.doris.nereids.trees.expressions.LessThanEqual;
import org.apache.doris.nereids.trees.expressions.Slot;
import
org.apache.doris.nereids.trees.expressions.functions.agg.AggregateFunction;
+import org.apache.doris.nereids.trees.expressions.functions.agg.CountAtMax;
import org.apache.doris.nereids.trees.expressions.functions.agg.Max;
-import org.apache.doris.nereids.trees.expressions.functions.scalar.Greatest;
-/** MAX-specific extreme comparisons and merge expression. */
+/** MAX-specific extreme comparisons and delta state. */
class IvmAggMaxProcessor extends IvmAggExtremalProcessor {
IvmAggMaxProcessor() {
- super(IvmAggFunctionKind.MAX, "DELMAX");
+ super(IvmAggFunctionKind.MAX, IvmAggStateKey.MAX_COUNT, "DELMAX");
}
@Override
@@ -44,16 +44,23 @@ class IvmAggMaxProcessor extends IvmAggExtremalProcessor {
}
@Override
- protected Expression deletedExtremeKeepsOldValueValid(Expression
deltaDeletedExtreme,
- Slot oldExtreme, Expression deltaInsertExtreme) {
- return new Or(
- new LessThan(deltaDeletedExtreme, oldExtreme),
- new GreaterThanEqual(deltaInsertExtreme, oldExtreme));
+ protected Expression buildExtremeCountAggregate(Expression input) {
+ return new CountAtMax(input);
}
@Override
- protected Expression mergeOldAndInsertedExtreme(Slot oldExtreme,
Expression deltaInsertExtreme) {
- return new Greatest(oldExtreme, deltaInsertExtreme);
+ protected Expression insertedIsBetter(Expression deltaInsert, Slot
oldExtreme) {
+ return new GreaterThan(deltaInsert, oldExtreme);
+ }
+
+ @Override
+ protected Expression deletedExtremeIsAtLeastOld(Expression deltaDelete,
Slot oldExtreme) {
+ return new LessThanEqual(deltaDelete, oldExtreme);
+ }
+
+ @Override
+ protected Expression deletedExtremeIsBeyondInsert(Expression deltaDelete,
Expression deltaInsert) {
+ return new LessThan(deltaDelete, deltaInsert);
}
@Override
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggMinProcessor.java
b/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggMinProcessor.java
index d1b6434907e..30cd23e01ac 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggMinProcessor.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggMinProcessor.java
@@ -20,17 +20,17 @@ package org.apache.doris.mtmv.ivm.agg;
import org.apache.doris.mtmv.ivm.IvmFailureClassifier;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.GreaterThan;
-import org.apache.doris.nereids.trees.expressions.LessThanEqual;
-import org.apache.doris.nereids.trees.expressions.Or;
+import org.apache.doris.nereids.trees.expressions.GreaterThanEqual;
+import org.apache.doris.nereids.trees.expressions.LessThan;
import org.apache.doris.nereids.trees.expressions.Slot;
import
org.apache.doris.nereids.trees.expressions.functions.agg.AggregateFunction;
+import org.apache.doris.nereids.trees.expressions.functions.agg.CountAtMin;
import org.apache.doris.nereids.trees.expressions.functions.agg.Min;
-import org.apache.doris.nereids.trees.expressions.functions.scalar.Least;
-/** MIN-specific extreme comparisons and merge expression. */
+/** MIN-specific extreme comparisons and delta state. */
class IvmAggMinProcessor extends IvmAggExtremalProcessor {
IvmAggMinProcessor() {
- super(IvmAggFunctionKind.MIN, "DELMIN");
+ super(IvmAggFunctionKind.MIN, IvmAggStateKey.MIN_COUNT, "DELMIN");
}
@Override
@@ -44,16 +44,23 @@ class IvmAggMinProcessor extends IvmAggExtremalProcessor {
}
@Override
- protected Expression deletedExtremeKeepsOldValueValid(Expression
deltaDeletedExtreme,
- Slot oldExtreme, Expression deltaInsertExtreme) {
- return new Or(
- new GreaterThan(deltaDeletedExtreme, oldExtreme),
- new LessThanEqual(deltaInsertExtreme, oldExtreme));
+ protected Expression buildExtremeCountAggregate(Expression input) {
+ return new CountAtMin(input);
}
@Override
- protected Expression mergeOldAndInsertedExtreme(Slot oldExtreme,
Expression deltaInsertExtreme) {
- return new Least(oldExtreme, deltaInsertExtreme);
+ protected Expression insertedIsBetter(Expression deltaInsert, Slot
oldExtreme) {
+ return new LessThan(deltaInsert, oldExtreme);
+ }
+
+ @Override
+ protected Expression deletedExtremeIsAtLeastOld(Expression deltaDelete,
Slot oldExtreme) {
+ return new GreaterThanEqual(deltaDelete, oldExtreme);
+ }
+
+ @Override
+ protected Expression deletedExtremeIsBeyondInsert(Expression deltaDelete,
Expression deltaInsert) {
+ return new GreaterThan(deltaDelete, deltaInsert);
}
@Override
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggStateKey.java
b/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggStateKey.java
index de83a22e1b3..76dfb3b303a 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggStateKey.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggStateKey.java
@@ -21,5 +21,7 @@ package org.apache.doris.mtmv.ivm.agg;
public enum IvmAggStateKey {
COUNT,
SUM,
- BITMAP_UNION
+ BITMAP_UNION,
+ MIN_COUNT,
+ MAX_COUNT
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/agg/CountAtMax.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/agg/CountAtMax.java
new file mode 100644
index 00000000000..9d504a78ba7
--- /dev/null
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/agg/CountAtMax.java
@@ -0,0 +1,113 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.trees.expressions.functions.agg;
+
+import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.catalog.Type;
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.functions.CustomSignature;
+import org.apache.doris.nereids.trees.expressions.literal.BigIntLiteral;
+import org.apache.doris.nereids.trees.expressions.shape.UnaryExpression;
+import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
+import org.apache.doris.nereids.types.BigIntType;
+import org.apache.doris.nereids.types.DataType;
+import org.apache.doris.nereids.types.DecimalV2Type;
+import org.apache.doris.nereids.types.DecimalV3Type;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/**
+ * count_at_max(expr): the number of input rows whose value equals MAX(expr).
+ *
+ * <p>NULL inputs are ignored. An empty input, or an input whose values are
all NULL, returns 0.
+ * This is a Doris extension; it is equivalent to
+ * {@code COUNT(*) KEEP (DENSE_RANK LAST ORDER BY expr)} in Oracle.
+ */
+public class CountAtMax extends NotNullableAggregateFunction
+ implements UnaryExpression, CustomSignature,
NullIgnoringAggregateFunction {
+
+ public CountAtMax(Expression arg) {
+ this(false, false, arg);
+ }
+
+ public CountAtMax(boolean distinct, Expression arg) {
+ this(distinct, false, arg);
+ }
+
+ private CountAtMax(boolean distinct, boolean alwaysNullable, Expression
arg) {
+ super("count_at_max", distinct, false, arg);
+ }
+
+ /** constructor for withChildren and reuse signature */
+ private CountAtMax(AggregateFunctionParams functionParams) {
+ super(functionParams);
+ }
+
+ @Override
+ public void checkLegalityBeforeTypeCoercion() {
+ if (distinct) {
+ throw new AnalysisException("COUNT_AT_MAX does not support
DISTINCT");
+ }
+ if (getArgumentType(0).isOnlyMetricType() &&
!getArgumentType(0).isArrayType()) {
+ throw new AnalysisException(Type.OnlyMetricTypeErrorMsg);
+ }
+ }
+
+ @Override
+ public void checkLegalityAfterRewrite() {
+ checkLegalityBeforeTypeCoercion();
+ }
+
+ @Override
+ public FunctionSignature customSignature() {
+ DataType dataType = getArgument(0).getDataType();
+ if (dataType instanceof DecimalV2Type) {
+ dataType = DecimalV3Type.forType(dataType);
+ }
+ return FunctionSignature.ret(BigIntType.INSTANCE).args(dataType);
+ }
+
+ @Override
+ protected List<DataType> intermediateTypes() {
+ DataType dataType = getArgument(0).getDataType();
+ if (dataType instanceof DecimalV2Type) {
+ dataType = DecimalV3Type.forType(dataType);
+ }
+ return ImmutableList.of(dataType, BigIntType.INSTANCE);
+ }
+
+ @Override
+ public Expression resultForEmptyInput() {
+ return new BigIntLiteral(0);
+ }
+
+ @Override
+ public CountAtMax withDistinctAndChildren(boolean distinct,
List<Expression> children) {
+ Preconditions.checkArgument(children.size() == 1);
+ return new CountAtMax(getFunctionParams(distinct, children));
+ }
+
+ @Override
+ public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
+ return visitor.visitCountAtMax(this, context);
+ }
+}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/agg/CountAtMin.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/agg/CountAtMin.java
new file mode 100644
index 00000000000..ad84f82afb3
--- /dev/null
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/agg/CountAtMin.java
@@ -0,0 +1,113 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.trees.expressions.functions.agg;
+
+import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.catalog.Type;
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.functions.CustomSignature;
+import org.apache.doris.nereids.trees.expressions.literal.BigIntLiteral;
+import org.apache.doris.nereids.trees.expressions.shape.UnaryExpression;
+import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
+import org.apache.doris.nereids.types.BigIntType;
+import org.apache.doris.nereids.types.DataType;
+import org.apache.doris.nereids.types.DecimalV2Type;
+import org.apache.doris.nereids.types.DecimalV3Type;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/**
+ * count_at_min(expr): the number of input rows whose value equals MIN(expr).
+ *
+ * <p>NULL inputs are ignored. An empty input, or an input whose values are
all NULL, returns 0.
+ * This is a Doris extension; it is equivalent to
+ * {@code COUNT(*) KEEP (DENSE_RANK FIRST ORDER BY expr)} in Oracle.
+ */
+public class CountAtMin extends NotNullableAggregateFunction
+ implements UnaryExpression, CustomSignature,
NullIgnoringAggregateFunction {
+
+ public CountAtMin(Expression arg) {
+ this(false, false, arg);
+ }
+
+ public CountAtMin(boolean distinct, Expression arg) {
+ this(distinct, false, arg);
+ }
+
+ private CountAtMin(boolean distinct, boolean alwaysNullable, Expression
arg) {
+ super("count_at_min", distinct, false, arg);
+ }
+
+ /** constructor for withChildren and reuse signature */
+ private CountAtMin(AggregateFunctionParams functionParams) {
+ super(functionParams);
+ }
+
+ @Override
+ public void checkLegalityBeforeTypeCoercion() {
+ if (distinct) {
+ throw new AnalysisException("COUNT_AT_MIN does not support
DISTINCT");
+ }
+ if (getArgumentType(0).isOnlyMetricType() &&
!getArgumentType(0).isArrayType()) {
+ throw new AnalysisException(Type.OnlyMetricTypeErrorMsg);
+ }
+ }
+
+ @Override
+ public void checkLegalityAfterRewrite() {
+ checkLegalityBeforeTypeCoercion();
+ }
+
+ @Override
+ public FunctionSignature customSignature() {
+ DataType dataType = getArgument(0).getDataType();
+ if (dataType instanceof DecimalV2Type) {
+ dataType = DecimalV3Type.forType(dataType);
+ }
+ return FunctionSignature.ret(BigIntType.INSTANCE).args(dataType);
+ }
+
+ @Override
+ protected List<DataType> intermediateTypes() {
+ DataType dataType = getArgument(0).getDataType();
+ if (dataType instanceof DecimalV2Type) {
+ dataType = DecimalV3Type.forType(dataType);
+ }
+ return ImmutableList.of(dataType, BigIntType.INSTANCE);
+ }
+
+ @Override
+ public Expression resultForEmptyInput() {
+ return new BigIntLiteral(0);
+ }
+
+ @Override
+ public CountAtMin withDistinctAndChildren(boolean distinct,
List<Expression> children) {
+ Preconditions.checkArgument(children.size() == 1);
+ return new CountAtMin(getFunctionParams(distinct, children));
+ }
+
+ @Override
+ public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
+ return visitor.visitCountAtMin(this, context);
+ }
+}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/AggregateFunctionVisitor.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/AggregateFunctionVisitor.java
index 5a77fdf9d7a..40d1c4311cd 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/AggregateFunctionVisitor.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/AggregateFunctionVisitor.java
@@ -36,6 +36,8 @@ import
org.apache.doris.nereids.trees.expressions.functions.agg.CollectSet;
import org.apache.doris.nereids.trees.expressions.functions.agg.Corr;
import org.apache.doris.nereids.trees.expressions.functions.agg.CorrWelford;
import org.apache.doris.nereids.trees.expressions.functions.agg.Count;
+import org.apache.doris.nereids.trees.expressions.functions.agg.CountAtMax;
+import org.apache.doris.nereids.trees.expressions.functions.agg.CountAtMin;
import org.apache.doris.nereids.trees.expressions.functions.agg.CountByEnum;
import org.apache.doris.nereids.trees.expressions.functions.agg.Covar;
import org.apache.doris.nereids.trees.expressions.functions.agg.CovarSamp;
@@ -188,6 +190,14 @@ public interface AggregateFunctionVisitor<R, C> {
return visitAggregateFunction(count, context);
}
+ default R visitCountAtMin(CountAtMin countAtMin, C context) {
+ return visitAggregateFunction(countAtMin, context);
+ }
+
+ default R visitCountAtMax(CountAtMax countAtMax, C context) {
+ return visitAggregateFunction(countAtMax, context);
+ }
+
default R visitCountByEnum(CountByEnum count, C context) {
return visitAggregateFunction(count, context);
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/IvmAggDeltaHandlerTest.java
b/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/IvmAggDeltaHandlerTest.java
index 579af9f450a..4a433111459 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/IvmAggDeltaHandlerTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/IvmAggDeltaHandlerTest.java
@@ -439,7 +439,7 @@ class IvmAggDeltaHandlerTest extends IvmDeltaTestBase {
// SUM, AVG's hidden COUNT dedups with SUM's hidden COUNT, and MIN/MAX
reuse visible cnt_name as
// their hidden COUNT, so shared state removes two duplicate COUNT
delta outputs.
List<String> outputNames =
deltaAgg.getOutput().stream().map(Slot::getName).collect(Collectors.toList());
- Assertions.assertEquals(10, outputNames.size());
+ Assertions.assertEquals(14, outputNames.size());
Assertions.assertTrue(outputNames.contains(Column.IVM_DELTA_GROUP_COUNT_COL));
// ordinal 0: COUNT(name) -> hidden COUNT (kept)
Assertions.assertTrue(outputNames.contains(IvmUtil.ivmAggHiddenColumnName(0,
"COUNT")));
@@ -450,14 +450,18 @@ class IvmAggDeltaHandlerTest extends IvmDeltaTestBase {
Assertions.assertFalse(outputNames.contains(IvmUtil.ivmAggHiddenColumnName(2,
"SUM")));
Assertions.assertFalse(outputNames.contains(IvmUtil.ivmAggHiddenColumnName(2,
"COUNT")));
Assertions.assertTrue(outputNames.contains("sum_id"));
- // ordinal 3: MIN(name) -> MIN + DELMIN kept; COUNT absorbed by
visible cnt_name
+ // ordinal 3: MIN(name) -> MIN + DELMIN + insert/delete extreme
counts; COUNT absorbed by visible cnt_name
Assertions.assertTrue(outputNames.contains(IvmUtil.ivmAggHiddenColumnName(3,
"MIN")));
Assertions.assertTrue(outputNames.contains(transientDeltaColumnName(3,
"DELMIN")));
+ Assertions.assertTrue(outputNames.contains(transientDeltaColumnName(3,
"EXTREME_INS_COUNT")));
+ Assertions.assertTrue(outputNames.contains(transientDeltaColumnName(3,
"EXTREME_DEL_COUNT")));
Assertions.assertFalse(outputNames.contains(IvmUtil.ivmAggHiddenColumnName(3,
"COUNT")));
Assertions.assertTrue(outputNames.contains("cnt_name"));
- // ordinal 4: MAX(name) -> MAX + DELMAX kept; COUNT absorbed by
visible cnt_name
+ // ordinal 4: MAX(name) -> MAX + DELMAX + insert/delete extreme
counts; COUNT absorbed by visible cnt_name
Assertions.assertTrue(outputNames.contains(IvmUtil.ivmAggHiddenColumnName(4,
"MAX")));
Assertions.assertTrue(outputNames.contains(transientDeltaColumnName(4,
"DELMAX")));
+ Assertions.assertTrue(outputNames.contains(transientDeltaColumnName(4,
"EXTREME_INS_COUNT")));
+ Assertions.assertTrue(outputNames.contains(transientDeltaColumnName(4,
"EXTREME_DEL_COUNT")));
Assertions.assertFalse(outputNames.contains(IvmUtil.ivmAggHiddenColumnName(4,
"COUNT")));
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/IvmNormalizeUniqKeysMTMVTest.java
b/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/IvmNormalizeUniqKeysMTMVTest.java
index 80b9f5fa505..b1c66199ace 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/IvmNormalizeUniqKeysMTMVTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/IvmNormalizeUniqKeysMTMVTest.java
@@ -798,9 +798,13 @@ class IvmNormalizeUniqKeysMTMVTest {
Pair.of(new Count(nameSlot), concat(groupCount,
IvmUtil.ivmAggHiddenColumnName(0, "COUNT"))),
Pair.of(new Min(nameSlot), concat(groupCount,
- IvmUtil.ivmAggHiddenColumnName(0, "COUNT"),
IvmUtil.ivmAggHiddenColumnName(0, "MIN"))),
+ IvmUtil.ivmAggHiddenColumnName(0, "COUNT"),
+ IvmUtil.ivmAggHiddenColumnName(0, "MIN_COUNT"),
+ IvmUtil.ivmAggHiddenColumnName(0, "MIN"))),
Pair.of(new Max(nameSlot), concat(groupCount,
- IvmUtil.ivmAggHiddenColumnName(0, "COUNT"),
IvmUtil.ivmAggHiddenColumnName(0, "MAX"))),
+ IvmUtil.ivmAggHiddenColumnName(0, "COUNT"),
+ IvmUtil.ivmAggHiddenColumnName(0, "MAX_COUNT"),
+ IvmUtil.ivmAggHiddenColumnName(0, "MAX"))),
Pair.of(new CollectList(nameSlot), concat(groupCount,
IvmUtil.ivmAggHiddenColumnName(0, "COLLECT_LIST"))),
Pair.of(new BitmapUnion(new BitmapFromString(nameSlot)),
concat(groupCount,
@@ -1068,7 +1072,7 @@ class IvmNormalizeUniqKeysMTMVTest {
JobContext jobContext = newJobContextForRoot(agg, true);
Plan result = new IvmNormalizeUniqKeysMTMV().normalize(agg,
jobContext);
- // Normalization should succeed; no hidden MIN column — only hidden
COUNT
+ // Normalization should succeed; no hidden MIN column, but COUNT and
MIN_COUNT state columns.
Assertions.assertInstanceOf(LogicalProject.class, result);
IvmAggMeta aggMeta =
jobContext.getCascadesContext().getIvmRewriteResult().get().getAggMeta();
Assertions.assertNotNull(aggMeta);
@@ -1076,12 +1080,14 @@ class IvmNormalizeUniqKeysMTMVTest {
IvmAggTarget target = aggMeta.getAggTargets().get(0);
Assertions.assertEquals(IvmAggFunctionKind.MIN,
target.getFunctionKind());
Assertions.assertNotNull(target.getHiddenStateSlot(IvmAggStateKey.COUNT));
+
Assertions.assertNotNull(target.getHiddenStateSlot(IvmAggStateKey.MIN_COUNT));
LogicalProject<?> topProject = (LogicalProject<?>) result;
Set<String> outputNames = topProject.getOutput().stream()
.map(Slot::getName).collect(Collectors.toSet());
Assertions.assertFalse(outputNames.contains(IvmUtil.ivmAggHiddenColumnName(0,
"MIN")));
Assertions.assertTrue(outputNames.contains(IvmUtil.ivmAggHiddenColumnName(0,
"COUNT")));
+
Assertions.assertTrue(outputNames.contains(IvmUtil.ivmAggHiddenColumnName(0,
"MIN_COUNT")));
}
@Test
@@ -1104,12 +1110,14 @@ class IvmNormalizeUniqKeysMTMVTest {
IvmAggTarget target = aggMeta.getAggTargets().get(0);
Assertions.assertEquals(IvmAggFunctionKind.MAX,
target.getFunctionKind());
Assertions.assertNotNull(target.getHiddenStateSlot(IvmAggStateKey.COUNT));
+
Assertions.assertNotNull(target.getHiddenStateSlot(IvmAggStateKey.MAX_COUNT));
LogicalProject<?> topProject = (LogicalProject<?>) result;
Set<String> outputNames = topProject.getOutput().stream()
.map(Slot::getName).collect(Collectors.toSet());
Assertions.assertFalse(outputNames.contains(IvmUtil.ivmAggHiddenColumnName(0,
"MAX")));
Assertions.assertTrue(outputNames.contains(IvmUtil.ivmAggHiddenColumnName(0,
"COUNT")));
+
Assertions.assertTrue(outputNames.contains(IvmUtil.ivmAggHiddenColumnName(0,
"MAX_COUNT")));
}
@Test
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/agg/IvmAggMaxProcessorTest.java
b/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/agg/IvmAggMaxProcessorTest.java
index 928a82e1383..c79c064c26c 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/agg/IvmAggMaxProcessorTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/agg/IvmAggMaxProcessorTest.java
@@ -17,13 +17,15 @@
package org.apache.doris.mtmv.ivm.agg;
+import org.apache.doris.mtmv.ivm.IvmUtil;
+import org.apache.doris.nereids.trees.expressions.CaseWhen;
import org.apache.doris.nereids.trees.expressions.Expression;
-import org.apache.doris.nereids.trees.expressions.LessThan;
+import org.apache.doris.nereids.trees.expressions.LessThanEqual;
import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.functions.agg.CountAtMax;
import org.apache.doris.nereids.trees.expressions.functions.agg.Max;
import org.apache.doris.nereids.trees.expressions.functions.agg.Sum;
import org.apache.doris.nereids.trees.expressions.functions.scalar.AssertTrue;
-import org.apache.doris.nereids.trees.expressions.functions.scalar.Greatest;
import org.apache.doris.nereids.types.BigIntType;
import org.apache.doris.nereids.types.IntegerType;
@@ -41,22 +43,34 @@ class IvmAggMaxProcessorTest extends
IvmAggProcessorTestBase {
IvmAggMaxProcessor processor = new IvmAggMaxProcessor();
Assertions.assertTrue(processor.supportsOriginalFunction(new
Max(value)));
Assertions.assertEquals(IvmAggFunctionKind.MAX,
processor.handledFunctionKind());
- Assertions.assertEquals(ImmutableList.of(IvmAggStateKey.COUNT),
processor.hiddenStateKeys(new Max(value)));
+ Assertions.assertEquals(ImmutableList.of(IvmAggStateKey.COUNT,
IvmAggStateKey.MAX_COUNT),
+ processor.hiddenStateKeys(new Max(value)));
IvmAggTarget target = target(3, IvmAggFunctionKind.MAX, "max_v",
IntegerType.INSTANCE,
- ImmutableMap.of(IvmAggStateKey.COUNT, slot("__ivm_count",
BigIntType.INSTANCE)), valueArg());
+ ImmutableMap.of(IvmAggStateKey.COUNT, slot("__ivm_count",
BigIntType.INSTANCE),
+ IvmAggStateKey.MAX_COUNT,
slot(IvmUtil.ivmAggHiddenColumnName(3, "MAX_COUNT"),
+ BigIntType.INSTANCE)),
+ valueArg());
List<NamedExpression> outputs = deltaOutputs(processor, target);
- Assertions.assertEquals(3, outputs.size());
+ Assertions.assertEquals(5, outputs.size());
Assertions.assertTrue(outputs.get(0).child(0) instanceof Max);
Assertions.assertTrue(outputs.get(1).getName().contains("DELMAX"));
Assertions.assertTrue(outputs.get(2).child(0) instanceof Sum);
+ Assertions.assertTrue(outputs.get(3).child(0) instanceof CountAtMax);
+ Assertions.assertTrue(outputs.get(4).child(0) instanceof CountAtMax);
Map<String, Expression> finalByName = apply(processor, target,
- ImmutableList.of(slot("max_v", IntegerType.INSTANCE),
slot("__ivm_count", BigIntType.INSTANCE)),
+ ImmutableList.of(slot("max_v", IntegerType.INSTANCE),
+ slot("__ivm_count", BigIntType.INSTANCE),
+ slot(IvmUtil.ivmAggHiddenColumnName(3, "MAX_COUNT"),
BigIntType.INSTANCE)),
mappedDeltaSlots(processor, target, outputs),
slot("delta_group_count", BigIntType.INSTANCE));
Expression visible = finalByName.get("max_v");
Assertions.assertTrue(visible.anyMatch(node -> node instanceof
AssertTrue));
- Assertions.assertTrue(visible.anyMatch(node -> node instanceof
LessThan));
- Assertions.assertTrue(visible.anyMatch(node -> node instanceof
Greatest));
+ Assertions.assertTrue(visible.anyMatch(node -> node instanceof
LessThanEqual));
+ Assertions.assertTrue(visible.anyMatch(node -> node instanceof
CaseWhen));
+
Assertions.assertTrue(finalByName.containsKey(IvmUtil.ivmAggHiddenColumnName(3,
"MAX_COUNT")));
+ Expression hiddenCount =
finalByName.get(IvmUtil.ivmAggHiddenColumnName(3, "MAX_COUNT"));
+ Assertions.assertTrue(containsSlotSubtract(hiddenCount,
"EXTREME_INS_COUNT", "EXTREME_DEL_COUNT"),
+ "MAX count initialization must subtract matching deletes");
}
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/agg/IvmAggMinProcessorTest.java
b/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/agg/IvmAggMinProcessorTest.java
index 67924bfa566..768e933842d 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/agg/IvmAggMinProcessorTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/agg/IvmAggMinProcessorTest.java
@@ -17,13 +17,15 @@
package org.apache.doris.mtmv.ivm.agg;
+import org.apache.doris.mtmv.ivm.IvmUtil;
+import org.apache.doris.nereids.trees.expressions.CaseWhen;
import org.apache.doris.nereids.trees.expressions.Expression;
-import org.apache.doris.nereids.trees.expressions.GreaterThan;
+import org.apache.doris.nereids.trees.expressions.GreaterThanEqual;
import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.functions.agg.CountAtMin;
import org.apache.doris.nereids.trees.expressions.functions.agg.Min;
import org.apache.doris.nereids.trees.expressions.functions.agg.Sum;
import org.apache.doris.nereids.trees.expressions.functions.scalar.AssertTrue;
-import org.apache.doris.nereids.trees.expressions.functions.scalar.Least;
import org.apache.doris.nereids.types.BigIntType;
import org.apache.doris.nereids.types.IntegerType;
@@ -41,22 +43,34 @@ class IvmAggMinProcessorTest extends
IvmAggProcessorTestBase {
IvmAggMinProcessor processor = new IvmAggMinProcessor();
Assertions.assertTrue(processor.supportsOriginalFunction(new
Min(value)));
Assertions.assertEquals(IvmAggFunctionKind.MIN,
processor.handledFunctionKind());
- Assertions.assertEquals(ImmutableList.of(IvmAggStateKey.COUNT),
processor.hiddenStateKeys(new Min(value)));
+ Assertions.assertEquals(ImmutableList.of(IvmAggStateKey.COUNT,
IvmAggStateKey.MIN_COUNT),
+ processor.hiddenStateKeys(new Min(value)));
IvmAggTarget target = target(2, IvmAggFunctionKind.MIN, "min_v",
IntegerType.INSTANCE,
- ImmutableMap.of(IvmAggStateKey.COUNT, slot("__ivm_count",
BigIntType.INSTANCE)), valueArg());
+ ImmutableMap.of(IvmAggStateKey.COUNT, slot("__ivm_count",
BigIntType.INSTANCE),
+ IvmAggStateKey.MIN_COUNT,
slot(IvmUtil.ivmAggHiddenColumnName(2, "MIN_COUNT"),
+ BigIntType.INSTANCE)),
+ valueArg());
List<NamedExpression> outputs = deltaOutputs(processor, target);
- Assertions.assertEquals(3, outputs.size());
+ Assertions.assertEquals(5, outputs.size());
Assertions.assertTrue(outputs.get(0).child(0) instanceof Min);
Assertions.assertTrue(outputs.get(1).getName().contains("DELMIN"));
Assertions.assertTrue(outputs.get(2).child(0) instanceof Sum);
+ Assertions.assertTrue(outputs.get(3).child(0) instanceof CountAtMin);
+ Assertions.assertTrue(outputs.get(4).child(0) instanceof CountAtMin);
Map<String, Expression> finalByName = apply(processor, target,
- ImmutableList.of(slot("min_v", IntegerType.INSTANCE),
slot("__ivm_count", BigIntType.INSTANCE)),
+ ImmutableList.of(slot("min_v", IntegerType.INSTANCE),
+ slot("__ivm_count", BigIntType.INSTANCE),
+ slot(IvmUtil.ivmAggHiddenColumnName(2, "MIN_COUNT"),
BigIntType.INSTANCE)),
mappedDeltaSlots(processor, target, outputs),
slot("delta_group_count", BigIntType.INSTANCE));
Expression visible = finalByName.get("min_v");
Assertions.assertTrue(visible.anyMatch(node -> node instanceof
AssertTrue));
- Assertions.assertTrue(visible.anyMatch(node -> node instanceof
GreaterThan));
- Assertions.assertTrue(visible.anyMatch(node -> node instanceof Least));
+ Assertions.assertTrue(visible.anyMatch(node -> node instanceof
GreaterThanEqual));
+ Assertions.assertTrue(visible.anyMatch(node -> node instanceof
CaseWhen));
+
Assertions.assertTrue(finalByName.containsKey(IvmUtil.ivmAggHiddenColumnName(2,
"MIN_COUNT")));
+ Expression hiddenCount =
finalByName.get(IvmUtil.ivmAggHiddenColumnName(2, "MIN_COUNT"));
+ Assertions.assertTrue(containsSlotSubtract(hiddenCount,
"EXTREME_INS_COUNT", "EXTREME_DEL_COUNT"),
+ "MIN count initialization must subtract matching deletes");
}
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/agg/IvmAggProcessorTestBase.java
b/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/agg/IvmAggProcessorTestBase.java
index 788202d8c04..b86da454861 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/agg/IvmAggProcessorTestBase.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/agg/IvmAggProcessorTestBase.java
@@ -21,6 +21,7 @@ import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.NamedExpression;
import org.apache.doris.nereids.trees.expressions.Slot;
import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.Subtract;
import org.apache.doris.nereids.trees.expressions.functions.agg.BitmapUnion;
import org.apache.doris.nereids.trees.expressions.functions.agg.Sum;
import org.apache.doris.nereids.trees.expressions.functions.scalar.BitmapCount;
@@ -102,6 +103,28 @@ abstract class IvmAggProcessorTestBase {
return ((Slot) expression).getName();
}
+ /**
+ * True when the expression subtracts the two delta count slots, which is
what the
+ * old-NULL branch must do to cancel deletes that match the insert-side
extreme.
+ */
+ protected boolean containsSlotSubtract(Expression expression, String
leftName, String rightName) {
+ if (expression instanceof Subtract) {
+ Expression left = expression.child(0);
+ Expression right = expression.child(1);
+ if (left instanceof Slot && right instanceof Slot
+ && ((Slot) left).getName().contains(leftName)
+ && ((Slot) right).getName().contains(rightName)) {
+ return true;
+ }
+ }
+ for (Expression child : expression.children()) {
+ if (containsSlotSubtract(child, leftName, rightName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
protected void assertTwoBitmapDeltaOutputs(List<NamedExpression> outputs) {
Assertions.assertEquals(2, outputs.size());
Assertions.assertTrue(outputs.get(0).child(0) instanceof BitmapUnion);
diff --git a/regression-test/data/mtmv_p0/ivm/test_ivm_agg_expr_over_agg_1.out
b/regression-test/data/mtmv_p0/ivm/test_ivm_agg_expr_over_agg_1.out
index ace0a73ecdc..e0761b78a2a 100644
--- a/regression-test/data/mtmv_p0/ivm/test_ivm_agg_expr_over_agg_1.out
+++ b/regression-test/data/mtmv_p0/ivm/test_ivm_agg_expr_over_agg_1.out
@@ -26,6 +26,7 @@ k int Yes false \N NONE
m2 bigint Yes false \N NONE
__DORIS_IVM_AGG_COUNT_COL__ bigint No false \N NONE
__DORIS_IVM_AGG_0_COUNT_COL__ bigint No false \N NONE
+__DORIS_IVM_AGG_0_MIN_COUNT_COL__ bigint No false \N NONE
__DORIS_IVM_AGG_0_MIN_COL__ int Yes false \N NONE
__DORIS_DELETE_SIGN__ tinyint No false 0 NONE
__DORIS_VERSION_COL__ bigint No false 0 NONE
@@ -37,6 +38,7 @@ k int Yes false \N NONE
m1 bigint Yes false \N NONE
__DORIS_IVM_AGG_COUNT_COL__ bigint No false \N NONE
__DORIS_IVM_AGG_0_COUNT_COL__ bigint No false \N NONE
+__DORIS_IVM_AGG_0_MAX_COUNT_COL__ bigint No false \N NONE
__DORIS_IVM_AGG_0_MAX_COL__ int Yes false \N NONE
__DORIS_DELETE_SIGN__ tinyint No false 0 NONE
__DORIS_VERSION_COL__ bigint No false 0 NONE
diff --git a/regression-test/data/mtmv_p0/ivm/test_ivm_agg_expr_over_agg_2.out
b/regression-test/data/mtmv_p0/ivm/test_ivm_agg_expr_over_agg_2.out
index 0c772bad147..02316300cce 100644
--- a/regression-test/data/mtmv_p0/ivm/test_ivm_agg_expr_over_agg_2.out
+++ b/regression-test/data/mtmv_p0/ivm/test_ivm_agg_expr_over_agg_2.out
@@ -165,6 +165,7 @@ __DORIS_IVM_ROW_ID_COL__ tinyint No true \N
m varchar(65533) Yes false \N NONE
__DORIS_IVM_AGG_COUNT_COL__ bigint No false \N NONE
__DORIS_IVM_AGG_0_COUNT_COL__ bigint No false \N NONE
+__DORIS_IVM_AGG_0_MIN_COUNT_COL__ bigint No false \N NONE
__DORIS_DELETE_SIGN__ tinyint No false 0 NONE
__DORIS_VERSION_COL__ bigint No false 0 NONE
__DORIS_SEQUENCE_COL__ bigint Yes false \N NONE
@@ -251,6 +252,7 @@ k int Yes false \N NONE
m2 bigint Yes false \N NONE
__DORIS_IVM_AGG_COUNT_COL__ bigint No false \N NONE
__DORIS_IVM_AGG_0_COUNT_COL__ bigint No false \N NONE
+__DORIS_IVM_AGG_0_MIN_COUNT_COL__ bigint No false \N NONE
__DORIS_IVM_AGG_0_MIN_COL__ int Yes false \N NONE
__DORIS_DELETE_SIGN__ tinyint No false 0 NONE
__DORIS_VERSION_COL__ bigint No false 0 NONE
diff --git a/regression-test/data/mtmv_p0/ivm/test_ivm_minmax_count.out
b/regression-test/data/mtmv_p0/ivm/test_ivm_minmax_count.out
new file mode 100644
index 00000000000..65ee6c479c4
--- /dev/null
+++ b/regression-test/data/mtmv_p0/ivm/test_ivm_minmax_count.out
@@ -0,0 +1,25 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !initial --
+a 10 30 3
+b 5 5 3
+
+-- !after_duplicate_min_delete --
+a 10 30 2
+b 5 5 3
+
+-- !after_duplicate_max_delete --
+a 10 30 2
+b 5 5 2
+
+-- !after_last_min_delete --
+a 30 30 1
+b 5 5 2
+
+-- !after_smaller_insert --
+a 7 30 3
+b 5 5 2
+
+-- !source --
+a 7 30 3
+b 5 5 2
+
diff --git a/regression-test/data/mtmv_p0/ivm/test_ivm_minmax_count_join.out
b/regression-test/data/mtmv_p0/ivm/test_ivm_minmax_count_join.out
new file mode 100644
index 00000000000..51b58359e29
--- /dev/null
+++ b/regression-test/data/mtmv_p0/ivm/test_ivm_minmax_count_join.out
@@ -0,0 +1,13 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !initial --
+\N
+
+-- !after_canceled_delete --
+1
+
+-- !after_boundary --
+2
+
+-- !source --
+2
+
diff --git a/regression-test/data/query_p0/aggregate/count_at_min_max.out
b/regression-test/data/query_p0/aggregate/count_at_min_max.out
new file mode 100644
index 00000000000..86f8c34874b
--- /dev/null
+++ b/regression-test/data/query_p0/aggregate/count_at_min_max.out
@@ -0,0 +1,36 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !grouped_integer --
+a 2 1
+b 2 2
+c 0 0
+
+-- !grouped_string --
+a 1 1
+b 1 1
+c 0 0
+
+-- !grouped_decimal --
+a 2 1
+b 2 2
+c 0 0
+
+-- !grouped_datetime --
+a 1 1
+b 1 1
+c 0 0
+
+-- !scalar --
+2 1 1 1
+
+-- !empty_input --
+0 0
+
+-- !all_null_group --
+0 0
+
+-- !foreach_all_null_element --
+[1, 0] [1, 0]
+
+-- !foreach_all_null_element_old_adapter --
+[1, 0] [1, 0]
+
diff --git a/regression-test/suites/mtmv_p0/ivm/test_ivm_minmax_count.groovy
b/regression-test/suites/mtmv_p0/ivm/test_ivm_minmax_count.groovy
new file mode 100644
index 00000000000..793cba7ca32
--- /dev/null
+++ b/regression-test/suites/mtmv_p0/ivm/test_ivm_minmax_count.groovy
@@ -0,0 +1,162 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import org.awaitility.Awaitility
+import static java.util.concurrent.TimeUnit.SECONDS
+
+suite("test_ivm_minmax_count") {
+ // The base-table queries below assert the same rows as the MV, so
transparent rewrite must be off.
+ sql """set enable_materialized_view_rewrite = false;"""
+ sql """set enable_dml_materialized_view_rewrite = false;"""
+ sql """drop materialized view if exists ivm_mm_count_mv"""
+ sql """drop table if exists ivm_mm_count_t"""
+
+ sql """
+ CREATE TABLE ivm_mm_count_t (
+ id BIGINT NOT NULL,
+ category VARCHAR(16) NOT NULL,
+ score BIGINT
+ )
+ UNIQUE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1",
+ "enable_unique_key_merge_on_write" = "true",
+ "binlog.enable" = "true",
+ "binlog.format" = "ROW",
+ "binlog.need_historical_value" = "true"
+ )
+ """
+
+ // category a: min=10 twice, max=30 once
+ // category b: min=5 three times, max=5
+ sql """INSERT INTO ivm_mm_count_t VALUES
+ (1, 'a', 10), (2, 'a', 10), (3, 'a', 30),
+ (4, 'b', 5), (5, 'b', 5), (6, 'b', 5)"""
+
+ sql """
+ CREATE MATERIALIZED VIEW ivm_mm_count_mv
+ BUILD DEFERRED REFRESH INCREMENTAL FALLBACK ON MANUAL
+ KEY(category)
+ DISTRIBUTED BY HASH(category) BUCKETS 1
+ PROPERTIES ("replication_num" = "1")
+ AS SELECT category, MIN(score) AS min_score, MAX(score) AS max_score,
COUNT(*) AS row_count
+ FROM ivm_mm_count_t
+ GROUP BY category
+ """
+
+ def assertNoFallback = { task ->
+ def reason = task.IvmFallbackReason
+ assertTrue(reason == null || reason.toString() == "\\N",
+ "unexpected IVM fallback: ${reason}")
+ }
+
+ // A task that just finished is briefly absent from tasks(): the job
removes it from its
+ // running list before the MV history publishes it, so the newest row can
temporarily be
+ // the previous refresh. Require a different TaskId before trusting a
terminal row.
+ String lastTaskId = null
+ def latestTask = {
+ def taskResult
+ Awaitility.await().atMost(300, SECONDS).pollInterval(2,
SECONDS).until({
+ taskResult = sql_return_maparray("""
+ SELECT TaskId, Status, RefreshMode, IvmFallbackReason, ErrorMsg
+ FROM tasks('type'='mv')
+ WHERE MvDatabaseName = '${context.dbName}'
+ AND MvName = 'ivm_mm_count_mv'
+ ORDER BY CreateTime DESC, TaskId DESC LIMIT 1
+ """)
+ return !taskResult.isEmpty()
+ && taskResult[0].TaskId.toString() != lastTaskId
+ && taskResult[0].Status.toString() != 'PENDING'
+ && taskResult[0].Status.toString() != 'RUNNING'
+ })
+ lastTaskId = taskResult[0].TaskId.toString()
+ return taskResult[0]
+ }
+
+ sql """REFRESH MATERIALIZED VIEW ivm_mm_count_mv INCREMENTAL"""
+ waitingMTMVTaskFinishedByMvName("ivm_mm_count_mv")
+ lastTaskId = sql_return_maparray("""
+ SELECT TaskId
+ FROM tasks('type'='mv')
+ WHERE MvDatabaseName = '${context.dbName}'
+ AND MvName = 'ivm_mm_count_mv'
+ ORDER BY CreateTime DESC, TaskId DESC LIMIT 1
+ """)[0].TaskId.toString()
+ order_qt_initial """
+ SELECT category, min_score, max_score, row_count
+ FROM ivm_mm_count_mv
+ """
+
+ // Delete one of the two rows that hold the minimum of category a. The
minimum survives,
+ // so the hidden min-count state keeps the refresh incremental.
+ sql """DELETE FROM ivm_mm_count_t WHERE id = 1"""
+ Thread.sleep(1000)
+ sql """REFRESH MATERIALIZED VIEW ivm_mm_count_mv INCREMENTAL"""
+ def duplicateMinTask = latestTask()
+ assertEquals("SUCCESS", duplicateMinTask.Status.toString())
+ assertNoFallback(duplicateMinTask)
+ order_qt_after_duplicate_min_delete """
+ SELECT category, min_score, max_score, row_count
+ FROM ivm_mm_count_mv
+ """
+
+ // Delete one of the three rows that hold the maximum of category b. The
maximum survives.
+ sql """DELETE FROM ivm_mm_count_t WHERE id = 4"""
+ Thread.sleep(1000)
+ sql """REFRESH MATERIALIZED VIEW ivm_mm_count_mv INCREMENTAL"""
+ def duplicateMaxTask = latestTask()
+ assertEquals("SUCCESS", duplicateMaxTask.Status.toString())
+ assertNoFallback(duplicateMaxTask)
+ order_qt_after_duplicate_max_delete """
+ SELECT category, min_score, max_score, row_count
+ FROM ivm_mm_count_mv
+ """
+
+ // Delete the last row holding category a's minimum. Its count reaches
zero and no smaller
+ // value was inserted, so the refresh still falls back to a complete
refresh.
+ sql """DELETE FROM ivm_mm_count_t WHERE id = 2"""
+ Thread.sleep(1000)
+ sql """REFRESH MATERIALIZED VIEW ivm_mm_count_mv INCREMENTAL FALLBACK"""
+ def lastMinTask = latestTask()
+ assertEquals("SUCCESS", lastMinTask.Status.toString())
+ assertEquals("COMPLETE", lastMinTask.RefreshMode.toString())
+ assertEquals("MIN_MAX_BOUNDARY_HIT",
lastMinTask.IvmFallbackReason.toString())
+ order_qt_after_last_min_delete """
+ SELECT category, min_score, max_score, row_count
+ FROM ivm_mm_count_mv
+ """
+
+ // A smaller inserted value becomes the new minimum without a complete
refresh.
+ sql """INSERT INTO ivm_mm_count_t VALUES (7, 'a', 7), (8, 'a', 7)"""
+ Thread.sleep(1000)
+ sql """REFRESH MATERIALIZED VIEW ivm_mm_count_mv INCREMENTAL"""
+ def smallerInsertTask = latestTask()
+ assertEquals("SUCCESS", smallerInsertTask.Status.toString())
+ assertNoFallback(smallerInsertTask)
+ order_qt_after_smaller_insert """
+ SELECT category, min_score, max_score, row_count
+ FROM ivm_mm_count_mv
+ """
+
+ // The source query must agree with the MV after every step.
+ order_qt_source """
+ SELECT category, MIN(score), MAX(score), COUNT(*)
+ FROM ivm_mm_count_t
+ GROUP BY category
+ """
+}
diff --git
a/regression-test/suites/mtmv_p0/ivm/test_ivm_minmax_count_join.groovy
b/regression-test/suites/mtmv_p0/ivm/test_ivm_minmax_count_join.groovy
new file mode 100644
index 00000000000..d2709f2a0b8
--- /dev/null
+++ b/regression-test/suites/mtmv_p0/ivm/test_ivm_minmax_count_join.groovy
@@ -0,0 +1,148 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import org.awaitility.Awaitility
+import static java.util.concurrent.TimeUnit.SECONDS
+
+suite("test_ivm_minmax_count_join") {
+ // The base-table queries below assert the same rows as the MV, so
transparent rewrite must be off.
+ sql """set enable_materialized_view_rewrite = false;"""
+ sql """set enable_dml_materialized_view_rewrite = false;"""
+ sql """drop materialized view if exists ivm_mm_join_mv"""
+ sql """drop table if exists ivm_mm_join_l"""
+ sql """drop table if exists ivm_mm_join_r"""
+
+ sql """
+ CREATE TABLE ivm_mm_join_l (
+ k BIGINT NOT NULL,
+ v BIGINT
+ )
+ UNIQUE KEY(k)
+ DISTRIBUTED BY HASH(k) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1",
+ "enable_unique_key_merge_on_write" = "true",
+ "binlog.enable" = "true",
+ "binlog.format" = "ROW",
+ "binlog.need_historical_value" = "true"
+ )
+ """
+ sql """
+ CREATE TABLE ivm_mm_join_r (
+ k BIGINT NOT NULL
+ )
+ UNIQUE KEY(k)
+ DISTRIBUTED BY HASH(k) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1",
+ "enable_unique_key_merge_on_write" = "true",
+ "binlog.enable" = "true",
+ "binlog.format" = "ROW",
+ "binlog.need_historical_value" = "true"
+ )
+ """
+
+ // The initial join produces only NULL values, so the MV starts with MIN =
NULL and the
+ // hidden MIN_COUNT is zero.
+ sql """INSERT INTO ivm_mm_join_l VALUES (0, NULL)"""
+ sql """INSERT INTO ivm_mm_join_r VALUES (0), (1), (2), (3)"""
+
+ sql """
+ CREATE MATERIALIZED VIEW ivm_mm_join_mv
+ BUILD DEFERRED REFRESH INCREMENTAL FALLBACK ON MANUAL
+ DISTRIBUTED BY RANDOM BUCKETS 1
+ PROPERTIES ("replication_num" = "1")
+ AS SELECT MIN(l.v) AS min_v
+ FROM ivm_mm_join_l l
+ INNER JOIN ivm_mm_join_r r ON l.k = r.k
+ """
+
+ def assertNoFallback = { task ->
+ def reason = task.IvmFallbackReason
+ assertTrue(reason == null || reason.toString() == "\\N",
+ "unexpected IVM fallback: ${reason}")
+ }
+
+ // A task that just finished is briefly absent from tasks(): the job
removes it from its
+ // running list before the MV history publishes it, so the newest row can
temporarily be
+ // the previous refresh. Require a different TaskId before trusting a
terminal row.
+ String lastTaskId = null
+ def latestTask = {
+ def taskResult
+ Awaitility.await().atMost(300, SECONDS).pollInterval(2,
SECONDS).until({
+ taskResult = sql_return_maparray("""
+ SELECT TaskId, Status, RefreshMode, IvmFallbackReason, ErrorMsg
+ FROM tasks('type'='mv')
+ WHERE MvDatabaseName = '${context.dbName}'
+ AND MvName = 'ivm_mm_join_mv'
+ ORDER BY CreateTime DESC, TaskId DESC LIMIT 1
+ """)
+ return !taskResult.isEmpty()
+ && taskResult[0].TaskId.toString() != lastTaskId
+ && taskResult[0].Status.toString() != 'PENDING'
+ && taskResult[0].Status.toString() != 'RUNNING'
+ })
+ lastTaskId = taskResult[0].TaskId.toString()
+ return taskResult[0]
+ }
+
+ sql """REFRESH MATERIALIZED VIEW ivm_mm_join_mv INCREMENTAL"""
+ waitingMTMVTaskFinishedByMvName("ivm_mm_join_mv")
+ lastTaskId = sql_return_maparray("""
+ SELECT TaskId
+ FROM tasks('type'='mv')
+ WHERE MvDatabaseName = '${context.dbName}'
+ AND MvName = 'ivm_mm_join_mv'
+ ORDER BY CreateTime DESC, TaskId DESC LIMIT 1
+ """)[0].TaskId.toString()
+ order_qt_initial """
+ SELECT min_v FROM ivm_mm_join_mv
+ """
+
+ // Insert L rows for R keys 1, 2 and 3, then delete R key 1. The join
rewrite emits
+ // +{1,1,2} from leftDelta JOIN rightPre and -{1} from leftPost JOIN
rightDelta, so the
+ // new minimum 1 has net count 1 (the delete cancels one of the two
inserted rows).
+ sql """INSERT INTO ivm_mm_join_l VALUES (1, 1), (2, 1), (3, 2)"""
+ sql """DELETE FROM ivm_mm_join_r WHERE k = 1"""
+ Thread.sleep(1000)
+ sql """REFRESH MATERIALIZED VIEW ivm_mm_join_mv INCREMENTAL"""
+ def canceledDeleteTask = latestTask()
+ assertEquals("SUCCESS", canceledDeleteTask.Status.toString())
+ assertNoFallback(canceledDeleteTask)
+ order_qt_after_canceled_delete """
+ SELECT min_v FROM ivm_mm_join_mv
+ """
+
+ // The stored count for value 1 is 1. Deleting the remaining row that
produces value 1
+ // exhausts it, so the refresh must fall back instead of retaining the
stale MIN = 1.
+ sql """DELETE FROM ivm_mm_join_l WHERE k = 2"""
+ Thread.sleep(1000)
+ sql """REFRESH MATERIALIZED VIEW ivm_mm_join_mv INCREMENTAL FALLBACK"""
+ def boundaryTask = latestTask()
+ assertEquals("SUCCESS", boundaryTask.Status.toString())
+ assertEquals("COMPLETE", boundaryTask.RefreshMode.toString())
+ assertEquals("MIN_MAX_BOUNDARY_HIT",
boundaryTask.IvmFallbackReason.toString())
+ order_qt_after_boundary """
+ SELECT min_v FROM ivm_mm_join_mv
+ """
+
+ order_qt_source """
+ SELECT MIN(l.v)
+ FROM ivm_mm_join_l l
+ INNER JOIN ivm_mm_join_r r ON l.k = r.k
+ """
+}
diff --git a/regression-test/suites/query_p0/aggregate/count_at_min_max.groovy
b/regression-test/suites/query_p0/aggregate/count_at_min_max.groovy
new file mode 100644
index 00000000000..4addaa28ac5
--- /dev/null
+++ b/regression-test/suites/query_p0/aggregate/count_at_min_max.groovy
@@ -0,0 +1,126 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("count_at_min_max") {
+ sql """drop table if exists count_at_min_max_t"""
+
+ sql """
+ CREATE TABLE count_at_min_max_t (
+ id INT NOT NULL,
+ grp VARCHAR(8) NOT NULL,
+ i BIGINT,
+ s STRING,
+ d DECIMAL(10, 2),
+ dt DATETIME
+ )
+ ENGINE = OLAP
+ DUPLICATE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 2
+ PROPERTIES (
+ "replication_num" = "1"
+ )
+ """
+
+ // grp a: i min=10 twice, max=30 once; s min='a', max='c'
+ // grp b: i min=max=5 twice; the NULL row must be ignored
+ // grp c: only NULL values, so both counts are zero
+ sql """INSERT INTO count_at_min_max_t VALUES
+ (1, 'a', 10, 'b', 10.50, '2024-01-01 10:00:00'),
+ (2, 'a', 10, 'a', 10.50, '2024-01-01 09:00:00'),
+ (3, 'a', 30, 'c', 30.50, '2024-01-01 11:00:00'),
+ (4, 'b', 5, 'x', 5.50, '2024-01-02 10:00:00'),
+ (5, 'b', 5, 'y', 5.50, '2024-01-02 09:00:00'),
+ (6, 'b', NULL, NULL, NULL, NULL),
+ (7, 'c', NULL, NULL, NULL, NULL)"""
+
+ order_qt_grouped_integer """
+ SELECT grp, count_at_min(i) AS min_cnt, count_at_max(i) AS max_cnt
+ FROM count_at_min_max_t
+ GROUP BY grp
+ """
+
+ order_qt_grouped_string """
+ SELECT grp, count_at_min(s) AS min_cnt, count_at_max(s) AS max_cnt
+ FROM count_at_min_max_t
+ GROUP BY grp
+ """
+
+ order_qt_grouped_decimal """
+ SELECT grp, count_at_min(d) AS min_cnt, count_at_max(d) AS max_cnt
+ FROM count_at_min_max_t
+ GROUP BY grp
+ """
+
+ order_qt_grouped_datetime """
+ SELECT grp, count_at_min(dt) AS min_cnt, count_at_max(dt) AS max_cnt
+ FROM count_at_min_max_t
+ GROUP BY grp
+ """
+
+ order_qt_scalar """
+ SELECT count_at_min(i) AS min_i_cnt, count_at_max(i) AS max_i_cnt,
+ count_at_min(s) AS min_s_cnt, count_at_max(s) AS max_s_cnt
+ FROM count_at_min_max_t
+ """
+
+ // Empty input returns zero, not NULL.
+ order_qt_empty_input """
+ SELECT count_at_min(i) AS min_cnt, count_at_max(i) AS max_cnt
+ FROM count_at_min_max_t
+ WHERE id < 0
+ """
+
+ // A group whose only rows are NULL has no non-NULL value.
+ order_qt_all_null_group """
+ SELECT count_at_min(i) AS min_cnt, count_at_max(i) AS max_cnt
+ FROM count_at_min_max_t
+ WHERE grp = 'c'
+ """
+
+ // The foreach combinator applies the aggregate per array position. An
all-NULL
+ // position must return 0, not NULL, for both the default (V2) and old
nullable
+ // adapters.
+ order_qt_foreach_all_null_element """
+ SELECT count_at_min_foreach(a) AS min_cnt, count_at_max_foreach(a) AS
max_cnt
+ FROM (
+ SELECT CAST([1, NULL] AS ARRAY<INT>) AS a
+ UNION ALL
+ SELECT CAST([2, NULL] AS ARRAY<INT>)
+ ) t
+ """
+
+ sql "set enable_aggregate_function_null_v2 = false"
+ order_qt_foreach_all_null_element_old_adapter """
+ SELECT count_at_min_foreach(a) AS min_cnt, count_at_max_foreach(a) AS
max_cnt
+ FROM (
+ SELECT CAST([1, NULL] AS ARRAY<INT>) AS a
+ UNION ALL
+ SELECT CAST([2, NULL] AS ARRAY<INT>)
+ ) t
+ """
+ sql "set enable_aggregate_function_null_v2 = true"
+
+ test {
+ sql "SELECT count_at_min(DISTINCT i) FROM count_at_min_max_t"
+ exception "COUNT_AT_MIN does not support DISTINCT"
+ }
+
+ test {
+ sql "SELECT count_at_max(DISTINCT i) FROM count_at_min_max_t"
+ exception "COUNT_AT_MAX does not support DISTINCT"
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]