jayzhan211 commented on code in PR #26953:
URL: https://github.com/apache/datafusion/pull/26953#discussion_r4492816067


##########
datafusion/physical-plan/src/sorts/sort.rs:
##########
@@ -1023,9 +1035,10 @@ impl ExternalSorter {
         }
 
         let expressions = self.expr.clone();
-        let batch_size = self.batch_size;
         let merge_pool = Arc::clone(&self.merge_pool);
         let elapsed_compute = self.metrics.baseline.elapsed_compute().clone();
+        // Set when `merge_run_batch_size` split this run
+        let split = batch_size < self.batch_size;

Review Comment:
   Sorry, my `gc` suggestion misses views that share bytes, which is what 
dictionary-encoded Parquet strings produce. A chunk that would write at least 
as much as the buffer holds skips `gc`, so all 8 chunks keep the buffer and 
each is charged for it. This fails at `-m 256M`, `512M` and `1G` with `Not 
enough memory to continue external sort`, and passes on `main`:
   
   ```sql
   COPY (SELECT value % 1000003 AS a, value AS b,
                concat(repeat('x', 240), (value % 1000)::varchar) AS c
         FROM range(5000000)) TO 'shared.parquet' STORED AS PARQUET;
   SET datafusion.execution.target_partitions = 1;
   SELECT * FROM 'shared.parquet' ORDER BY a, b;
   ```
   
   Could these runs keep `batch_size`? The helper below goes next to 
`gc_view_arrays` in `spill/mod.rs`, and `sort.rs` needs to import it.
   
   ```suggestion
           // Split batches of views sharing bytes would each be charged for 
them.
           let batch_size = if views_share_bytes(&batch) {
               self.batch_size
           } else {
               batch_size
           };
           // Set when `merge_run_batch_size` split this run
           let split = batch_size < self.batch_size;
   ```
   
   ```rs
   pub(crate) fn views_share_bytes(batch: &RecordBatch) -> bool {
       use arrow::array::AsArray;
       fn shares<T: ByteViewType>(array: &GenericByteViewArray<T>) -> bool {
           let (_, unique) = dedup_buffer_entries(array.data_buffers());
           array.total_buffer_bytes_used() > unique.iter().map(|b| 
b.len()).sum::<usize>()
       }
       batch.columns().iter().any(|array| match array.data_type() {
           DataType::Utf8View => shares(array.as_string_view()),
           DataType::BinaryView => shares(array.as_binary_view()),
           _ => false,
       })
   }
   ```
   
   Nested views already keep `batch_size` through `sorted_copy_keeps_buffers`, 
so only these two top-level types need the check. Could you add a test with a 
payload of `cast(&dict, &DataType::Utf8View)`, using 8192 keys over 1024 values 
of 64 bytes? It should succeed with correct output. Skip the `assert_released` 
peak check there, because the `grow` in `try_sort_and_spill_in_mem_batches` 
goes over the pool on `main` too. Please also unit test the boundary: 
`views_share_bytes` is `true` for that array and `false` for 8192 values each 
used once.



##########
datafusion/physical-plan/src/sorts/sort.rs:
##########
@@ -1036,7 +1049,16 @@ impl ExternalSorter {
             let schema = batch.schema();
 
             // Sort the batch immediately and get all output batches
-            let sorted_batches = sort_batch_chunked(&batch, &expressions, 
batch_size)?;
+            let mut sorted_batches =
+                sort_batch_chunked(&batch, &expressions, batch_size)?;
+            // `take` keeps the data buffers of view arrays, so splitting would
+            // charge every batch for all of them.
+            if split && sorted_batches.len() > 1 {

Review Comment:
   The per-chunk `gc` also causes a second failure, on clustered input. An 
output batch taken from one run holds 8 chunk buffers with no unused bytes, so 
`gc` skips it. After the spill is read back, each buffer is a slice of one IPC 
message and is charged for the whole message, so a batch's 
`max_record_batch_memory` reached 22.1 MB against 3.0 MB on `main`. This fails 
at `-m 32M` and `64M`, and passes on `main`:
   
   ```sql
   COPY (SELECT value / 1000 AS a, value AS b,
                concat(repeat('x', 60), value::varchar) AS c,
                concat(repeat('y', 60), value::varchar) AS d,
                concat(repeat('z', 60), value::varchar) AS e
         FROM range(5000000)) TO 'clustered3.parquet' STORED AS PARQUET;
   SET datafusion.execution.target_partitions = 1;
   SELECT * FROM 'clustered3.parquet' ORDER BY a, b;
   ```
   
   Could spilling compact these arrays to one buffer? With this and the 
`views_share_bytes` change, all of my probes match or beat `main`, and all 
`sorts::` and `spill::` tests pass, including yours. The issue's reproducer 
still passes too.
   
   ```diff
   --- a/datafusion/physical-plan/src/spill/mod.rs
   +++ b/datafusion/physical-plan/src/spill/mod.rs
   @@ fn compact_view_array
   -    if should_gc_view_array(array, unique_size) {
   +    // Several buffers read back as slices of one IPC message, each charged 
for all of it
   +    if should_gc_view_array(array, unique_size)
   +        || (unique.len() > 1 && array.total_buffer_bytes_used() <= 
unique_size)
   +    {
            return Some(array.gc());
        }
   ```
   
   The `<=` keeps `gc` away from views that share bytes, where it would copy 
once per row (#23564). Could you add a test with two `Utf8View` keys of unique 
66-byte strings, prefixed by batch number so the runs don't overlap, in a pool 
of 8 buffered batches? It should succeed; today it fails in 
`ExternalSorterMerge`.



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