Return one NULL row for last_by over a never-written device (#16985)#18267
Return one NULL row for last_by over a never-written device (#16985)#18267PDGGK wants to merge 1 commit into
Conversation
| // A no-GROUP-BY global aggregation whose device set resolves to zero devices | ||
| // (e.g. last_by on a device that was never written) has nothing for the device | ||
| // loop to iterate, so without this it would emit zero rows. Per SQL semantics a | ||
| // global aggregate over empty input must return exactly one row whose value is | ||
| // NULL (except count()); this one-shot flag drives that single row. | ||
| private boolean emptyGlobalResultEmitted = false; |
There was a problem hiding this comment.
I think it is better to use default Agg operator when the DeviceEntries is empty when generate Operator Tree. The empty process logic has been there.
Just modify one line in DataNodeTableOperatorGenerator is OK:
DataNodeTableOperatorGenerator -> visitAggregationTableScan ->
OptimizeType optimizeType = node.getDeviceEntries().isEmpty() ? OptimizeType.NOOP : canUseLastCacheOptimize( parameter.getTableAggregators(), node, parameter.getTimeColumnName());
…6985) A global aggregation without GROUP BY always produces exactly one row; over empty input every aggregate except count() evaluates to NULL. On the last-cache-optimized last/last_by path, a device set that resolves to zero devices (e.g. a filter on a device that was never written) left the operator with nothing to iterate, so it returned zero rows instead of the single NULL row required by SQL semantics (consistent with engines such as Trino). Route the empty-device case to the default aggregation operator, which already emits the single all-NULL row for a global aggregation over empty input: in DataNodeTableOperatorGenerator.visitAggregationTableScan, use OptimizeType.NOOP when the device set is empty so the last-cache optimization (which has no device to read) is skipped. The deleted-device case (the device still exists, so the operator iterates and already emits the NULL row) is unchanged; GROUP BY over an empty group still returns no rows. Signed-off-by: Zihan Dai <99155080+PDGGK@users.noreply.github.com>
8c7ddfc to
d2ad541
Compare
|
Thanks @Wei-hao-Li — that's cleaner, adopted. I switched to routing the empty device set to the default aggregation operator in One edge I wanted to flag: a plain OptimizeType optimizeType =
node.getDeviceEntries().isEmpty() && node.getGroupingKeys().isEmpty()
? OptimizeType.NOOP
: canUseLastCacheOptimize(
parameter.getTableAggregators(), node, parameter.getTimeColumnName());So the global (no-GROUP-BY) empty case falls to the default op and gets the single NULL row, while |
Description
Per @Wei-hao-Li's clarification in #16985: an aggregate function should always return NULL when there is no input data (except
count), consistent with SQL-standard engines such as Trino. Soselect last_by(s0, time) from t where deviceId = 'never'— where the device was never written — must return exactly one row whose value is NULL, not zero rows.Root cause: for the last-cache-optimized
last/last_bypath, a device set that resolves to zero devices leavesLastQueryAggTableScanOperatorwithallDeviceCount == 0, so the device loop never runs and the operator returns no rows.How
Following review feedback, rather than special-casing the last-cache operator, the empty-device case is routed to the default aggregation operator, which already emits the single all-NULL row required for a global aggregation over empty input. One change in
DataNodeTableOperatorGenerator.visitAggregationTableScan:When the device set resolves to zero devices and there are no grouping keys, the last-cache optimization is skipped (
NOOP) and planning falls toDefaultAggTableScanOperator. That operator builds anEMPTY_DEVICE_IDentry for an empty device set and itshasNext()is time-range-based, so the global aggregation still runs its time range once and each aggregator's no-inputevaluate()writes NULL. The change inLastQueryAggTableScanOperatoris reverted (net zero there).Guards:
deviceEntriesis non-empty and planning is identical to before.Tests
IoTDBDeletionTableIT: a never-written device queried with 2-arg and 3-arglast_byreturns one NULL row; multiplelast_byaggregates return one all-NULL row; GROUP BY over a never-written device returns no rows; and a regression check that a deleted device still returns one NULL row (both the last-value-cache and recomputed paths).Scope
Scoped to the last-cache
last/last_bypath, which is where the zero-row behavior originated. Other aggregations (max/min/sum/avg) do not use the last-cache optimization, so their planning is unchanged by this guard.Relates to #16985.