gortiz opened a new pull request, #19905:
URL: https://github.com/apache/pinot/pull/19905

   ## Summary
   
   `ANY_VALUE` in a group-by query evaluated a transform input once per row 
instead of once per block. This change reads the input values once per block. 
One block of N rows did O(N^2) work before this change, and does O(N) work 
after it.
   
   ## Background
   
   `AnyValueAggregationFunction.aggregateHelper()` reads a non-dictionary input 
through `getDirectValue(bvs, i)`. That method called `bvs.get*ValuesSV()[i]` 
for each row.
   
   When the input is a transform, `TransformBlockValSet` does not cache the 
result. Each `get*ValuesSV()` call evaluates the transform on the full block 
again. In group-by mode the loop never stops early, so a block of N rows 
evaluates the transform N times. Without group-by, the loop stops after the 
first value, and the cost stays small.
   
   ```
   GroupByExecutor.process(block)                  one call, no termination 
check inside
   └─ AnyValueAggregationFunction.aggregateGroupBySV()
      └─ aggregateHelper(): for each row i (N rows)
         └─ getDirectValue(bvs, i)
            └─ TransformBlockValSet.getStringValuesSV()
               └─ jsonExtractScalar on all N rows   → N x N JSON parses
   ```
   
   We saw this on a production cluster with this query shape on the leaf stage 
of a multi-stage query:
   
   ```sql
   SELECT series_id, <time bucket>,
          LASTWITHTIME(value, ts, 'DOUBLE'),
          ANY_VALUE(jsonExtractScalar(attributes, 
'$[''cloud.availability_zone'']', 'STRING', 'unknown'))
   FROM t
   WHERE ...
   GROUP BY series_id, <time bucket>
   ```
   
   The `attributes` values are about 1.6 KB of JSON. A full block of 10,000 
rows did 100 million JSON parses. Each leaf worker stayed in one `process()` 
call for 15 to 40 minutes. The query timeout and the cancellation do not stop 
the work, because the work does not reach a termination check. As a result, the 
work continued for a long time after the query failed, and the CPU usage on the 
servers increased by about 5 cores each.
   
   ## Change
   
   - Replace `getDirectValue(BlockValSet, int)` with 
`getDirectValueReader(BlockValSet)`. The new method reads the values array once 
and returns an `IntFunction` over that array.
   - `aggregateHelper()` gets the reader once per block, before the loop over 
the rows.
   
   ## Tests
   
   `AnyValueGroupByRecomputationTest` calls `aggregateGroupBySV()` on a block 
of 1,000 rows with a non-dictionary STRING input. The test makes sure that 
`getStringValuesSV()` is called once. Before the fix, the test failed with 
1,000 calls.
   
   A local benchmark (not part of this PR) ran the query shape above on one 
segment, with one group per row and about 1.6 KB of JSON per row:
   
   | Rows in the block | Before | After | Baseline: `DISTINCTCOUNT` on the same 
transform |
   |---|---|---|---|
   | 1,000 | 1.9 s | 7 ms | 7 ms |
   | 2,000 | 16.7 s | 8 ms | 7 ms |
   | 4,000 | 31.6 s | 13 ms | 15 ms |
   | 8,000 | 238 s | 23 ms | 26 ms |
   
   The existing `AnyValueAggregationFunctionTest` tests (38) pass.
   
   ## Follow-up
   
   This change does not add a termination check inside 
`GroupByExecutor.process()`. A different aggregation with an expensive 
per-block cost can still run past the query deadline. That is a separate change.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to