This is an automated email from the ASF dual-hosted git repository.
yashmayya pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new 9d6545bde17 Support partial metadata based aggregation when some
projection aggregations are not metadata compatible (#18334)
9d6545bde17 is described below
commit 9d6545bde17d311129c1e52c3ff26d80f08fcb71
Author: Anil Dasari <[email protected]>
AuthorDate: Sun Aug 2 12:49:50 2026 -0700
Support partial metadata based aggregation when some projection
aggregations are not metadata compatible (#18334)
---
.../core/operator/query/AggregationOperator.java | 57 ++-
.../query/NonScanBasedAggregationOperator.java | 352 +------------------
.../pinot/core/plan/AggregationPlanNode.java | 123 +++++--
.../aggregation/DefaultAggregationExecutor.java | 27 +-
.../function/AggregationFunctionUtils.java | 385 ++++++++++++++++++++-
.../executor/StarTreeAggregationExecutor.java | 3 +
...adataAndDictionaryAggregationPlanMakerTest.java | 269 ++++++++++++++
.../DefaultAggregationExecutorTest.java | 70 +++-
.../function/AggregationFunctionUtilsTest.java | 84 +++++
.../pinot/queries/ExplainPlanQueriesTest.java | 4 +-
...nerSegmentAggregationMultiValueQueriesTest.java | 2 +-
...SegmentAggregationMultiValueRawQueriesTest.java | 2 +-
...erSegmentAggregationSingleValueQueriesTest.java | 2 +-
13 files changed, 974 insertions(+), 406 deletions(-)
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/operator/query/AggregationOperator.java
b/pinot-core/src/main/java/org/apache/pinot/core/operator/query/AggregationOperator.java
index a03f8364929..eae9166af53 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/operator/query/AggregationOperator.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/operator/query/AggregationOperator.java
@@ -21,7 +21,9 @@ package org.apache.pinot.core.operator.query;
import com.google.common.base.CaseFormat;
import java.util.Arrays;
import java.util.List;
+import java.util.Objects;
import java.util.stream.Collectors;
+import javax.annotation.Nullable;
import org.apache.pinot.core.operator.BaseOperator;
import org.apache.pinot.core.operator.BaseProjectOperator;
import org.apache.pinot.core.operator.ExecutionStatistics;
@@ -31,9 +33,11 @@ import
org.apache.pinot.core.operator.blocks.results.AggregationResultsBlock;
import org.apache.pinot.core.query.aggregation.AggregationExecutor;
import org.apache.pinot.core.query.aggregation.DefaultAggregationExecutor;
import org.apache.pinot.core.query.aggregation.function.AggregationFunction;
+import
org.apache.pinot.core.query.aggregation.function.AggregationFunctionUtils;
import
org.apache.pinot.core.query.aggregation.function.AggregationFunctionUtils.AggregationInfo;
import org.apache.pinot.core.query.request.context.QueryContext;
import org.apache.pinot.core.startree.executor.StarTreeAggregationExecutor;
+import org.apache.pinot.segment.spi.datasource.DataSource;
import org.apache.pinot.spi.query.QueryScanCostContext;
@@ -46,16 +50,42 @@ public class AggregationOperator extends
BaseOperator<AggregationResultsBlock> {
private final AggregationFunction[] _aggregationFunctions;
private final BaseProjectOperator<?> _projectOperator;
private final boolean _useStarTree;
- private final long _numTotalDocs;
+ private final int _numTotalDocs;
private int _numDocsScanned = 0;
- public AggregationOperator(QueryContext queryContext, AggregationInfo
aggregationInfo, long numTotalDocs) {
+ // Marks the functions that can be resolved from the column
dictionary/metadata without scanning the segment. The
+ // aligned dataSources array holds the argument data source of each such
function (a null entry is only valid for
+ // COUNT). Resolution is deferred to execution time (see getNextBlock) to
keep query planning cheap. Both are null
+ // when no function is resolvable without scanning, in which case every
function is computed by scanning.
+ @Nullable
+ private final boolean[] _nonScanResolvable;
+ @Nullable
+ private final DataSource[] _dataSources;
+
+ public AggregationOperator(QueryContext queryContext, AggregationInfo
aggregationInfo, int numTotalDocs) {
+ this(queryContext, aggregationInfo, numTotalDocs, null, null);
+ }
+
+ /// Constructs an aggregation operator that optionally resolves some
functions from the column dictionary/metadata
+ /// instead of scanning the segment. For each function flagged in {@code
nonScanResolvable}, the result is resolved
+ /// from the aligned {@code dataSources} entry (via {@link
AggregationFunctionUtils#getAggregationResult}) at
+ /// execution time and the function is skipped during the scan; all other
functions are computed by scanning the
+ /// segment. Passing {@code null} for both means every function is computed
by scanning.
+ ///
+ /// @param nonScanResolvable per-function flags (aligned by function index)
marking the functions to resolve from
+ /// dictionary/metadata, or {@code null} if none are resolvable
+ /// @param dataSources per-function argument data sources aligned by
function index (a {@code null} entry is only
+ /// valid for {@code COUNT}), or {@code null} if none are resolvable
+ public AggregationOperator(QueryContext queryContext, AggregationInfo
aggregationInfo, int numTotalDocs,
+ @Nullable boolean[] nonScanResolvable, @Nullable DataSource[]
dataSources) {
_queryContext = queryContext;
_aggregationFunctions = queryContext.getAggregationFunctions();
_projectOperator = aggregationInfo.getProjectOperator();
_useStarTree = aggregationInfo.isUseStarTree();
_numTotalDocs = numTotalDocs;
+ _nonScanResolvable = nonScanResolvable;
+ _dataSources = dataSources;
}
@Override
@@ -63,9 +93,10 @@ public class AggregationOperator extends
BaseOperator<AggregationResultsBlock> {
// Perform aggregation on all the transform blocks
AggregationExecutor aggregationExecutor;
if (_useStarTree) {
+ // StarTreeAggregationExecutor doesn't support non-scan results.
aggregationExecutor = new
StarTreeAggregationExecutor(_aggregationFunctions);
} else {
- aggregationExecutor = new
DefaultAggregationExecutor(_aggregationFunctions);
+ aggregationExecutor = new
DefaultAggregationExecutor(_aggregationFunctions, resolveNonScanResults());
}
ValueBlock valueBlock;
while ((valueBlock = _projectOperator.nextBlock()) != null) {
@@ -83,6 +114,26 @@ public class AggregationOperator extends
BaseOperator<AggregationResultsBlock> {
return new AggregationResultsBlock(_aggregationFunctions,
aggregationExecutor.getResult(), _queryContext);
}
+ /// Returns {@code null} when no function is resolvable without scanning, in
which case all functions are computed by
+ /// scanning. Each returned non-null entry is consumed by {@link
DefaultAggregationExecutor}, which skips the scan for
+ /// that function and emits the resolved value directly.
+ @Nullable
+ private Object[] resolveNonScanResults() {
+ if (_nonScanResolvable == null) {
+ return null;
+ }
+
+ Objects.requireNonNull(_dataSources);
+ Object[] nonScanResults = new Object[_aggregationFunctions.length];
+ for (int i = 0; i < _aggregationFunctions.length; i++) {
+ if (_nonScanResolvable[i]) {
+ nonScanResults[i] =
AggregationFunctionUtils.getAggregationResult(_aggregationFunctions[i],
+ _dataSources[i], _numTotalDocs, EXPLAIN_NAME);
+ }
+ }
+ return nonScanResults;
+ }
+
@Override
public List<BaseProjectOperator<?>> getChildOperators() {
return List.of(_projectOperator);
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/operator/query/NonScanBasedAggregationOperator.java
b/pinot-core/src/main/java/org/apache/pinot/core/operator/query/NonScanBasedAggregationOperator.java
index fee1fbeae90..4a00993f6b8 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/operator/query/NonScanBasedAggregationOperator.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/operator/query/NonScanBasedAggregationOperator.java
@@ -18,43 +18,16 @@
*/
package org.apache.pinot.core.operator.query;
-import com.clearspring.analytics.stream.cardinality.HyperLogLog;
-import com.clearspring.analytics.stream.cardinality.HyperLogLogPlus;
-import com.dynatrace.hash4j.distinctcount.UltraLogLog;
-import com.google.common.base.Preconditions;
-import it.unimi.dsi.fastutil.doubles.DoubleOpenHashSet;
-import it.unimi.dsi.fastutil.floats.FloatOpenHashSet;
-import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
-import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
-import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
-import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
-import java.util.Objects;
-import java.util.Set;
-import org.apache.pinot.core.common.ObjectSerDeUtils;
import org.apache.pinot.core.common.Operator;
import org.apache.pinot.core.operator.BaseOperator;
import org.apache.pinot.core.operator.ExecutionStatistics;
import org.apache.pinot.core.operator.blocks.results.AggregationResultsBlock;
import org.apache.pinot.core.query.aggregation.function.AggregationFunction;
-import
org.apache.pinot.core.query.aggregation.function.DistinctCountHLLAggregationFunction;
-import
org.apache.pinot.core.query.aggregation.function.DistinctCountHLLPlusAggregationFunction;
-import
org.apache.pinot.core.query.aggregation.function.DistinctCountOffHeapAggregationFunction;
-import
org.apache.pinot.core.query.aggregation.function.DistinctCountRawHLLAggregationFunction;
-import
org.apache.pinot.core.query.aggregation.function.DistinctCountRawHLLPlusAggregationFunction;
-import
org.apache.pinot.core.query.aggregation.function.DistinctCountSmartHLLAggregationFunction;
-import
org.apache.pinot.core.query.aggregation.function.DistinctCountSmartHLLPlusAggregationFunction;
-import
org.apache.pinot.core.query.aggregation.function.DistinctCountSmartULLAggregationFunction;
-import
org.apache.pinot.core.query.aggregation.function.DistinctCountULLAggregationFunction;
+import
org.apache.pinot.core.query.aggregation.function.AggregationFunctionUtils;
import org.apache.pinot.core.query.request.context.QueryContext;
-import org.apache.pinot.segment.local.customobject.MinMaxRangePair;
-import org.apache.pinot.segment.local.utils.UltraLogLogUtils;
import org.apache.pinot.segment.spi.datasource.DataSource;
-import org.apache.pinot.segment.spi.index.reader.Dictionary;
-import org.apache.pinot.spi.data.FieldSpec.DataType;
-import org.apache.pinot.spi.query.QueryThreadContext;
-import org.apache.pinot.spi.utils.ByteArray;
/// Aggregation operator that utilizes dictionary or column metadata for
serving aggregation queries to avoid scanning.
@@ -93,97 +66,8 @@ public class NonScanBasedAggregationOperator extends
BaseOperator<AggregationRes
AggregationFunction aggregationFunction = _aggregationFunctions[i];
// note that dataSource will be null for COUNT, sp do not interact with
it until it's known this isn't a COUNT
DataSource dataSource = _dataSources[i];
- Object result;
- switch (aggregationFunction.getType()) {
- case COUNT:
- result = (long) _numTotalDocs;
- break;
- case MIN:
- case MINMV:
- result = getMinValueNumeric(dataSource);
- break;
- case MINLONG:
- result = getMinValueLong(dataSource);
- break;
- case MINSTRING:
- assert dataSource.getDictionary() != null;
- result = dataSource.getDictionary().getMinVal();
- break;
- case MAX:
- case MAXMV:
- result = getMaxValueNumeric(dataSource);
- break;
- case MAXLONG:
- result = getMaxValueLong(dataSource);
- break;
- case MAXSTRING:
- assert dataSource.getDictionary() != null;
- result = dataSource.getDictionary().getMaxVal();
- break;
- case MINMAXRANGE:
- case MINMAXRANGEMV:
- result = new MinMaxRangePair(getMinValueNumeric(dataSource),
getMaxValueNumeric(dataSource));
- break;
- case DISTINCTCOUNT:
- case DISTINCTSUM:
- case DISTINCTAVG:
- case DISTINCTCOUNTMV:
- case DISTINCTSUMMV:
- case DISTINCTAVGMV:
- result =
getDistinctValueSet(Objects.requireNonNull(dataSource.getDictionary()));
- break;
- case DISTINCTCOUNTOFFHEAP:
- result = ((DistinctCountOffHeapAggregationFunction)
aggregationFunction).extractAggregationResult(
- Objects.requireNonNull(dataSource.getDictionary()));
- break;
- case DISTINCTCOUNTHLL:
- case DISTINCTCOUNTHLLMV:
- result =
getDistinctCountHLLResult(Objects.requireNonNull(dataSource.getDictionary()),
- (DistinctCountHLLAggregationFunction) aggregationFunction);
- break;
- case DISTINCTCOUNTRAWHLL:
- case DISTINCTCOUNTRAWHLLMV:
- result =
getDistinctCountHLLResult(Objects.requireNonNull(dataSource.getDictionary()),
- ((DistinctCountRawHLLAggregationFunction)
aggregationFunction).getDistinctCountHLLAggregationFunction());
- break;
- case DISTINCTCOUNTHLLPLUS:
- case DISTINCTCOUNTHLLPLUSMV:
- result =
getDistinctCountHLLPlusResult(Objects.requireNonNull(dataSource.getDictionary()),
- (DistinctCountHLLPlusAggregationFunction) aggregationFunction);
- break;
- case DISTINCTCOUNTRAWHLLPLUS:
- case DISTINCTCOUNTRAWHLLPLUSMV:
- result =
getDistinctCountHLLPlusResult(Objects.requireNonNull(dataSource.getDictionary()),
- ((DistinctCountRawHLLPlusAggregationFunction)
aggregationFunction)
- .getDistinctCountHLLPlusAggregationFunction());
- break;
- case SEGMENTPARTITIONEDDISTINCTCOUNT:
- result = (long)
Objects.requireNonNull(dataSource.getDictionary()).length();
- break;
- case DISTINCTCOUNTSMARTHLL:
- result =
getDistinctCountSmartHLLResult(Objects.requireNonNull(dataSource.getDictionary()),
- (DistinctCountSmartHLLAggregationFunction) aggregationFunction);
- break;
- case DISTINCTCOUNTSMARTHLLPLUS:
- result =
getDistinctCountSmartHLLPlusResult(Objects.requireNonNull(dataSource.getDictionary()),
- (DistinctCountSmartHLLPlusAggregationFunction)
aggregationFunction);
- break;
- case DISTINCTCOUNTULL:
- result =
getDistinctCountULLResult(Objects.requireNonNull(dataSource.getDictionary()),
- (DistinctCountULLAggregationFunction) aggregationFunction);
- break;
- case DISTINCTCOUNTSMARTULL:
- result =
getDistinctCountSmartULLResult(Objects.requireNonNull(dataSource.getDictionary()),
- (DistinctCountSmartULLAggregationFunction) aggregationFunction);
- break;
- case DISTINCTCOUNTRAWULL:
- result =
getDistinctCountULLResult(Objects.requireNonNull(dataSource.getDictionary()),
- (DistinctCountULLAggregationFunction) aggregationFunction);
- break;
- default:
- throw new IllegalStateException(
- "Non-scan based aggregation operator does not support function
type: " + aggregationFunction.getType());
- }
+ Object result =
AggregationFunctionUtils.getAggregationResult(aggregationFunction, dataSource,
+ _numTotalDocs, EXPLAIN_NAME);
aggregationResults.add(result);
}
@@ -191,236 +75,6 @@ public class NonScanBasedAggregationOperator extends
BaseOperator<AggregationRes
return new AggregationResultsBlock(_aggregationFunctions,
aggregationResults, _queryContext);
}
- private static Double getMinValueNumeric(DataSource dataSource) {
- Dictionary dictionary = dataSource.getDictionary();
- if (dictionary != null) {
- return toDouble(dictionary.getMinVal());
- }
- return toDouble(dataSource.getDataSourceMetadata().getMinValue());
- }
-
- private static Long getMinValueLong(DataSource dataSource) {
- DataType dataType =
dataSource.getDataSourceMetadata().getDataType().getStoredType();
- Preconditions.checkArgument(
- dataType == DataType.LONG || dataType == DataType.INT,
- "MINLONG aggregation function can only be applied to columns of
integer types");
- Dictionary dictionary = dataSource.getDictionary();
- if (dictionary != null) {
- return ((Number) dictionary.getMinVal()).longValue();
- }
- return ((Number)
dataSource.getDataSourceMetadata().getMinValue()).longValue();
- }
-
- private static Double getMaxValueNumeric(DataSource dataSource) {
- Dictionary dictionary = dataSource.getDictionary();
- if (dictionary != null) {
- return toDouble(dictionary.getMaxVal());
- }
- return toDouble(dataSource.getDataSourceMetadata().getMaxValue());
- }
-
- private static Long getMaxValueLong(DataSource dataSource) {
- DataType dataType =
dataSource.getDataSourceMetadata().getDataType().getStoredType();
- Preconditions.checkArgument(
- dataType == DataType.LONG || dataType == DataType.INT,
- "MAXLONG aggregation function can only be applied to columns of
integer types");
- Dictionary dictionary = dataSource.getDictionary();
- if (dictionary != null) {
- return ((Number) dictionary.getMaxVal()).longValue();
- }
- return ((Number)
dataSource.getDataSourceMetadata().getMaxValue()).longValue();
- }
-
- private static Double toDouble(Comparable<?> value) {
- if (value instanceof Double) {
- return (Double) value;
- } else if (value instanceof Number) {
- return ((Number) value).doubleValue();
- } else {
- return Double.parseDouble(value.toString());
- }
- }
-
- private static Set getDistinctValueSet(Dictionary dictionary) {
- int dictionarySize = dictionary.length();
- switch (dictionary.getValueType()) {
- case INT:
- IntOpenHashSet intSet = new IntOpenHashSet(dictionarySize);
- for (int dictId = 0; dictId < dictionarySize; dictId++) {
-
QueryThreadContext.checkTerminationAndSampleUsagePeriodically(dictId,
EXPLAIN_NAME);
- intSet.add(dictionary.getIntValue(dictId));
- }
- return intSet;
- case LONG:
- LongOpenHashSet longSet = new LongOpenHashSet(dictionarySize);
- for (int dictId = 0; dictId < dictionarySize; dictId++) {
-
QueryThreadContext.checkTerminationAndSampleUsagePeriodically(dictId,
EXPLAIN_NAME);
- longSet.add(dictionary.getLongValue(dictId));
- }
- return longSet;
- case FLOAT:
- FloatOpenHashSet floatSet = new FloatOpenHashSet(dictionarySize);
- for (int dictId = 0; dictId < dictionarySize; dictId++) {
-
QueryThreadContext.checkTerminationAndSampleUsagePeriodically(dictId,
EXPLAIN_NAME);
- floatSet.add(dictionary.getFloatValue(dictId));
- }
- return floatSet;
- case DOUBLE:
- DoubleOpenHashSet doubleSet = new DoubleOpenHashSet(dictionarySize);
- for (int dictId = 0; dictId < dictionarySize; dictId++) {
-
QueryThreadContext.checkTerminationAndSampleUsagePeriodically(dictId,
EXPLAIN_NAME);
- doubleSet.add(dictionary.getDoubleValue(dictId));
- }
- return doubleSet;
- case BIG_DECIMAL:
- ObjectOpenHashSet<BigDecimal> bigDecimalSet = new
ObjectOpenHashSet<>(dictionarySize);
- for (int dictId = 0; dictId < dictionarySize; dictId++) {
-
QueryThreadContext.checkTerminationAndSampleUsagePeriodically(dictId,
EXPLAIN_NAME);
- bigDecimalSet.add(dictionary.getBigDecimalValue(dictId));
- }
- return bigDecimalSet;
- case STRING:
- ObjectOpenHashSet<String> stringSet = new
ObjectOpenHashSet<>(dictionarySize);
- for (int dictId = 0; dictId < dictionarySize; dictId++) {
-
QueryThreadContext.checkTerminationAndSampleUsagePeriodically(dictId,
EXPLAIN_NAME);
- stringSet.add(dictionary.getStringValue(dictId));
- }
- return stringSet;
- case BYTES:
- ObjectOpenHashSet<ByteArray> bytesSet = new
ObjectOpenHashSet<>(dictionarySize);
- for (int dictId = 0; dictId < dictionarySize; dictId++) {
-
QueryThreadContext.checkTerminationAndSampleUsagePeriodically(dictId,
EXPLAIN_NAME);
- bytesSet.add(new ByteArray(dictionary.getBytesValue(dictId)));
- }
- return bytesSet;
- default:
- throw new IllegalStateException();
- }
- }
-
- private static HyperLogLog getDistinctValueHLL(Dictionary dictionary, int
log2m) {
- HyperLogLog hll = new HyperLogLog(log2m);
- int length = dictionary.length();
- for (int i = 0; i < length; i++) {
- QueryThreadContext.checkTerminationAndSampleUsagePeriodically(i,
EXPLAIN_NAME);
- hll.offer(dictionary.get(i));
- }
- return hll;
- }
-
- private static UltraLogLog getDistinctValueULL(Dictionary dictionary, int p)
{
- UltraLogLog ull = UltraLogLog.create(p);
- int length = dictionary.length();
- for (int i = 0; i < length; i++) {
- QueryThreadContext.checkTerminationAndSampleUsagePeriodically(i,
EXPLAIN_NAME);
- Object value = dictionary.get(i);
- UltraLogLogUtils.hashObject(value).ifPresent(ull::add);
- }
- return ull;
- }
-
- private static HyperLogLogPlus getDistinctValueHLLPlus(Dictionary
dictionary, int p, int sp) {
- HyperLogLogPlus hllPlus = new HyperLogLogPlus(p, sp);
- int length = dictionary.length();
- for (int i = 0; i < length; i++) {
- QueryThreadContext.checkTerminationAndSampleUsagePeriodically(i,
EXPLAIN_NAME);
- hllPlus.offer(dictionary.get(i));
- }
- return hllPlus;
- }
-
- private static HyperLogLog getDistinctCountHLLResult(Dictionary dictionary,
- DistinctCountHLLAggregationFunction function) {
- if (dictionary.getValueType() == DataType.BYTES) {
- // Treat BYTES value as serialized HyperLogLog
- try {
- QueryThreadContext.checkTerminationAndSampleUsage(EXPLAIN_NAME);
- HyperLogLog hll =
ObjectSerDeUtils.HYPER_LOG_LOG_SER_DE.deserialize(dictionary.getBytesValue(0));
- int length = dictionary.length();
- for (int i = 1; i < length; i++) {
- QueryThreadContext.checkTerminationAndSampleUsagePeriodically(i,
EXPLAIN_NAME);
-
hll.addAll(ObjectSerDeUtils.HYPER_LOG_LOG_SER_DE.deserialize(dictionary.getBytesValue(i)));
- }
- return hll;
- } catch (Exception e) {
- throw new RuntimeException("Caught exception while merging
HyperLogLogs", e);
- }
- } else {
- return getDistinctValueHLL(dictionary, function.getLog2m());
- }
- }
-
- private static HyperLogLogPlus getDistinctCountHLLPlusResult(Dictionary
dictionary,
- DistinctCountHLLPlusAggregationFunction function) {
- if (dictionary.getValueType() == DataType.BYTES) {
- // Treat BYTES value as serialized HyperLogLogPlus
- try {
- QueryThreadContext.checkTerminationAndSampleUsage(EXPLAIN_NAME);
- HyperLogLogPlus hllplus =
ObjectSerDeUtils.HYPER_LOG_LOG_PLUS_SER_DE.deserialize(dictionary.getBytesValue(0));
- int length = dictionary.length();
- for (int i = 1; i < length; i++) {
- QueryThreadContext.checkTerminationAndSampleUsagePeriodically(i,
EXPLAIN_NAME);
-
hllplus.addAll(ObjectSerDeUtils.HYPER_LOG_LOG_PLUS_SER_DE.deserialize(dictionary.getBytesValue(i)));
- }
- return hllplus;
- } catch (Exception e) {
- throw new RuntimeException("Caught exception while merging
HyperLogLogPluses", e);
- }
- } else {
- return getDistinctValueHLLPlus(dictionary, function.getP(),
function.getSp());
- }
- }
-
- private static Object getDistinctCountSmartHLLResult(Dictionary dictionary,
- DistinctCountSmartHLLAggregationFunction function) {
- if (dictionary.length() > function.getThreshold()) {
- // Store values into a HLL when the dictionary size exceeds the
conversion threshold
- return getDistinctValueHLL(dictionary, function.getLog2m());
- } else {
- return getDistinctValueSet(dictionary);
- }
- }
-
- private static Object getDistinctCountSmartHLLPlusResult(Dictionary
dictionary,
- DistinctCountSmartHLLPlusAggregationFunction function) {
- if (dictionary.length() > function.getThreshold()) {
- // Store values into a HLLPlus when the dictionary size exceeds the
conversion threshold
- return getDistinctValueHLLPlus(dictionary, function.getP(),
function.getSp());
- } else {
- return getDistinctValueSet(dictionary);
- }
- }
-
- private static UltraLogLog getDistinctCountULLResult(Dictionary dictionary,
- DistinctCountULLAggregationFunction function) {
- if (dictionary.getValueType() == DataType.BYTES) {
- // Treat BYTES value as serialized UltraLogLog and merge
- try {
- QueryThreadContext.checkTerminationAndSampleUsage(EXPLAIN_NAME);
- UltraLogLog ull =
ObjectSerDeUtils.ULTRA_LOG_LOG_OBJECT_SER_DE.deserialize(dictionary.getBytesValue(0));
- int length = dictionary.length();
- for (int i = 1; i < length; i++) {
- QueryThreadContext.checkTerminationAndSampleUsagePeriodically(i,
EXPLAIN_NAME);
-
ull.add(ObjectSerDeUtils.ULTRA_LOG_LOG_OBJECT_SER_DE.deserialize(dictionary.getBytesValue(i)));
- }
- return ull;
- } catch (Exception e) {
- throw new RuntimeException("Caught exception while merging
UltraLogLogs", e);
- }
- } else {
- return getDistinctValueULL(dictionary, function.getP());
- }
- }
-
- private static Object getDistinctCountSmartULLResult(Dictionary dictionary,
- DistinctCountSmartULLAggregationFunction function) {
- if (dictionary.length() > function.getThreshold()) {
- return getDistinctValueULL(dictionary, function.getP());
- } else {
- return getDistinctValueSet(dictionary);
- }
- }
-
@Override
public String toExplainString() {
return EXPLAIN_NAME;
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/plan/AggregationPlanNode.java
b/pinot-core/src/main/java/org/apache/pinot/core/plan/AggregationPlanNode.java
index e73e1b20c2e..054bd26cc49 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/plan/AggregationPlanNode.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/plan/AggregationPlanNode.java
@@ -20,6 +20,7 @@ package org.apache.pinot.core.plan;
import java.util.EnumSet;
import java.util.List;
+import javax.annotation.Nullable;
import org.apache.pinot.common.request.context.ExpressionContext;
import org.apache.pinot.core.common.Operator;
import org.apache.pinot.core.operator.blocks.results.AggregationResultsBlock;
@@ -60,6 +61,12 @@ public class AggregationPlanNode implements PlanNode {
private static final EnumSet<AggregationFunctionType>
METADATA_BASED_FUNCTIONS =
EnumSet.of(COUNT, MIN, MINMV, MINLONG, MAX, MAXMV, MAXLONG, MINMAXRANGE,
MINMAXRANGEMV);
+ // MIN/MAX/MINMAXRANGE derive their result numerically from the column
min/max, so they can only be resolved from
+ // metadata/dictionary for numeric columns. Non-numeric columns (e.g. BYTES)
store min/max as raw values that cannot
+ // be parsed as numbers.
+ private static final EnumSet<AggregationFunctionType>
NUMERIC_METADATA_FUNCTIONS =
+ EnumSet.of(MIN, MINMV, MINLONG, MAX, MAXMV, MAXLONG, MINMAXRANGE,
MINMAXRANGEMV);
+
private final IndexSegment _indexSegment;
private final SegmentContext _segmentContext;
private final QueryContext _queryContext;
@@ -109,17 +116,41 @@ public class AggregationPlanNode implements PlanNode {
boolean hasNullValues = _queryContext.isNullHandlingEnabled() &&
hasNullValues(aggregationFunctions);
if (!hasNullValues) {
- // Priority 2: Check if non-scan based aggregation is feasible
- if (filterOperator.isResultMatchingAll() && isFitForNonScanBasedPlan()) {
+ // when the filter matches all documents, resolve as many functions as
possible from the column
+ // dictionary/metadata without scanning the segment. Eligibility is
evaluated once per function here
+ // and reused for both the fully non-scan path (all functions
resolvable) and
+ // the partial path (some functions resolvable).
+ if (filterOperator.isResultMatchingAll()) {
+ boolean[] nonScanResolvable = new boolean[aggregationFunctions.length];
DataSource[] dataSources = new DataSource[aggregationFunctions.length];
+ int numResolved = 0;
for (int i = 0; i < aggregationFunctions.length; i++) {
- List<?> inputExpressions =
aggregationFunctions[i].getInputExpressions();
- if (!inputExpressions.isEmpty()) {
- String column = ((ExpressionContext)
inputExpressions.get(0)).getIdentifier();
- dataSources[i] = _indexSegment.getDataSource(column,
_queryContext.getSchema());
+ DataSource dataSource =
getDataSourceForAggregationFunction(aggregationFunctions[i]);
+ if (isFitForNonScanBasedPlan(aggregationFunctions[i], dataSource)) {
+ nonScanResolvable[i] = true;
+ dataSources[i] = dataSource;
+ numResolved++;
+ }
+ }
+
+ if (numResolved == aggregationFunctions.length) {
+ // Priority 2: all functions can be resolved from
dictionary/metadata -> fully non-scan based execution
+ return new NonScanBasedAggregationOperator(_queryContext,
dataSources, numTotalDocs);
+ }
+ if (numResolved > 0) {
+ // Some functions are resolved from dictionary/metadata; the rest
are scanned by the AggregationOperator.
+ // Project only the scanned functions' columns so a column that is
resolved by metadata is not counted as
+ // scanned (keeps it out of numEntriesScannedPostFilter).
+ AggregationFunction[] scannedFunctions = new
AggregationFunction[aggregationFunctions.length - numResolved];
+ for (int i = 0, j = 0; i < aggregationFunctions.length; i++) {
+ if (!nonScanResolvable[i]) {
+ scannedFunctions[j++] = aggregationFunctions[i];
+ }
}
+ aggregationInfo =
AggregationFunctionUtils.buildAggregationInfoWithoutStarTree(_segmentContext,
_queryContext,
+ aggregationFunctions, scannedFunctions, filterOperator);
+ return new AggregationOperator(_queryContext, aggregationInfo,
numTotalDocs, nonScanResolvable, dataSources);
}
- return new NonScanBasedAggregationOperator(_queryContext, dataSources,
numTotalDocs);
}
// Priority 3: Check if fast filtered count can be used
@@ -164,34 +195,40 @@ public class AggregationPlanNode implements PlanNode {
return false;
}
- /// Returns `true` if the given aggregations can be solved with dictionary
or column metadata, `false`
- /// otherwise.
- private boolean isFitForNonScanBasedPlan() {
- AggregationFunction[] aggregationFunctions =
_queryContext.getAggregationFunctions();
- assert aggregationFunctions != null;
- for (AggregationFunction<?, ?> aggregationFunction : aggregationFunctions)
{
- if (aggregationFunction.getType() == COUNT) {
- continue;
- }
- ExpressionContext argument =
aggregationFunction.getInputExpressions().get(0);
- if (argument.getType() != ExpressionContext.Type.IDENTIFIER) {
- return false;
- }
- DataSource dataSource =
_indexSegment.getDataSource(argument.getIdentifier(),
_queryContext.getSchema());
- if (DICTIONARY_BASED_FUNCTIONS.contains(aggregationFunction.getType())) {
- if (dataSource.getDictionary() != null) {
- continue;
- }
- }
- if (METADATA_BASED_FUNCTIONS.contains(aggregationFunction.getType())) {
- if (dataSource.getDataSourceMetadata().getMaxValue() != null
- && dataSource.getDataSourceMetadata().getMinValue() != null) {
- continue;
- }
- }
+ /// Returns {@code true} if the given aggregation function can be resolved
from the column dictionary or metadata
+ /// (without scanning the segment), {@code false} otherwise. {@code COUNT}
is always eligible. Functions whose result
+ /// is derived numerically from the column min/max (e.g. MIN, MAX,
MINMAXRANGE) are only eligible for numeric columns,
+ /// since non-numeric columns (e.g. BYTES) store min/max as raw values that
cannot be parsed as numbers.
+ ///
+ /// @param aggregationFunction aggregation function to test
+ /// @param dataSource the function argument's data source (see {@link
#getDataSourceForAggregationFunction})
+ private boolean isFitForNonScanBasedPlan(AggregationFunction<?, ?>
aggregationFunction,
+ @Nullable DataSource dataSource) {
+ AggregationFunctionType functionType = aggregationFunction.getType();
+ if (functionType == COUNT) {
+ return true;
+ }
+
+ if (dataSource == null) {
+ // Aggregation function does not have a single identifier argument (e.g.
SUM(1)),
+ // so it cannot be resolved from metadata
+ return false;
+ }
+
+ // MIN/MAX/MINMAXRANGE derive their result numerically from the column
min/max, which is only valid for numeric
+ // columns. Non-numeric columns (e.g. BYTES) store min/max as raw values
that cannot be parsed as numbers.
+ if (NUMERIC_METADATA_FUNCTIONS.contains(functionType)
+ &&
!dataSource.getDataSourceMetadata().getDataType().getStoredType().isNumeric()) {
return false;
}
- return true;
+
+ if (dataSource.getDictionary() != null &&
DICTIONARY_BASED_FUNCTIONS.contains(functionType)) {
+ return true;
+ }
+
+ return METADATA_BASED_FUNCTIONS.contains(functionType)
+ && dataSource.getDataSourceMetadata().getMaxValue() != null
+ && dataSource.getDataSourceMetadata().getMinValue() != null;
}
private static boolean canOptimizeFilteredCount(BaseFilterOperator
filterOperator,
@@ -199,4 +236,24 @@ public class AggregationPlanNode implements PlanNode {
return (aggregationFunctions.length == 1 &&
aggregationFunctions[0].getType() == COUNT)
&& filterOperator.canOptimizeCount();
}
+
+ /// Returns the data source for the given aggregation function's argument,
or {@code null} if the function has no
+ /// argument (e.g. {@code COUNT(*)}) or its argument is not a single column
identifier (e.g. {@code COUNT(1)} or a
+ /// transform expression), in which case it cannot be resolved from
dictionary/metadata.
+ ///
+ /// @param aggregationFunction aggregation function whose argument data
source is resolved
+ /// @return the argument's data source, or {@code null} if it has no single
identifier argument
+ @Nullable
+ private DataSource
getDataSourceForAggregationFunction(AggregationFunction<?, ?>
aggregationFunction) {
+ List<ExpressionContext> inputExpressions =
aggregationFunction.getInputExpressions();
+ if (!inputExpressions.isEmpty()) {
+ ExpressionContext argument = inputExpressions.get(0);
+ if (argument.getType() != ExpressionContext.Type.IDENTIFIER) {
+ return null;
+ }
+ return _indexSegment.getDataSource(argument.getIdentifier(),
_queryContext.getSchema());
+ }
+
+ return null;
+ }
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/DefaultAggregationExecutor.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/DefaultAggregationExecutor.java
index d6c64c1cfb6..806413253fd 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/DefaultAggregationExecutor.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/DefaultAggregationExecutor.java
@@ -20,6 +20,7 @@ package org.apache.pinot.core.query.aggregation;
import java.util.ArrayList;
import java.util.List;
+import javax.annotation.Nullable;
import org.apache.pinot.core.operator.blocks.ValueBlock;
import org.apache.pinot.core.query.aggregation.function.AggregationFunction;
import
org.apache.pinot.core.query.aggregation.function.AggregationFunctionUtils;
@@ -29,8 +30,19 @@ import
org.apache.pinot.core.query.aggregation.function.AggregationFunctionUtils
public class DefaultAggregationExecutor implements AggregationExecutor {
protected final AggregationFunction[] _aggregationFunctions;
protected final AggregationResultHolder[] _aggregationResultHolders;
+ @Nullable
+ protected final Object[] _nonScanResults;
- public DefaultAggregationExecutor(AggregationFunction[]
aggregationFunctions) {
+ /// Creates an executor that skips functions with a non-scan result
(resolved from the column dictionary or
+ /// metadata). For each index {@code i} where {@code nonScanResults[i]} is
non-null, the function is not aggregated
+ /// over the scanned blocks and the resolved value is emitted directly in
the results. A {@code null} array disables
+ /// this behavior and all functions are computed by scanning.
+ ///
+ /// @param nonScanResults per-function results resolved without scanning
(from the column dictionary or metadata),
+ /// or {@code null} if none are resolved
+ public DefaultAggregationExecutor(AggregationFunction[] aggregationFunctions,
+ @Nullable Object[] nonScanResults) {
+ _nonScanResults = nonScanResults;
_aggregationFunctions = aggregationFunctions;
int numAggregationFunctions = aggregationFunctions.length;
_aggregationResultHolders = new
AggregationResultHolder[numAggregationFunctions];
@@ -39,11 +51,18 @@ public class DefaultAggregationExecutor implements
AggregationExecutor {
}
}
+ public DefaultAggregationExecutor(AggregationFunction[]
aggregationFunctions) {
+ this(aggregationFunctions, null);
+ }
+
@Override
public void aggregate(ValueBlock valueBlock) {
int numAggregationFunctions = _aggregationFunctions.length;
int length = valueBlock.getNumDocs();
for (int i = 0; i < numAggregationFunctions; i++) {
+ if (_nonScanResults != null && _nonScanResults[i] != null) {
+ continue; // skip — already resolved without scanning
(dictionary/metadata)
+ }
AggregationFunction aggregationFunction = _aggregationFunctions[i];
aggregationFunction.aggregate(length, _aggregationResultHolders[i],
AggregationFunctionUtils.getBlockValSetMap(aggregationFunction,
valueBlock));
@@ -55,7 +74,11 @@ public class DefaultAggregationExecutor implements
AggregationExecutor {
int numFunctions = _aggregationFunctions.length;
List<Object> aggregationResults = new ArrayList<>(numFunctions);
for (int i = 0; i < numFunctions; i++) {
-
aggregationResults.add(_aggregationFunctions[i].extractAggregationResult(_aggregationResultHolders[i]));
+ if (_nonScanResults != null && _nonScanResults[i] != null) {
+ aggregationResults.add(_nonScanResults[i]);
+ } else {
+
aggregationResults.add(_aggregationFunctions[i].extractAggregationResult(_aggregationResultHolders[i]));
+ }
}
return aggregationResults;
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionUtils.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionUtils.java
index 6e9107b7444..a9c6e7d2e9f 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionUtils.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionUtils.java
@@ -18,11 +18,20 @@
*/
package org.apache.pinot.core.query.aggregation.function;
+import com.clearspring.analytics.stream.cardinality.HyperLogLog;
+import com.clearspring.analytics.stream.cardinality.HyperLogLogPlus;
+import com.dynatrace.hash4j.distinctcount.UltraLogLog;
+import com.google.common.base.Preconditions;
import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
+import it.unimi.dsi.fastutil.doubles.DoubleOpenHashSet;
import it.unimi.dsi.fastutil.floats.FloatArrayList;
+import it.unimi.dsi.fastutil.floats.FloatOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongArrayList;
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import java.io.IOException;
import java.math.BigDecimal;
import java.sql.Timestamp;
@@ -31,6 +40,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;
import org.apache.commons.lang3.tuple.Pair;
@@ -42,6 +52,7 @@ import
org.apache.pinot.common.request.context.predicate.Predicate;
import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
import org.apache.pinot.common.utils.config.QueryOptionsUtils;
import org.apache.pinot.core.common.BlockValSet;
+import org.apache.pinot.core.common.ObjectSerDeUtils;
import org.apache.pinot.core.common.datatable.DataTableBuilder;
import org.apache.pinot.core.operator.BaseProjectOperator;
import org.apache.pinot.core.operator.blocks.ValueBlock;
@@ -53,15 +64,22 @@ import org.apache.pinot.core.plan.FilterPlanNode;
import org.apache.pinot.core.plan.ProjectPlanNode;
import org.apache.pinot.core.query.request.context.QueryContext;
import org.apache.pinot.core.startree.StarTreeUtils;
+import org.apache.pinot.segment.local.customobject.MinMaxRangePair;
+import org.apache.pinot.segment.local.utils.UltraLogLogUtils;
import org.apache.pinot.segment.spi.AggregationFunctionType;
import org.apache.pinot.segment.spi.SegmentContext;
+import org.apache.pinot.segment.spi.datasource.DataSource;
+import org.apache.pinot.segment.spi.index.reader.Dictionary;
import
org.apache.pinot.segment.spi.index.startree.AggregationFunctionColumnPair;
+import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.query.QueryThreadContext;
import org.apache.pinot.spi.utils.ByteArray;
/// The `AggregationFunctionUtils` class provides utility methods for
aggregation function.
@SuppressWarnings({"rawtypes", "unchecked"})
public class AggregationFunctionUtils {
+
private AggregationFunctionUtils() {
}
@@ -362,7 +380,23 @@ public class AggregationFunctionUtils {
return new AggregationInfo(aggregationFunctions, projectOperator, false);
}
- /// Builds swim-lanes (list of [AggregationInfo]) for filtered aggregations.
+ /// Builds {@link AggregationInfo} for aggregations without using star-tree
index, projecting only the columns
+ /// required by {@code projectionFunctions} (a subset of {@code
allFunctions}). The partial metadata path passes just
+ /// the scanned functions here so that columns used solely by
metadata-resolved functions are excluded from the scan,
+ /// while {@link AggregationInfo} still carries the full function set. For a
full scan the two arrays are identical.
+ public static AggregationInfo
buildAggregationInfoWithoutStarTree(SegmentContext segmentContext,
+ QueryContext queryContext, AggregationFunction[] allFunctions,
AggregationFunction[] projectionFunctions,
+ BaseFilterOperator filterOperator) {
+ Set<ExpressionContext> expressionsToTransform =
+ collectExpressionsToTransform(projectionFunctions,
queryContext.getGroupByExpressions());
+ BaseProjectOperator<?> projectOperator =
+ new ProjectPlanNode(segmentContext, queryContext,
expressionsToTransform, DocIdSetPlanNode.MAX_DOC_PER_CALL,
+ filterOperator).run();
+ return new AggregationInfo(allFunctions, projectOperator, false);
+ }
+
+
+ /// Builds swim-lanes (list of {@link AggregationInfo}) for filtered
aggregations.
public static List<AggregationInfo>
buildFilteredAggregationInfos(SegmentContext segmentContext,
QueryContext queryContext) {
assert queryContext.getAggregationFunctions() != null &&
queryContext.getFilteredAggregationFunctions() != null;
@@ -477,4 +511,353 @@ public class AggregationFunctionUtils {
}
return columnName;
}
+
+ /// Resolves the result of the given aggregation function from the column
dictionary or metadata, without scanning the
+ /// segment. This is used by the non-scan based aggregation operator and by
the partial metadata-based path in
+ /// {@link org.apache.pinot.core.operator.query.AggregationOperator} to
resolve metadata-eligible functions.
+ /// <p>
+ /// {@code COUNT} is resolved directly from {@code numTotalDocs}. Every
other supported function reads its result from
+ /// the column dictionary or metadata and therefore requires a non-null
{@code dataSource}. Callers must only invoke
+ /// this method for functions that are metadata/dictionary eligible (as
determined by the fitness check in
+ /// {@link org.apache.pinot.core.plan.AggregationPlanNode}); unsupported
function types cause an
+ /// {@link IllegalStateException}.
+ ///
+ /// @param aggregationFunction aggregation function to resolve
+ /// @param dataSource data source of the function argument; may be {@code
null} only for {@code COUNT}
+ /// @param numTotalDocs total number of documents in the segment, used to
resolve {@code COUNT}
+ /// @param explainPlanName explain-plan name used for periodic query
termination checks
+ /// @return the result resolved from dictionary/metadata for the function
+ /// @throws IllegalStateException if the function type cannot be resolved
from dictionary or metadata
+ public static Object getAggregationResult(AggregationFunction
aggregationFunction, @Nullable DataSource dataSource,
+ int numTotalDocs, String explainPlanName) {
+ AggregationFunctionType functionType = aggregationFunction.getType();
+ if (functionType == AggregationFunctionType.COUNT) {
+ return (long) numTotalDocs;
+ }
+ // Every other supported function resolves its result from the column
dictionary or metadata, all of which require
+ // a non-null data source.
+ Objects.requireNonNull(dataSource, "DataSource is null for aggregation
function: " + functionType);
+
+ Object result;
+ switch (functionType) {
+ case MIN:
+ case MINMV:
+ result = getMinValueNumeric(dataSource);
+ break;
+ case MINLONG:
+ result = getMinValueLong(dataSource);
+ break;
+ case MINSTRING:
+ assert dataSource.getDictionary() != null;
+ result = dataSource.getDictionary().getMinVal();
+ break;
+ case MAX:
+ case MAXMV:
+ result = getMaxValueNumeric(dataSource);
+ break;
+ case MAXLONG:
+ result = getMaxValueLong(dataSource);
+ break;
+ case MAXSTRING:
+ assert dataSource.getDictionary() != null;
+ result = dataSource.getDictionary().getMaxVal();
+ break;
+ case MINMAXRANGE:
+ case MINMAXRANGEMV:
+ result = new MinMaxRangePair(getMinValueNumeric(dataSource),
getMaxValueNumeric(dataSource));
+ break;
+ case DISTINCTCOUNT:
+ case DISTINCTSUM:
+ case DISTINCTAVG:
+ case DISTINCTCOUNTMV:
+ case DISTINCTSUMMV:
+ case DISTINCTAVGMV:
+ result =
getDistinctValueSet(Objects.requireNonNull(dataSource.getDictionary()),
explainPlanName);
+ break;
+ case DISTINCTCOUNTOFFHEAP:
+ result = ((DistinctCountOffHeapAggregationFunction)
aggregationFunction).extractAggregationResult(
+ Objects.requireNonNull(dataSource.getDictionary()));
+ break;
+ case DISTINCTCOUNTHLL:
+ case DISTINCTCOUNTHLLMV:
+ result =
getDistinctCountHLLResult(Objects.requireNonNull(dataSource.getDictionary()),
+ (DistinctCountHLLAggregationFunction) aggregationFunction,
explainPlanName);
+ break;
+ case DISTINCTCOUNTRAWHLL:
+ case DISTINCTCOUNTRAWHLLMV:
+ result =
getDistinctCountHLLResult(Objects.requireNonNull(dataSource.getDictionary()),
+ ((DistinctCountRawHLLAggregationFunction)
aggregationFunction).getDistinctCountHLLAggregationFunction(),
+ explainPlanName);
+ break;
+ case DISTINCTCOUNTHLLPLUS:
+ case DISTINCTCOUNTHLLPLUSMV:
+ result =
getDistinctCountHLLPlusResult(Objects.requireNonNull(dataSource.getDictionary()),
+ (DistinctCountHLLPlusAggregationFunction) aggregationFunction,
explainPlanName);
+ break;
+ case DISTINCTCOUNTRAWHLLPLUS:
+ case DISTINCTCOUNTRAWHLLPLUSMV:
+ result =
getDistinctCountHLLPlusResult(Objects.requireNonNull(dataSource.getDictionary()),
+ ((DistinctCountRawHLLPlusAggregationFunction) aggregationFunction)
+ .getDistinctCountHLLPlusAggregationFunction(),
explainPlanName);
+ break;
+ case SEGMENTPARTITIONEDDISTINCTCOUNT:
+ result = (long)
Objects.requireNonNull(dataSource.getDictionary()).length();
+ break;
+ case DISTINCTCOUNTSMARTHLL:
+ result =
getDistinctCountSmartHLLResult(Objects.requireNonNull(dataSource.getDictionary()),
+ (DistinctCountSmartHLLAggregationFunction) aggregationFunction,
explainPlanName);
+ break;
+ case DISTINCTCOUNTSMARTHLLPLUS:
+ result =
getDistinctCountSmartHLLPlusResult(Objects.requireNonNull(dataSource.getDictionary()),
+ (DistinctCountSmartHLLPlusAggregationFunction)
aggregationFunction, explainPlanName);
+ break;
+ case DISTINCTCOUNTULL:
+ result =
getDistinctCountULLResult(Objects.requireNonNull(dataSource.getDictionary()),
+ (DistinctCountULLAggregationFunction) aggregationFunction,
explainPlanName);
+ break;
+ case DISTINCTCOUNTSMARTULL:
+ result =
getDistinctCountSmartULLResult(Objects.requireNonNull(dataSource.getDictionary()),
+ (DistinctCountSmartULLAggregationFunction) aggregationFunction,
explainPlanName);
+ break;
+ case DISTINCTCOUNTRAWULL:
+ result =
getDistinctCountULLResult(Objects.requireNonNull(dataSource.getDictionary()),
+ (DistinctCountULLAggregationFunction) aggregationFunction,
explainPlanName);
+ break;
+ default:
+ throw new IllegalStateException(
+ "Non-scan based aggregation operator does not support function
type: " + functionType);
+ }
+
+ return result;
+ }
+
+ private static Double getMinValueNumeric(DataSource dataSource) {
+ Dictionary dictionary = dataSource.getDictionary();
+ if (dictionary != null) {
+ return toDouble(dictionary.getMinVal());
+ }
+ return toDouble(dataSource.getDataSourceMetadata().getMinValue());
+ }
+
+ private static Long getMinValueLong(DataSource dataSource) {
+ FieldSpec.DataType dataType =
dataSource.getDataSourceMetadata().getDataType().getStoredType();
+ Preconditions.checkArgument(
+ dataType == FieldSpec.DataType.LONG || dataType ==
FieldSpec.DataType.INT,
+ "MINLONG aggregation function can only be applied to columns of
integer types");
+ Dictionary dictionary = dataSource.getDictionary();
+ if (dictionary != null) {
+ return ((Number) dictionary.getMinVal()).longValue();
+ }
+ return ((Number)
dataSource.getDataSourceMetadata().getMinValue()).longValue();
+ }
+
+ private static Double getMaxValueNumeric(DataSource dataSource) {
+ Dictionary dictionary = dataSource.getDictionary();
+ if (dictionary != null) {
+ return toDouble(dictionary.getMaxVal());
+ }
+ return toDouble(dataSource.getDataSourceMetadata().getMaxValue());
+ }
+
+ private static Long getMaxValueLong(DataSource dataSource) {
+ FieldSpec.DataType dataType =
dataSource.getDataSourceMetadata().getDataType().getStoredType();
+ Preconditions.checkArgument(
+ dataType == FieldSpec.DataType.LONG || dataType ==
FieldSpec.DataType.INT,
+ "MAXLONG aggregation function can only be applied to columns of
integer types");
+ Dictionary dictionary = dataSource.getDictionary();
+ if (dictionary != null) {
+ return ((Number) dictionary.getMaxVal()).longValue();
+ }
+ return ((Number)
dataSource.getDataSourceMetadata().getMaxValue()).longValue();
+ }
+
+ private static Double toDouble(Comparable<?> value) {
+ if (value instanceof Double) {
+ return (Double) value;
+ } else if (value instanceof Number) {
+ return ((Number) value).doubleValue();
+ } else {
+ return Double.parseDouble(value.toString());
+ }
+ }
+
+ private static Set getDistinctValueSet(Dictionary dictionary, String
explainPlanName) {
+ int dictionarySize = dictionary.length();
+ switch (dictionary.getValueType()) {
+ case INT:
+ IntOpenHashSet intSet = new IntOpenHashSet(dictionarySize);
+ for (int dictId = 0; dictId < dictionarySize; dictId++) {
+
QueryThreadContext.checkTerminationAndSampleUsagePeriodically(dictId,
explainPlanName);
+ intSet.add(dictionary.getIntValue(dictId));
+ }
+ return intSet;
+ case LONG:
+ LongOpenHashSet longSet = new LongOpenHashSet(dictionarySize);
+ for (int dictId = 0; dictId < dictionarySize; dictId++) {
+
QueryThreadContext.checkTerminationAndSampleUsagePeriodically(dictId,
explainPlanName);
+ longSet.add(dictionary.getLongValue(dictId));
+ }
+ return longSet;
+ case FLOAT:
+ FloatOpenHashSet floatSet = new FloatOpenHashSet(dictionarySize);
+ for (int dictId = 0; dictId < dictionarySize; dictId++) {
+
QueryThreadContext.checkTerminationAndSampleUsagePeriodically(dictId,
explainPlanName);
+ floatSet.add(dictionary.getFloatValue(dictId));
+ }
+ return floatSet;
+ case DOUBLE:
+ DoubleOpenHashSet doubleSet = new DoubleOpenHashSet(dictionarySize);
+ for (int dictId = 0; dictId < dictionarySize; dictId++) {
+
QueryThreadContext.checkTerminationAndSampleUsagePeriodically(dictId,
explainPlanName);
+ doubleSet.add(dictionary.getDoubleValue(dictId));
+ }
+ return doubleSet;
+ case BIG_DECIMAL:
+ ObjectOpenHashSet<BigDecimal> bigDecimalSet = new
ObjectOpenHashSet<>(dictionarySize);
+ for (int dictId = 0; dictId < dictionarySize; dictId++) {
+
QueryThreadContext.checkTerminationAndSampleUsagePeriodically(dictId,
explainPlanName);
+ bigDecimalSet.add(dictionary.getBigDecimalValue(dictId));
+ }
+ return bigDecimalSet;
+ case STRING:
+ ObjectOpenHashSet<String> stringSet = new
ObjectOpenHashSet<>(dictionarySize);
+ for (int dictId = 0; dictId < dictionarySize; dictId++) {
+
QueryThreadContext.checkTerminationAndSampleUsagePeriodically(dictId,
explainPlanName);
+ stringSet.add(dictionary.getStringValue(dictId));
+ }
+ return stringSet;
+ case BYTES:
+ ObjectOpenHashSet<ByteArray> bytesSet = new
ObjectOpenHashSet<>(dictionarySize);
+ for (int dictId = 0; dictId < dictionarySize; dictId++) {
+
QueryThreadContext.checkTerminationAndSampleUsagePeriodically(dictId,
explainPlanName);
+ bytesSet.add(new ByteArray(dictionary.getBytesValue(dictId)));
+ }
+ return bytesSet;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ private static HyperLogLog getDistinctValueHLL(Dictionary dictionary, int
log2m, String explainPlanName) {
+ HyperLogLog hll = new HyperLogLog(log2m);
+ int length = dictionary.length();
+ for (int i = 0; i < length; i++) {
+ QueryThreadContext.checkTerminationAndSampleUsagePeriodically(i,
explainPlanName);
+ hll.offer(dictionary.get(i));
+ }
+ return hll;
+ }
+
+ private static UltraLogLog getDistinctValueULL(Dictionary dictionary, int p,
String explainPlanName) {
+ UltraLogLog ull = UltraLogLog.create(p);
+ int length = dictionary.length();
+ for (int i = 0; i < length; i++) {
+ QueryThreadContext.checkTerminationAndSampleUsagePeriodically(i,
explainPlanName);
+ Object value = dictionary.get(i);
+ UltraLogLogUtils.hashObject(value).ifPresent(ull::add);
+ }
+ return ull;
+ }
+
+ private static HyperLogLogPlus getDistinctValueHLLPlus(Dictionary
dictionary, int p, int sp, String explainPlanName) {
+ HyperLogLogPlus hllPlus = new HyperLogLogPlus(p, sp);
+ int length = dictionary.length();
+ for (int i = 0; i < length; i++) {
+ QueryThreadContext.checkTerminationAndSampleUsagePeriodically(i,
explainPlanName);
+ hllPlus.offer(dictionary.get(i));
+ }
+ return hllPlus;
+ }
+
+ private static HyperLogLog getDistinctCountHLLResult(Dictionary dictionary,
+ DistinctCountHLLAggregationFunction function, String explainPlanName) {
+ if (dictionary.getValueType() == FieldSpec.DataType.BYTES) {
+ // Treat BYTES value as serialized HyperLogLog
+ try {
+ QueryThreadContext.checkTerminationAndSampleUsage(explainPlanName);
+ HyperLogLog hll =
ObjectSerDeUtils.HYPER_LOG_LOG_SER_DE.deserialize(dictionary.getBytesValue(0));
+ int length = dictionary.length();
+ for (int i = 1; i < length; i++) {
+ QueryThreadContext.checkTerminationAndSampleUsagePeriodically(i,
explainPlanName);
+
hll.addAll(ObjectSerDeUtils.HYPER_LOG_LOG_SER_DE.deserialize(dictionary.getBytesValue(i)));
+ }
+ return hll;
+ } catch (Exception e) {
+ throw new RuntimeException("Caught exception while merging
HyperLogLogs", e);
+ }
+ } else {
+ return getDistinctValueHLL(dictionary, function.getLog2m(),
explainPlanName);
+ }
+ }
+
+ private static HyperLogLogPlus getDistinctCountHLLPlusResult(Dictionary
dictionary,
+ DistinctCountHLLPlusAggregationFunction function, String
explainPlanName) {
+ if (dictionary.getValueType() == FieldSpec.DataType.BYTES) {
+ // Treat BYTES value as serialized HyperLogLogPlus
+ try {
+ QueryThreadContext.checkTerminationAndSampleUsage(explainPlanName);
+ HyperLogLogPlus hllplus =
ObjectSerDeUtils.HYPER_LOG_LOG_PLUS_SER_DE.deserialize(dictionary.getBytesValue(0));
+ int length = dictionary.length();
+ for (int i = 1; i < length; i++) {
+ QueryThreadContext.checkTerminationAndSampleUsagePeriodically(i,
explainPlanName);
+
hllplus.addAll(ObjectSerDeUtils.HYPER_LOG_LOG_PLUS_SER_DE.deserialize(dictionary.getBytesValue(i)));
+ }
+ return hllplus;
+ } catch (Exception e) {
+ throw new RuntimeException("Caught exception while merging
HyperLogLogPluses", e);
+ }
+ } else {
+ return getDistinctValueHLLPlus(dictionary, function.getP(),
function.getSp(), explainPlanName);
+ }
+ }
+
+ private static Object getDistinctCountSmartHLLResult(Dictionary dictionary,
+ DistinctCountSmartHLLAggregationFunction function, String
explainPlanName) {
+ if (dictionary.length() > function.getThreshold()) {
+ // Store values into a HLL when the dictionary size exceeds the
conversion threshold
+ return getDistinctValueHLL(dictionary, function.getLog2m(),
explainPlanName);
+ } else {
+ return getDistinctValueSet(dictionary, explainPlanName);
+ }
+ }
+
+ private static Object getDistinctCountSmartHLLPlusResult(Dictionary
dictionary,
+ DistinctCountSmartHLLPlusAggregationFunction function, String
explainPlanName) {
+ if (dictionary.length() > function.getThreshold()) {
+ // Store values into a HLLPlus when the dictionary size exceeds the
conversion threshold
+ return getDistinctValueHLLPlus(dictionary, function.getP(),
function.getSp(), explainPlanName);
+ } else {
+ return getDistinctValueSet(dictionary, explainPlanName);
+ }
+ }
+
+ private static UltraLogLog getDistinctCountULLResult(Dictionary dictionary,
+ DistinctCountULLAggregationFunction function, String explainPlanName) {
+ if (dictionary.getValueType() == FieldSpec.DataType.BYTES) {
+ // Treat BYTES value as serialized UltraLogLog and merge
+ try {
+ QueryThreadContext.checkTerminationAndSampleUsage(explainPlanName);
+ UltraLogLog ull =
ObjectSerDeUtils.ULTRA_LOG_LOG_OBJECT_SER_DE.deserialize(dictionary.getBytesValue(0));
+ int length = dictionary.length();
+ for (int i = 1; i < length; i++) {
+ QueryThreadContext.checkTerminationAndSampleUsagePeriodically(i,
explainPlanName);
+
ull.add(ObjectSerDeUtils.ULTRA_LOG_LOG_OBJECT_SER_DE.deserialize(dictionary.getBytesValue(i)));
+ }
+ return ull;
+ } catch (Exception e) {
+ throw new RuntimeException("Caught exception while merging
UltraLogLogs", e);
+ }
+ } else {
+ return getDistinctValueULL(dictionary, function.getP(), explainPlanName);
+ }
+ }
+
+ private static Object getDistinctCountSmartULLResult(Dictionary dictionary,
+ DistinctCountSmartULLAggregationFunction function, String
explainPlanName) {
+ if (dictionary.length() > function.getThreshold()) {
+ return getDistinctValueULL(dictionary, function.getP(), explainPlanName);
+ } else {
+ return getDistinctValueSet(dictionary, explainPlanName);
+ }
+ }
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/startree/executor/StarTreeAggregationExecutor.java
b/pinot-core/src/main/java/org/apache/pinot/core/startree/executor/StarTreeAggregationExecutor.java
index f9a032dfe77..0874619f5b5 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/startree/executor/StarTreeAggregationExecutor.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/startree/executor/StarTreeAggregationExecutor.java
@@ -33,7 +33,10 @@ import
org.apache.pinot.segment.spi.index.startree.AggregationFunctionColumnPair
public class StarTreeAggregationExecutor extends DefaultAggregationExecutor {
private final AggregationFunctionColumnPair[]
_aggregationFunctionColumnPairs;
+
public StarTreeAggregationExecutor(AggregationFunction[]
aggregationFunctions) {
+ // StarTreeAggregationExecutor doesn't support pre-aggregated results.
+ // So, we don't need to pass pre-aggregated results to the super class.
super(aggregationFunctions);
int numAggregationFunctions = aggregationFunctions.length;
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/plan/maker/MetadataAndDictionaryAggregationPlanMakerTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/plan/maker/MetadataAndDictionaryAggregationPlanMakerTest.java
index 769bd57b9d6..8cd0278f734 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/plan/maker/MetadataAndDictionaryAggregationPlanMakerTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/plan/maker/MetadataAndDictionaryAggregationPlanMakerTest.java
@@ -22,10 +22,14 @@ import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.FileUtils;
import org.apache.pinot.common.metrics.ServerMetrics;
import org.apache.pinot.core.common.Operator;
+import org.apache.pinot.core.operator.BaseProjectOperator;
+import org.apache.pinot.core.operator.ExecutionStatistics;
+import org.apache.pinot.core.operator.blocks.results.AggregationResultsBlock;
import org.apache.pinot.core.operator.query.AggregationOperator;
import org.apache.pinot.core.operator.query.FastFilteredCountOperator;
import org.apache.pinot.core.operator.query.GroupByOperator;
@@ -37,6 +41,7 @@ import
org.apache.pinot.segment.local.data.manager.TableDataManager;
import
org.apache.pinot.segment.local.indexsegment.immutable.ImmutableSegmentImpl;
import
org.apache.pinot.segment.local.indexsegment.immutable.ImmutableSegmentLoader;
import
org.apache.pinot.segment.local.segment.creator.impl.SegmentIndexCreationDriverImpl;
+import org.apache.pinot.segment.local.segment.readers.GenericRowRecordReader;
import
org.apache.pinot.segment.local.upsert.ConcurrentMapPartitionUpsertMetadataManager;
import org.apache.pinot.segment.local.upsert.UpsertContext;
import org.apache.pinot.segment.local.upsert.UpsertUtils;
@@ -44,7 +49,9 @@ import org.apache.pinot.segment.spi.IndexSegment;
import org.apache.pinot.segment.spi.SegmentContext;
import org.apache.pinot.segment.spi.creator.SegmentGeneratorConfig;
import org.apache.pinot.segment.spi.creator.SegmentIndexCreationDriver;
+import org.apache.pinot.segment.spi.datasource.DataSource;
import
org.apache.pinot.segment.spi.index.mutable.ThreadSafeMutableRoaringBitmap;
+import org.apache.pinot.segment.spi.index.reader.Dictionary;
import org.apache.pinot.spi.config.table.TableConfig;
import org.apache.pinot.spi.config.table.TableType;
import org.apache.pinot.spi.config.table.ingestion.IngestionConfig;
@@ -52,6 +59,8 @@ import org.apache.pinot.spi.data.FieldSpec;
import org.apache.pinot.spi.data.FieldSpec.DataType;
import org.apache.pinot.spi.data.Schema;
import org.apache.pinot.spi.data.TimeGranularitySpec;
+import org.apache.pinot.spi.data.readers.GenericRow;
+import org.apache.pinot.spi.exception.BadQueryRequestException;
import org.apache.pinot.spi.utils.ReadMode;
import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
import org.testng.annotations.AfterClass;
@@ -61,21 +70,34 @@ import org.testng.annotations.BeforeTest;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
+import static org.mockito.AdditionalAnswers.delegatesTo;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.expectThrows;
public class MetadataAndDictionaryAggregationPlanMakerTest {
private static final String AVRO_DATA = "data" + File.separator +
"test_data-sv.avro";
private static final String SEGMENT_NAME = "testTable_201711219_20171120";
+ // A small segment with fully predictable data so aggregation results can be
asserted exactly. It holds the 10 rows
+ // metricCol = i, intCol = i + 10, bytesCol = new byte[]{(byte) i} for i =
1..10, with every row duplicated (20 rows
+ // total).
+ private static final String PREDICTABLE_TABLE_NAME = "predictableTable";
+ private static final String PREDICTABLE_SEGMENT_NAME =
"predictableTable_segment";
private static final File INDEX_DIR =
new File(FileUtils.getTempDirectory(),
"MetadataAndDictionaryAggregationPlanMakerTest");
private static final InstancePlanMakerImplV2 PLAN_MAKER = new
InstancePlanMakerImplV2();
private IndexSegment _indexSegment;
private IndexSegment _upsertIndexSegment;
+ private IndexSegment _predictableSegment;
@BeforeTest
public void buildSegment()
@@ -122,6 +144,43 @@ public class MetadataAndDictionaryAggregationPlanMakerTest
{
SegmentIndexCreationDriver driver = new SegmentIndexCreationDriverImpl();
driver.init(segmentGeneratorConfig);
driver.build();
+
+ buildPredictableSegment();
+ }
+
+ /// Builds a segment with fully predictable data so that aggregation results
can be asserted exactly. It contains the
+ /// 10 rows where row {@code i} (for {@code i = 1..10}) has {@code metricCol
= i}, {@code intCol = i + 10} and
+ /// {@code bytesCol = new byte[]{(byte) i}}, and every row is duplicated (20
rows total).
+ private void buildPredictableSegment()
+ throws Exception {
+ Schema schema = new
Schema.SchemaBuilder().setSchemaName(PREDICTABLE_TABLE_NAME)
+ .addMetric("metricCol", FieldSpec.DataType.INT)
+ .addMetric("intCol", FieldSpec.DataType.INT)
+ .addSingleValueDimension("bytesCol", FieldSpec.DataType.BYTES)
+ .build();
+ TableConfig tableConfig =
+ new
TableConfigBuilder(TableType.OFFLINE).setTableName(PREDICTABLE_TABLE_NAME).build();
+
+ List<GenericRow> records = new ArrayList<>(20);
+ // Add each row twice.
+ for (int n = 0; n < 2; n++) {
+ for (int i = 1; i <= 10; i++) {
+ GenericRow record = new GenericRow();
+ record.putValue("metricCol", i);
+ record.putValue("intCol", i + 10);
+ record.putValue("bytesCol", new byte[]{(byte) i});
+ records.add(record);
+ }
+ }
+
+ SegmentGeneratorConfig segmentGeneratorConfig = new
SegmentGeneratorConfig(tableConfig, schema);
+ segmentGeneratorConfig.setTableName(PREDICTABLE_TABLE_NAME);
+ segmentGeneratorConfig.setSegmentName(PREDICTABLE_SEGMENT_NAME);
+ segmentGeneratorConfig.setOutDir(INDEX_DIR.getAbsolutePath());
+
+ SegmentIndexCreationDriverImpl driver = new
SegmentIndexCreationDriverImpl();
+ driver.init(segmentGeneratorConfig, new GenericRowRecordReader(records));
+ driver.build();
}
@BeforeClass
@@ -130,6 +189,7 @@ public class MetadataAndDictionaryAggregationPlanMakerTest {
ServerMetrics.register(mock(ServerMetrics.class));
_indexSegment = ImmutableSegmentLoader.load(new File(INDEX_DIR,
SEGMENT_NAME), ReadMode.heap);
_upsertIndexSegment = ImmutableSegmentLoader.load(new File(INDEX_DIR,
SEGMENT_NAME), ReadMode.heap);
+ _predictableSegment = ImmutableSegmentLoader.load(new File(INDEX_DIR,
PREDICTABLE_SEGMENT_NAME), ReadMode.heap);
TableDataManager tableDataManager = mock(TableDataManager.class);
when(tableDataManager.getTableDataDir()).thenReturn(INDEX_DIR);
UpsertContext upsertContext = new UpsertContext.Builder()
@@ -150,6 +210,7 @@ public class MetadataAndDictionaryAggregationPlanMakerTest {
_indexSegment.destroy();
_upsertIndexSegment.offload();
_upsertIndexSegment.destroy();
+ _predictableSegment.destroy();
}
@AfterTest
@@ -170,6 +231,211 @@ public class
MetadataAndDictionaryAggregationPlanMakerTest {
assertTrue(upsertOperatorClass.isInstance(upsertOperator));
}
+ /// Verifies the partial metadata-based aggregation path. When a query mixes
a metadata-eligible function (MAX) with a
+ /// non-eligible one (SUM), the plan uses an {@link AggregationOperator}
that pre-aggregates the eligible function
+ /// from metadata while scanning the rest. Before this feature, a non-scan
based operator was used only when _all_
+ /// functions were metadata eligible; a mixed query would have scanned every
function.
+ ///
+ /// To prove the eligible function is actually served from metadata (and not
scanned), the column dictionary is
+ /// overridden to report a bogus max value that does not exist in the data.
MAX equals the bogus value only if the
+ /// metadata path is taken (a scan would return the true max of 10), while
SUM equals the true scanned sum of
+ /// {@code metricCol} ((1 + 2 + ... + 10) over the two row copies = 110).
+ @Test
+ public void testPartialMetadataBasedAggregationServesEligibleFromMetadata() {
+ int bogusMax = 999_999_999;
+ QueryContext queryContext = QueryContextConverterUtils.getQueryContext(
+ "select max(metricCol), sum(metricCol) from " +
PREDICTABLE_TABLE_NAME);
+
+ // Override only metricCol's dictionary max value; everything else
delegates to the real segment.
+ DataSource realDataSource = _predictableSegment.getDataSource("metricCol",
queryContext.getSchema());
+ Dictionary dictionaryWithBogusMax = mock(Dictionary.class,
delegatesTo(realDataSource.getDictionary()));
+ doReturn(bogusMax).when(dictionaryWithBogusMax).getMaxVal();
+ DataSource dataSourceWithBogusMax = mock(DataSource.class,
delegatesTo(realDataSource));
+
doReturn(dictionaryWithBogusMax).when(dataSourceWithBogusMax).getDictionary();
+ IndexSegment segment = mock(IndexSegment.class,
delegatesTo(_predictableSegment));
+
doReturn(dataSourceWithBogusMax).when(segment).getDataSource(eq("metricCol"),
any());
+
+ Operator<?> operator = PLAN_MAKER.makeSegmentPlanNode(new
SegmentContext(segment), queryContext).run();
+ // A mixed query must use the (partial) AggregationOperator, not the fully
non-scan based operator.
+ assertTrue(operator instanceof AggregationOperator);
+
+ AggregationResultsBlock resultsBlock = (AggregationResultsBlock)
operator.nextBlock();
+ List<Object> results = resultsBlock.getResults();
+ assertNotNull(results);
+ // MAX is served from the (overridden) dictionary metadata, so the bogus
value proves the metadata path was used.
+ assertEquals(((Number) results.get(0)).doubleValue(), (double) bogusMax);
+ // SUM is scanned, so the result is the true sum of metricCol: (1 + 2 +
... + 10) over the two row copies = 110.
+ assertEquals(((Number) results.get(1)).doubleValue(), 110.0);
+ }
+
+ /// {@code distinctcount} over a dictionary-encoded column is resolved
entirely from the dictionary (its cardinality
+ /// is the number of distinct values), so it must plan to the fully non-scan
{@link NonScanBasedAggregationOperator}
+ /// without a mock. {@code metricCol} holds the 10 distinct values 1..10
across 20 (duplicated) rows, so the distinct
+ /// count is 10 even though the segment has 20 docs.
+ @Test
+ public void testDistinctCountResolvedFromDictionary() {
+ // Sanity check that the fixture actually has duplicate rows, so distinct
count < total docs is a meaningful result.
+ assertEquals(_predictableSegment.getSegmentMetadata().getTotalDocs(), 20);
+
+ QueryContext queryContext =
+ QueryContextConverterUtils.getQueryContext("select
distinctcount(metricCol) from " + PREDICTABLE_TABLE_NAME);
+
+ Operator<?> operator =
+ PLAN_MAKER.makeSegmentPlanNode(new
SegmentContext(_predictableSegment), queryContext).run();
+ // The column is dictionary-encoded, so DISTINCTCOUNT is resolved from the
dictionary without scanning the segment.
+ assertTrue(operator instanceof NonScanBasedAggregationOperator);
+
+ AggregationResultsBlock resultsBlock = (AggregationResultsBlock)
operator.nextBlock();
+ List<Object> results = resultsBlock.getResults();
+ assertNotNull(results);
+ // The intermediate result is the set of distinct dictionary values:
metricCol has 10 distinct values (1..10),
+ // fewer than the 20 total docs, proving DISTINCTCOUNT counts distinct
values rather than rows.
+ assertEquals(((Set<?>) results.get(0)).size(), 10);
+ }
+
+ /// Tests for aggregations that cannot be resolved from dictionary/metadata
and must therefore fall back to
+ /// the scan-based {@link AggregationOperator}, still returning the correct
scanned result:
+ ///
+ /// - a single non-resolvable aggregation with no filter ({@code
sum(metricCol)}): the partial metadata path
+ /// evaluates eligibility per function, and when none is resolvable it
must fall back to a full scan rather than
+ /// emitting a (partial) non-scan operator with zero resolved functions;
+ /// - an aggregation over an expression argument ({@code max(add(metricCol,
intCol))}): the argument is not a plain
+ /// column reference, so it cannot be resolved from dictionary/metadata.
+ @Test(dataProvider = "nonResolvableScanQueries")
+ public void testNonResolvableAggregationFallsToScan(String description,
String query, double expectedResult) {
+ QueryContext queryContext =
QueryContextConverterUtils.getQueryContext(query);
+
+ Operator<?> operator =
+ PLAN_MAKER.makeSegmentPlanNode(new
SegmentContext(_predictableSegment), queryContext).run();
+ assertTrue(operator instanceof AggregationOperator, description);
+
+ AggregationResultsBlock resultsBlock = (AggregationResultsBlock)
operator.nextBlock();
+ List<Object> results = resultsBlock.getResults();
+ assertNotNull(results, description);
+ assertEquals(((Number) results.get(0)).doubleValue(), expectedResult,
description);
+ }
+
+ @DataProvider(name = "nonResolvableScanQueries")
+ public Object[][] nonResolvableScanQueries() {
+ return new Object[][]{
+ // SUM is not resolvable from metadata and is scanned: (1 + 2 + ... +
10) over the two row copies = 110.
+ {"single non-resolvable aggregation", "select sum(metricCol) from " +
PREDICTABLE_TABLE_NAME, 110.0},
+ // add(metricCol, intCol) = i + (i + 10) is an expression argument, so
it is scanned; max over i = 1..10 is 30.
+ {"aggregation over expression argument",
+ "select max(add(metricCol, intCol)) from " +
PREDICTABLE_TABLE_NAME, 30.0}
+ };
+ }
+
+ /// MIN/MAX derive their result numerically from the column min/max, which
is only valid for numeric columns.
+ /// For a non-numeric (BYTES) column the dictionary stores raw values that
cannot be
+ /// parsed as numbers, so {@code max(bytesCol)} and {@code min(bytesCol)}
must fall back to the scan-based
+ /// {@link AggregationOperator} rather than being (wrongly) resolved from
the dictionary by a
+ /// {@link NonScanBasedAggregationOperator}. The scan path in turn throws a
+ /// {@link BadQueryRequestException} for non-numeric aggregation.
+ @Test
+ public void testMinMaxOnNonNumericColumnFallsToScan() {
+
assertNotNull(_predictableSegment.getDataSourceNullable("bytesCol").getDictionary());
+
+ for (String function : List.of("max", "min")) {
+ String query = "select " + function + "(bytesCol) from " +
PREDICTABLE_TABLE_NAME;
+ QueryContext queryContext =
QueryContextConverterUtils.getQueryContext(query);
+ Operator<?> operator =
+ PLAN_MAKER.makeSegmentPlanNode(new
SegmentContext(_predictableSegment), queryContext).run();
+ // MIN/MAX on a non-numeric column must fall back to the scan-based
AggregationOperator, not the metadata path.
+ assertTrue(operator instanceof AggregationOperator, query);
+ assertFalse(operator instanceof NonScanBasedAggregationOperator, query);
+
+ // Running the scan path proves it still executes and correctly rejects
the non-numeric aggregation rather than
+ // producing a wrong result.
+ BadQueryRequestException exception =
expectThrows(BadQueryRequestException.class, operator::nextBlock);
+ assertTrue(exception.getMessage().contains("Cannot compute " + function
+ " for non-numeric type: BYTES"),
+ exception.getMessage());
+ }
+ }
+
+ /// Verifies that a column aggregation resolved by metadata is excluded from
the scan projection,
+ /// so the partial path only reads the columns needed by the scanned
aggregations. The reduced projection is also
+ /// reflected in numEntriesScannedPostFilter (20 docs * 1 column instead of
40).
+ @Test
+ public void testResolvedOnlyColumnExcludedFromProjection() {
+ QueryContext queryContext = QueryContextConverterUtils.getQueryContext(
+ "select max(metricCol), sum(intCol) from " + PREDICTABLE_TABLE_NAME);
+ Operator<?> operator =
+ PLAN_MAKER.makeSegmentPlanNode(new
SegmentContext(_predictableSegment), queryContext).run();
+ assertTrue(operator instanceof AggregationOperator);
+
+ BaseProjectOperator<?> projectOperator = ((AggregationOperator)
operator).getChildOperators().get(0);
+ // Only intCol (used by the scanned sum) is projected; metricCol (used
solely by the resolved max) is excluded.
+ assertEquals(projectOperator.getNumColumnsProjected(), 1);
+
assertTrue(projectOperator.getSourceColumnContextMap().containsKey("intCol"));
+
assertFalse(projectOperator.getSourceColumnContextMap().containsKey("metricCol"));
+
+ AggregationResultsBlock resultsBlock = (AggregationResultsBlock)
operator.nextBlock();
+ List<Object> results = resultsBlock.getResults();
+ assertNotNull(results);
+ // max(metricCol) is resolved from the dictionary; sum(intCol) is scanned.
+ // intCol = i + 10 for i = 1..10 over the two row copies, so sum(intCol) =
2 * (11 + 12 + ... + 20) = 310.
+ assertEquals(((Number) results.get(0)).doubleValue(), 10.0);
+ assertEquals(((Number) results.get(1)).doubleValue(), 310.0);
+
+ // All 20 docs are still scanned for the unresolved sum(intCol).
+ assertEquals(operator.getExecutionStatistics().getNumDocsScanned(), 20);
+ // With metricCol excluded, only 1 column is projected, so entries scanned
post-filter is 20 docs * 1 column = 20
+ // (it would be 40 if the resolved-only metricCol were still projected).
+
assertEquals(operator.getExecutionStatistics().getNumEntriesScannedPostFilter(),
20);
+ }
+
+ /// Verifies numDocsScanned and numEntriesScannedPostFilter across the three
routing paths: partial metadata
+ /// resolution, fully metadata-resolvable (non-scan), and full scan over
multiple aggregations.
+ @Test(dataProvider = "scanCostStatistics")
+ public void testScanCostStatistics(String description, String query,
Class<?> expectedOperatorClass,
+ long expectedNumDocsScanned, long expectedNumEntriesScannedPostFilter) {
+ QueryContext queryContext =
QueryContextConverterUtils.getQueryContext(query);
+ Operator<?> operator =
+ PLAN_MAKER.makeSegmentPlanNode(new
SegmentContext(_predictableSegment), queryContext).run();
+ assertTrue(expectedOperatorClass.isInstance(operator), description);
+ // Drain the blocks so scan operators accumulate their document counts
(harmless for the non-scan operator, whose
+ // statistics are constant).
+ operator.nextBlock();
+ ExecutionStatistics stats = operator.getExecutionStatistics();
+ assertEquals(stats.getNumDocsScanned(), expectedNumDocsScanned,
description);
+ assertEquals(stats.getNumEntriesScannedPostFilter(),
expectedNumEntriesScannedPostFilter, description);
+ }
+
+ @DataProvider(name = "scanCostStatistics")
+ public Object[][] scanCostStatistics() {
+ return new Object[][]{
+ // Partial metadata resolution: max(metricCol) is resolved from the
dictionary and only sum(intCol) is
+ // scanned, so a single column is projected -> 20 docs * 1 column = 20
entries scanned post-filter.
+ {
+ "partial metadata resolution",
+ "select max(metricCol), sum(intCol) from " +
PREDICTABLE_TABLE_NAME,
+ AggregationOperator.class,
+ 20L,
+ 20L
+ },
+ // Fully metadata-resolvable: max + min on the numeric dictionary
column are both resolved, so the non-scan
+ // operator is used and nothing is scanned post-filter. numDocsScanned
is reported as numTotalDocs (20) for
+ // backward compatibility even though no rows are actually scanned.
+ {
+ "fully metadata-resolvable",
+ "select max(metricCol), min(metricCol) from " +
PREDICTABLE_TABLE_NAME,
+ NonScanBasedAggregationOperator.class,
+ 20L,
+ 0L
+ },
+ // Full scan over multiple aggregations: neither sum is resolvable, so
both columns are scanned for all 20
+ // docs -> 20 docs * 2 columns = 40 entries scanned post-filter.
+ {
+ "full scan multiple aggregations",
+ "select sum(metricCol), sum(intCol) from " +
PREDICTABLE_TABLE_NAME,
+ AggregationOperator.class,
+ 20L,
+ 40L
+ }
+ };
+ }
+
@DataProvider(name = "testPlanMakerDataProvider")
public Object[][] testPlanMakerDataProvider() {
List<Object[]> entries = new ArrayList<>();
@@ -223,6 +489,9 @@ public class MetadataAndDictionaryAggregationPlanMakerTest {
entries.add(new Object[]{
"select sum(column1) from testTable", AggregationOperator.class,
AggregationOperator.class
});
+ entries.add(new Object[]{
+ "select count(*), sum(column1) from testTable",
AggregationOperator.class, AggregationOperator.class
+ });
// Aggregation group-by
entries.add(new Object[]{
"select sum(column1) from testTable group by daysSinceEpoch",
GroupByOperator.class, GroupByOperator.class
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/DefaultAggregationExecutorTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/DefaultAggregationExecutorTest.java
index 776a16bd6f4..38feebf12f9 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/DefaultAggregationExecutorTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/DefaultAggregationExecutorTest.java
@@ -114,6 +114,62 @@ public class DefaultAggregationExecutorTest {
/// Asserts that the aggregation results returned by the executor are as
expected.
@Test
void testAggregation() {
+ TransformBlock transformBlock = nextTransformBlock();
+ AggregationFunction[] aggregationFunctions =
_queryContext.getAggregationFunctions();
+ assert aggregationFunctions != null;
+ AggregationExecutor aggregationExecutor = new
DefaultAggregationExecutor(aggregationFunctions);
+ aggregationExecutor.aggregate(transformBlock);
+ List<Object> result = aggregationExecutor.getResult();
+ for (int i = 0; i < result.size(); i++) {
+ double actual = (double) result.get(i);
+ double expected = computeAggregation(AGGREGATION_FUNCTIONS[i],
_inputData[i]);
+ Assert.assertEquals(actual, expected,
+ "Aggregation mis-match for function " + AGGREGATION_FUNCTIONS[i] +
", Expected: " + expected + " Actual: "
+ + actual);
+ }
+ }
+
+ /// Verifies that functions with a non-scan result are not re-computed by
scanning: the injected value is
+ /// returned as-is, while functions with a {@code null} non-scan entry are
still computed from the scanned
+ /// block.
+ @Test
+ void testNonScanResultsSkipScan() {
+ TransformBlock transformBlock = nextTransformBlock();
+ AggregationFunction[] aggregationFunctions =
_queryContext.getAggregationFunctions();
+ assert aggregationFunctions != null;
+
+ // Resolve only the second function (index 1 -> MAX) without scanning; the
rest fall back to scan-based execution.
+ int nonScanIndex = 1;
+ Object[] nonScanResults = new Object[aggregationFunctions.length];
+ // inject a Double sentinel that cannot occur in the data (all values
+ // are non-negative), proving the injected value is returned untouched
rather than recomputed by scanning.
+ double injectedMax = -1.0;
+ nonScanResults[nonScanIndex] = injectedMax;
+
+ AggregationExecutor aggregationExecutor =
+ new DefaultAggregationExecutor(aggregationFunctions, nonScanResults);
+ aggregationExecutor.aggregate(transformBlock);
+ List<Object> result = aggregationExecutor.getResult();
+
+ // The non-scan function returns the injected value untouched (not a
scanned MAX).
+ Assert.assertEquals(result.get(nonScanIndex), injectedMax,
+ "Non-scan function should return the injected value, not a scanned
result");
+
+ // Remaining functions are still computed by scanning the segment.
+ for (int i = 0; i < result.size(); i++) {
+ if (i == nonScanIndex) {
+ continue;
+ }
+ double actual = (double) result.get(i);
+ double expected = computeAggregation(AGGREGATION_FUNCTIONS[i],
_inputData[i]);
+ Assert.assertEquals(actual, expected,
+ "Aggregation mis-match for function " + AGGREGATION_FUNCTIONS[i] +
", Expected: " + expected + " Actual: "
+ + actual);
+ }
+ }
+
+ /// Builds a transform block over all physical columns of the test segment
with a match-all filter.
+ private TransformBlock nextTransformBlock() {
Map<String, DataSource> dataSourceMap = new HashMap<>();
List<ExpressionContext> expressions = new ArrayList<>();
for (String column : _indexSegment.getPhysicalColumnNames()) {
@@ -127,19 +183,7 @@ public class DefaultAggregationExecutorTest {
ProjectionOperator projectionOperator =
new ProjectionOperator(dataSourceMap, docIdSetOperator, new
QueryContext.Builder().build());
TransformOperator transformOperator = new TransformOperator(_queryContext,
projectionOperator, expressions);
- TransformBlock transformBlock = transformOperator.nextBlock();
- AggregationFunction[] aggregationFunctions =
_queryContext.getAggregationFunctions();
- assert aggregationFunctions != null;
- AggregationExecutor aggregationExecutor = new
DefaultAggregationExecutor(aggregationFunctions);
- aggregationExecutor.aggregate(transformBlock);
- List<Object> result = aggregationExecutor.getResult();
- for (int i = 0; i < result.size(); i++) {
- double actual = (double) result.get(i);
- double expected = computeAggregation(AGGREGATION_FUNCTIONS[i],
_inputData[i]);
- Assert.assertEquals(actual, expected,
- "Aggregation mis-match for function " + AGGREGATION_FUNCTIONS[i] +
", Expected: " + expected + " Actual: "
- + actual);
- }
+ return transformOperator.nextBlock();
}
/// Helper method to setup the index segment on which to perform aggregation
tests.
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionUtilsTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionUtilsTest.java
new file mode 100644
index 00000000000..29041933af2
--- /dev/null
+++
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionUtilsTest.java
@@ -0,0 +1,84 @@
+/**
+ * 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.pinot.core.query.aggregation.function;
+
+import org.apache.pinot.segment.spi.AggregationFunctionType;
+import org.apache.pinot.segment.spi.datasource.DataSource;
+import org.apache.pinot.segment.spi.index.reader.Dictionary;
+import org.testng.annotations.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertThrows;
+
+
+/// Unit test for {@link AggregationFunctionUtils#getAggregationResult}, the
metadata/dictionary based aggregation
+/// result resolver used by the non-scan based and partial metadata based
aggregation paths.
+@SuppressWarnings("rawtypes")
+public class AggregationFunctionUtilsTest {
+
+ private static AggregationFunction mockFunction(AggregationFunctionType
type) {
+ AggregationFunction aggregationFunction = mock(AggregationFunction.class);
+ when(aggregationFunction.getType()).thenReturn(type);
+ return aggregationFunction;
+ }
+
+ @Test
+ public void testCountResolvedFromNumTotalDocs() {
+ AggregationFunction countFunction =
mockFunction(AggregationFunctionType.COUNT);
+ // COUNT is resolved directly from numTotalDocs and must not touch the
(possibly null) data source.
+ Object result =
AggregationFunctionUtils.getAggregationResult(countFunction, null, 42, "TEST");
+ assertEquals(result, 42L);
+ }
+
+ @Test
+ public void testMinAndMaxResolvedFromDictionary() {
+ Dictionary dictionary = mock(Dictionary.class);
+ when(dictionary.getMinVal()).thenReturn(5);
+ when(dictionary.getMaxVal()).thenReturn(10);
+ DataSource dataSource = mock(DataSource.class);
+ when(dataSource.getDictionary()).thenReturn(dictionary);
+
+ Object minResult =
AggregationFunctionUtils.getAggregationResult(mockFunction(AggregationFunctionType.MIN),
+ dataSource, 100, "TEST");
+ assertEquals(minResult, 5.0);
+
+ Object maxResult =
AggregationFunctionUtils.getAggregationResult(mockFunction(AggregationFunctionType.MAX),
+ dataSource, 100, "TEST");
+ assertEquals(maxResult, 10.0);
+ }
+
+ @Test
+ public void testUnsupportedFunctionThrows() {
+ // MODE cannot be resolved from dictionary/metadata; the resolver must
reject it rather than return a wrong result.
+ DataSource dataSource = mock(DataSource.class);
+ assertThrows(IllegalStateException.class,
+ () ->
AggregationFunctionUtils.getAggregationResult(mockFunction(AggregationFunctionType.MODE),
dataSource,
+ 100, "TEST"));
+ }
+
+ @Test
+ public void testNonCountWithNullDataSourceThrows() {
+ // Every non-COUNT function reads from the column dictionary/metadata and
therefore requires a non-null data source.
+ assertThrows(NullPointerException.class,
+ () ->
AggregationFunctionUtils.getAggregationResult(mockFunction(AggregationFunctionType.MIN),
null, 100,
+ "TEST"));
+ }
+}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/queries/ExplainPlanQueriesTest.java
b/pinot-core/src/test/java/org/apache/pinot/queries/ExplainPlanQueriesTest.java
index 94492d55310..b888456b12f 100644
---
a/pinot-core/src/test/java/org/apache/pinot/queries/ExplainPlanQueriesTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/queries/ExplainPlanQueriesTest.java
@@ -1811,7 +1811,7 @@ public class ExplainPlanQueriesTest extends
BaseQueriesTest {
});
result3.add(
new Object[]{"AGGREGATE(aggregations:count(*), max(noIndexCol1),
sum(noIndexCol2), avg(noIndexCol2))", 3, 2});
- result3.add(new Object[]{"PROJECT(noIndexCol2, noIndexCol1)", 4, 3});
+ result3.add(new Object[]{"PROJECT(noIndexCol2)", 4, 3});
result3.add(new Object[]{"DOC_ID_SET", 5, 4});
result3.add(new Object[]{"FILTER_MATCH_ENTIRE_SEGMENT(docs:3)", 6, 5});
check(query3, new ResultTable(DATA_SCHEMA, result3));
@@ -1924,7 +1924,7 @@ public class ExplainPlanQueriesTest extends
BaseQueriesTest {
});
result3.add(
new Object[]{"AGGREGATE(aggregations:count(*), max(noIndexCol1),
sum(noIndexCol2), avg(noIndexCol2))", 3, 2});
- result3.add(new Object[]{"PROJECT(noIndexCol2, noIndexCol1)", 4, 3});
+ result3.add(new Object[]{"PROJECT(noIndexCol2)", 4, 3});
result3.add(new Object[]{"DOC_ID_SET", 5, 4});
result3.add(new Object[]{"FILTER_MATCH_ENTIRE_SEGMENT(docs:3)", 6, 5});
check(query3, new ResultTable(DATA_SCHEMA, result3));
diff --git
a/pinot-core/src/test/java/org/apache/pinot/queries/InnerSegmentAggregationMultiValueQueriesTest.java
b/pinot-core/src/test/java/org/apache/pinot/queries/InnerSegmentAggregationMultiValueQueriesTest.java
index 354732a376e..169511132ad 100644
---
a/pinot-core/src/test/java/org/apache/pinot/queries/InnerSegmentAggregationMultiValueQueriesTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/queries/InnerSegmentAggregationMultiValueQueriesTest.java
@@ -47,7 +47,7 @@ public class InnerSegmentAggregationMultiValueQueriesTest
extends BaseMultiValue
AggregationOperator aggregationOperator = getOperator(AGGREGATION_QUERY);
AggregationResultsBlock resultsBlock = aggregationOperator.nextBlock();
QueriesTestUtils.testInnerSegmentExecutionStatistics(aggregationOperator.getExecutionStatistics(),
100000L, 0L,
- 400000L, 100000L);
+ 200000L, 100000L);
QueriesTestUtils.testInnerSegmentAggregationResult(resultsBlock.getResults(),
100000L, 100991525475000L, 2147434110,
1182655, 83439903673981L, 100000L);
diff --git
a/pinot-core/src/test/java/org/apache/pinot/queries/InnerSegmentAggregationMultiValueRawQueriesTest.java
b/pinot-core/src/test/java/org/apache/pinot/queries/InnerSegmentAggregationMultiValueRawQueriesTest.java
index 33543cad291..7e094a64b5b 100644
---
a/pinot-core/src/test/java/org/apache/pinot/queries/InnerSegmentAggregationMultiValueRawQueriesTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/queries/InnerSegmentAggregationMultiValueRawQueriesTest.java
@@ -47,7 +47,7 @@ public class InnerSegmentAggregationMultiValueRawQueriesTest
extends BaseMultiVa
AggregationOperator aggregationOperator = getOperator(AGGREGATION_QUERY);
AggregationResultsBlock resultsBlock = aggregationOperator.nextBlock();
QueriesTestUtils.testInnerSegmentExecutionStatistics(aggregationOperator.getExecutionStatistics(),
100000L, 0L,
- 400000L, 100000L);
+ 200000L, 100000L);
QueriesTestUtils.testInnerSegmentAggregationResult(resultsBlock.getResults(),
100000L, 100991525475000L, 2147434110,
1182655, 83439903673981L, 100000L);
diff --git
a/pinot-core/src/test/java/org/apache/pinot/queries/InnerSegmentAggregationSingleValueQueriesTest.java
b/pinot-core/src/test/java/org/apache/pinot/queries/InnerSegmentAggregationSingleValueQueriesTest.java
index dbf9565f67c..ca7c283d41a 100644
---
a/pinot-core/src/test/java/org/apache/pinot/queries/InnerSegmentAggregationSingleValueQueriesTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/queries/InnerSegmentAggregationSingleValueQueriesTest.java
@@ -46,7 +46,7 @@ public class InnerSegmentAggregationSingleValueQueriesTest
extends BaseSingleVal
AggregationOperator aggregationOperator = getOperator(AGGREGATION_QUERY);
AggregationResultsBlock resultsBlock = aggregationOperator.nextBlock();
QueriesTestUtils.testInnerSegmentExecutionStatistics(aggregationOperator.getExecutionStatistics(),
30000L, 0L,
- 120000L, 30000L);
+ 60000L, 30000L);
QueriesTestUtils.testInnerSegmentAggregationResult(resultsBlock.getResults(),
30000L, 32317185437847L, 2147419555,
1689277, 28175373944314L, 30000L);
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]