This is an automated email from the ASF dual-hosted git repository.
HappenLee 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 dc4fc151b99 [Feature](func) Support AggFn percentile_approx_array
(#65847)
dc4fc151b99 is described below
commit dc4fc151b997a6d1909e01ebdc6bcc60eed5b8a2
Author: linrrarity <[email protected]>
AuthorDate: Mon Aug 3 09:53:04 2026 +0800
[Feature](func) Support AggFn percentile_approx_array (#65847)
### Release note
doc: https://github.com/apache/doris-website/pull/4016
Support function `PERCENTILE_APPROX_ARRAY`
performance:
```text
Doris> select percentile_approx(FUniqID, 0.01), percentile_approx(FUniqID,
0.99) from hits_100m;
+----------------------------------+----------------------------------+
| percentile_approx(FUniqID, 0.01) | percentile_approx(FUniqID, 0.99) |
+----------------------------------+----------------------------------+
| 0 | 9.170476181709914e+18 |
+----------------------------------+----------------------------------+
1 row in set (13.962 sec)
Doris> select percentile_approx_array(FUniqID, [0.01, 0.99]) from hits_100m;
+------------------------------------------------+
| percentile_approx_array(FUniqID, [0.01, 0.99]) |
+------------------------------------------------+
| [0, 9.170476181709914e+18] |
+------------------------------------------------+
1 row in set (7.869 sec)
```
---
.../aggregate/aggregate_function_percentile.cpp | 20 ++
.../aggregate/aggregate_function_percentile.h | 234 +++++++++++++-
be/src/util/percentile_util.h | 2 +-
be/src/util/tdigest.h | 267 ++++++++++------
be/test/exprs/aggregate/agg_percentile_test.cpp | 353 +++++++++++++++++++++
be/test/util/tdigest_test.cpp | 81 ++++-
.../doris/catalog/BuiltinAggregateFunctions.java | 2 +
.../functions/agg/PercentileApproxArray.java | 127 ++++++++
.../functions/combinator/ForEachCombinator.java | 1 +
.../functions/combinator/MergeCombinator.java | 8 +-
.../functions/combinator/StateCombinator.java | 5 +
.../functions/combinator/UnionCombinator.java | 8 +-
.../visitor/AggregateFunctionVisitor.java | 5 +
.../agg_state/nereids/test_agg_state_nereids.out | 5 +
.../test_aggregate_percentile_approx_array.out | 38 +++
.../nereids/test_agg_state_nereids.groovy | 35 ++
.../suites/function_p0/test_agg_foreach.groovy | 5 +
.../test_aggregate_percentile_approx_array.groovy | 211 ++++++++++++
18 files changed, 1297 insertions(+), 110 deletions(-)
diff --git a/be/src/exprs/aggregate/aggregate_function_percentile.cpp
b/be/src/exprs/aggregate/aggregate_function_percentile.cpp
index b1309ea0508..eb829f1e318 100644
--- a/be/src/exprs/aggregate/aggregate_function_percentile.cpp
+++ b/be/src/exprs/aggregate/aggregate_function_percentile.cpp
@@ -59,6 +59,24 @@ AggregateFunctionPtr
create_aggregate_function_percentile_approx_weighted(
return nullptr;
}
+AggregateFunctionPtr create_aggregate_function_percentile_approx_array(
+ const std::string& name, const DataTypes& argument_types, const
DataTypePtr& result_type,
+ const bool result_is_nullable, const AggregateFunctionAttr& attr) {
+ const DataTypePtr& argument_type = remove_nullable(argument_types[0]);
+ if (argument_type->get_primitive_type() != PrimitiveType::TYPE_DOUBLE) {
+ return nullptr;
+ }
+ if (argument_types.size() == 2) {
+ return
creator_without_type::create<AggregateFunctionPercentileApproxArray<false>>(
+ argument_types, result_is_nullable, attr);
+ }
+ if (argument_types.size() == 3) {
+ return
creator_without_type::create<AggregateFunctionPercentileApproxArray<true>>(
+ argument_types, result_is_nullable, attr);
+ }
+ return nullptr;
+}
+
void register_aggregate_function_percentile(AggregateFunctionSimpleFactory&
factory) {
using creator = creator_with_type_list<TYPE_TINYINT, TYPE_SMALLINT,
TYPE_INT, TYPE_BIGINT,
TYPE_LARGEINT, TYPE_FLOAT,
TYPE_DOUBLE>;
@@ -85,6 +103,8 @@ void
register_aggregate_function_percentile_old(AggregateFunctionSimpleFactory&
void
register_aggregate_function_percentile_approx(AggregateFunctionSimpleFactory&
factory) {
factory.register_function_both("percentile_approx",
create_aggregate_function_percentile_approx);
+ factory.register_function_both("percentile_approx_array",
+
create_aggregate_function_percentile_approx_array);
factory.register_function_both("percentile_approx_weighted",
create_aggregate_function_percentile_approx_weighted);
diff --git a/be/src/exprs/aggregate/aggregate_function_percentile.h
b/be/src/exprs/aggregate/aggregate_function_percentile.h
index fdf85b9a994..cfd8e239538 100644
--- a/be/src/exprs/aggregate/aggregate_function_percentile.h
+++ b/be/src/exprs/aggregate/aggregate_function_percentile.h
@@ -86,7 +86,7 @@ struct PercentileApproxState {
//The compression parameter setting range is [2048, 10000].
//If the value of compression parameter is not specified set, or
is outside the range of [2048, 10000],
//will use the default value of 10000
- if (compression < 2048 || compression > 10000) {
+ if (!std::isfinite(compression) || compression < 2048 ||
compression > 10000) {
compression = 10000;
}
digest = TDigest::create_unique(compression);
@@ -329,6 +329,238 @@ public:
}
};
+struct PercentileApproxArrayState {
+ void init(const PaddedPODArray<Float64>& quantiles, const NullMap&
null_map, size_t start,
+ size_t size, float compression = 10000) {
+ if (init_flag) {
+ return;
+ }
+
+ if (!std::isfinite(compression) || compression < 2048 || compression >
10000) {
+ compression = 10000;
+ }
+ compressions = compression;
+ levels.quantiles.resize(size);
+ levels.permutation.resize(size);
+ for (size_t i = 0; i < size; ++i) {
+ if (null_map[start + i]) {
+ throw Exception(ErrorCode::INVALID_ARGUMENT,
+ "percentile_approx_array quantile should not
be null");
+ }
+ check_quantile(quantiles[start + i]);
+ levels.quantiles[i] = quantiles[start + i];
+ levels.permutation[i] = i;
+ }
+ if (!levels.empty()) {
+ digest = TDigest::create_unique(compressions);
+ }
+ init_flag = true;
+ }
+
+ void add_range(const Float64* values, size_t size) {
+ if (levels.empty()) {
+ return;
+ }
+ for (size_t i = 0; i < size; ++i) {
+ digest->add(static_cast<float>(values[i]));
+ }
+ }
+
+ void write(BufferWritable& buf) const {
+ buf.write_binary(init_flag);
+ if (!init_flag) {
+ return;
+ }
+
+ levels.write(buf);
+ buf.write_binary(compressions);
+ if (levels.empty()) {
+ return;
+ }
+ uint32_t serialize_size = digest->serialized_size();
+ std::string result(serialize_size, '0');
+ digest->serialize(reinterpret_cast<uint8_t*>(result.data()));
+ buf.write_binary(result);
+ }
+
+ void read(BufferReadable& buf) {
+ reset();
+ buf.read_binary(init_flag);
+ if (!init_flag) {
+ return;
+ }
+
+ levels.read(buf);
+ buf.read_binary(compressions);
+ if (levels.empty()) {
+ return;
+ }
+ std::string str;
+ buf.read_binary(str);
+ digest = TDigest::create_unique(compressions);
+ digest->unserialize(reinterpret_cast<const uint8_t*>(str.data()));
+ }
+
+ void merge(const PercentileApproxArrayState& rhs) {
+ if (!rhs.init_flag) {
+ return;
+ }
+
+ if (!init_flag) {
+ levels.merge(rhs.levels);
+ compressions = rhs.compressions;
+ if (!levels.empty()) {
+ digest = TDigest::create_unique(compressions);
+ }
+ init_flag = true;
+ } else {
+ if (compressions != rhs.compressions || levels.quantiles !=
rhs.levels.quantiles) {
+ throw Exception(
+ ErrorCode::INVALID_ARGUMENT,
+ "percentile_approx_array aggregate states have
incompatible quantiles "
+ "or compression");
+ }
+ }
+ if (!levels.empty()) {
+ digest->merge(rhs.digest.get());
+ }
+ }
+
+ void reset() {
+ init_flag = false;
+ levels.clear();
+ digest.reset();
+ compressions = 10000;
+ }
+
+ void insert_result_into(IColumn& to) const {
+ auto& column_data = assert_cast<ColumnFloat64&,
TypeCheckOnRelease::DISABLE>(to).get_data();
+ if (levels.empty()) {
+ return;
+ }
+
+ const size_t old_size = column_data.size();
+ const size_t size = levels.quantiles.size();
+ column_data.resize(old_size + size);
+ digest->quantiles(levels.quantiles.data(),
levels.get_permutation().data(), size,
+ column_data.data() + old_size);
+ }
+
+ bool init_flag = false;
+ PercentileLevels levels;
+ std::unique_ptr<TDigest> digest;
+ float compressions = 10000;
+};
+
+template <bool has_compression>
+class AggregateFunctionPercentileApproxArray final
+ : public IAggregateFunctionDataHelper<
+ PercentileApproxArrayState,
+ AggregateFunctionPercentileApproxArray<has_compression>>,
+ MultiExpression,
+ NotNullableAggregateFunction {
+public:
+ using Base =
+ IAggregateFunctionDataHelper<PercentileApproxArrayState,
+
AggregateFunctionPercentileApproxArray<has_compression>>;
+
+ AggregateFunctionPercentileApproxArray(const DataTypes& argument_types_)
+ : Base(argument_types_) {}
+
+ String get_name() const override { return "percentile_approx_array"; }
+
+ DataTypePtr get_return_type() const override {
+ return
std::make_shared<DataTypeArray>(make_nullable(std::make_shared<DataTypeFloat64>()));
+ }
+
+ void add(AggregateDataPtr __restrict place, const IColumn** columns,
ssize_t row_num,
+ Arena&) const override {
+ const auto& sources =
+ assert_cast<const ColumnFloat64&,
TypeCheckOnRelease::DISABLE>(*columns[0]);
+ _add_values(this->data(place), &sources.get_data()[row_num], 1,
columns,
+ cast_set<size_t>(row_num));
+ }
+
+ void add_batch_single_place(size_t batch_size, AggregateDataPtr place,
const IColumn** columns,
+ Arena&) const override {
+ const auto& sources =
+ assert_cast<const ColumnFloat64&,
TypeCheckOnRelease::DISABLE>(*columns[0]);
+ DCHECK_EQ(sources.get_data().size(), batch_size);
+ _add_values(this->data(place), sources.get_data().data(), batch_size,
columns, 0);
+ }
+
+ void add_batch_range(size_t batch_begin, size_t batch_end,
AggregateDataPtr place,
+ const IColumn** columns, Arena&, bool has_null)
override {
+ DCHECK(!has_null);
+ const auto& sources =
+ assert_cast<const ColumnFloat64&,
TypeCheckOnRelease::DISABLE>(*columns[0]);
+ _add_values(this->data(place), sources.get_data().data() + batch_begin,
+ batch_end - batch_begin + 1, columns, batch_begin);
+ }
+
+ void check_input_columns_type(const IColumn** columns) const override {
+ this->template check_argument_column_type<ColumnFloat64>(columns[0]);
+ check_percentile_array_column_type(*this, *columns[1], 1);
+ if constexpr (has_compression) {
+ this->template
check_argument_column_type<ColumnFloat64>(columns[2]);
+ }
+ }
+
+ void reset(AggregateDataPtr __restrict place) const override {
this->data(place).reset(); }
+
+ void merge(AggregateDataPtr __restrict place, ConstAggregateDataPtr rhs,
+ Arena&) const override {
+ this->data(place).merge(this->data(rhs));
+ }
+
+ void serialize(ConstAggregateDataPtr __restrict place, BufferWritable&
buf) const override {
+ this->data(place).write(buf);
+ }
+
+ void deserialize(AggregateDataPtr __restrict place, BufferReadable& buf,
+ Arena&) const override {
+ this->data(place).read(buf);
+ }
+
+ void insert_result_into(ConstAggregateDataPtr __restrict place, IColumn&
to) const override {
+ auto& to_arr = assert_cast<ColumnArray&,
TypeCheckOnRelease::DISABLE>(to);
+ auto& nullable_data =
+ assert_cast<ColumnNullable&,
TypeCheckOnRelease::DISABLE>(to_arr.get_data());
+ auto& nested_data = nullable_data.get_nested_column();
+ this->data(place).insert_result_into(nested_data);
+ nullable_data.get_null_map_data().resize_fill(nested_data.size(), 0);
+ to_arr.get_offsets().push_back(nested_data.size());
+ }
+
+private:
+ void _add_values(PercentileApproxArrayState& state, const Float64* values,
size_t value_size,
+ const IColumn** columns, size_t quantile_row) const {
+ if (!state.init_flag) {
+ const auto& quantile_array =
+ assert_cast<const ColumnArray&,
TypeCheckOnRelease::DISABLE>(*columns[1]);
+ const auto& offsets = quantile_array.get_offsets();
+ const auto& nullable_quantiles =
+ assert_cast<const ColumnNullable&,
TypeCheckOnRelease::DISABLE>(
+ quantile_array.get_data());
+ const auto& quantiles = assert_cast<const ColumnFloat64&,
TypeCheckOnRelease::DISABLE>(
+
nullable_quantiles.get_nested_column())
+ .get_data();
+ const auto& null_map = nullable_quantiles.get_null_map_data();
+ const size_t start = quantile_row == 0 ? 0 : offsets[quantile_row
- 1];
+ const size_t quantile_size = offsets[quantile_row] - start;
+
+ float compression = 10000;
+ if constexpr (has_compression) {
+ const auto& compression_column =
+ assert_cast<const ColumnFloat64&,
TypeCheckOnRelease::DISABLE>(*columns[2]);
+ compression =
static_cast<float>(compression_column.get_element(0));
+ }
+ state.init(quantiles, null_map, start, quantile_size, compression);
+ }
+ state.add_range(values, value_size);
+ }
+};
+
template <PrimitiveType T>
struct PercentileState {
mutable std::vector<Counts<typename PrimitiveTypeTraits<T>::CppType>>
vec_counts;
diff --git a/be/src/util/percentile_util.h b/be/src/util/percentile_util.h
index 05003e5cb63..030e8df8069 100644
--- a/be/src/util/percentile_util.h
+++ b/be/src/util/percentile_util.h
@@ -35,7 +35,7 @@
namespace doris {
inline void check_quantile(double quantile) {
- if (quantile < 0 || quantile > 1) {
+ if (!std::isfinite(quantile) || quantile < 0 || quantile > 1) {
throw Exception(ErrorCode::INVALID_ARGUMENT,
"quantile in func percentile should in [0, 1], but
real data is:" +
std::to_string(quantile));
diff --git a/be/src/util/tdigest.h b/be/src/util/tdigest.h
index 7209c5f8dab..cd5570edeb6 100644
--- a/be/src/util/tdigest.h
+++ b/be/src/util/tdigest.h
@@ -62,7 +62,7 @@ using Value = float;
using Weight = float;
using Index = size_t;
-const size_t kHighWater = 40000;
+constexpr size_t K_HIGH_WATER = 40000;
class Centroid {
public:
@@ -126,7 +126,7 @@ class TDigest {
TDigestComparator() = default;
bool operator()(const TDigest* left, const TDigest* right) const {
- return left->totalSize() > right->totalSize();
+ return left->total_size() > right->total_size();
}
};
using TDigestQueue =
@@ -137,19 +137,19 @@ public:
explicit TDigest(Value compression) : TDigest(compression, 0) {}
- TDigest(Value compression, Index bufferSize) : TDigest(compression,
bufferSize, 0) {}
+ TDigest(Value compression, Index buffer_size) : TDigest(compression,
buffer_size, 0) {}
- TDigest(Value compression, Index unmergedSize, Index mergedSize)
+ TDigest(Value compression, Index unmerged_size, Index merged_size)
: _compression(compression),
- _max_processed(processedSize(mergedSize, compression)),
- _max_unprocessed(unprocessedSize(unmergedSize, compression)) {
+ _max_processed(processed_size(merged_size, compression)),
+ _max_unprocessed(unprocessed_size(unmerged_size, compression)) {
_processed.reserve(_max_processed);
_unprocessed.reserve(_max_unprocessed + 1);
}
TDigest(std::vector<Centroid>&& processed, std::vector<Centroid>&&
unprocessed,
- Value compression, Index unmergedSize, Index mergedSize)
- : TDigest(compression, unmergedSize, mergedSize) {
+ Value compression, Index unmerged_size, Index merged_size)
+ : TDigest(compression, unmerged_size, merged_size) {
_processed = std::move(processed);
_unprocessed = std::move(unprocessed);
@@ -159,7 +159,7 @@ public:
_min = std::min(_min, _processed[0].mean());
_max = std::max(_max, (_processed.cend() - 1)->mean());
}
- updateCumulative();
+ _update_cumulative();
}
static Weight weight(std::vector<Centroid>& centroids) noexcept {
@@ -188,11 +188,11 @@ public:
: TDigest(std::move(o._processed), std::move(o._unprocessed),
o._compression,
o._max_unprocessed, o._max_processed) {}
- static inline Index processedSize(Index size, Value compression) noexcept {
+ static inline Index processed_size(Index size, Value compression) noexcept
{
return (size == 0) ? static_cast<Index>(2 * std::ceil(compression)) :
size;
}
- static inline Index unprocessedSize(Index size, Value compression)
noexcept {
+ static inline Index unprocessed_size(Index size, Value compression)
noexcept {
return (size == 0) ? static_cast<Index>(8 * std::ceil(compression)) :
size;
}
@@ -206,15 +206,15 @@ public:
const std::vector<Centroid>& unprocessed() const { return _unprocessed; }
- Index maxUnprocessed() const { return _max_unprocessed; }
+ Index max_unprocessed() const { return _max_unprocessed; }
- Index maxProcessed() const { return _max_processed; }
+ Index max_processed() const { return _max_processed; }
void add(std::vector<const TDigest*> digests) { add(digests.cbegin(),
digests.cend()); }
// merge in a vector of tdigests in the most efficient manner possible
// in constant space
- // works for any value of kHighWater
+ // works for any value of K_HIGH_WATER
void add(std::vector<const TDigest*>::const_iterator iter,
std::vector<const TDigest*>::const_iterator end) {
if (iter != end) {
@@ -226,48 +226,48 @@ public:
std::vector<const TDigest*> batch;
batch.reserve(size);
- size_t totalSize = 0;
+ size_t total_size = 0;
while (!pq.empty()) {
const auto* td = pq.top();
batch.push_back(td);
pq.pop();
- totalSize += td->totalSize();
- if (totalSize >= kHighWater || pq.empty()) {
- mergeProcessed(batch);
- mergeUnprocessed(batch);
- processIfNecessary();
+ total_size += td->total_size();
+ if (total_size >= K_HIGH_WATER || pq.empty()) {
+ _merge_processed(batch);
+ _merge_unprocessed(batch);
+ _process_if_necessary();
batch.clear();
- totalSize = 0;
+ total_size = 0;
}
}
- updateCumulative();
+ _update_cumulative();
}
}
- Weight processedWeight() const { return _processed_weight; }
+ Weight processed_weight() const { return _processed_weight; }
- Weight unprocessedWeight() const { return _unprocessed_weight; }
+ Weight unprocessed_weight() const { return _unprocessed_weight; }
- bool haveUnprocessed() const { return _unprocessed.size() > 0; }
+ bool have_unprocessed() const { return _unprocessed.size() > 0; }
- size_t totalSize() const { return _processed.size() + _unprocessed.size();
}
+ size_t total_size() const { return _processed.size() +
_unprocessed.size(); }
- long totalWeight() const { return static_cast<long>(_processed_weight +
_unprocessed_weight); }
+ long total_weight() const { return static_cast<long>(_processed_weight +
_unprocessed_weight); }
// return the cdf on the t-digest
Value cdf(Value x) {
- if (haveUnprocessed() || isDirty()) {
- process();
+ if (have_unprocessed() || is_dirty()) {
+ _process();
}
- return cdfProcessed(x);
+ return cdf_processed(x);
}
- bool isDirty() {
+ bool is_dirty() {
return _processed.size() > _max_processed || _unprocessed.size() >
_max_unprocessed;
}
// return the cdf on the processed values
- Value cdfProcessed(Value x) const {
+ Value cdf_processed(Value x) const {
VLOG_CRITICAL << "cdf value " << x;
VLOG_CRITICAL << "processed size " << _processed.size();
if (_processed.size() == 0) {
@@ -306,13 +306,13 @@ public:
}
// check for the left tail
- if (x <= mean(0)) {
+ if (x <= _mean(0)) {
VLOG_CRITICAL << "left tail "
- << " _min " << _min << " mean(0) " << mean(0) <<
" x " << x;
+ << " _min " << _min << " mean(0) " << _mean(0)
<< " x " << x;
// note that this is different than mean(0) > _min ... this
guarantees interpolation works
- if (mean(0) - _min > 0) {
- return static_cast<Value>((x - _min) / (mean(0) - _min) *
weight(0) /
+ if (_mean(0) - _min > 0) {
+ return static_cast<Value>((x - _min) / (_mean(0) - _min) *
_weight(0) /
_processed_weight / 2.0);
} else {
return 0;
@@ -320,13 +320,13 @@ public:
}
// and the right tail
- if (x >= mean(n - 1)) {
+ if (x >= _mean(n - 1)) {
VLOG_CRITICAL << "right tail"
- << " _max " << _max << " mean(n - 1) " << mean(n
- 1) << " x " << x;
+ << " _max " << _max << " mean(n - 1) " <<
_mean(n - 1) << " x " << x;
- if (_max - mean(n - 1) > 0) {
- return static_cast<Value>(1.0 - (_max - x) / (_max -
mean(n - 1)) *
- weight(n - 1) /
_processed_weight /
+ if (_max - _mean(n - 1) > 0) {
+ return static_cast<Value>(1.0 - (_max - x) / (_max -
_mean(n - 1)) *
+ _weight(n - 1) /
_processed_weight /
2.0);
} else {
return 1;
@@ -345,21 +345,78 @@ public:
VLOG_CRITICAL << "middle "
<< " z1 " << z1 << " z2 " << z2 << " x " << x;
- return weightedAverage(_cumulative[i - 1], z2, _cumulative[i], z1)
/ _processed_weight;
+ return _weighted_average(_cumulative[i - 1], z2, _cumulative[i],
z1) /
+ _processed_weight;
}
}
// this returns a quantile on the t-digest
Value quantile(Value q) {
- if (haveUnprocessed() || isDirty()) {
- process();
+ if (have_unprocessed() || is_dirty()) {
+ _process();
+ }
+ return quantile_processed(q);
+ }
+
+ void quantiles(const double* quantile_levels, const size_t* permutation,
size_t size,
+ double* result) {
+ if (size == 0) {
+ return;
+ }
+ if (have_unprocessed() || is_dirty()) {
+ _process();
+ }
+
+ if (_processed.empty()) {
+ std::fill(result, result + size, NAN);
+ return;
+ }
+
+ if (_processed.size() == 1) {
+ std::fill(result, result + size, static_cast<double>(_mean(0)));
+ return;
+ }
+
+ const auto n = _processed.size();
+ size_t cumulative_index = 0;
+ for (size_t result_index = 0; result_index < size; ++result_index) {
+ const size_t level_index = permutation[result_index];
+ const auto q = static_cast<Value>(quantile_levels[level_index]);
+ DCHECK_GE(q, 0);
+ DCHECK_LE(q, 1);
+
+ const auto index = q * _processed_weight;
+ if (index <= _weight(0) / 2.0) {
+ DCHECK_GT(_weight(0), 0);
+ result[level_index] =
+ static_cast<Value>(_min + 2.0 * index / _weight(0) *
(_mean(0) - _min));
+ continue;
+ }
+
+ while (cumulative_index < _cumulative.size() &&
_cumulative[cumulative_index] < index) {
+ ++cumulative_index;
+ }
+
+ if (cumulative_index > 0 && cumulative_index + 1 <
_cumulative.size()) {
+ auto z1 = index - _cumulative[cumulative_index - 1];
+ auto z2 = _cumulative[cumulative_index] - index;
+ result[level_index] = static_cast<double>(_weighted_average(
+ _mean(cumulative_index - 1), z2,
_mean(cumulative_index), z1));
+ continue;
+ }
+
+ DCHECK_LE(index, _processed_weight);
+ DCHECK_GE(index, _processed_weight - _weight(n - 1) / 2.0);
+ auto z1 = static_cast<Value>(index - _processed_weight - _weight(n
- 1) / 2.0);
+ auto z2 = static_cast<Value>(_weight(n - 1) / 2 - z1);
+ result[level_index] =
+ static_cast<double>(_weighted_average(_mean(n - 1), z1,
_max, z2));
}
- return quantileProcessed(q);
}
// this returns a quantile on the currently processed values without
changing the t-digest
// the value will not represent the unprocessed values
- Value quantileProcessed(Value q) const {
+ Value quantile_processed(Value q) const {
if (q < 0 || q > 1) {
VLOG_CRITICAL << "q should be in [0,1], got " << q;
return NAN;
@@ -371,7 +428,7 @@ public:
} else if (_processed.size() == 1) {
// with one data point, all quantiles lead to Rome
- return mean(0);
+ return _mean(0);
}
// we know that there are at least two sorted now
@@ -381,9 +438,9 @@ public:
const auto index = q * _processed_weight;
// at the boundaries, we return _min or _max
- if (index <= weight(0) / 2.0) {
- DCHECK_GT(weight(0), 0);
- return static_cast<Value>(_min + 2.0 * index / weight(0) *
(mean(0) - _min));
+ if (index <= _weight(0) / 2.0) {
+ DCHECK_GT(_weight(0), 0);
+ return static_cast<Value>(_min + 2.0 * index / _weight(0) *
(_mean(0) - _min));
}
auto iter = std::lower_bound(_cumulative.cbegin(), _cumulative.cend(),
index);
@@ -394,22 +451,22 @@ public:
auto z1 = index - *(iter - 1);
auto z2 = *(iter)-index;
// VLOG_CRITICAL << "z2 " << z2 << " index " << index << " z1 " <<
z1;
- return weightedAverage(mean(i - 1), z2, mean(i), z1);
+ return _weighted_average(_mean(i - 1), z2, _mean(i), z1);
}
DCHECK_LE(index, _processed_weight);
- DCHECK_GE(index, _processed_weight - weight(n - 1) / 2.0);
+ DCHECK_GE(index, _processed_weight - _weight(n - 1) / 2.0);
- auto z1 = static_cast<Value>(index - _processed_weight - weight(n - 1)
/ 2.0);
- auto z2 = static_cast<Value>(weight(n - 1) / 2 - z1);
- return weightedAverage(mean(n - 1), z1, _max, z2);
+ auto z1 = static_cast<Value>(index - _processed_weight - _weight(n -
1) / 2.0);
+ auto z2 = static_cast<Value>(_weight(n - 1) / 2 - z1);
+ return _weighted_average(_mean(n - 1), z1, _max, z2);
}
Value compression() const { return _compression; }
void add(Value x) { add(x, 1); }
- void compress() { process(); }
+ void compress() { _process(); }
// add a single centroid to the unprocessed vector, processing previously
unprocessed sorted if our limit has
// been reached.
@@ -419,7 +476,7 @@ public:
}
_unprocessed.emplace_back(x, w);
_unprocessed_weight += w;
- processIfNecessary();
+ _process_if_necessary();
return true;
}
@@ -433,7 +490,7 @@ public:
_unprocessed.push_back(*(iter++));
}
if (_unprocessed.size() >= _max_unprocessed) {
- process();
+ _process();
}
}
}
@@ -541,7 +598,9 @@ private:
Value _min = std::numeric_limits<Value>::max();
- Value _max = std::numeric_limits<Value>::min();
+ // min() is the smallest positive value, so use lowest() for all-negative
input,
+ // e.g. {-3, -2, -1} must set _max to -1.
+ Value _max = std::numeric_limits<Value>::lowest();
Index _max_processed;
@@ -558,13 +617,13 @@ private:
std::vector<Weight> _cumulative;
// return mean of i-th centroid
- Value mean(int64_t i) const noexcept { return _processed[i].mean(); }
+ Value _mean(int64_t i) const noexcept { return _processed[i].mean(); }
// return weight of i-th centroid
- Weight weight(int64_t i) const noexcept { return _processed[i].weight(); }
+ Weight _weight(int64_t i) const noexcept { return _processed[i].weight(); }
// append all unprocessed centroids into current unprocessed vector
- void mergeUnprocessed(const std::vector<const TDigest*>& tdigests) {
+ void _merge_unprocessed(const std::vector<const TDigest*>& tdigests) {
if (tdigests.size() == 0) {
return;
}
@@ -583,7 +642,7 @@ private:
}
// merge all processed centroids together into a single sorted vector
- void mergeProcessed(const std::vector<const TDigest*>& tdigests) {
+ void _merge_processed(const std::vector<const TDigest*>& tdigests) {
if (tdigests.size() == 0) {
return;
}
@@ -627,21 +686,21 @@ private:
}
}
- void processIfNecessary() {
- if (isDirty()) {
- process();
+ void _process_if_necessary() {
+ if (is_dirty()) {
+ _process();
}
}
- void updateCumulative() {
+ void _update_cumulative() {
const auto n = _processed.size();
_cumulative.clear();
_cumulative.reserve(n + 1);
Weight previous = 0.0;
for (Index i = 0; i < n; i++) {
- Weight current = weight(i);
- auto halfCurrent = static_cast<Weight>(current / 2.0);
- _cumulative.push_back(previous + halfCurrent);
+ Weight current = _weight(i);
+ auto half_current = static_cast<Weight>(current / 2.0);
+ _cumulative.push_back(previous + half_current);
previous = previous + current;
}
_cumulative.push_back(previous);
@@ -649,7 +708,7 @@ private:
// merges _unprocessed centroids and _processed centroids together and
processes them
// when complete, _unprocessed will be empty and _processed will have at
most _max_processed centroids
- void process() {
+ void _process() {
CentroidComparator cc;
// select percentile_approx(lo_orderkey,0.5) from lineorder;
// have test pdqsort and RadixSort, find here pdqsort performance is
better when data is struct Centroid
@@ -665,20 +724,20 @@ private:
_processed.clear();
_processed.push_back(_unprocessed[0]);
- Weight wSoFar = _unprocessed[0].weight();
- Weight wLimit = _processed_weight * integratedQ(1.0);
+ Weight w_so_far = _unprocessed[0].weight();
+ Weight w_limit = _processed_weight * _integrated_q(1.0);
auto end = _unprocessed.end();
for (auto iter = _unprocessed.cbegin() + 1; iter < end; iter++) {
const auto& centroid = *iter;
- Weight projectedW = wSoFar + centroid.weight();
- if (projectedW <= wLimit) {
- wSoFar = projectedW;
+ Weight projected_w = w_so_far + centroid.weight();
+ if (projected_w <= w_limit) {
+ w_so_far = projected_w;
(_processed.end() - 1)->add(centroid);
} else {
- auto k1 = integratedLocation(wSoFar / _processed_weight);
- wLimit = _processed_weight * integratedQ(static_cast<Value>(k1
+ 1.0));
- wSoFar += centroid.weight();
+ auto k1 = _integrated_location(w_so_far / _processed_weight);
+ w_limit = _processed_weight *
_integrated_q(static_cast<Value>(k1 + 1.0));
+ w_so_far += centroid.weight();
_processed.emplace_back(centroid);
}
}
@@ -687,34 +746,34 @@ private:
VLOG_CRITICAL << "new _min " << _min;
_max = std::max(_max, (_processed.cend() - 1)->mean());
VLOG_CRITICAL << "new _max " << _max;
- updateCumulative();
+ _update_cumulative();
}
- size_t checkWeights(const std::vector<Centroid>& sorted, Value total) {
- size_t badWeight = 0;
+ size_t _check_weights(const std::vector<Centroid>& sorted, Value total) {
+ size_t bad_weight = 0;
auto k1 = 0.0;
auto q = 0.0;
for (auto iter = sorted.cbegin(); iter != sorted.cend(); iter++) {
auto w = iter->weight();
auto dq = w / total;
- auto k2 = integratedLocation(static_cast<Value>(q + dq));
+ auto k2 = _integrated_location(static_cast<Value>(q + dq));
if (k2 - k1 > 1 && w != 1) {
VLOG_CRITICAL << "Oversize centroid at " <<
std::distance(sorted.cbegin(), iter)
<< " k1 " << k1 << " k2 " << k2 << " dk " << (k2
- k1) << " w " << w
<< " q " << q;
- badWeight++;
+ bad_weight++;
}
if (k2 - k1 > 1.5 && w != 1) {
VLOG_CRITICAL << "Egregiously Oversize centroid at "
<< std::distance(sorted.cbegin(), iter) << " k1
" << k1 << " k2 "
<< k2 << " dk " << (k2 - k1) << " w " << w << "
q " << q;
- badWeight++;
+ bad_weight++;
}
q += dq;
k1 = k2;
}
- return badWeight;
+ return bad_weight;
}
/**
@@ -734,23 +793,23 @@ private:
* @param q The quantile scale value to be mapped.
* @return The centroid scale value corresponding to q.
*/
- Value integratedLocation(Value q) const {
+ Value _integrated_location(Value q) const {
return static_cast<Value>(_compression * (std::asin(2.0 * q - 1.0) +
M_PI / 2) / M_PI);
}
- Value integratedQ(Value k) const {
+ Value _integrated_q(Value k) const {
return static_cast<Value>(
(std::sin(std::min(k, _compression) * M_PI / _compression -
M_PI / 2) + 1) / 2);
}
/**
- * Same as {@link #weightedAverageSorted(Value, Value, Value, Value)} but
flips
+ * Same as {@link #_weighted_average_sorted(Value, Value, Value, Value)}
but flips
* the order of the variables if <code>x2</code> is greater than
* <code>x1</code>.
*/
- static Value weightedAverage(Value x1, Value w1, Value x2, Value w2) {
- return (x1 <= x2) ? weightedAverageSorted(x1, w1, x2, w2)
- : weightedAverageSorted(x2, w2, x1, w1);
+ static Value _weighted_average(Value x1, Value w1, Value x2, Value w2) {
+ return (x1 <= x2) ? _weighted_average_sorted(x1, w1, x2, w2)
+ : _weighted_average_sorted(x2, w2, x1, w1);
}
/**
@@ -760,13 +819,13 @@ private:
* and is guaranteed to return a number between <code>x1</code> and
* <code>x2</code>.
*/
- static Value weightedAverageSorted(Value x1, Value w1, Value x2, Value w2)
{
+ static Value _weighted_average_sorted(Value x1, Value w1, Value x2, Value
w2) {
DCHECK_LE(x1, x2);
const Value x = (x1 * w1 + x2 * w2) / (w1 + w2);
return std::max(x1, std::min(x, x2));
}
- static Value interpolate(Value x, Value x0, Value x1) { return (x - x0) /
(x1 - x0); }
+ static Value _interpolate(Value x, Value x0, Value x1) { return (x - x0) /
(x1 - x0); }
/**
* Computes an interpolated value of a quantile that is between two sorted.
@@ -774,18 +833,18 @@ private:
* Index is the quantile desired multiplied by the total number of samples
- 1.
*
* @param index Denormalized quantile desired
- * @param previousIndex The denormalized quantile corresponding to the
center of the previous centroid.
- * @param nextIndex The denormalized quantile corresponding to the
center of the following centroid.
- * @param previousMean The mean of the previous centroid.
- * @param nextMean The mean of the following centroid.
+ * @param previous_index The denormalized quantile corresponding to the
center of the previous centroid.
+ * @param next_index The denormalized quantile corresponding to the
center of the following centroid.
+ * @param previous_mean The mean of the previous centroid.
+ * @param next_mean The mean of the following centroid.
* @return The interpolated mean.
*/
- static Value quantile(Value index, Value previousIndex, Value nextIndex,
Value previousMean,
- Value nextMean) {
- const auto delta = nextIndex - previousIndex;
- const auto previousWeight = (nextIndex - index) / delta;
- const auto nextWeight = (index - previousIndex) / delta;
- return previousMean * previousWeight + nextMean * nextWeight;
+ static Value _quantile(Value index, Value previous_index, Value
next_index, Value previous_mean,
+ Value next_mean) {
+ const auto delta = next_index - previous_index;
+ const auto previous_weight = (next_index - index) / delta;
+ const auto next_weight = (index - previous_index) / delta;
+ return previous_mean * previous_weight + next_mean * next_weight;
}
};
} // namespace doris
diff --git a/be/test/exprs/aggregate/agg_percentile_test.cpp
b/be/test/exprs/aggregate/agg_percentile_test.cpp
new file mode 100644
index 00000000000..8e42fc2cc5e
--- /dev/null
+++ b/be/test/exprs/aggregate/agg_percentile_test.cpp
@@ -0,0 +1,353 @@
+// 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 <limits>
+#include <memory>
+#include <optional>
+#include <vector>
+
+#include "core/column/column_array.h"
+#include "core/column/column_nullable.h"
+#include "core/column/column_string.h"
+#include "core/column/column_vector.h"
+#include "core/data_type/data_type_array.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/data_type/data_type_number.h"
+#include "core/string_buffer.hpp"
+#include "exprs/aggregate/aggregate_function_percentile.h"
+#include "exprs/aggregate/aggregate_function_simple_factory.h"
+#include "util/tdigest.h"
+
+namespace doris {
+
+namespace {
+
+AggregateFunctionPtr create_percentile_approx_array_function(bool
has_compression) {
+ DataTypes argument_types {
+ std::make_shared<DataTypeFloat64>(),
+
std::make_shared<DataTypeArray>(make_nullable(std::make_shared<DataTypeFloat64>()))};
+ if (has_compression) {
+ argument_types.push_back(std::make_shared<DataTypeFloat64>());
+ }
+ auto result_type =
+
std::make_shared<DataTypeArray>(make_nullable(std::make_shared<DataTypeFloat64>()));
+ return AggregateFunctionSimpleFactory::instance().get(
+ "percentile_approx_array", argument_types, result_type, false,
+ BeExecVersionManager::get_newest_version(), {.column_names = {}});
+}
+
+MutableColumnPtr create_values(const std::vector<double>& values) {
+ auto column = ColumnFloat64::create();
+ for (double value : values) {
+ column->insert_value(value);
+ }
+ return column;
+}
+
+MutableColumnPtr create_quantiles(const std::vector<double>& quantiles, size_t
rows,
+ std::optional<size_t> null_index =
std::nullopt) {
+ auto nested_column = ColumnFloat64::create();
+ auto null_map = ColumnUInt8::create();
+ auto offsets = ColumnArray::ColumnOffsets::create();
+ for (size_t row = 0; row < rows; ++row) {
+ for (size_t i = 0; i < quantiles.size(); ++i) {
+ nested_column->insert_value(quantiles[i]);
+ null_map->insert_value(null_index.has_value() && *null_index == i);
+ }
+ offsets->insert_value((row + 1) * quantiles.size());
+ }
+ return ColumnArray::create(
+ ColumnNullable::create(std::move(nested_column),
std::move(null_map)),
+ std::move(offsets));
+}
+
+std::vector<double> read_result(const AggregateFunctionPtr& function,
AggregateDataPtr place) {
+ auto result_column = ColumnArray::create(
+ ColumnNullable::create(ColumnFloat64::create(),
ColumnUInt8::create()),
+ ColumnArray::ColumnOffsets::create());
+ function->insert_result_into(place, *result_column);
+ const auto& nullable_data = assert_cast<const
ColumnNullable&>(result_column->get_data());
+ const auto& data = assert_cast<const
ColumnFloat64&>(nullable_data.get_nested_column());
+ return {data.get_data().begin(), data.get_data().end()};
+}
+
+std::vector<double> expected_quantiles(const std::vector<double>& values,
+ const std::vector<double>& quantiles,
float compression) {
+ TDigest digest(compression);
+ for (double value : values) {
+ digest.add(value);
+ }
+ std::vector<double> result;
+ result.reserve(quantiles.size());
+ for (double quantile : quantiles) {
+ result.push_back(digest.quantile(quantile));
+ }
+ return result;
+}
+
+void expect_results_equal(const std::vector<double>& actual, const
std::vector<double>& expected) {
+ ASSERT_EQ(actual.size(), expected.size());
+ for (size_t i = 0; i < actual.size(); ++i) {
+ EXPECT_DOUBLE_EQ(actual[i], expected[i]);
+ }
+}
+
+} // namespace
+
+TEST(AggregateFunctionPercentileApproxArrayTest, AddAndBatchPaths) {
+ const std::vector<double> values {1, 2, 3, 4, 5, 100};
+ const std::vector<double> quantiles {0.9, 0.0, 0.5, 0.5, 1.0};
+ auto function = create_percentile_approx_array_function(false);
+ ASSERT_NE(function, nullptr);
+
+ auto value_column = create_values(values);
+ auto quantile_column = create_quantiles(quantiles, values.size());
+ const IColumn* columns[] = {value_column.get(), quantile_column.get()};
+ Arena arena;
+
+ std::unique_ptr<char[]> row_memory(new char[function->size_of_data()]);
+ AggregateDataPtr row_place = row_memory.get();
+ function->create(row_place);
+ for (size_t i = 0; i < values.size(); ++i) {
+ function->add(row_place, columns, i, arena);
+ }
+
+ std::unique_ptr<char[]> batch_memory(new char[function->size_of_data()]);
+ AggregateDataPtr batch_place = batch_memory.get();
+ function->create(batch_place);
+ function->add_batch_single_place(values.size(), batch_place, columns,
arena);
+
+ std::unique_ptr<char[]> range_memory(new char[function->size_of_data()]);
+ AggregateDataPtr range_place = range_memory.get();
+ function->create(range_place);
+ function->add_batch_range(1, 4, range_place, columns, arena, false);
+
+ const auto expected = expected_quantiles(values, quantiles, 10000);
+ expect_results_equal(read_result(function, row_place), expected);
+ expect_results_equal(read_result(function, batch_place), expected);
+ expect_results_equal(read_result(function, range_place),
+ expected_quantiles({2, 3, 4, 5}, quantiles, 10000));
+
+ function->destroy(row_place);
+ function->destroy(batch_place);
+ function->destroy(range_place);
+}
+
+TEST(AggregateFunctionPercentileApproxArrayTest,
CompressionSerializationAndMerge) {
+ const std::vector<double> values {5, 10, 15, 20, 25, 30};
+ const std::vector<double> quantiles {0.25, 0.5, 0.75};
+ auto function = create_percentile_approx_array_function(true);
+ ASSERT_NE(function, nullptr);
+
+ auto value_column = create_values(values);
+ auto quantile_column = create_quantiles(quantiles, values.size());
+ auto compression_column = ColumnFloat64::create();
+ compression_column->insert_value(2048);
+ const IColumn* columns[] = {value_column.get(), quantile_column.get(),
+ compression_column.get()};
+ Arena arena;
+
+ std::unique_ptr<char[]> source_memory(new char[function->size_of_data()]);
+ AggregateDataPtr source_place = source_memory.get();
+ function->create(source_place);
+ function->add_batch_single_place(values.size(), source_place, columns,
arena);
+
+ ColumnString serialized_column;
+ VectorBufferWriter writer(serialized_column);
+ function->serialize(source_place, writer);
+ writer.commit();
+
+ std::unique_ptr<char[]> restored_memory(new
char[function->size_of_data()]);
+ AggregateDataPtr restored_place = restored_memory.get();
+ function->create(restored_place);
+ VectorBufferReader reader(serialized_column.get_data_at(0));
+ function->deserialize(restored_place, reader, arena);
+
+ std::unique_ptr<char[]> merged_memory(new char[function->size_of_data()]);
+ AggregateDataPtr merged_place = merged_memory.get();
+ function->create(merged_place);
+ function->merge(merged_place, restored_place, arena);
+
+ const auto expected = expected_quantiles(values, quantiles, 2048);
+ expect_results_equal(read_result(function, restored_place), expected);
+ expect_results_equal(read_result(function, merged_place), expected);
+
+ function->destroy(source_place);
+ function->destroy(restored_place);
+ function->destroy(merged_place);
+}
+
+TEST(AggregateFunctionPercentileApproxArrayTest,
MergeCompatibleInitializedStates) {
+ const std::vector<double> left_values {5, 10, 15};
+ const std::vector<double> right_values {20, 25, 30};
+ const std::vector<double> quantiles {0.25, 0.5, 0.75};
+ auto function = create_percentile_approx_array_function(true);
+ ASSERT_NE(function, nullptr);
+ Arena arena;
+
+ auto create_state = [&](const std::vector<double>& values) {
+ auto value_column = create_values(values);
+ auto quantile_column = create_quantiles(quantiles, values.size());
+ auto compression_column = ColumnFloat64::create();
+ compression_column->insert_value(2048);
+ const IColumn* columns[] = {value_column.get(), quantile_column.get(),
+ compression_column.get()};
+ std::unique_ptr<char[]> memory(new char[function->size_of_data()]);
+ function->create(memory.get());
+ function->add_batch_single_place(values.size(), memory.get(), columns,
arena);
+ return memory;
+ };
+
+ auto left = create_state(left_values);
+ auto right = create_state(right_values);
+ function->merge(left.get(), right.get(), arena);
+
+ std::vector<double> all_values = left_values;
+ all_values.insert(all_values.end(), right_values.begin(),
right_values.end());
+ expect_results_equal(read_result(function, left.get()),
+ expected_quantiles(all_values, quantiles, 2048));
+
+ function->destroy(left.get());
+ function->destroy(right.get());
+}
+
+TEST(AggregateFunctionPercentileApproxArrayTest,
EmptyQuantilesAndInvalidQuantile) {
+ auto function = create_percentile_approx_array_function(false);
+ ASSERT_NE(function, nullptr);
+ Arena arena;
+
+ auto value_column = create_values({1, 2, 3});
+ auto empty_quantile_column = create_quantiles({}, 3);
+ const IColumn* empty_columns[] = {value_column.get(),
empty_quantile_column.get()};
+ std::unique_ptr<char[]> empty_memory(new char[function->size_of_data()]);
+ AggregateDataPtr empty_place = empty_memory.get();
+ function->create(empty_place);
+ function->add_batch_single_place(3, empty_place, empty_columns, arena);
+ EXPECT_TRUE(read_result(function, empty_place).empty());
+ const auto& empty_state = *reinterpret_cast<const
PercentileApproxArrayState*>(empty_place);
+ EXPECT_TRUE(empty_state.init_flag);
+ EXPECT_EQ(empty_state.digest.get(), nullptr);
+
+ ColumnString serialized_column;
+ VectorBufferWriter writer(serialized_column);
+ function->serialize(empty_place, writer);
+ writer.commit();
+ std::unique_ptr<char[]> restored_memory(new
char[function->size_of_data()]);
+ AggregateDataPtr restored_place = restored_memory.get();
+ function->create(restored_place);
+ VectorBufferReader reader(serialized_column.get_data_at(0));
+ function->deserialize(restored_place, reader, arena);
+ const auto& restored_state =
+ *reinterpret_cast<const
PercentileApproxArrayState*>(restored_place);
+ EXPECT_TRUE(restored_state.init_flag);
+ EXPECT_EQ(restored_state.digest.get(), nullptr);
+ EXPECT_TRUE(read_result(function, restored_place).empty());
+
+ std::unique_ptr<char[]> merged_memory(new char[function->size_of_data()]);
+ AggregateDataPtr merged_place = merged_memory.get();
+ function->create(merged_place);
+ function->merge(merged_place, restored_place, arena);
+ const auto& merged_state = *reinterpret_cast<const
PercentileApproxArrayState*>(merged_place);
+ EXPECT_TRUE(merged_state.init_flag);
+ EXPECT_EQ(merged_state.digest.get(), nullptr);
+ EXPECT_TRUE(read_result(function, merged_place).empty());
+
+ auto invalid_quantile_column = create_quantiles({0.5, 0.9}, 3, 1);
+ const IColumn* invalid_columns[] = {value_column.get(),
invalid_quantile_column.get()};
+ std::unique_ptr<char[]> invalid_memory(new char[function->size_of_data()]);
+ AggregateDataPtr invalid_place = invalid_memory.get();
+ function->create(invalid_place);
+ EXPECT_THROW(function->add_batch_single_place(3, invalid_place,
invalid_columns, arena),
+ Exception);
+
+ function->destroy(empty_place);
+ function->destroy(restored_place);
+ function->destroy(merged_place);
+ function->destroy(invalid_place);
+}
+
+TEST(AggregateFunctionPercentileApproxArrayTest, RejectsIncompatibleStates) {
+ auto function = create_percentile_approx_array_function(true);
+ ASSERT_NE(function, nullptr);
+ Arena arena;
+
+ auto create_state = [&](const std::vector<double>& quantiles, double
compression) {
+ auto value_column = create_values({1, 2, 3});
+ auto quantile_column = create_quantiles(quantiles, 3);
+ auto compression_column = ColumnFloat64::create();
+ compression_column->insert_value(compression);
+ const IColumn* columns[] = {value_column.get(), quantile_column.get(),
+ compression_column.get()};
+ std::unique_ptr<char[]> memory(new char[function->size_of_data()]);
+ function->create(memory.get());
+ function->add_batch_single_place(3, memory.get(), columns, arena);
+ return memory;
+ };
+
+ auto destination = create_state({0.5}, 2048);
+ auto different_quantiles = create_state({0.9}, 2048);
+ auto different_compression = create_state({0.5}, 10000);
+ EXPECT_THROW(function->merge(destination.get(), different_quantiles.get(),
arena), Exception);
+ EXPECT_THROW(function->merge(destination.get(),
different_compression.get(), arena), Exception);
+
+ function->destroy(destination.get());
+ function->destroy(different_quantiles.get());
+ function->destroy(different_compression.get());
+}
+
+TEST(AggregateFunctionPercentileApproxArrayTest, HandlesNonFiniteParameters) {
+ const std::vector<double> non_finite_values
{std::numeric_limits<double>::quiet_NaN(),
+
std::numeric_limits<double>::infinity(),
+
-std::numeric_limits<double>::infinity()};
+ auto value_column = create_values({1, 2, 3});
+ Arena arena;
+
+ auto function_without_compression =
create_percentile_approx_array_function(false);
+ ASSERT_NE(function_without_compression, nullptr);
+ for (double quantile : non_finite_values) {
+ auto quantile_column = create_quantiles({quantile}, 3);
+ const IColumn* columns[] = {value_column.get(), quantile_column.get()};
+ std::unique_ptr<char[]> memory(new
char[function_without_compression->size_of_data()]);
+ function_without_compression->create(memory.get());
+ EXPECT_THROW(function_without_compression->add_batch_single_place(3,
memory.get(), columns,
+
arena),
+ Exception);
+ function_without_compression->destroy(memory.get());
+ }
+
+ auto function_with_compression =
create_percentile_approx_array_function(true);
+ ASSERT_NE(function_with_compression, nullptr);
+ auto quantile_column = create_quantiles({0.5}, 3);
+ const auto expected = expected_quantiles({1, 2, 3}, {0.5}, 10000);
+ for (double compression : non_finite_values) {
+ auto compression_column = ColumnFloat64::create();
+ compression_column->insert_value(compression);
+ const IColumn* columns[] = {value_column.get(), quantile_column.get(),
+ compression_column.get()};
+ std::unique_ptr<char[]> memory(new
char[function_with_compression->size_of_data()]);
+ function_with_compression->create(memory.get());
+ function_with_compression->add_batch_single_place(3, memory.get(),
columns, arena);
+ const auto& state = *reinterpret_cast<const
PercentileApproxArrayState*>(memory.get());
+ EXPECT_FLOAT_EQ(state.compressions, 10000);
+ expect_results_equal(read_result(function_with_compression,
memory.get()), expected);
+ function_with_compression->destroy(memory.get());
+ }
+}
+
+} // namespace doris
diff --git a/be/test/util/tdigest_test.cpp b/be/test/util/tdigest_test.cpp
index efb40774324..b5fec84ba48 100644
--- a/be/test/util/tdigest_test.cpp
+++ b/be/test/util/tdigest_test.cpp
@@ -21,6 +21,7 @@
#include <gtest/gtest-test-part.h>
#include <memory>
+#include <numeric>
#include <random>
#include "gtest/gtest_pred_impl.h"
@@ -158,8 +159,8 @@ TEST_F(TDigestTest, MoreThan2BValues) {
const auto count = 1L << 28;
digest.add(next, count);
}
- EXPECT_EQ(static_cast<long>(1000 + float(10L * (1 << 28))),
digest.totalWeight());
- EXPECT_GT(digest.totalWeight(), std::numeric_limits<int32_t>::max());
+ EXPECT_EQ(static_cast<long>(1000 + float(10L * (1 << 28))),
digest.total_weight());
+ EXPECT_GT(digest.total_weight(), std::numeric_limits<int32_t>::max());
std::vector<double> quantiles {0, 0.1, 0.5, 0.9, 1, reals(gen)};
std::sort(quantiles.begin(), quantiles.end());
auto prev = std::numeric_limits<double>::min();
@@ -216,6 +217,82 @@ TEST_F(TDigestTest, ExtremeQuantiles) {
}
}
+TEST_F(TDigestTest, BatchQuantilesMatchSingleQuantiles) {
+ TDigest digest(1000);
+ for (int i = 0; i < 10000; ++i) {
+ digest.add(static_cast<double>((i * 37) % 1000));
+ }
+
+ std::vector<double> levels {0.9, 0.0, 0.5, 0.1, 1.0, 0.5, 0.99};
+ std::vector<size_t> permutation(levels.size());
+ std::iota(permutation.begin(), permutation.end(), 0);
+ std::sort(permutation.begin(), permutation.end(),
+ [&levels](size_t lhs, size_t rhs) { return levels[lhs] <
levels[rhs]; });
+
+ std::vector<double> results(levels.size());
+ digest.quantiles(levels.data(), permutation.data(), levels.size(),
results.data());
+
+ for (size_t i = 0; i < levels.size(); ++i) {
+ EXPECT_DOUBLE_EQ(results[i],
static_cast<double>(digest.quantile(levels[i])));
+ }
+}
+
+TEST_F(TDigestTest, BatchQuantilesMatchSingleQuantileInLeftTail) {
+ TDigest digest(2);
+ digest.add(0.0F);
+ digest.compress();
+ digest.add(1.5F, 2.0F);
+ digest.add(3.0F, 3.0F);
+ digest.compress();
+
+ ASSERT_EQ(digest.processed().size(), 2);
+ ASSERT_FLOAT_EQ(digest.processed()[0].mean(), 1.0F);
+ ASSERT_FLOAT_EQ(digest.processed()[0].weight(), 3.0F);
+ ASSERT_EQ(digest.total_weight(), 6);
+
+ std::vector<double> levels {0.1};
+ std::vector<size_t> permutation {0};
+ std::vector<double> results(levels.size());
+ digest.quantiles(levels.data(), permutation.data(), levels.size(),
results.data());
+
+ EXPECT_DOUBLE_EQ(results[0],
static_cast<double>(digest.quantile(levels[0])));
+}
+
+TEST_F(TDigestTest, AllNegativeValuesHaveCorrectMaximum) {
+ TDigest digest(1000);
+ digest.add(-3.0);
+ digest.add(-2.0);
+ digest.add(-1.0);
+
+ std::vector<double> levels {1.0};
+ std::vector<size_t> permutation {0};
+ std::vector<double> results(levels.size());
+ digest.quantiles(levels.data(), permutation.data(), levels.size(),
results.data());
+
+ EXPECT_FLOAT_EQ(digest.quantile(levels[0]), -1.0F);
+ EXPECT_DOUBLE_EQ(results[0], -1.0);
+ EXPECT_DOUBLE_EQ(results[0],
static_cast<double>(digest.quantile(levels[0])));
+}
+
+TEST_F(TDigestTest, BatchQuantilesHandleEmptyAndSingleValueDigests) {
+ std::vector<double> levels {0.0, 0.5, 1.0};
+ std::vector<size_t> permutation {0, 1, 2};
+ std::vector<double> results(levels.size());
+
+ TDigest empty_digest(1000);
+ empty_digest.quantiles(levels.data(), permutation.data(), levels.size(),
results.data());
+ for (double result : results) {
+ EXPECT_TRUE(std::isnan(result));
+ }
+
+ TDigest single_value_digest(1000);
+ single_value_digest.add(42.0);
+ single_value_digest.quantiles(levels.data(), permutation.data(),
levels.size(), results.data());
+ for (double result : results) {
+ EXPECT_DOUBLE_EQ(result, 42.0);
+ }
+}
+
TEST_F(TDigestTest, Montonicity) {
TDigest digest(1000);
std::uniform_real_distribution<> reals(0.0, 1.0);
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 1f60a228c11..7f3fa4cddec 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
@@ -77,6 +77,7 @@ import
org.apache.doris.nereids.trees.expressions.functions.agg.OrthogonalBitmap
import
org.apache.doris.nereids.trees.expressions.functions.agg.OrthogonalBitmapUnionCount;
import org.apache.doris.nereids.trees.expressions.functions.agg.Percentile;
import
org.apache.doris.nereids.trees.expressions.functions.agg.PercentileApprox;
+import
org.apache.doris.nereids.trees.expressions.functions.agg.PercentileApproxArray;
import
org.apache.doris.nereids.trees.expressions.functions.agg.PercentileApproxWeighted;
import
org.apache.doris.nereids.trees.expressions.functions.agg.PercentileArray;
import
org.apache.doris.nereids.trees.expressions.functions.agg.PercentileReservoir;
@@ -190,6 +191,7 @@ public class BuiltinAggregateFunctions implements
FunctionHelper {
agg(Percentile.class, "percentile", "percentile_cont"),
agg(PercentileReservoir.class, "percentile_reservoir"),
agg(PercentileApprox.class, "percentile_approx"),
+ agg(PercentileApproxArray.class, "percentile_approx_array"),
agg(PercentileApproxWeighted.class,
"percentile_approx_weighted"),
agg(PercentileArray.class, "percentile_array"),
agg(QuantileUnion.class, "quantile_union"),
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/agg/PercentileApproxArray.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/agg/PercentileApproxArray.java
new file mode 100644
index 00000000000..14010a401fe
--- /dev/null
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/agg/PercentileApproxArray.java
@@ -0,0 +1,127 @@
+// 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.nereids.exceptions.AnalysisException;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
+import org.apache.doris.nereids.trees.expressions.literal.ArrayLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.Literal;
+import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
+import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
+import org.apache.doris.nereids.types.ArrayType;
+import org.apache.doris.nereids.types.DoubleType;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * AggregateFunction 'percentile_approx_array'.
+ */
+public class PercentileApproxArray extends NotNullableAggregateFunction
+ implements ExplicitlyCastableSignature {
+
+ public static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
+ FunctionSignature.ret(ArrayType.of(DoubleType.INSTANCE))
+ .args(DoubleType.INSTANCE,
ArrayType.of(DoubleType.INSTANCE)),
+ FunctionSignature.ret(ArrayType.of(DoubleType.INSTANCE))
+ .args(DoubleType.INSTANCE,
ArrayType.of(DoubleType.INSTANCE), DoubleType.INSTANCE)
+ );
+
+ public PercentileApproxArray(Expression arg0, Expression arg1) {
+ this(false, arg0, arg1);
+ }
+
+ public PercentileApproxArray(boolean distinct, Expression arg0, Expression
arg1) {
+ super("percentile_approx_array", distinct, arg0, arg1);
+ }
+
+ public PercentileApproxArray(Expression arg0, Expression arg1, Expression
arg2) {
+ this(false, arg0, arg1, arg2);
+ }
+
+ public PercentileApproxArray(boolean distinct, Expression arg0, Expression
arg1,
+ Expression arg2) {
+ super("percentile_approx_array", distinct, arg0, arg1, arg2);
+ }
+
+ /** constructor for withChildren and reuse signature */
+ private PercentileApproxArray(AggregateFunctionParams functionParams) {
+ super(functionParams);
+ }
+
+ @Override
+ public void checkLegalityBeforeTypeCoercion() {
+ if (!getArgument(1).isConstant()) {
+ throw new AnalysisException(
+ "percentile_approx_array requires second parameter must be
a constant : " + this.toSql());
+ }
+ if (arity() == 3 && !getArgument(2).isConstant()) {
+ throw new AnalysisException(
+ "percentile_approx_array requires the third parameter must
be a constant : " + this.toSql());
+ }
+ }
+
+ @Override
+ public void checkLegalityAfterRewrite() {
+ checkLegalityBeforeTypeCoercion();
+ Expression quantiles = getArgument(1);
+ if (!(quantiles instanceof ArrayLiteral)) {
+ return;
+ }
+ for (Expression item : ((ArrayLiteral) quantiles).getValue()) {
+ if (item instanceof NullLiteral) {
+ throw new AnalysisException(
+ "percentile_approx_array quantile should not be null :
" + this.toSql());
+ }
+ if (!(item instanceof Literal) ||
!item.getDataType().isNumericType()) {
+ continue;
+ }
+ double value = ((Literal) item).getDouble();
+ if (!Double.isFinite(value) || value < 0.0 || value > 1.0) {
+ throw new AnalysisException("percentile_approx_array quantile
must be in [0, 1], but got "
+ + value + ": " + this.toSql());
+ }
+ }
+ }
+
+ @Override
+ public PercentileApproxArray withDistinctAndChildren(boolean distinct,
List<Expression> children) {
+ Preconditions.checkArgument(children.size() == 2 || children.size() ==
3);
+ return new PercentileApproxArray(getFunctionParams(distinct,
children));
+ }
+
+ @Override
+ public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
+ return visitor.visitPercentileApproxArray(this, context);
+ }
+
+ @Override
+ public List<FunctionSignature> getSignatures() {
+ return SIGNATURES;
+ }
+
+ @Override
+ public Expression resultForEmptyInput() {
+ return new ArrayLiteral(new ArrayList<>(), this.getDataType());
+ }
+}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/combinator/ForEachCombinator.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/combinator/ForEachCombinator.java
index 3c5fce06159..0067be76642 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/combinator/ForEachCombinator.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/combinator/ForEachCombinator.java
@@ -48,6 +48,7 @@ public class ForEachCombinator extends
NullableAggregateFunction
add("percentile");
add("percentile_array");
add("percentile_approx");
+ add("percentile_approx_array");
add("percentile_approx_weighted");
}
});
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/combinator/MergeCombinator.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/combinator/MergeCombinator.java
index 0ac2a6a8f3f..071df33f566 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/combinator/MergeCombinator.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/combinator/MergeCombinator.java
@@ -117,6 +117,12 @@ public class MergeCombinator extends AggregateFunction
@Override
public void checkLegalityBeforeTypeCoercion() {
- nested.checkLegalityBeforeTypeCoercion();
+ // A directly nested state retains the original value expressions, so
their legality
+ // checks are still meaningful. After a subquery or stored-column
boundary, the nested
+ // function is rebuilt with mocked slots from AggStateType; replaying
value-expression
+ // checks there would reject valid states whose constants are already
serialized.
+ if (getArgument(0) instanceof StateCombinator) {
+ nested.checkLegalityBeforeTypeCoercion();
+ }
}
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/combinator/StateCombinator.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/combinator/StateCombinator.java
index c0a45ddc609..b10db1b3e65 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/combinator/StateCombinator.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/combinator/StateCombinator.java
@@ -144,4 +144,9 @@ public class StateCombinator extends ScalarFunction
public void checkLegalityBeforeTypeCoercion() {
nested.checkLegalityBeforeTypeCoercion();
}
+
+ @Override
+ public void checkLegalityAfterRewrite() {
+ nested.withChildren(children()).checkLegalityAfterRewrite();
+ }
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/combinator/UnionCombinator.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/combinator/UnionCombinator.java
index 5e4a701a4b7..4c54762e5a5 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/combinator/UnionCombinator.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/combinator/UnionCombinator.java
@@ -112,6 +112,12 @@ public class UnionCombinator extends AggregateFunction
@Override
public void checkLegalityBeforeTypeCoercion() {
- nested.checkLegalityBeforeTypeCoercion();
+ // A directly nested state retains the original value expressions, so
their legality
+ // checks are still meaningful. After a subquery or stored-column
boundary, the nested
+ // function is rebuilt with mocked slots from AggStateType; replaying
value-expression
+ // checks there would reject valid states whose constants are already
serialized.
+ if (getArgument(0) instanceof StateCombinator) {
+ nested.checkLegalityBeforeTypeCoercion();
+ }
}
}
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 81b0830be66..fd76221f962 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
@@ -71,6 +71,7 @@ import
org.apache.doris.nereids.trees.expressions.functions.agg.OrthogonalBitmap
import
org.apache.doris.nereids.trees.expressions.functions.agg.OrthogonalBitmapUnionCount;
import org.apache.doris.nereids.trees.expressions.functions.agg.Percentile;
import
org.apache.doris.nereids.trees.expressions.functions.agg.PercentileApprox;
+import
org.apache.doris.nereids.trees.expressions.functions.agg.PercentileApproxArray;
import
org.apache.doris.nereids.trees.expressions.functions.agg.PercentileApproxWeighted;
import
org.apache.doris.nereids.trees.expressions.functions.agg.PercentileArray;
import
org.apache.doris.nereids.trees.expressions.functions.agg.PercentileReservoir;
@@ -331,6 +332,10 @@ public interface AggregateFunctionVisitor<R, C> {
return visitNullableAggregateFunction(percentileApprox, context);
}
+ default R visitPercentileApproxArray(PercentileApproxArray
percentileApproxArray, C context) {
+ return visitAggregateFunction(percentileApproxArray, context);
+ }
+
default R visitPercentileArray(PercentileArray percentileArray, C context)
{
return visitAggregateFunction(percentileArray, context);
}
diff --git
a/regression-test/data/datatype_p0/agg_state/nereids/test_agg_state_nereids.out
b/regression-test/data/datatype_p0/agg_state/nereids/test_agg_state_nereids.out
index c06bf577d2e..5ccaee80cf2 100644
---
a/regression-test/data/datatype_p0/agg_state/nereids/test_agg_state_nereids.out
+++
b/regression-test/data/datatype_p0/agg_state/nereids/test_agg_state_nereids.out
@@ -48,3 +48,8 @@ k2 agg_state<max_by(int not null, int null)> No
false \N GENERIC
-- !max_by_null --
\N \N
+-- !percentile_approx_merge_after_subquery --
+2
+
+-- !percentile_approx_union_after_subquery --
+2
diff --git
a/regression-test/data/query_p0/sql_functions/aggregate_functions/test_aggregate_percentile_approx_array.out
b/regression-test/data/query_p0/sql_functions/aggregate_functions/test_aggregate_percentile_approx_array.out
new file mode 100644
index 00000000000..24c27e94901
--- /dev/null
+++
b/regression-test/data/query_p0/sql_functions/aggregate_functions/test_aggregate_percentile_approx_array.out
@@ -0,0 +1,38 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !percentile_approx_array_1 --
+[1, 2.25, 10, 27.5, 100]
+
+-- !percentile_approx_array_2 --
+[2.25, 10, 27.5]
+
+-- !percentile_approx_array_3 --
+[85.99997711181641, 1.200000047683716, 10, 10]
+
+-- !percentile_approx_array_4 --
+[2.25, 10, 27.5] [2.25, 10, 27.5]
+
+-- !percentile_approx_array_5 --
+[10, 10, 10]
+
+-- !percentile_approx_array_6 --
+[]
+
+-- !percentile_approx_array_7 --
+1 [1, 2, 3]
+2 [10, 25, 100]
+3 []
+
+-- !percentile_approx_array_8 --
+[]
+
+-- !percentile_approx_array_9 --
+[2.25, 10, 27.5] [2.25, 10, 27.5] [2.25, 10, 27.5] [2.25,
10, 27.5]
+
+-- !percentile_approx_array_direct_merge --
+[2.25, 10, 27.5]
+
+-- !percentile_approx_array_subquery_merge --
+[2.25, 10, 27.5]
+
+-- !percentile_approx_array_subquery_union --
+[2.25, 10, 27.5]
diff --git
a/regression-test/suites/datatype_p0/agg_state/nereids/test_agg_state_nereids.groovy
b/regression-test/suites/datatype_p0/agg_state/nereids/test_agg_state_nereids.groovy
index 5c0217e9d47..75a9758a2d9 100644
---
a/regression-test/suites/datatype_p0/agg_state/nereids/test_agg_state_nereids.groovy
+++
b/regression-test/suites/datatype_p0/agg_state/nereids/test_agg_state_nereids.groovy
@@ -74,4 +74,39 @@ suite("test_agg_state_nereids") {
qt_union """ select max_by_merge(kstate) from (select k1,max_by_union(k2)
kstate from a_table group by k1 order by k1) t; """
qt_max_by_null """ select
max_by_merge(max_by_state(k1,null)),min_by_merge(min_by_state(null,k3)) from
d_table; """
+
+ test {
+ sql """
+ SELECT percentile_approx_merge(percentile_approx_state(k1, k2 /
10.0))
+ FROM d_table
+ """
+ exception "percentile_approx requires second parameter must be a
constant"
+ }
+
+ test {
+ sql """
+ SELECT percentile_approx_union(percentile_approx_state(k1, k2 /
10.0))
+ FROM d_table
+ """
+ exception "percentile_approx requires second parameter must be a
constant"
+ }
+
+ qt_percentile_approx_merge_after_subquery """
+ SELECT percentile_approx_merge(state)
+ FROM (
+ SELECT percentile_approx_state(k1, 0.5) AS state
+ FROM d_table
+ ) states
+ """
+
+ qt_percentile_approx_union_after_subquery """
+ SELECT percentile_approx_merge(state)
+ FROM (
+ SELECT percentile_approx_union(state) AS state
+ FROM (
+ SELECT percentile_approx_state(k1, 0.5) AS state
+ FROM d_table
+ ) states
+ ) unions
+ """
}
diff --git a/regression-test/suites/function_p0/test_agg_foreach.groovy
b/regression-test/suites/function_p0/test_agg_foreach.groovy
index 16fa21141c6..d2280e0c9ce 100644
--- a/regression-test/suites/function_p0/test_agg_foreach.groovy
+++ b/regression-test/suites/function_p0/test_agg_foreach.groovy
@@ -145,4 +145,9 @@ suite("test_agg_foreach") {
sql """select PERCENTILE_APPROX_foreach(a,a) from
foreach_table;"""
exception "Unsupport the func"
}
+
+ test {
+ sql """select PERCENTILE_APPROX_ARRAY_foreach(a,a) from
foreach_table;"""
+ exception "Unsupport the func"
+ }
}
diff --git
a/regression-test/suites/query_p0/sql_functions/aggregate_functions/test_aggregate_percentile_approx_array.groovy
b/regression-test/suites/query_p0/sql_functions/aggregate_functions/test_aggregate_percentile_approx_array.groovy
new file mode 100644
index 00000000000..90777b79eeb
--- /dev/null
+++
b/regression-test/suites/query_p0/sql_functions/aggregate_functions/test_aggregate_percentile_approx_array.groovy
@@ -0,0 +1,211 @@
+// 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("test_aggregate_percentile_approx_array") {
+ sql "SET enable_agg_state = true"
+ sql "DROP TABLE IF EXISTS percentile_approx_array_test"
+ sql """
+ CREATE TABLE percentile_approx_array_test (
+ group_id INT,
+ value DOUBLE NULL
+ )
+ DUPLICATE KEY(group_id)
+ DISTRIBUTED BY HASH(group_id) BUCKETS 1
+ PROPERTIES("replication_num" = "1")
+ """
+ sql """
+ INSERT INTO percentile_approx_array_test VALUES
+ (1, 1.0), (1, 2.0), (1, 3.0), (1, NULL),
+ (2, 10.0), (2, 20.0), (2, 30.0), (2, 100.0),
+ (3, NULL)
+ """
+
+ qt_percentile_approx_array_1 """
+ SELECT percentile_approx_array(value, [0.0, 0.25, 0.5, 0.75, 1.0])
+ FROM percentile_approx_array_test
+ """
+
+ qt_percentile_approx_array_2 """
+ SELECT percentile_approx_array(value, [0.25, 0.5, 0.75], 2048)
+ FROM percentile_approx_array_test
+ """
+
+ qt_percentile_approx_array_3 """
+ SELECT percentile_approx_array(value, [0.9, 0.1, 0.5, 0.5])
+ FROM percentile_approx_array_test
+ """
+
+ qt_percentile_approx_array_4 """
+ SELECT
+ percentile_approx_array(value, [0.25, 0.5, 0.75]),
+ percentile_approx_array(value, [0.25, 0.5, 0.75], 10001)
+ FROM percentile_approx_array_test
+ """
+
+ qt_percentile_approx_array_5 """
+ SELECT percentile_approx_array(10, [0.0, 0.5, 1.0])
+ FROM percentile_approx_array_test
+ """
+
+ qt_percentile_approx_array_6 """
+ SELECT percentile_approx_array(CAST(NULL AS DOUBLE), [0.0, 0.5, 1.0])
+ FROM percentile_approx_array_test
+ """
+
+ order_qt_percentile_approx_array_7 """
+ SELECT group_id, percentile_approx_array(value, [0.0, 0.5, 1.0])
+ FROM percentile_approx_array_test
+ GROUP BY group_id
+ ORDER BY group_id
+ """
+
+ qt_percentile_approx_array_8 """
+ SELECT percentile_approx_array(value, [])
+ FROM percentile_approx_array_test
+ """
+
+ qt_percentile_approx_array_9 """
+ SELECT
+ percentile_approx_array(value, [0.25, 0.5, 0.75]),
+ percentile_approx_array(value, [0.25, 0.5, 0.75], CAST('NaN' AS
DOUBLE)),
+ percentile_approx_array(value, [0.25, 0.5, 0.75], CAST('Infinity'
AS DOUBLE)),
+ percentile_approx_array(value, [0.25, 0.5, 0.75], CAST('-Infinity'
AS DOUBLE))
+ FROM percentile_approx_array_test
+ """
+
+ qt_percentile_approx_array_direct_merge """
+ SELECT percentile_approx_array_merge(
+ percentile_approx_array_state(value, [0.25, 0.5, 0.75], 2048)
+ )
+ FROM percentile_approx_array_test
+ """
+ qt_percentile_approx_array_subquery_merge """
+ SELECT percentile_approx_array_merge(state)
+ FROM (
+ SELECT percentile_approx_array_state(
+ value, [0.25, 0.5, 0.75], 2048
+ ) AS state
+ FROM percentile_approx_array_test
+ ) states
+ """
+
+ qt_percentile_approx_array_subquery_union """
+ SELECT percentile_approx_array_merge(state)
+ FROM (
+ SELECT percentile_approx_array_union(state) AS state
+ FROM (
+ SELECT percentile_approx_array_state(
+ value, [0.25, 0.5, 0.75], 2048
+ ) AS state
+ FROM percentile_approx_array_test
+ ) states
+ ) unions
+ """
+
+ test {
+ sql """
+ SELECT percentile_approx_array_merge(
+ percentile_approx_array_state(value, array(value / 100.0, 0.5))
+ )
+ FROM percentile_approx_array_test
+ """
+ exception "percentile_approx_array requires second parameter must be a
constant"
+ }
+
+ test {
+ sql """
+ SELECT percentile_approx_array_merge(
+ percentile_approx_array_union(
+ percentile_approx_array_state(value, array(value / 100.0,
0.5))
+ )
+ )
+ FROM percentile_approx_array_test
+ """
+ exception "percentile_approx_array requires second parameter must be a
constant"
+ }
+
+ explain {
+ sql """
+ SELECT percentile_approx_array(value, [0.25, 0.5, 0.75])
+ FROM percentile_approx_array_test
+ """
+ notContains "cast(value as DOUBLE)"
+ }
+
+ test {
+ sql """
+ SELECT percentile_approx_array(value, array(value / 100.0, 0.5))
+ FROM percentile_approx_array_test
+ """
+ exception "percentile_approx_array requires second parameter must be a
constant"
+ }
+
+ test {
+ sql """
+ SELECT percentile_approx_array(value, [0.5, NULL])
+ FROM percentile_approx_array_test
+ """
+ exception "percentile_approx_array quantile should not be null"
+ }
+
+ test {
+ sql """
+ SELECT percentile_approx_array(value, [0.5, 1.1])
+ FROM percentile_approx_array_test
+ """
+ exception "quantile"
+ }
+
+ test {
+ sql """
+ SELECT percentile_approx_array_state(CAST(NULL AS DOUBLE), [1.1])
+ """
+ exception "percentile_approx_array quantile must be in [0, 1]"
+ }
+
+ test {
+ sql """
+ SELECT percentile_approx_array(value, array(CAST('NaN' AS DOUBLE)))
+ FROM percentile_approx_array_test
+ """
+ exception "quantile must be in [0, 1]"
+ }
+
+ test {
+ sql """
+ SELECT percentile_approx_array(value, array(CAST('Infinity' AS
DOUBLE)))
+ FROM percentile_approx_array_test
+ """
+ exception "quantile must be in [0, 1]"
+ }
+
+ test {
+ sql """
+ SELECT percentile_approx_array(value, array(CAST('-Infinity' AS
DOUBLE)))
+ FROM percentile_approx_array_test
+ """
+ exception "quantile must be in [0, 1]"
+ }
+
+ test {
+ sql """
+ SELECT percentile_approx_array(value, [0.5], value)
+ FROM percentile_approx_array_test
+ """
+ exception "percentile_approx_array requires the third parameter must
be a constant"
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]