diff --git a/src/engine/Filter.cpp b/src/engine/Filter.cpp index 66821df13..0d9964562 100644 --- a/src/engine/Filter.cpp +++ b/src/engine/Filter.cpp @@ -61,7 +61,9 @@ ProtoResult Filter::computeResult(bool requestLaziness) { for (auto& [idTable, localVocab] : subRes->idTables()) { IdTable result = self->filterIdTable(subRes->sortedBy(), idTable, localVocab); - co_yield {std::move(result), std::move(localVocab)}; + if (!result.empty()) { + co_yield {std::move(result), std::move(localVocab)}; + } } }(std::move(subRes), this), resultSortedOn()}; diff --git a/src/engine/GroupBy.cpp b/src/engine/GroupBy.cpp index a2f52e9e6..77a7a520d 100644 --- a/src/engine/GroupBy.cpp +++ b/src/engine/GroupBy.cpp @@ -332,7 +332,7 @@ ProtoResult GroupBy::computeResult(bool requestLaziness) { if (useHashMapOptimization) { const auto* child = _subtree->getRootOperation()->getChildren().at(0); // Skip sorting - subresult = child->getResult(); + subresult = child->getResult(true); // Update runtime information auto runTimeInfoChildren = child->getRootOperation()->getRuntimeInfoPointer(); @@ -368,1221 +368,1260 @@ ProtoResult GroupBy::computeResult(bool requestLaziness) { } if (useHashMapOptimization) { - auto localVocab = subresult->getCopyOfLocalVocab(); - IdTable idTable = CALL_FIXED_SIZE( - groupByCols.size(), &GroupBy::computeGroupByForHashMapOptimization, - this, metadataForUnsequentialData->aggregateAliases_, - subresult->idTable(), groupByCols, &localVocab); - - return {std::move(idTable), resultSortedOn(), std::move(localVocab)}; + if (subresult->isFullyMaterialized()) { + using Pair = std::pair, LocalVocab>; + auto gen = [](const Result& input) -> cppcoro::generator { + Pair p{input.idTable(), input.getCopyOfLocalVocab()}; + co_yield p; + }(*subresult); + auto doCompute = [&] { + return computeGroupByForHashMapOptimization( + metadataForUnsequentialData->aggregateAliases_, std::move(gen), + groupByCols); + }; + return ad_utility::callFixedSize(groupByCols.size(), doCompute); + } else { + auto doCompute = [&] { + return computeGroupByForHashMapOptimization( + metadataForUnsequentialData->aggregateAliases_, + std::move(subresult->idTables()), groupByCols); + }; + return ad_utility::callFixedSize(groupByCols.size(), doCompute); + } } - size_t inWidth = _subtree->getResultWidth(); - size_t outWidth = getResultWidth(); + size_t inWidth = _subtree->getResultWidth(); + size_t outWidth = getResultWidth(); - if (!subresult->isFullyMaterialized()) { - AD_CORRECTNESS_CHECK(metadataForUnsequentialData.has_value()); + if (!subresult->isFullyMaterialized()) { + AD_CORRECTNESS_CHECK(metadataForUnsequentialData.has_value()); - Result::Generator generator = CALL_FIXED_SIZE( - (std::array{inWidth, outWidth}), &GroupBy::computeResultLazily, this, - std::move(subresult), std::move(aggregates), - std::move(metadataForUnsequentialData).value().aggregateAliases_, - std::move(groupByCols), !requestLaziness); + Result::Generator generator = CALL_FIXED_SIZE( + (std::array{inWidth, outWidth}), &GroupBy::computeResultLazily, this, + std::move(subresult), std::move(aggregates), + std::move(metadataForUnsequentialData).value().aggregateAliases_, + std::move(groupByCols), !requestLaziness); - return requestLaziness - ? ProtoResult{std::move(generator), resultSortedOn()} - : ProtoResult{cppcoro::getSingleElement(std::move(generator)), - resultSortedOn()}; - } + return requestLaziness + ? ProtoResult{std::move(generator), resultSortedOn()} + : ProtoResult{cppcoro::getSingleElement(std::move(generator)), + resultSortedOn()}; + } - AD_CORRECTNESS_CHECK(subresult->idTable().numColumns() == inWidth); + AD_CORRECTNESS_CHECK(subresult->idTable().numColumns() == inWidth); - // Make a copy of the local vocab. Note: the LocalVocab has reference - // semantics via `shared_ptr`, so no actual strings are copied here. + // Make a copy of the local vocab. Note: the LocalVocab has reference + // semantics via `shared_ptr`, so no actual strings are copied here. - auto localVocab = subresult->getCopyOfLocalVocab(); + auto localVocab = subresult->getCopyOfLocalVocab(); - IdTable idTable = CALL_FIXED_SIZE( - (std::array{inWidth, outWidth}), &GroupBy::doGroupBy, this, - subresult->idTable(), groupByCols, aggregates, &localVocab); + IdTable idTable = CALL_FIXED_SIZE( + (std::array{inWidth, outWidth}), &GroupBy::doGroupBy, this, + subresult->idTable(), groupByCols, aggregates, &localVocab); - LOG(DEBUG) << "GroupBy result computation done." << std::endl; - return {std::move(idTable), resultSortedOn(), std::move(localVocab)}; -} + LOG(DEBUG) << "GroupBy result computation done." << std::endl; + return {std::move(idTable), resultSortedOn(), std::move(localVocab)}; + } -// _____________________________________________________________________________ -template -size_t GroupBy::searchBlockBoundaries( - const std::invocable auto& onBlockChange, - const IdTableView& idTable, GroupBlock& currentGroupBlock) const { - size_t blockStart = 0; - - for (size_t pos = 0; pos < idTable.size(); pos++) { - checkCancellation(); - bool rowMatchesCurrentBlock = - std::ranges::all_of(currentGroupBlock, [&](const auto& colIdxAndValue) { - return idTable(pos, colIdxAndValue.first) == colIdxAndValue.second; - }); - if (!rowMatchesCurrentBlock) { - onBlockChange(blockStart, pos); - // setup for processing the next block - blockStart = pos; - for (auto& [colIdx, value] : currentGroupBlock) { - value = idTable(pos, colIdx); + // _____________________________________________________________________________ + template + size_t GroupBy::searchBlockBoundaries( + const std::invocable auto& onBlockChange, + const IdTableView& idTable, GroupBlock& currentGroupBlock) const { + size_t blockStart = 0; + + for (size_t pos = 0; pos < idTable.size(); pos++) { + checkCancellation(); + bool rowMatchesCurrentBlock = std::ranges::all_of( + currentGroupBlock, [&](const auto& colIdxAndValue) { + return idTable(pos, colIdxAndValue.first) == colIdxAndValue.second; + }); + if (!rowMatchesCurrentBlock) { + onBlockChange(blockStart, pos); + // setup for processing the next block + blockStart = pos; + for (auto& [colIdx, value] : currentGroupBlock) { + value = idTable(pos, colIdx); + } } } + return blockStart; + } + + // _____________________________________________________________________________ + template + void GroupBy::processBlock( + IdTableStatic & output, + const std::vector& aggregates, + sparqlExpression::EvaluationContext& evaluationContext, size_t blockStart, + size_t blockEnd, LocalVocab* localVocab, + const vector& groupByCols) const { + output.emplace_back(); + size_t rowIdx = output.size() - 1; + for (size_t i = 0; i < groupByCols.size(); ++i) { + output(rowIdx, i) = + evaluationContext._inputTable(blockStart, groupByCols[i]); + } + for (const Aggregate& aggregate : aggregates) { + processGroup(aggregate, evaluationContext, blockStart, + blockEnd, &output, rowIdx, aggregate._outCol, + localVocab); + } } - return blockStart; -} - -// _____________________________________________________________________________ -template -void GroupBy::processBlock( - IdTableStatic& output, const std::vector& aggregates, - sparqlExpression::EvaluationContext& evaluationContext, size_t blockStart, - size_t blockEnd, LocalVocab* localVocab, - const vector& groupByCols) const { - output.emplace_back(); - size_t rowIdx = output.size() - 1; - for (size_t i = 0; i < groupByCols.size(); ++i) { - output(rowIdx, i) = - evaluationContext._inputTable(blockStart, groupByCols[i]); - } - for (const Aggregate& aggregate : aggregates) { - processGroup(aggregate, evaluationContext, blockStart, blockEnd, - &output, rowIdx, aggregate._outCol, localVocab); - } -} -// _____________________________________________________________________________ -template -void GroupBy::processEmptyImplicitGroup( - IdTable& resultTable, const std::vector& aggregates, - LocalVocab* localVocab) const { - size_t inWidth = _subtree->getResultWidth(); - IdTable idTable{inWidth, ad_utility::makeAllocatorWithLimit(0_B)}; - - sparqlExpression::EvaluationContext evaluationContext = - createEvaluationContext(*localVocab, idTable); - resultTable.emplace_back(); + // _____________________________________________________________________________ + template + void GroupBy::processEmptyImplicitGroup( + IdTable & resultTable, const std::vector& aggregates, + LocalVocab* localVocab) const { + size_t inWidth = _subtree->getResultWidth(); + IdTable idTable{inWidth, ad_utility::makeAllocatorWithLimit(0_B)}; - IdTableStatic table = std::move(resultTable).toStatic(); - for (const Aggregate& aggregate : aggregates) { - processGroup(aggregate, evaluationContext, 0, 0, &table, 0, - aggregate._outCol, localVocab); - } - resultTable = std::move(table).toDynamic(); -} - -// _____________________________________________________________________________ -template -Result::Generator GroupBy::computeResultLazily( - std::shared_ptr subresult, std::vector aggregates, - std::vector aggregateAliases, - std::vector groupByCols, bool singleIdTable) const { - size_t inWidth = _subtree->getResultWidth(); - AD_CONTRACT_CHECK(inWidth == IN_WIDTH || IN_WIDTH == 0); - LocalVocab currentLocalVocab{}; - std::vector storedLocalVocabs; - LazyGroupBy lazyGroupBy{currentLocalVocab, std::move(aggregateAliases), - getExecutionContext()->getAllocator(), - groupByCols.size()}; - - IdTable resultTable{getResultWidth(), getExecutionContext()->getAllocator()}; - - bool groupSplitAcrossTables = false; + sparqlExpression::EvaluationContext evaluationContext = + createEvaluationContext(*localVocab, idTable); + resultTable.emplace_back(); + + IdTableStatic table = + std::move(resultTable).toStatic(); + for (const Aggregate& aggregate : aggregates) { + processGroup(aggregate, evaluationContext, 0, 0, &table, 0, + aggregate._outCol, localVocab); + } + resultTable = std::move(table).toDynamic(); + } + + // _____________________________________________________________________________ + template + Result::Generator GroupBy::computeResultLazily( + std::shared_ptr subresult, + std::vector aggregates, + std::vector aggregateAliases, + std::vector groupByCols, bool singleIdTable) const { + size_t inWidth = _subtree->getResultWidth(); + AD_CONTRACT_CHECK(inWidth == IN_WIDTH || IN_WIDTH == 0); + LocalVocab currentLocalVocab{}; + std::vector storedLocalVocabs; + LazyGroupBy lazyGroupBy{currentLocalVocab, std::move(aggregateAliases), + getExecutionContext()->getAllocator(), + groupByCols.size()}; + + IdTable resultTable{getResultWidth(), + getExecutionContext()->getAllocator()}; + + bool groupSplitAcrossTables = false; + + GroupBlock currentGroupBlock; + + for (Result::IdTableVocabPair& pair : subresult->idTables()) { + auto& idTable = pair.idTable_; + if (idTable.empty()) { + continue; + } + AD_CORRECTNESS_CHECK(idTable.numColumns() == inWidth); + checkCancellation(); + storedLocalVocabs.emplace_back(std::move(pair.localVocab_)); - GroupBlock currentGroupBlock; + if (currentGroupBlock.empty()) { + for (size_t col : groupByCols) { + currentGroupBlock.emplace_back(col, idTable(0, col)); + } + } - for (Result::IdTableVocabPair& pair : subresult->idTables()) { - auto& idTable = pair.idTable_; - if (idTable.empty()) { - continue; + sparqlExpression::EvaluationContext evaluationContext = + createEvaluationContext(currentLocalVocab, idTable); + + size_t lastBlockStart = searchBlockBoundaries( + [this, &groupSplitAcrossTables, &lazyGroupBy, &evaluationContext, + &resultTable, ¤tGroupBlock, &aggregates, ¤tLocalVocab, + &groupByCols](size_t blockStart, size_t blockEnd) { + if (groupSplitAcrossTables) { + lazyGroupBy.processBlock(evaluationContext, blockStart, blockEnd); + lazyGroupBy.commitRow(resultTable, evaluationContext, + currentGroupBlock); + groupSplitAcrossTables = false; + } else { + // This processes the whole block in batches if possible + IdTableStatic table = + std::move(resultTable).toStatic(); + processBlock(table, aggregates, evaluationContext, + blockStart, blockEnd, ¤tLocalVocab, + groupByCols); + resultTable = std::move(table).toDynamic(); + } + }, + idTable.asStaticView(), currentGroupBlock); + groupSplitAcrossTables = true; + lazyGroupBy.processBlock(evaluationContext, lastBlockStart, + idTable.size()); + if (!singleIdTable && !resultTable.empty()) { + currentLocalVocab.mergeWith(storedLocalVocabs); + Result::IdTableVocabPair outputPair{std::move(resultTable), + std::move(currentLocalVocab)}; + co_yield outputPair; + // Reuse buffer if not moved out + resultTable = std::move(outputPair.idTable_); + resultTable.clear(); + // Keep last local vocab for next commit. + currentLocalVocab = std::move(storedLocalVocabs.back()); + storedLocalVocabs.clear(); + } } - AD_CORRECTNESS_CHECK(idTable.numColumns() == inWidth); - checkCancellation(); - storedLocalVocabs.emplace_back(std::move(pair.localVocab_)); - - if (currentGroupBlock.empty()) { - for (size_t col : groupByCols) { - currentGroupBlock.emplace_back(col, idTable(0, col)); + // No need for final commit when loop was never entered. + if (!groupSplitAcrossTables) { + // If we have an implicit group by we need to produce one result row + if (groupByCols.empty()) { + processEmptyImplicitGroup(resultTable, aggregates, + ¤tLocalVocab); + co_yield {std::move(resultTable), std::move(currentLocalVocab)}; + } else if (singleIdTable) { + // Yield at least a single empty table if requested. + co_yield {std::move(resultTable), std::move(currentLocalVocab)}; } + co_return; + } + + // Process remaining items in the last group. For those we have already + // called `lazyGroupBy.processBlock()` but the call to `commitRow` is still + // missing. We have to setup a dummy input table and evaluation context, + // that have the values of the `currentGroupBlock` in the correct columns. + IdTable idTable{inWidth, ad_utility::makeAllocatorWithLimit( + 1_B * sizeof(Id) * inWidth)}; + idTable.emplace_back(); + for (const auto& [colIdx, value] : currentGroupBlock) { + idTable.at(0, colIdx) = value; } sparqlExpression::EvaluationContext evaluationContext = createEvaluationContext(currentLocalVocab, idTable); - - size_t lastBlockStart = searchBlockBoundaries( - [this, &groupSplitAcrossTables, &lazyGroupBy, &evaluationContext, - &resultTable, ¤tGroupBlock, &aggregates, ¤tLocalVocab, - &groupByCols](size_t blockStart, size_t blockEnd) { - if (groupSplitAcrossTables) { - lazyGroupBy.processBlock(evaluationContext, blockStart, blockEnd); - lazyGroupBy.commitRow(resultTable, evaluationContext, - currentGroupBlock); - groupSplitAcrossTables = false; - } else { - // This processes the whole block in batches if possible - IdTableStatic table = - std::move(resultTable).toStatic(); - processBlock(table, aggregates, evaluationContext, - blockStart, blockEnd, ¤tLocalVocab, - groupByCols); - resultTable = std::move(table).toDynamic(); - } - }, - idTable.asStaticView(), currentGroupBlock); - groupSplitAcrossTables = true; - lazyGroupBy.processBlock(evaluationContext, lastBlockStart, idTable.size()); - if (!singleIdTable && !resultTable.empty()) { - currentLocalVocab.mergeWith(storedLocalVocabs); - Result::IdTableVocabPair outputPair{std::move(resultTable), - std::move(currentLocalVocab)}; - co_yield outputPair; - // Reuse buffer if not moved out - resultTable = std::move(outputPair.idTable_); - resultTable.clear(); - // Keep last local vocab for next commit. - currentLocalVocab = std::move(storedLocalVocabs.back()); - storedLocalVocabs.clear(); - } - } - // No need for final commit when loop was never entered. - if (!groupSplitAcrossTables) { - // If we have an implicit group by we need to produce one result row - if (groupByCols.empty()) { - processEmptyImplicitGroup(resultTable, aggregates, - ¤tLocalVocab); - co_yield {std::move(resultTable), std::move(currentLocalVocab)}; - } else if (singleIdTable) { - // Yield at least a single empty table if requested. - co_yield {std::move(resultTable), std::move(currentLocalVocab)}; - } - co_return; + lazyGroupBy.commitRow(resultTable, evaluationContext, currentGroupBlock); + currentLocalVocab.mergeWith(storedLocalVocabs); + co_yield {std::move(resultTable), std::move(currentLocalVocab)}; } - // Process remaining items in the last group. For those we have already - // called `lazyGroupBy.processBlock()` but the call to `commitRow` is still - // missing. We have to setup a dummy input table and evaluation context, that - // have the values of the `currentGroupBlock` in the correct columns. - IdTable idTable{inWidth, ad_utility::makeAllocatorWithLimit( - 1_B * sizeof(Id) * inWidth)}; - idTable.emplace_back(); - for (const auto& [colIdx, value] : currentGroupBlock) { - idTable.at(0, colIdx) = value; - } + // _____________________________________________________________________________ + std::optional GroupBy::computeGroupByForSingleIndexScan() const { + // The child must be an `IndexScan` for this optimization. + auto indexScan = std::dynamic_pointer_cast( + _subtree->getRootOperation()); - sparqlExpression::EvaluationContext evaluationContext = - createEvaluationContext(currentLocalVocab, idTable); - lazyGroupBy.commitRow(resultTable, evaluationContext, currentGroupBlock); - currentLocalVocab.mergeWith(storedLocalVocabs); - co_yield {std::move(resultTable), std::move(currentLocalVocab)}; -} + if (!indexScan) { + return std::nullopt; + } -// _____________________________________________________________________________ -std::optional GroupBy::computeGroupByForSingleIndexScan() const { - // The child must be an `IndexScan` for this optimization. - auto indexScan = - std::dynamic_pointer_cast(_subtree->getRootOperation()); + if (indexScan->getResultWidth() <= 1 || + indexScan->graphsToFilter().has_value() || !_groupByVariables.empty()) { + return std::nullopt; + } - if (!indexScan) { - return std::nullopt; - } + // Alias must be a single count of a variable + auto varAndDistinctness = getVariableForCountOfSingleAlias(); + if (!varAndDistinctness.has_value()) { + return std::nullopt; + } - if (indexScan->getResultWidth() <= 1 || - indexScan->graphsToFilter().has_value() || !_groupByVariables.empty()) { - return std::nullopt; - } + // Distinct counts are only supported for triples with three variables. + bool countIsDistinct = varAndDistinctness.value().isDistinct_; + if (countIsDistinct && indexScan->getResultWidth() != 3) { + return std::nullopt; + } - // Alias must be a single count of a variable - auto varAndDistinctness = getVariableForCountOfSingleAlias(); - if (!varAndDistinctness.has_value()) { - return std::nullopt; + IdTable table{1, getExecutionContext()->getAllocator()}; + table.emplace_back(); + const auto& var = varAndDistinctness.value().variable_; + if (!isVariableBoundInSubtree(var)) { + // The variable is never bound, so its count is zero. + table(0, 0) = Id::makeFromInt(0); + } else if (indexScan->getResultWidth() == 3) { + if (countIsDistinct) { + auto permutation = + getPermutationForThreeVariableTriple(*_subtree, var, var); + AD_CONTRACT_CHECK(permutation.has_value()); + table(0, 0) = Id::makeFromInt( + getIndex().getImpl().numDistinctCol0(permutation.value()).normal); + } else { + table(0, 0) = Id::makeFromInt(getIndex().numTriples().normal); + } + } else { + table(0, 0) = Id::makeFromInt(indexScan->getExactSize()); + } + return table; } - // Distinct counts are only supported for triples with three variables. - bool countIsDistinct = varAndDistinctness.value().isDistinct_; - if (countIsDistinct && indexScan->getResultWidth() != 3) { - return std::nullopt; - } + // ____________________________________________________________________________ + std::optional GroupBy::computeGroupByObjectWithCount() const { + // The child must be an `IndexScan` with exactly two variables. + auto indexScan = + std::dynamic_pointer_cast(_subtree->getRootOperation()); + if (!indexScan || indexScan->graphsToFilter().has_value() || + indexScan->numVariables() != 2) { + return std::nullopt; + } + const auto& permutedTriple = indexScan->getPermutedTriple(); + const auto& vocabulary = getExecutionContext()->getIndex().getVocab(); + std::optional col0Id = permutedTriple[0]->toValueId(vocabulary); + if (!col0Id.has_value()) { + return std::nullopt; + } - IdTable table{1, getExecutionContext()->getAllocator()}; - table.emplace_back(); - const auto& var = varAndDistinctness.value().variable_; - if (!isVariableBoundInSubtree(var)) { - // The variable is never bound, so its count is zero. - table(0, 0) = Id::makeFromInt(0); - } else if (indexScan->getResultWidth() == 3) { - if (countIsDistinct) { - auto permutation = - getPermutationForThreeVariableTriple(*_subtree, var, var); - AD_CONTRACT_CHECK(permutation.has_value()); - table(0, 0) = Id::makeFromInt( - getIndex().getImpl().numDistinctCol0(permutation.value()).normal); - } else { - table(0, 0) = Id::makeFromInt(getIndex().numTriples().normal); + // There must be exactly one GROUP BY variable and the result of the index + // scan must be sorted by it. + if (_groupByVariables.size() != 1) { + return std::nullopt; + } + const auto& groupByVariable = _groupByVariables.at(0); + AD_CORRECTNESS_CHECK( + *(permutedTriple[1]) == groupByVariable, + "Result of index scan for GROUP BY must be sorted by the " + "GROUP BY variable, this is a bug in the query planner", + permutedTriple[1]->toString(), groupByVariable.name()); + + // There must be exactly one alias, which is a non-distinct count of one of + // the two variables of the index scan. + auto countedVariable = getVariableForNonDistinctCountOfSingleAlias(); + bool countedVariableIsOneOfIndexScanVariables = + countedVariable == *(permutedTriple[1]) || + countedVariable == *(permutedTriple[2]); + if (!countedVariableIsOneOfIndexScanVariables) { + return std::nullopt; } - } else { - table(0, 0) = Id::makeFromInt(indexScan->getExactSize()); - } - return table; -} -// ____________________________________________________________________________ -std::optional GroupBy::computeGroupByObjectWithCount() const { - // The child must be an `IndexScan` with exactly two variables. - auto indexScan = - std::dynamic_pointer_cast(_subtree->getRootOperation()); - if (!indexScan || indexScan->graphsToFilter().has_value() || - indexScan->numVariables() != 2) { - return std::nullopt; - } - const auto& permutedTriple = indexScan->getPermutedTriple(); - const auto& vocabulary = getExecutionContext()->getIndex().getVocab(); - std::optional col0Id = permutedTriple[0]->toValueId(vocabulary); - if (!col0Id.has_value()) { - return std::nullopt; - } + // Compute the result and update the runtime information (we don't actually + // do the index scan, but something smarter). + const auto& permutation = + getExecutionContext()->getIndex().getPimpl().getPermutation( + indexScan->permutation()); + auto result = permutation.getDistinctCol1IdsAndCounts( + col0Id.value(), cancellationHandle_, locatedTriplesSnapshot()); + indexScan->updateRuntimeInformationWhenOptimizedOut( + {}, RuntimeInformation::Status::optimizedOut); - // There must be exactly one GROUP BY variable and the result of the index - // scan must be sorted by it. - if (_groupByVariables.size() != 1) { - return std::nullopt; - } - const auto& groupByVariable = _groupByVariables.at(0); - AD_CORRECTNESS_CHECK( - *(permutedTriple[1]) == groupByVariable, - "Result of index scan for GROUP BY must be sorted by the " - "GROUP BY variable, this is a bug in the query planner", - permutedTriple[1]->toString(), groupByVariable.name()); - - // There must be exactly one alias, which is a non-distinct count of one of - // the two variables of the index scan. - auto countedVariable = getVariableForNonDistinctCountOfSingleAlias(); - bool countedVariableIsOneOfIndexScanVariables = - countedVariable == *(permutedTriple[1]) || - countedVariable == *(permutedTriple[2]); - if (!countedVariableIsOneOfIndexScanVariables) { - return std::nullopt; + return result; } - // Compute the result and update the runtime information (we don't actually - // do the index scan, but something smarter). - const auto& permutation = - getExecutionContext()->getIndex().getPimpl().getPermutation( - indexScan->permutation()); - auto result = permutation.getDistinctCol1IdsAndCounts( - col0Id.value(), cancellationHandle_, locatedTriplesSnapshot()); - indexScan->updateRuntimeInformationWhenOptimizedOut( - {}, RuntimeInformation::Status::optimizedOut); + // _____________________________________________________________________________ + std::optional GroupBy::computeGroupByForFullIndexScan() const { + if (_groupByVariables.size() != 1) { + return std::nullopt; + } + const auto& groupByVariable = _groupByVariables.at(0); - return result; -} + // The child must be an `IndexScan` with three variables that contains + // the grouped variable. + auto permutationEnum = getPermutationForThreeVariableTriple( + *_subtree, groupByVariable, groupByVariable); -// _____________________________________________________________________________ -std::optional GroupBy::computeGroupByForFullIndexScan() const { - if (_groupByVariables.size() != 1) { - return std::nullopt; - } - const auto& groupByVariable = _groupByVariables.at(0); + if (!permutationEnum.has_value()) { + return std::nullopt; + } + + // Check that all the aliases are non-distinct counts. We currently support + // only one or no such count. Redundant additional counts will lead to an + // exception (it is easy to reformulate the query to trigger this + // optimization). Also keep track of whether the counted variable is + // actually bound by the index scan (else all counts will be 0). + size_t numCounts = 0; + bool variableIsBoundInSubtree = true; + for (size_t i = 0; i < _aliases.size(); ++i) { + const auto& alias = _aliases[i]; + if (auto count = alias._expression.getVariableForCount()) { + if (count.value().isDistinct_) { + return std::nullopt; + } + numCounts++; + variableIsBoundInSubtree = + isVariableBoundInSubtree(count.value().variable_); + } else { + return std::nullopt; + } + } - // The child must be an `IndexScan` with three variables that contains - // the grouped variable. - auto permutationEnum = getPermutationForThreeVariableTriple( - *_subtree, groupByVariable, groupByVariable); + if (numCounts > 1) { + throw std::runtime_error{ + "This query contains two or more COUNT expressions in the same GROUP " + "BY that would lead to identical values. This redundancy is " + "currently " + "not supported."}; + } - if (!permutationEnum.has_value()) { - return std::nullopt; + _subtree->getRootOperation()->updateRuntimeInformationWhenOptimizedOut({}); + + const auto& permutation = + getExecutionContext()->getIndex().getPimpl().getPermutation( + permutationEnum.value()); + auto table = permutation.getDistinctCol0IdsAndCounts( + cancellationHandle_, locatedTriplesSnapshot()); + if (numCounts == 0) { + table.setColumnSubset({{0}}); + } else if (!variableIsBoundInSubtree) { + // The variable inside the COUNT() is not part of the input, so it is + // always unbound and has a count of 0 in each group. + std::ranges::fill(table.getColumn(1), Id::makeFromInt(0)); + } + + // TODO This optimization should probably also apply if + // the query is `SELECT DISTINCT ?s WHERE {?s ?p ?o} ` without a + // GROUP BY, but that needs to be implemented in the `DISTINCT` operation. + return table; } - // Check that all the aliases are non-distinct counts. We currently support - // only one or no such count. Redundant additional counts will lead to an - // exception (it is easy to reformulate the query to trigger this - // optimization). Also keep track of whether the counted variable is actually - // bound by the index scan (else all counts will be 0). - size_t numCounts = 0; - bool variableIsBoundInSubtree = true; - for (size_t i = 0; i < _aliases.size(); ++i) { - const auto& alias = _aliases[i]; - if (auto count = alias._expression.getVariableForCount()) { - if (count.value().isDistinct_) { + // ____________________________________________________________________________ + std::optional + GroupBy::getPermutationForThreeVariableTriple( + const QueryExecutionTree& tree, const Variable& variableByWhichToSort, + const Variable& variableThatMustBeContained) { + auto indexScan = + std::dynamic_pointer_cast(tree.getRootOperation()); + + if (!indexScan || indexScan->graphsToFilter().has_value() || + indexScan->getResultWidth() != 3) { + return std::nullopt; + } + { + auto v = variableThatMustBeContained; + if (v != indexScan->subject() && v != indexScan->predicate() && + v != indexScan->object()) { return std::nullopt; } - numCounts++; - variableIsBoundInSubtree = - isVariableBoundInSubtree(count.value().variable_); + } + + if (variableByWhichToSort == indexScan->subject()) { + return Permutation::SPO; + } else if (variableByWhichToSort == indexScan->predicate()) { + return Permutation::POS; + } else if (variableByWhichToSort == indexScan->object()) { + return Permutation::OSP; } else { return std::nullopt; } - } + }; - if (numCounts > 1) { - throw std::runtime_error{ - "This query contains two or more COUNT expressions in the same GROUP " - "BY that would lead to identical values. This redundancy is currently " - "not supported."}; - } + // ____________________________________________________________________________ + std::optional GroupBy::checkIfJoinWithFullScan( + const Join& join) const { + if (_groupByVariables.size() != 1) { + return std::nullopt; + } + const Variable& groupByVariable = _groupByVariables.front(); - _subtree->getRootOperation()->updateRuntimeInformationWhenOptimizedOut({}); - - const auto& permutation = - getExecutionContext()->getIndex().getPimpl().getPermutation( - permutationEnum.value()); - auto table = permutation.getDistinctCol0IdsAndCounts( - cancellationHandle_, locatedTriplesSnapshot()); - if (numCounts == 0) { - table.setColumnSubset({{0}}); - } else if (!variableIsBoundInSubtree) { - // The variable inside the COUNT() is not part of the input, so it is always - // unbound and has a count of 0 in each group. - std::ranges::fill(table.getColumn(1), Id::makeFromInt(0)); - } + auto countedVariable = getVariableForNonDistinctCountOfSingleAlias(); + if (!countedVariable.has_value()) { + return std::nullopt; + } - // TODO This optimization should probably also apply if - // the query is `SELECT DISTINCT ?s WHERE {?s ?p ?o} ` without a - // GROUP BY, but that needs to be implemented in the `DISTINCT` operation. - return table; -} + // Determine if any of the two children of the join operation is a + // triple with three variables that fulfills the condition. + auto* child1 = static_cast(join).getChildren().at(0); + auto* child2 = static_cast(join).getChildren().at(1); + + // TODO Use `optional::or_else` + auto permutation = getPermutationForThreeVariableTriple( + *child1, groupByVariable, countedVariable.value()); + if (!permutation.has_value()) { + std::swap(child1, child2); + permutation = getPermutationForThreeVariableTriple( + *child1, groupByVariable, countedVariable.value()); + } + if (!permutation.has_value()) { + return std::nullopt; + } -// ____________________________________________________________________________ -std::optional GroupBy::getPermutationForThreeVariableTriple( - const QueryExecutionTree& tree, const Variable& variableByWhichToSort, - const Variable& variableThatMustBeContained) { - auto indexScan = - std::dynamic_pointer_cast(tree.getRootOperation()); + // TODO This is rather implicit. We should have a (soft) check, + // that the join column is correct, and a HARD check, that the result is + // sorted. + // This check fails if we ever decide to not eagerly sort the children of + // a JOIN. We can detect this case and change something here then. + if (child2->getPrimarySortKeyVariable() != groupByVariable) { + return std::nullopt; + } + auto columnIndex = child2->getVariableColumn(groupByVariable); - if (!indexScan || indexScan->graphsToFilter().has_value() || - indexScan->getResultWidth() != 3) { - return std::nullopt; + return OptimizedGroupByData{*child1, *child2, permutation.value(), + columnIndex}; } - { - auto v = variableThatMustBeContained; - if (v != indexScan->subject() && v != indexScan->predicate() && - v != indexScan->object()) { + + // ____________________________________________________________________________ + std::optional GroupBy::computeGroupByForJoinWithFullScan() const { + auto join = std::dynamic_pointer_cast(_subtree->getRootOperation()); + if (!join) { return std::nullopt; } - } - if (variableByWhichToSort == indexScan->subject()) { - return Permutation::SPO; - } else if (variableByWhichToSort == indexScan->predicate()) { - return Permutation::POS; - } else if (variableByWhichToSort == indexScan->object()) { - return Permutation::OSP; - } else { - return std::nullopt; - } -}; + auto optimizedAggregateData = checkIfJoinWithFullScan(*join); + if (!optimizedAggregateData.has_value()) { + return std::nullopt; + } + const auto& [threeVarSubtree, subtree, permutation, columnIndex] = + optimizedAggregateData.value(); + + auto subresult = subtree.getResult(); + threeVarSubtree.getRootOperation() + ->updateRuntimeInformationWhenOptimizedOut({}); + + join->updateRuntimeInformationWhenOptimizedOut( + {subtree.getRootOperation()->getRuntimeInfoPointer(), + threeVarSubtree.getRootOperation()->getRuntimeInfoPointer()}); + IdTable result{2, getExecutionContext()->getAllocator()}; + if (subresult->idTable().size() == 0) { + return result; + } -// ____________________________________________________________________________ -std::optional GroupBy::checkIfJoinWithFullScan( - const Join& join) const { - if (_groupByVariables.size() != 1) { - return std::nullopt; + auto idTable = std::move(result).toStatic<2>(); + const auto& index = getExecutionContext()->getIndex(); + + // TODO Simplify the following pattern by using + // `std::views::chunk_by` and implement a lazy version of this view for + // input iterators. + + // Take care of duplicate values in the input. + Id currentId = subresult->idTable()(0, columnIndex); + size_t currentCount = 0; + size_t currentCardinality = + index.getCardinality(currentId, permutation, locatedTriplesSnapshot()); + + auto pushRow = [&]() { + // If the count is 0 this means that the element with the `currentId` + // doesn't exist in the knowledge graph. Thus, the join with a three + // variable triple would have filtered it out and we don't include it in + // the final result. + if (currentCount > 0) { + // TODO: use `emplace_back(id1, + // id2)` (requires parenthesized initialization of aggregates. + idTable.push_back({currentId, Id::makeFromInt(currentCount)}); + } + }; + for (size_t i = 0; i < subresult->idTable().size(); ++i) { + auto id = subresult->idTable()(i, columnIndex); + if (id != currentId) { + pushRow(); + currentId = id; + currentCount = 0; + // TODO This is also not quite correct, we want the cardinality + // without the internally added triples, but that is not easy to + // retrieve right now. + currentCardinality = + index.getCardinality(id, permutation, locatedTriplesSnapshot()); + } + currentCount += currentCardinality; + } + pushRow(); + return std::move(idTable).toDynamic(); } - const Variable& groupByVariable = _groupByVariables.front(); - auto countedVariable = getVariableForNonDistinctCountOfSingleAlias(); - if (!countedVariable.has_value()) { + // _____________________________________________________________________________ + std::optional GroupBy::computeOptimizedGroupByIfPossible() const { + // TODO Use `std::optional::or_else`. + if (!RuntimeParameters() + .get<"group-by-disable-index-scan-optimizations">()) { + if (auto result = computeGroupByForSingleIndexScan()) { + return result; + } + if (auto result = computeGroupByForFullIndexScan()) { + return result; + } + } + if (auto result = computeGroupByForJoinWithFullScan()) { + return result; + } + if (auto result = computeGroupByObjectWithCount()) { + return result; + } return std::nullopt; } - // Determine if any of the two children of the join operation is a - // triple with three variables that fulfills the condition. - auto* child1 = static_cast(join).getChildren().at(0); - auto* child2 = static_cast(join).getChildren().at(1); - - // TODO Use `optional::or_else` - auto permutation = getPermutationForThreeVariableTriple( - *child1, groupByVariable, countedVariable.value()); - if (!permutation.has_value()) { - std::swap(child1, child2); - permutation = getPermutationForThreeVariableTriple(*child1, groupByVariable, - countedVariable.value()); - } - if (!permutation.has_value()) { - return std::nullopt; - } + // _____________________________________________________________________________ + std::optional + GroupBy::computeUnsequentialProcessingMetadata( + std::vector & aliases, + const std::vector& groupByVariables) { + // Get pointers to all aggregate expressions and their parents + size_t numAggregates = 0; + std::vector aliasesWithAggregateInfo; + for (auto& alias : aliases) { + auto expr = alias._expression.getPimpl(); - // TODO This is rather implicit. We should have a (soft) check, - // that the join column is correct, and a HARD check, that the result is - // sorted. - // This check fails if we ever decide to not eagerly sort the children of - // a JOIN. We can detect this case and change something here then. - if (child2->getPrimarySortKeyVariable() != groupByVariable) { - return std::nullopt; - } - auto columnIndex = child2->getVariableColumn(groupByVariable); + // Find all aggregates in the expression of the current alias. + auto foundAggregates = findAggregates(expr); + if (!foundAggregates.has_value()) return std::nullopt; - return OptimizedGroupByData{*child1, *child2, permutation.value(), - columnIndex}; -} + for (auto& aggregate : foundAggregates.value()) { + aggregate.aggregateDataIndex_ = numAggregates++; + } -// ____________________________________________________________________________ -std::optional GroupBy::computeGroupByForJoinWithFullScan() const { - auto join = std::dynamic_pointer_cast(_subtree->getRootOperation()); - if (!join) { - return std::nullopt; - } + // Find all grouped variables occurring in the alias expression + std::vector groupedVariables; + groupedVariables.reserve(groupByVariables.size()); + // TODO use views::enumerate + size_t i = 0; + for (const auto& groupedVariable : groupByVariables) { + groupedVariables.emplace_back( + groupedVariable, i, findGroupedVariable(expr, groupedVariable)); + ++i; + } - auto optimizedAggregateData = checkIfJoinWithFullScan(*join); - if (!optimizedAggregateData.has_value()) { - return std::nullopt; - } - const auto& [threeVarSubtree, subtree, permutation, columnIndex] = - optimizedAggregateData.value(); - - auto subresult = subtree.getResult(); - threeVarSubtree.getRootOperation()->updateRuntimeInformationWhenOptimizedOut( - {}); - - join->updateRuntimeInformationWhenOptimizedOut( - {subtree.getRootOperation()->getRuntimeInfoPointer(), - threeVarSubtree.getRootOperation()->getRuntimeInfoPointer()}); - IdTable result{2, getExecutionContext()->getAllocator()}; - if (subresult->idTable().size() == 0) { - return result; + aliasesWithAggregateInfo.emplace_back(alias._expression, alias._outCol, + foundAggregates.value(), + groupedVariables); + } + + return HashMapOptimizationData{aliasesWithAggregateInfo}; } - auto idTable = std::move(result).toStatic<2>(); - const auto& index = getExecutionContext()->getIndex(); - - // TODO Simplify the following pattern by using - // `std::views::chunk_by` and implement a lazy version of this view for - // input iterators. - - // Take care of duplicate values in the input. - Id currentId = subresult->idTable()(0, columnIndex); - size_t currentCount = 0; - size_t currentCardinality = - index.getCardinality(currentId, permutation, locatedTriplesSnapshot()); - - auto pushRow = [&]() { - // If the count is 0 this means that the element with the `currentId` - // doesn't exist in the knowledge graph. Thus, the join with a three - // variable triple would have filtered it out and we don't include it in - // the final result. - if (currentCount > 0) { - // TODO: use `emplace_back(id1, - // id2)` (requires parenthesized initialization of aggregates. - idTable.push_back({currentId, Id::makeFromInt(currentCount)}); - } - }; - for (size_t i = 0; i < subresult->idTable().size(); ++i) { - auto id = subresult->idTable()(i, columnIndex); - if (id != currentId) { - pushRow(); - currentId = id; - currentCount = 0; - // TODO This is also not quite correct, we want the cardinality - // without the internally added triples, but that is not easy to - // retrieve right now. - currentCardinality = - index.getCardinality(id, permutation, locatedTriplesSnapshot()); + // _____________________________________________________________________________ + std::optional + GroupBy::checkIfHashMapOptimizationPossible(std::vector & aliases) + const { + if (!RuntimeParameters().get<"group-by-hash-map-enabled">()) { + return std::nullopt; } - currentCount += currentCardinality; - } - pushRow(); - return std::move(idTable).toDynamic(); -} -// _____________________________________________________________________________ -std::optional GroupBy::computeOptimizedGroupByIfPossible() const { - // TODO Use `std::optional::or_else`. - if (!RuntimeParameters().get<"group-by-disable-index-scan-optimizations">()) { - if (auto result = computeGroupByForSingleIndexScan()) { - return result; + if (!std::dynamic_pointer_cast(_subtree->getRootOperation())) { + return std::nullopt; } - if (auto result = computeGroupByForFullIndexScan()) { - return result; + return computeUnsequentialProcessingMetadata(aliases, _groupByVariables); + } + + // _____________________________________________________________________________ + std::variant, GroupBy::OccurAsRoot> + GroupBy::findGroupedVariable(sparqlExpression::SparqlExpression * expr, + const Variable& groupedVariable) { + std::variant, OccurAsRoot> substitutions; + findGroupedVariableImpl(expr, std::nullopt, substitutions, groupedVariable); + return substitutions; + } + + // _____________________________________________________________________________ + void GroupBy::findGroupedVariableImpl( + sparqlExpression::SparqlExpression * expr, + std::optional parentAndChildIndex, + std::variant, OccurAsRoot> & + substitutions, + const Variable& groupedVariable) { + AD_CORRECTNESS_CHECK(expr != nullptr); + if (auto value = + dynamic_cast(expr)) { + const auto& variable = value->value(); + if (variable != groupedVariable) return; + if (parentAndChildIndex.has_value()) { + auto vector = + std::get_if>(&substitutions); + AD_CONTRACT_CHECK(vector != nullptr); + vector->emplace_back(parentAndChildIndex.value()); + } else { + substitutions = OccurAsRoot{}; + return; + } } - } - if (auto result = computeGroupByForJoinWithFullScan()) { - return result; - } - if (auto result = computeGroupByObjectWithCount()) { - return result; - } - return std::nullopt; -} -// _____________________________________________________________________________ -std::optional -GroupBy::computeUnsequentialProcessingMetadata( - std::vector& aliases, - const std::vector& groupByVariables) { - // Get pointers to all aggregate expressions and their parents - size_t numAggregates = 0; - std::vector aliasesWithAggregateInfo; - for (auto& alias : aliases) { - auto expr = alias._expression.getPimpl(); - - // Find all aggregates in the expression of the current alias. - auto foundAggregates = findAggregates(expr); - if (!foundAggregates.has_value()) return std::nullopt; - - for (auto& aggregate : foundAggregates.value()) { - aggregate.aggregateDataIndex_ = numAggregates++; - } + auto children = expr->children(); - // Find all grouped variables occurring in the alias expression - std::vector groupedVariables; - groupedVariables.reserve(groupByVariables.size()); // TODO use views::enumerate - size_t i = 0; - for (const auto& groupedVariable : groupByVariables) { - groupedVariables.emplace_back(groupedVariable, i, - findGroupedVariable(expr, groupedVariable)); - ++i; + size_t childIndex = 0; + for (const auto& child : children) { + ParentAndChildIndex parentAndChildIndexForChild{expr, childIndex++}; + findGroupedVariableImpl(child.get(), parentAndChildIndexForChild, + substitutions, groupedVariable); } - - aliasesWithAggregateInfo.emplace_back(alias._expression, alias._outCol, - foundAggregates.value(), - groupedVariables); } - return HashMapOptimizationData{aliasesWithAggregateInfo}; -} - -// _____________________________________________________________________________ -std::optional -GroupBy::checkIfHashMapOptimizationPossible( - std::vector& aliases) const { - if (!RuntimeParameters().get<"group-by-hash-map-enabled">()) { - return std::nullopt; + // _____________________________________________________________________________ + std::optional> + GroupBy::findAggregates(sparqlExpression::SparqlExpression * expr) { + std::vector result; + if (!findAggregatesImpl(expr, std::nullopt, result)) + return std::nullopt; + else + return result; } - if (!std::dynamic_pointer_cast(_subtree->getRootOperation())) { - return std::nullopt; - } - return computeUnsequentialProcessingMetadata(aliases, _groupByVariables); -} + // _____________________________________________________________________________ + std::optional + GroupBy::isSupportedAggregate(sparqlExpression::SparqlExpression * expr) { + using enum HashMapAggregateType; + using namespace sparqlExpression; -// _____________________________________________________________________________ -std::variant, GroupBy::OccurAsRoot> -GroupBy::findGroupedVariable(sparqlExpression::SparqlExpression* expr, - const Variable& groupedVariable) { - std::variant, OccurAsRoot> substitutions; - findGroupedVariableImpl(expr, std::nullopt, substitutions, groupedVariable); - return substitutions; -} + // `expr` is not a distinct aggregate + if (expr->isAggregate() != + SparqlExpression::AggregateStatus::NonDistinctAggregate) + return std::nullopt; -// _____________________________________________________________________________ -void GroupBy::findGroupedVariableImpl( - sparqlExpression::SparqlExpression* expr, - std::optional parentAndChildIndex, - std::variant, OccurAsRoot>& substitutions, - const Variable& groupedVariable) { - AD_CORRECTNESS_CHECK(expr != nullptr); - if (auto value = dynamic_cast(expr)) { - const auto& variable = value->value(); - if (variable != groupedVariable) return; - if (parentAndChildIndex.has_value()) { - auto vector = - std::get_if>(&substitutions); - AD_CONTRACT_CHECK(vector != nullptr); - vector->emplace_back(parentAndChildIndex.value()); - } else { - substitutions = OccurAsRoot{}; - return; + // `expr` is not a nested aggregated + if (std::ranges::any_of(expr->children(), [](const auto& ptr) { + return ptr->containsAggregate(); + })) { + return std::nullopt; } - } - - auto children = expr->children(); - - // TODO use views::enumerate - size_t childIndex = 0; - for (const auto& child : children) { - ParentAndChildIndex parentAndChildIndexForChild{expr, childIndex++}; - findGroupedVariableImpl(child.get(), parentAndChildIndexForChild, - substitutions, groupedVariable); - } -} - -// _____________________________________________________________________________ -std::optional> -GroupBy::findAggregates(sparqlExpression::SparqlExpression* expr) { - std::vector result; - if (!findAggregatesImpl(expr, std::nullopt, result)) - return std::nullopt; - else - return result; -} -// _____________________________________________________________________________ -std::optional -GroupBy::isSupportedAggregate(sparqlExpression::SparqlExpression* expr) { - using enum HashMapAggregateType; - using namespace sparqlExpression; - - // `expr` is not a distinct aggregate - if (expr->isAggregate() != - SparqlExpression::AggregateStatus::NonDistinctAggregate) - return std::nullopt; + using H = HashMapAggregateTypeWithData; + + if (dynamic_cast(expr)) return H{AVG}; + if (dynamic_cast(expr)) return H{COUNT}; + // We reuse the COUNT implementation which works, but leaves some + // optimization potential on the table because `COUNT(*)` doesn't need to + // check for undefined values. + if (dynamic_cast(expr)) return H{COUNT}; + if (dynamic_cast(expr)) return H{MIN}; + if (dynamic_cast(expr)) return H{MAX}; + if (dynamic_cast(expr)) return H{SUM}; + if (auto val = dynamic_cast(expr)) { + return H{GROUP_CONCAT, val->getSeparator()}; + } + // NOTE: The STDEV function is not suitable for lazy and hash map + // optimizations. + if (dynamic_cast(expr)) return H{SAMPLE}; - // `expr` is not a nested aggregated - if (std::ranges::any_of(expr->children(), [](const auto& ptr) { - return ptr->containsAggregate(); - })) { + // `expr` is an unsupported aggregate return std::nullopt; } - using H = HashMapAggregateTypeWithData; - - if (dynamic_cast(expr)) return H{AVG}; - if (dynamic_cast(expr)) return H{COUNT}; - // We reuse the COUNT implementation which works, but leaves some optimization - // potential on the table because `COUNT(*)` doesn't need to check for - // undefined values. - if (dynamic_cast(expr)) return H{COUNT}; - if (dynamic_cast(expr)) return H{MIN}; - if (dynamic_cast(expr)) return H{MAX}; - if (dynamic_cast(expr)) return H{SUM}; - if (auto val = dynamic_cast(expr)) { - return H{GROUP_CONCAT, val->getSeparator()}; - } - // NOTE: The STDEV function is not suitable for lazy and hash map - // optimizations. - if (dynamic_cast(expr)) return H{SAMPLE}; + // _____________________________________________________________________________ + bool GroupBy::findAggregatesImpl( + sparqlExpression::SparqlExpression * expr, + std::optional parentAndChildIndex, + std::vector & info) { + if (expr->isAggregate() != + sparqlExpression::SparqlExpression::AggregateStatus::NoAggregate) { + if (auto aggregateType = isSupportedAggregate(expr)) { + info.emplace_back(expr, 0, aggregateType.value(), parentAndChildIndex); + return true; + } else { + return false; + } + } - // `expr` is an unsupported aggregate - return std::nullopt; -} + auto children = expr->children(); -// _____________________________________________________________________________ -bool GroupBy::findAggregatesImpl( - sparqlExpression::SparqlExpression* expr, - std::optional parentAndChildIndex, - std::vector& info) { - if (expr->isAggregate() != - sparqlExpression::SparqlExpression::AggregateStatus::NoAggregate) { - if (auto aggregateType = isSupportedAggregate(expr)) { - info.emplace_back(expr, 0, aggregateType.value(), parentAndChildIndex); - return true; - } else { - return false; + bool childrenContainOnlySupportedAggregates = true; + // TODO use views::enumerate + size_t childIndex = 0; + for (const auto& child : children) { + ParentAndChildIndex parentAndChildIndexForChild{expr, childIndex++}; + childrenContainOnlySupportedAggregates = + childrenContainOnlySupportedAggregates && + findAggregatesImpl(child.get(), parentAndChildIndexForChild, info); } - } - auto children = expr->children(); + return childrenContainOnlySupportedAggregates; + } + + // _____________________________________________________________________________ + void GroupBy::extractValues( + sparqlExpression::ExpressionResult && expressionResult, + sparqlExpression::EvaluationContext & evaluationContext, + IdTable * resultTable, LocalVocab * localVocab, size_t outCol) { + auto visitor = [&evaluationContext, &resultTable, &localVocab, + &outCol]( + T&& singleResult) mutable { + auto generator = sparqlExpression::detail::makeGenerator( + std::forward(singleResult), evaluationContext.size(), + &evaluationContext); + + auto targetIterator = resultTable->getColumn(outCol).begin() + + evaluationContext._beginIndex; + for (sparqlExpression::IdOrLiteralOrIri val : generator) { + *targetIterator = + sparqlExpression::detail::constantExpressionResultToId( + std::move(val), *localVocab); + ++targetIterator; + } + }; - bool childrenContainOnlySupportedAggregates = true; - // TODO use views::enumerate - size_t childIndex = 0; - for (const auto& child : children) { - ParentAndChildIndex parentAndChildIndexForChild{expr, childIndex++}; - childrenContainOnlySupportedAggregates = - childrenContainOnlySupportedAggregates && - findAggregatesImpl(child.get(), parentAndChildIndexForChild, info); + std::visit(visitor, std::move(expressionResult)); } - return childrenContainOnlySupportedAggregates; -} - -// _____________________________________________________________________________ -void GroupBy::extractValues( - sparqlExpression::ExpressionResult&& expressionResult, - sparqlExpression::EvaluationContext& evaluationContext, - IdTable* resultTable, LocalVocab* localVocab, size_t outCol) { - auto visitor = [&evaluationContext, &resultTable, &localVocab, - &outCol]( - T&& singleResult) mutable { - auto generator = sparqlExpression::detail::makeGenerator( - std::forward(singleResult), evaluationContext.size(), - &evaluationContext); - - auto targetIterator = - resultTable->getColumn(outCol).begin() + evaluationContext._beginIndex; - for (sparqlExpression::IdOrLiteralOrIri val : generator) { - *targetIterator = sparqlExpression::detail::constantExpressionResultToId( - std::move(val), *localVocab); - ++targetIterator; + // _____________________________________________________________________________ + static constexpr auto resizeIfVector = [](auto& val, size_t size) { + if constexpr (requires { val.resize(size); }) { + val.resize(size); } }; - std::visit(visitor, std::move(expressionResult)); -} - -// _____________________________________________________________________________ -static constexpr auto resizeIfVector = [](auto& val, size_t size) { - if constexpr (requires { val.resize(size); }) { - val.resize(size); - } -}; + // _____________________________________________________________________________ + template + sparqlExpression::VectorWithMemoryLimit + GroupBy::getHashMapAggregationResults( + IdTable * resultTable, + const HashMapAggregationData& aggregationData, + size_t dataIndex, size_t beginIndex, size_t endIndex, + LocalVocab* localVocab, const Allocator& allocator) { + sparqlExpression::VectorWithMemoryLimit aggregateResults( + allocator); + aggregateResults.resize(endIndex - beginIndex); + + auto& aggregateDataVariant = + aggregationData.getAggregationDataVariant(dataIndex); + + using B = + HashMapAggregationData::template ArrayOrVector; + for (size_t rowIdx = beginIndex; rowIdx < endIndex; ++rowIdx) { + size_t vectorIdx; + // Special case for lazy consumer where the hashmap is not used + if (aggregationData.getNumberOfGroups() == 0) { + vectorIdx = 0; + } else { + B mapKey; + resizeIfVector(mapKey, aggregationData.numOfGroupedColumns_); + + for (size_t idx = 0; idx < mapKey.size(); ++idx) { + mapKey.at(idx) = resultTable->getColumn(idx)[rowIdx]; + } + vectorIdx = aggregationData.getIndex(mapKey); + } -// _____________________________________________________________________________ -template -sparqlExpression::VectorWithMemoryLimit -GroupBy::getHashMapAggregationResults( - IdTable* resultTable, - const HashMapAggregationData& aggregationData, - size_t dataIndex, size_t beginIndex, size_t endIndex, - LocalVocab* localVocab, const Allocator& allocator) { - sparqlExpression::VectorWithMemoryLimit aggregateResults(allocator); - aggregateResults.resize(endIndex - beginIndex); - - auto& aggregateDataVariant = - aggregationData.getAggregationDataVariant(dataIndex); - - using B = - HashMapAggregationData::template ArrayOrVector; - for (size_t rowIdx = beginIndex; rowIdx < endIndex; ++rowIdx) { - size_t vectorIdx; - // Special case for lazy consumer where the hashmap is not used - if (aggregationData.getNumberOfGroups() == 0) { - vectorIdx = 0; - } else { - B mapKey; - resizeIfVector(mapKey, aggregationData.numOfGroupedColumns_); + auto visitor = [&aggregateResults, vectorIdx, rowIdx, beginIndex, + localVocab](auto& aggregateDataVariant) { + aggregateResults[rowIdx - beginIndex] = + aggregateDataVariant.at(vectorIdx).calculateResult(localVocab); + }; - for (size_t idx = 0; idx < mapKey.size(); ++idx) { - mapKey.at(idx) = resultTable->getColumn(idx)[rowIdx]; - } - vectorIdx = aggregationData.getIndex(mapKey); + std::visit(visitor, aggregateDataVariant); } - auto visitor = [&aggregateResults, vectorIdx, rowIdx, beginIndex, - localVocab](auto& aggregateDataVariant) { - aggregateResults[rowIdx - beginIndex] = - aggregateDataVariant.at(vectorIdx).calculateResult(localVocab); - }; - - std::visit(visitor, aggregateDataVariant); + return aggregateResults; } - return aggregateResults; -} + // _____________________________________________________________________________ + void GroupBy::substituteGroupVariable( + const std::vector& occurrences, IdTable* resultTable, + size_t beginIndex, size_t count, size_t columnIndex, + const Allocator& allocator) { + decltype(auto) groupValues = + resultTable->getColumn(columnIndex).subspan(beginIndex, count); -// _____________________________________________________________________________ -void GroupBy::substituteGroupVariable( - const std::vector& occurrences, IdTable* resultTable, - size_t beginIndex, size_t count, size_t columnIndex, - const Allocator& allocator) { - decltype(auto) groupValues = - resultTable->getColumn(columnIndex).subspan(beginIndex, count); - - for (const auto& occurrence : occurrences) { - sparqlExpression::VectorWithMemoryLimit values(allocator); - values.resize(groupValues.size()); - std::ranges::copy(groupValues, values.begin()); - - auto newExpression = std::make_unique( - std::move(values)); - - occurrence.parent_->replaceChild(occurrence.nThChild_, - std::move(newExpression)); + for (const auto& occurrence : occurrences) { + sparqlExpression::VectorWithMemoryLimit values(allocator); + values.resize(groupValues.size()); + std::ranges::copy(groupValues, values.begin()); + + auto newExpression = + std::make_unique( + std::move(values)); + + occurrence.parent_->replaceChild(occurrence.nThChild_, + std::move(newExpression)); + } } -} -// _____________________________________________________________________________ -template -std::vector> -GroupBy::substituteAllAggregates( - std::vector& info, size_t beginIndex, - size_t endIndex, - const HashMapAggregationData& aggregationData, - IdTable* resultTable, LocalVocab* localVocab, const Allocator& allocator) { + // _____________________________________________________________________________ + template std::vector> - originalChildren; - originalChildren.reserve(info.size()); - // Substitute in the results of all aggregates of `info`. - for (auto& aggregate : info) { - auto aggregateResults = getHashMapAggregationResults( - resultTable, aggregationData, aggregate.aggregateDataIndex_, beginIndex, - endIndex, localVocab, allocator); - - // Substitute the resulting vector as a literal - auto newExpression = std::make_unique( - std::move(aggregateResults)); - - AD_CONTRACT_CHECK(aggregate.parentAndIndex_.has_value()); - auto parentAndIndex = aggregate.parentAndIndex_.value(); - originalChildren.push_back(parentAndIndex.parent_->replaceChild( - parentAndIndex.nThChild_, std::move(newExpression))); - } - return originalChildren; -} + GroupBy::substituteAllAggregates( + std::vector & info, size_t beginIndex, + size_t endIndex, + const HashMapAggregationData& aggregationData, + IdTable* resultTable, LocalVocab* localVocab, + const Allocator& allocator) { + std::vector> + originalChildren; + originalChildren.reserve(info.size()); + // Substitute in the results of all aggregates of `info`. + for (auto& aggregate : info) { + auto aggregateResults = getHashMapAggregationResults( + resultTable, aggregationData, aggregate.aggregateDataIndex_, + beginIndex, endIndex, localVocab, allocator); + + // Substitute the resulting vector as a literal + auto newExpression = + std::make_unique( + std::move(aggregateResults)); + + AD_CONTRACT_CHECK(aggregate.parentAndIndex_.has_value()); + auto parentAndIndex = aggregate.parentAndIndex_.value(); + originalChildren.push_back(parentAndIndex.parent_->replaceChild( + parentAndIndex.nThChild_, std::move(newExpression))); + } + return originalChildren; + } + + // _____________________________________________________________________________ + template + std::vector + GroupBy::HashMapAggregationData::getHashEntries( + const ArrayOrVector>& groupByCols) { + AD_CONTRACT_CHECK(groupByCols.size() > 0); + + std::vector hashEntries; + size_t numberOfEntries = groupByCols.at(0).size(); + hashEntries.reserve(numberOfEntries); + + // TODO: We pass the `Id`s column-wise into this function, and then handle + // them row-wise. Is there any advantage to this, or should we + // transform the data into a row-wise format before passing it? + for (size_t i = 0; i < numberOfEntries; ++i) { + ArrayOrVector row; + resizeIfVector(row, numOfGroupedColumns_); + + // TODO use views::enumerate + auto idx = 0; + for (const auto& val : groupByCols) { + row[idx] = val[i]; + ++idx; + } -// _____________________________________________________________________________ -template -std::vector -GroupBy::HashMapAggregationData::getHashEntries( - const ArrayOrVector>& groupByCols) { - AD_CONTRACT_CHECK(groupByCols.size() > 0); - - std::vector hashEntries; - size_t numberOfEntries = groupByCols.at(0).size(); - hashEntries.reserve(numberOfEntries); - - // TODO: We pass the `Id`s column-wise into this function, and then handle - // them row-wise. Is there any advantage to this, or should we transform - // the data into a row-wise format before passing it? - for (size_t i = 0; i < numberOfEntries; ++i) { - ArrayOrVector row; - resizeIfVector(row, numOfGroupedColumns_); + auto [iterator, wasAdded] = map_.try_emplace(row, getNumberOfGroups()); + hashEntries.push_back(iterator->second); + } + + auto resizeVectors = + []( + T& arg, size_t numberOfGroups, + [[maybe_unused]] const HashMapAggregateTypeWithData& info) { + if constexpr (std::same_as) { + arg.resize(numberOfGroups, + GroupConcatAggregationData{info.separator_.value()}); + } else { + arg.resize(numberOfGroups); + } + }; // TODO use views::enumerate auto idx = 0; - for (const auto& val : groupByCols) { - row[idx] = val[i]; + for (auto& aggregation : aggregationData_) { + const auto& aggregationTypeWithData = aggregateTypeWithData_.at(idx); + const auto numberOfGroups = getNumberOfGroups(); + + std::visit( + [&resizeVectors, &aggregationTypeWithData, + numberOfGroups](T& arg) { + resizeVectors(arg, numberOfGroups, aggregationTypeWithData); + }, + aggregation); ++idx; } - auto [iterator, wasAdded] = map_.try_emplace(row, getNumberOfGroups()); - hashEntries.push_back(iterator->second); + return hashEntries; } - auto resizeVectors = - []( - T& arg, size_t numberOfGroups, - [[maybe_unused]] const HashMapAggregateTypeWithData& info) { - if constexpr (std::same_as) { - arg.resize(numberOfGroups, - GroupConcatAggregationData{info.separator_.value()}); - } else { - arg.resize(numberOfGroups); - } - }; - - // TODO use views::enumerate - auto idx = 0; - for (auto& aggregation : aggregationData_) { - const auto& aggregationTypeWithData = aggregateTypeWithData_.at(idx); - const auto numberOfGroups = getNumberOfGroups(); - - std::visit( - [&resizeVectors, &aggregationTypeWithData, - numberOfGroups](T& arg) { - resizeVectors(arg, numberOfGroups, aggregationTypeWithData); - }, - aggregation); - ++idx; - } - - return hashEntries; -} + // _____________________________________________________________________________ + template + [[nodiscard]] GroupBy::HashMapAggregationData< + NUM_GROUP_COLUMNS>::ArrayOrVector> + GroupBy::HashMapAggregationData::getSortedGroupColumns() + const { + // Get data in a row-wise manner. + std::vector> sortedKeys; + for (const auto& val : map_) { + sortedKeys.push_back(val.first); + } -// _____________________________________________________________________________ -template -[[nodiscard]] GroupBy::HashMapAggregationData::ArrayOrVector< - std::vector> -GroupBy::HashMapAggregationData::getSortedGroupColumns() - const { - // Get data in a row-wise manner. - std::vector> sortedKeys; - for (const auto& val : map_) { - sortedKeys.push_back(val.first); - } + // Sort data. + std::ranges::sort(sortedKeys.begin(), sortedKeys.end()); - // Sort data. - std::ranges::sort(sortedKeys.begin(), sortedKeys.end()); + // Get data in a column-wise manner. + ArrayOrVector> result; + resizeIfVector(result, numOfGroupedColumns_); - // Get data in a column-wise manner. - ArrayOrVector> result; - resizeIfVector(result, numOfGroupedColumns_); + for (size_t idx = 0; idx < result.size(); ++idx) + for (auto& val : sortedKeys) { + result.at(idx).push_back(val.at(idx)); + } - for (size_t idx = 0; idx < result.size(); ++idx) - for (auto& val : sortedKeys) { - result.at(idx).push_back(val.at(idx)); - } + return result; + } - return result; -} + // _____________________________________________________________________________ + template + void GroupBy::evaluateAlias( + HashMapAliasInformation & alias, IdTable * result, + sparqlExpression::EvaluationContext & evaluationContext, + const HashMapAggregationData& aggregationData, + LocalVocab* localVocab, const Allocator& allocator) { + auto& info = alias.aggregateInfo_; + + // Either: + // - One of the variables occurs at the top. This can be copied as the + // result + // - There is only one aggregate, and it appears at the top. No + // substitutions necessary, can evaluate aggregate and copy results + // - Possibly multiple aggregates and occurrences of grouped variables. All + // have to be substituted away before evaluation + + auto substitutions = alias.groupedVariables_; + auto topLevelGroupedVariable = std::ranges::find_if( + substitutions, [](HashMapGroupedVariableInformation& val) { + return std::get_if(&val.occurrences_); + }); -// _____________________________________________________________________________ -template -void GroupBy::evaluateAlias( - HashMapAliasInformation& alias, IdTable* result, - sparqlExpression::EvaluationContext& evaluationContext, - const HashMapAggregationData& aggregationData, - LocalVocab* localVocab, const Allocator& allocator) { - auto& info = alias.aggregateInfo_; - - // Either: - // - One of the variables occurs at the top. This can be copied as the result - // - There is only one aggregate, and it appears at the top. No substitutions - // necessary, can evaluate aggregate and copy results - // - Possibly multiple aggregates and occurrences of grouped variables. All - // have to be substituted away before evaluation - - auto substitutions = alias.groupedVariables_; - auto topLevelGroupedVariable = std::ranges::find_if( - substitutions, [](HashMapGroupedVariableInformation& val) { - return std::get_if(&val.occurrences_); - }); - - if (topLevelGroupedVariable != substitutions.end()) { - // If the aggregate is at the top of the alias, e.g. `SELECT (?a as ?x) - // WHERE {...} GROUP BY ?a`, we can copy values directly from the column - // of the grouped variable - decltype(auto) groupValues = - result->getColumn(topLevelGroupedVariable->resultColumnIndex_) - .subspan(evaluationContext._beginIndex, evaluationContext.size()); - decltype(auto) outValues = result->getColumn(alias.outCol_); - std::ranges::copy(groupValues, - outValues.begin() + evaluationContext._beginIndex); - - // We also need to store it for possible future use - sparqlExpression::VectorWithMemoryLimit values(allocator); - values.resize(groupValues.size()); - std::ranges::copy(groupValues, values.begin()); - - evaluationContext._previousResultsFromSameGroup.at(alias.outCol_) = - sparqlExpression::copyExpressionResult( - sparqlExpression::ExpressionResult{std::move(values)}); - } else if (info.size() == 1 && !info.at(0).parentAndIndex_.has_value()) { - // Only one aggregate, and it is at the top of the alias, e.g. - // `(AVG(?x) as ?y)`. The grouped by variable cannot occur inside - // an aggregate, hence we don't need to substitute anything here - auto& aggregate = info.at(0); - - // Get aggregate results - auto aggregateResults = getHashMapAggregationResults( - result, aggregationData, aggregate.aggregateDataIndex_, - evaluationContext._beginIndex, evaluationContext._endIndex, localVocab, - allocator); + if (topLevelGroupedVariable != substitutions.end()) { + // If the aggregate is at the top of the alias, e.g. `SELECT (?a as ?x) + // WHERE {...} GROUP BY ?a`, we can copy values directly from the column + // of the grouped variable + decltype(auto) groupValues = + result->getColumn(topLevelGroupedVariable->resultColumnIndex_) + .subspan(evaluationContext._beginIndex, evaluationContext.size()); + decltype(auto) outValues = result->getColumn(alias.outCol_); + std::ranges::copy(groupValues, + outValues.begin() + evaluationContext._beginIndex); + + // We also need to store it for possible future use + sparqlExpression::VectorWithMemoryLimit values(allocator); + values.resize(groupValues.size()); + std::ranges::copy(groupValues, values.begin()); + + evaluationContext._previousResultsFromSameGroup.at(alias.outCol_) = + sparqlExpression::copyExpressionResult( + sparqlExpression::ExpressionResult{std::move(values)}); + } else if (info.size() == 1 && !info.at(0).parentAndIndex_.has_value()) { + // Only one aggregate, and it is at the top of the alias, e.g. + // `(AVG(?x) as ?y)`. The grouped by variable cannot occur inside + // an aggregate, hence we don't need to substitute anything here + auto& aggregate = info.at(0); + + // Get aggregate results + auto aggregateResults = getHashMapAggregationResults( + result, aggregationData, aggregate.aggregateDataIndex_, + evaluationContext._beginIndex, evaluationContext._endIndex, + localVocab, allocator); + + // Copy to result table + decltype(auto) outValues = result->getColumn(alias.outCol_); + std::ranges::copy(aggregateResults, + outValues.begin() + evaluationContext._beginIndex); + + // Copy the result so that future aliases may reuse it + evaluationContext._previousResultsFromSameGroup.at(alias.outCol_) = + sparqlExpression::copyExpressionResult( + sparqlExpression::ExpressionResult{std::move(aggregateResults)}); + } else { + for (const auto& substitution : substitutions) { + const auto& occurrences = + get>(substitution.occurrences_); + // Substitute in the values of the grouped variable + substituteGroupVariable(occurrences, result, + evaluationContext._beginIndex, + evaluationContext.size(), + substitution.resultColumnIndex_, allocator); + } - // Copy to result table - decltype(auto) outValues = result->getColumn(alias.outCol_); - std::ranges::copy(aggregateResults, - outValues.begin() + evaluationContext._beginIndex); + // Substitute in the results of all aggregates contained in the + // expression of the current alias, if `info` is non-empty. + std::vector> + originalChildren = substituteAllAggregates( + info, evaluationContext._beginIndex, evaluationContext._endIndex, + aggregationData, result, localVocab, allocator); + + // Evaluate top-level alias expression + sparqlExpression::ExpressionResult expressionResult = + alias.expr_.getPimpl()->evaluate(&evaluationContext); + + // Restore original children. Only necessary when the expression will be + // used in the future (not the case for the hash map optimization). + // TODO Use `std::views::zip(info, originalChildren)`. + for (size_t i = 0; i < info.size(); ++i) { + auto& aggregate = info.at(i); + auto parentAndIndex = aggregate.parentAndIndex_.value(); + parentAndIndex.parent_->replaceChild(parentAndIndex.nThChild_, + std::move(originalChildren.at(i))); + } - // Copy the result so that future aliases may reuse it - evaluationContext._previousResultsFromSameGroup.at(alias.outCol_) = - sparqlExpression::copyExpressionResult( - sparqlExpression::ExpressionResult{std::move(aggregateResults)}); - } else { - for (const auto& substitution : substitutions) { - const auto& occurrences = - get>(substitution.occurrences_); - // Substitute in the values of the grouped variable - substituteGroupVariable( - occurrences, result, evaluationContext._beginIndex, - evaluationContext.size(), substitution.resultColumnIndex_, allocator); - } + // Copy the result so that future aliases may reuse it + evaluationContext._previousResultsFromSameGroup.at(alias.outCol_) = + sparqlExpression::copyExpressionResult(expressionResult); - // Substitute in the results of all aggregates contained in the - // expression of the current alias, if `info` is non-empty. - std::vector> - originalChildren = substituteAllAggregates( - info, evaluationContext._beginIndex, evaluationContext._endIndex, - aggregationData, result, localVocab, allocator); - - // Evaluate top-level alias expression - sparqlExpression::ExpressionResult expressionResult = - alias.expr_.getPimpl()->evaluate(&evaluationContext); - - // Restore original children. Only necessary when the expression will be - // used in the future (not the case for the hash map optimization). - // TODO Use `std::views::zip(info, originalChildren)`. - for (size_t i = 0; i < info.size(); ++i) { - auto& aggregate = info.at(i); - auto parentAndIndex = aggregate.parentAndIndex_.value(); - parentAndIndex.parent_->replaceChild(parentAndIndex.nThChild_, - std::move(originalChildren.at(i))); + // Extract values + extractValues(std::move(expressionResult), evaluationContext, result, + localVocab, alias.outCol_); } - - // Copy the result so that future aliases may reuse it - evaluationContext._previousResultsFromSameGroup.at(alias.outCol_) = - sparqlExpression::copyExpressionResult(expressionResult); - - // Extract values - extractValues(std::move(expressionResult), evaluationContext, result, - localVocab, alias.outCol_); } -} -// _____________________________________________________________________________ -sparqlExpression::ExpressionResult -GroupBy::evaluateChildExpressionOfAggregateFunction( - const HashMapAggregateInformation& aggregate, - sparqlExpression::EvaluationContext& evaluationContext) { - // The code below assumes that DISTINCT is not supported yet. - AD_CORRECTNESS_CHECK(aggregate.expr_->isAggregate() == - sparqlExpression::SparqlExpression::AggregateStatus:: - NonDistinctAggregate); - // Evaluate child expression on block - auto exprChildren = aggregate.expr_->children(); - // `COUNT(*)` is the only expression without children, so we fake the - // expression result in this case by providing an arbitrary, constant and - // defined value. This value will be verified as non-undefined by the - // `CountExpression` class and ignored afterward as long as `DISTINCT` is - // not set (which is not supported yet). - bool isCountStar = - dynamic_cast(aggregate.expr_); - AD_CORRECTNESS_CHECK(isCountStar || exprChildren.size() == 1); - return isCountStar ? Id::makeFromBool(true) - : exprChildren[0]->evaluate(&evaluationContext); -} - -// _____________________________________________________________________________ -template -IdTable GroupBy::createResultFromHashMap( - const HashMapAggregationData& aggregationData, - std::vector& aggregateAliases, - LocalVocab* localVocab) const { - // Create result table, filling in the group values, since they might be - // required in evaluation - ad_utility::Timer sortingTimer{ad_utility::Timer::Started}; - auto sortedKeys = aggregationData.getSortedGroupColumns(); - runtimeInfo().addDetail("timeResultSorting", sortingTimer.msecs()); - - size_t numberOfGroups = aggregationData.getNumberOfGroups(); - IdTable result{getResultWidth(), getExecutionContext()->getAllocator()}; - result.resize(numberOfGroups); - - // Copy grouped by values - for (size_t idx = 0; idx < aggregationData.numOfGroupedColumns_; ++idx) { - std::ranges::copy(sortedKeys.at(idx), result.getColumn(idx).begin()); - } + // _____________________________________________________________________________ + sparqlExpression::ExpressionResult + GroupBy::evaluateChildExpressionOfAggregateFunction( + const HashMapAggregateInformation& aggregate, + sparqlExpression::EvaluationContext& evaluationContext) { + // The code below assumes that DISTINCT is not supported yet. + AD_CORRECTNESS_CHECK(aggregate.expr_->isAggregate() == + sparqlExpression::SparqlExpression::AggregateStatus:: + NonDistinctAggregate); + // Evaluate child expression on block + auto exprChildren = aggregate.expr_->children(); + // `COUNT(*)` is the only expression without children, so we fake the + // expression result in this case by providing an arbitrary, constant and + // defined value. This value will be verified as non-undefined by the + // `CountExpression` class and ignored afterward as long as `DISTINCT` is + // not set (which is not supported yet). + bool isCountStar = + dynamic_cast(aggregate.expr_); + AD_CORRECTNESS_CHECK(isCountStar || exprChildren.size() == 1); + return isCountStar ? Id::makeFromBool(true) + : exprChildren[0]->evaluate(&evaluationContext); + } + + // _____________________________________________________________________________ + template + IdTable GroupBy::createResultFromHashMap( + const HashMapAggregationData& aggregationData, + std::vector& aggregateAliases, + LocalVocab* localVocab) const { + // Create result table, filling in the group values, since they might be + // required in evaluation + ad_utility::Timer sortingTimer{ad_utility::Timer::Started}; + auto sortedKeys = aggregationData.getSortedGroupColumns(); + runtimeInfo().addDetail("timeResultSorting", sortingTimer.msecs()); + + size_t numberOfGroups = aggregationData.getNumberOfGroups(); + IdTable result{getResultWidth(), getExecutionContext()->getAllocator()}; + result.resize(numberOfGroups); + + // Copy grouped by values + for (size_t idx = 0; idx < aggregationData.numOfGroupedColumns_; ++idx) { + std::ranges::copy(sortedKeys.at(idx), result.getColumn(idx).begin()); + } - // Initialize evaluation context - sparqlExpression::EvaluationContext evaluationContext = - createEvaluationContext(*localVocab, result); + // Initialize evaluation context + sparqlExpression::EvaluationContext evaluationContext = + createEvaluationContext(*localVocab, result); - ad_utility::Timer evaluationAndResultsTimer{ad_utility::Timer::Started}; - for (size_t i = 0; i < numberOfGroups; i += GROUP_BY_HASH_MAP_BLOCK_SIZE) { - checkCancellation(); + ad_utility::Timer evaluationAndResultsTimer{ad_utility::Timer::Started}; + for (size_t i = 0; i < numberOfGroups; i += GROUP_BY_HASH_MAP_BLOCK_SIZE) { + checkCancellation(); - evaluationContext._beginIndex = i; - evaluationContext._endIndex = - std::min(i + GROUP_BY_HASH_MAP_BLOCK_SIZE, numberOfGroups); + evaluationContext._beginIndex = i; + evaluationContext._endIndex = + std::min(i + GROUP_BY_HASH_MAP_BLOCK_SIZE, numberOfGroups); - for (auto& alias : aggregateAliases) { - evaluateAlias(alias, &result, evaluationContext, aggregationData, - localVocab, allocator()); + for (auto& alias : aggregateAliases) { + evaluateAlias(alias, &result, evaluationContext, aggregationData, + localVocab, allocator()); + } } + runtimeInfo().addDetail("timeEvaluationAndResults", + evaluationAndResultsTimer.msecs()); + return result; } - runtimeInfo().addDetail("timeEvaluationAndResults", - evaluationAndResultsTimer.msecs()); - return result; -} -// _____________________________________________________________________________ -// Visitor function to extract values from the result of an evaluation of -// the child expression of an aggregate, and subsequently processing the -// values by calling the `addValue` function of the corresponding aggregate. -static constexpr auto makeProcessGroupsVisitor = - [](size_t blockSize, - const sparqlExpression::EvaluationContext* evaluationContext, - const std::vector& hashEntries) { - return [blockSize, evaluationContext, - &hashEntries]( - T&& singleResult, A& aggregationDataVector) { - auto generator = sparqlExpression::detail::makeGenerator( - std::forward(singleResult), blockSize, evaluationContext); - - auto hashEntryIndex = 0; - - for (const auto& val : generator) { - auto vectorOffset = hashEntries[hashEntryIndex]; - auto& aggregateData = aggregationDataVector.at(vectorOffset); - - aggregateData.addValue(val, evaluationContext); - - ++hashEntryIndex; - } - }; - }; - -// _____________________________________________________________________________ -template -IdTable GroupBy::computeGroupByForHashMapOptimization( - std::vector& aggregateAliases, - const IdTable& subresult, const std::vector& columnIndices, - LocalVocab* localVocab) const { - AD_CONTRACT_CHECK(columnIndices.size() == NUM_GROUP_COLUMNS || - NUM_GROUP_COLUMNS == 0); - - // Initialize aggregation data - HashMapAggregationData aggregationData( - getExecutionContext()->getAllocator(), aggregateAliases, - columnIndices.size()); - - // Initialize evaluation context - sparqlExpression::EvaluationContext evaluationContext( - *getExecutionContext(), _subtree->getVariableColumns(), subresult, - getExecutionContext()->getAllocator(), *localVocab, cancellationHandle_, - deadline_); + // _____________________________________________________________________________ + // Visitor function to extract values from the result of an evaluation of + // the child expression of an aggregate, and subsequently processing the + // values by calling the `addValue` function of the corresponding aggregate. + static constexpr auto makeProcessGroupsVisitor = + [](size_t blockSize, + const sparqlExpression::EvaluationContext* evaluationContext, + const std::vector& hashEntries) { + return [blockSize, evaluationContext, + &hashEntries]( + T&& singleResult, A& aggregationDataVector) { + auto generator = sparqlExpression::detail::makeGenerator( + std::forward(singleResult), blockSize, evaluationContext); - evaluationContext._groupedVariables = ad_utility::HashSet{ - _groupByVariables.begin(), _groupByVariables.end()}; - evaluationContext._isPartOfGroupBy = true; + auto hashEntryIndex = 0; - ad_utility::Timer lookupTimer{ad_utility::Timer::Stopped}; - ad_utility::Timer aggregationTimer{ad_utility::Timer::Stopped}; - for (size_t i = 0; i < subresult.size(); i += GROUP_BY_HASH_MAP_BLOCK_SIZE) { - checkCancellation(); + for (const auto& val : generator) { + auto vectorOffset = hashEntries[hashEntryIndex]; + auto& aggregateData = aggregationDataVector.at(vectorOffset); - evaluationContext._beginIndex = i; - evaluationContext._endIndex = - std::min(i + GROUP_BY_HASH_MAP_BLOCK_SIZE, subresult.size()); + aggregateData.addValue(val, evaluationContext); - auto currentBlockSize = evaluationContext.size(); - - // Perform HashMap lookup once for all groups in current block - using U = HashMapAggregationData::template ArrayOrVector< - std::span>; - U groupValues; - resizeIfVector(groupValues, columnIndices.size()); + ++hashEntryIndex; + } + }; + }; - // TODO use views::enumerate - size_t j = 0; - for (auto& idx : columnIndices) { - groupValues[j] = subresult.getColumn(idx).subspan( - evaluationContext._beginIndex, currentBlockSize); - ++j; - } - lookupTimer.cont(); - auto hashEntries = aggregationData.getHashEntries(groupValues); - lookupTimer.stop(); - - aggregationTimer.cont(); - for (auto& aggregateAlias : aggregateAliases) { - for (auto& aggregate : aggregateAlias.aggregateInfo_) { - sparqlExpression::ExpressionResult expressionResult = - GroupBy::evaluateChildExpressionOfAggregateFunction( - aggregate, evaluationContext); - - auto& aggregationDataVariant = - aggregationData.getAggregationDataVariant( - aggregate.aggregateDataIndex_); - - std::visit(makeProcessGroupsVisitor(currentBlockSize, - &evaluationContext, hashEntries), - std::move(expressionResult), aggregationDataVariant); + // _____________________________________________________________________________ + template + Result GroupBy::computeGroupByForHashMapOptimization( + std::vector & aggregateAliases, auto subresults, + const std::vector& columnIndices) const { + AD_CORRECTNESS_CHECK(columnIndices.size() == NUM_GROUP_COLUMNS || + NUM_GROUP_COLUMNS == 0); + LocalVocab localVocab; + + // Initialize aggregation data + HashMapAggregationData aggregationData( + getExecutionContext()->getAllocator(), aggregateAliases, + columnIndices.size()); + + for (const auto& [inputTableRef, inputLocalVocab] : subresults) { + const IdTable& inputTable = inputTableRef; + localVocab.mergeWith(std::span{&inputLocalVocab, 1}); + // Initialize evaluation context + sparqlExpression::EvaluationContext evaluationContext( + *getExecutionContext(), _subtree->getVariableColumns(), inputTable, + getExecutionContext()->getAllocator(), localVocab, + cancellationHandle_, deadline_); + + evaluationContext._groupedVariables = ad_utility::HashSet{ + _groupByVariables.begin(), _groupByVariables.end()}; + evaluationContext._isPartOfGroupBy = true; + + ad_utility::Timer lookupTimer{ad_utility::Timer::Stopped}; + ad_utility::Timer aggregationTimer{ad_utility::Timer::Stopped}; + for (size_t i = 0; i < inputTable.size(); + i += GROUP_BY_HASH_MAP_BLOCK_SIZE) { + checkCancellation(); + + evaluationContext._beginIndex = i; + evaluationContext._endIndex = + std::min(i + GROUP_BY_HASH_MAP_BLOCK_SIZE, inputTable.size()); + + auto currentBlockSize = evaluationContext.size(); + + // Perform HashMap lookup once for all groups in current block + using U = HashMapAggregationData< + NUM_GROUP_COLUMNS>::template ArrayOrVector>; + U groupValues; + resizeIfVector(groupValues, columnIndices.size()); + + // TODO use views::enumerate + size_t j = 0; + for (auto& idx : columnIndices) { + groupValues[j] = inputTable.getColumn(idx).subspan( + evaluationContext._beginIndex, currentBlockSize); + ++j; + } + lookupTimer.cont(); + auto hashEntries = aggregationData.getHashEntries(groupValues); + lookupTimer.stop(); + + aggregationTimer.cont(); + for (auto& aggregateAlias : aggregateAliases) { + for (auto& aggregate : aggregateAlias.aggregateInfo_) { + sparqlExpression::ExpressionResult expressionResult = + GroupBy::evaluateChildExpressionOfAggregateFunction( + aggregate, evaluationContext); + + auto& aggregationDataVariant = + aggregationData.getAggregationDataVariant( + aggregate.aggregateDataIndex_); + + std::visit(makeProcessGroupsVisitor( + currentBlockSize, &evaluationContext, hashEntries), + std::move(expressionResult), aggregationDataVariant); + } + } + aggregationTimer.stop(); } + runtimeInfo().addDetail("timeMapLookup", lookupTimer.msecs()); + runtimeInfo().addDetail("timeAggregation", aggregationTimer.msecs()); } - aggregationTimer.stop(); - } - runtimeInfo().addDetail("timeMapLookup", lookupTimer.msecs()); - runtimeInfo().addDetail("timeAggregation", aggregationTimer.msecs()); - return createResultFromHashMap(aggregationData, aggregateAliases, localVocab); -} + IdTable resultTable = + createResultFromHashMap(aggregationData, aggregateAliases, &localVocab); + return {std::move(resultTable), resultSortedOn(), std::move(localVocab)}; + } -// _____________________________________________________________________________ -std::optional GroupBy::getVariableForNonDistinctCountOfSingleAlias() - const { - auto varAndDistinctness = getVariableForCountOfSingleAlias(); - if (!varAndDistinctness.has_value() || - varAndDistinctness.value().isDistinct_) { - return std::nullopt; + // _____________________________________________________________________________ + std::optional GroupBy::getVariableForNonDistinctCountOfSingleAlias() + const { + auto varAndDistinctness = getVariableForCountOfSingleAlias(); + if (!varAndDistinctness.has_value() || + varAndDistinctness.value().isDistinct_) { + return std::nullopt; + } + return std::move(varAndDistinctness.value().variable_); } - return std::move(varAndDistinctness.value().variable_); -} -// _____________________________________________________________________________ -std::optional -GroupBy::getVariableForCountOfSingleAlias() const { - return _aliases.size() == 1 - ? _aliases.front()._expression.getVariableForCount() - : std::nullopt; -} + // _____________________________________________________________________________ + std::optional< + sparqlExpression::SparqlExpressionPimpl::VariableAndDistinctness> + GroupBy::getVariableForCountOfSingleAlias() const { + return _aliases.size() == 1 + ? _aliases.front()._expression.getVariableForCount() + : std::nullopt; + } -// _____________________________________________________________________________ -bool GroupBy::isVariableBoundInSubtree(const Variable& variable) const { - return _subtree->getVariableColumnOrNullopt(variable).has_value(); -} + // _____________________________________________________________________________ + bool GroupBy::isVariableBoundInSubtree(const Variable& variable) const { + return _subtree->getVariableColumnOrNullopt(variable).has_value(); + } diff --git a/src/engine/GroupBy.h b/src/engine/GroupBy.h index afe824d49..87649272c 100644 --- a/src/engine/GroupBy.h +++ b/src/engine/GroupBy.h @@ -316,10 +316,10 @@ class GroupBy : public Operation { // Create result IdTable by using a HashMap mapping groups to aggregation data // and subsequently calling `createResultFromHashMap`. template - IdTable computeGroupByForHashMapOptimization( + Result computeGroupByForHashMapOptimization( std::vector& aggregateAliases, - const IdTable& subresult, const std::vector& columnIndices, - LocalVocab* localVocab) const; + auto subresults, + const std::vector& columnIndices) const; using AggregationData = std::variant, class EqualElem = absl::container_internal::hash_default_eq, class Alloc = ad_utility::AllocatorWithLimit>> -using HashMapWithMemoryLimit = HashMap; +using HashMapWithMemoryLimit = + std::conditional_t && + std::is_trivially_destructible_v, + HashMap, + std::unordered_map>; } // namespace ad_utility diff --git a/src/util/JoinAlgorithms/JoinAlgorithms.h b/src/util/JoinAlgorithms/JoinAlgorithms.h index 7231f9396..2cb3bcbd8 100644 --- a/src/util/JoinAlgorithms/JoinAlgorithms.h +++ b/src/util/JoinAlgorithms/JoinAlgorithms.h @@ -798,9 +798,11 @@ struct BlockZipperJoinImpl { auto& it = side.it_; auto& end = side.end_; for (size_t numBlocksRead = 0; it != end && numBlocksRead < 3; - ++it, ++numBlocksRead) { + ++it) { if (std::ranges::empty(*it)) { continue; + } else { + ++numBlocksRead; } if (!eq((*it)[0], currentEl)) { AD_CORRECTNESS_CHECK(lessThan_(currentEl, (*it)[0]));