Skip to content

Commit 6577a09

Browse files
kKPullameta-codesync[bot]
authored andcommitted
feat(exec): Push dynamic filters through StreamingAggregation grouping keys (#17918)
Summary: Pull Request resolved: #17918 Dynamic filters generated by a hash-join probe are pushed toward the probe-side source by tracing each filtered column through the identity projections of intervening operators. The trace stops at the first operator that does not expose the filtered channel as an identity projection. `StreamingAggregation` did not register its grouping keys in `identityProjections_`, unlike `HashAggregation`, so the trace broke at the aggregation and the filter never reached the scan. A plan with a streaming (segmented) aggregation between a hash-join probe and a table scan therefore missed dynamic filtering on the aggregation's grouping keys. Grouping keys are identity passthroughs to the output, so register them as identity projections and let the existing Driver trace push dynamic filters through to the source. No new operator support is needed. Fixes #17827 Reviewed By: mbasmanova Differential Revision: D109527298 fbshipit-source-id: 42ac6d6cd8ee358a3369926991b8d5c54975de8a
1 parent 29dd5b5 commit 6577a09

2 files changed

Lines changed: 88 additions & 0 deletions

File tree

velox/exec/StreamingAggregation.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ void StreamingAggregation::initialize() {
7575
groupingKeyTypes.push_back(inputType->childAt(channel));
7676
}
7777

78+
// Grouping keys pass through unchanged to the output: output channel 'i' is
79+
// input channel 'groupingKeys_[i]'. Exposing them as identity projections
80+
// lets dynamic filters on grouping keys be pushed down through this operator
81+
// to the source.
82+
for (column_index_t i = 0; i < groupingKeys_.size(); ++i) {
83+
identityProjections_.emplace_back(groupingKeys_[i], i);
84+
}
85+
7886
std::shared_ptr<core::ExpressionEvaluator> expressionEvaluator;
7987
aggregates_ = toAggregateInfo(
8088
*aggregationNode_, *operatorCtx_, numKeys, expressionEvaluator, true);

velox/exec/tests/HashJoinTestExtra.cpp

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1878,6 +1878,86 @@ TEST_P(HashJoinTest, dynamicFiltersPushDownThroughAgg) {
18781878
.run();
18791879
}
18801880

1881+
TEST_P(HashJoinTest, dynamicFiltersPushDownThroughStreamingAgg) {
1882+
const int32_t numRowsProbe = 300;
1883+
const int32_t numRowsBuild = 100;
1884+
1885+
// Probe c0 is strictly increasing (row - 10), so the input is already
1886+
// clustered on the grouping key required by StreamingAggregation.
1887+
std::vector<RowVectorPtr> probeVectors{makeRowVector({
1888+
makeFlatVector<int32_t>(numRowsProbe, [&](auto row) { return row - 10; }),
1889+
makeFlatVector<int64_t>(numRowsProbe, folly::identity),
1890+
})};
1891+
std::shared_ptr<TempFilePath> probeFile = TempFilePath::create();
1892+
writeToFile(probeFile->getPath(), probeVectors);
1893+
1894+
// Create build data
1895+
std::vector<RowVectorPtr> buildVectors{makeRowVector(
1896+
{"u0"}, {makeFlatVector<int32_t>(numRowsBuild, [&](auto row) {
1897+
return 35 + 2 * (row + numRowsBuild / 5);
1898+
})})};
1899+
1900+
createDuckDbTable("t", probeVectors);
1901+
createDuckDbTable("u", buildVectors);
1902+
1903+
auto probeType = ROW({"c0", "c1"}, {INTEGER(), BIGINT()});
1904+
1905+
// For sum() the partial accumulator equals the final value, so the same
1906+
// reference query and verifier apply to both single and partial streaming
1907+
// aggregation steps.
1908+
for (const auto step :
1909+
{core::AggregationNode::Step::kSingle,
1910+
core::AggregationNode::Step::kPartial}) {
1911+
SCOPED_TRACE(fmt::format("step {}", core::mapAggregationStepToName(step)));
1912+
auto planNodeIdGenerator = std::make_shared<core::PlanNodeIdGenerator>();
1913+
auto buildSide =
1914+
PlanBuilder(planNodeIdGenerator).values(buildVectors).planNode();
1915+
1916+
// Inner join.
1917+
core::PlanNodeId scanNodeId;
1918+
core::PlanNodeId joinNodeId;
1919+
core::PlanNodeId aggNodeId;
1920+
auto op = PlanBuilder(planNodeIdGenerator, pool_.get())
1921+
.tableScan(probeType)
1922+
.capturePlanNodeId(scanNodeId)
1923+
.streamingAggregation({"c0"}, {"sum(c1)"}, {}, step, false)
1924+
.capturePlanNodeId(aggNodeId)
1925+
.hashJoin(
1926+
{"c0"},
1927+
{"u0"},
1928+
buildSide,
1929+
"",
1930+
{"c0", "a0"},
1931+
core::JoinType::kInner)
1932+
.capturePlanNodeId(joinNodeId)
1933+
.planNode();
1934+
1935+
SplitPath splitPaths = {{scanNodeId, {probeFile->getPath()}}};
1936+
HashJoinBuilder(*pool_, duckDbQueryRunner_, driverExecutor_.get())
1937+
.planNode(std::move(op))
1938+
.inputSplits(splitPaths)
1939+
.injectSpill(false)
1940+
.checkSpillStats(false)
1941+
.referenceQuery(
1942+
"SELECT c0, sum(c1) FROM t, u WHERE c0 = u0 group by c0")
1943+
.verifier([&](const std::shared_ptr<Task>& task, bool hasSpill) {
1944+
auto planStats = toPlanStats(task->taskStats());
1945+
auto dynamicFilterStats = planStats.at(scanNodeId).dynamicFilterStats;
1946+
ASSERT_EQ(
1947+
1, getFiltersProduced(task, getOperatorIndex(joinNodeId)).sum);
1948+
ASSERT_EQ(
1949+
1, getFiltersAccepted(task, getOperatorIndex(scanNodeId)).sum);
1950+
ASSERT_LT(
1951+
getInputPositions(task, getOperatorIndex(aggNodeId)),
1952+
numRowsProbe);
1953+
ASSERT_EQ(
1954+
dynamicFilterStats.producerNodeIds,
1955+
std::unordered_set({joinNodeId}));
1956+
})
1957+
.run();
1958+
}
1959+
}
1960+
18811961
TEST_P(HashJoinTest, noDynamicFiltersPushDownThroughRightJoin) {
18821962
std::vector<RowVectorPtr> innerBuild = {makeRowVector(
18831963
{"a"},

0 commit comments

Comments
 (0)