kosiew commented on code in PR #23869:
URL: https://github.com/apache/datafusion/pull/23869#discussion_r3681020879


##########
datafusion/physical-plan/src/topk/mod.rs:
##########
@@ -1811,6 +1811,394 @@ impl PartitionedTopKRank {
     }
 }
 
+/// Per-partition state for `DENSE_RANK()` semantics.
+///
+/// A `HashMap<Vec<u8>, Vec<TieEntry>>` keyed by the row-encoded ORDER BY
+/// bytes, capped at `k` distinct keys. Each key's `Vec<TieEntry>` holds
+/// every row seen at that ob value, one entry per contributing source
+/// `RecordBatch`. `TieEntry` is reused verbatim from [`RankPartitionState`].
+struct DenseRankPartitionState {
+    groups: HashMap<Vec<u8>, Vec<TieEntry>>,
+    /// Cached admission boundary: the largest tracked ob value once
+    /// `groups` is full. A `HashMap` is unordered, so finding it otherwise
+    /// costs an O(K) scan of the keys on *every* new distinct ob value —
+    /// O(N·K) overall on mostly-distinct input. Caching makes the common
+    /// "new ob is worse than the boundary → drop" path O(1); the O(K) scan
+    /// is paid only when the cache is stale (`None`) — after a fill or an
+    /// eviction changes the key set.
+    max_key: Option<Vec<u8>>,
+}
+
+impl DenseRankPartitionState {
+    fn size(&self) -> usize {
+        let table_overhead =
+            self.groups.capacity() * (size_of::<Vec<u8>>() + 
size_of::<Vec<TieEntry>>());
+        let contents: usize = self
+            .groups
+            .iter()
+            .map(|(key, entries)| {
+                key.capacity()
+                    + entries.capacity() * size_of::<TieEntry>()
+                    + entries
+                        .iter()
+                        .map(|e| {
+                            e.row_indices.capacity() * size_of::<u32>() + 
e.batch_bytes

Review Comment:
   I think there is a memory accounting issue here. 
`DenseRankPartitionState::size()` adds `e.batch_bytes` for every `TieEntry`, 
but the new dense-rank path creates one `TieEntry` per retained ORDER BY group 
while each entry only holds an `Arc` clone of the same source `RecordBatch` 
(see the insertions at lines 2056, 2068, and 2104).
   
   That means a single input batch with K retained distinct values is charged 
roughly `K * get_record_batch_memory_size(batch)`, even though the Arrow 
buffers are only retained once. The existing rank tie path has some precedent 
for conservative duplicate charging, but for dense-rank this becomes the common 
case because one batch can contribute many retained groups.
   
   I reproduced this with datafusion-cli using the script below. With 
`enable_window_topn=true`, `target_partitions=1`, `--batch-size 2000`, and 
`--memory-limit 50M`, the query fails trying to allocate about 2001.6 MiB for 
`PartitionedTopKDenseRank[0]`. Running the same query with 
`enable_window_topn=false` succeeds and returns 200 rows.
   
   ```bash
   python3 - <<'PY'
   with open('/tmp/df_dr.csv','w') as f:
       f.write('pk,val,payload\n')
       payload='x'*10000
       for g in range(1,1001):
           f.write(f'{g%2},{g},{payload}\n')
   PY
   
   cargo run -q -p datafusion-cli -- \
     --batch-size 2000 --memory-limit 50M \
     --command "
   SET datafusion.optimizer.enable_window_topn = true;
   SET datafusion.execution.target_partitions = 1;
   CREATE EXTERNAL TABLE dr(pk INT, val INT, payload VARCHAR)
   STORED AS CSV LOCATION '/tmp/df_dr.csv'
   OPTIONS (format.has_header true);
   
   SELECT count(pk), count(val), count(payload)
   FROM (
     SELECT pk, val, payload,
            DENSE_RANK() OVER (PARTITION BY pk ORDER BY val) AS drnk
     FROM dr
   )
   WHERE drnk <= 100;
   "
   ```
   
   Could we account for retained batches uniquely instead? For example, by 
reusing or extending the existing `RecordBatchStore` style accounting, or by 
storing batch IDs with ref-counted byte accounting. It would also be great to 
add a regression test covering the case where one batch contributes multiple 
retained dense-rank groups.



##########
datafusion/physical-plan/src/topk/mod.rs:
##########
@@ -1811,6 +1811,394 @@ impl PartitionedTopKRank {
     }
 }
 
+/// Per-partition state for `DENSE_RANK()` semantics.
+///
+/// A `HashMap<Vec<u8>, Vec<TieEntry>>` keyed by the row-encoded ORDER BY

Review Comment:
   Small naming suggestion. The dense-rank implementation currently describes 
the retained groups as `HashMap<Vec<u8>, Vec<TieEntry>>`. After fixing the 
memory accounting, it might be worth introducing a type name that reflects 
retained batch slices instead of reusing `TieEntry`, since these entries 
represent more than just boundary ties. I think that would make the ownership 
and accounting invariant a bit clearer.



-- 
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