Skip to content

Commit c5cc3af

Browse files
committed
fix: size and compact dictionary state in aggregate spill batches
Dictionary rows were sized by their keys only, and `take` keeps every value of a dictionary, so a chunk of `array_agg` states over dictionary-encoded values carried the whole state dictionary, which replay could not hold. Dictionaries whose values are mostly unique, with fewer than two keys per value, now count the values their keys reference, and each spilled chunk holds only those values. Dictionaries whose values many keys share still count their keys only and stay shared, since compacting each chunk would write the shared values once per chunk. Replay also compacts the dictionaries of the nested columns of merged batches.
1 parent 4a3ef10 commit c5cc3af

7 files changed

Lines changed: 445 additions & 48 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎datafusion/physical-plan/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ arrow-data = { workspace = true }
6464
arrow-ipc = { workspace = true, features = ["lz4", "zstd"] }
6565
arrow-ord = { workspace = true }
6666
arrow-schema = { workspace = true }
67+
arrow-select = { workspace = true }
6768
async-trait = { workspace = true }
6869
bytes = { workspace = true }
6970
datafusion-common = { workspace = true }

‎datafusion/physical-plan/src/aggregates/spill.rs‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use crate::SendableRecordBatchStream;
2424
use crate::metrics::{BaselineMetrics, SpillMetrics};
2525
use crate::sorts::IncrementalSortIterator;
2626
use crate::sorts::streaming_merge::{SortedSpillFile, StreamingMergeBuilder};
27-
use crate::spill::gc_nested_view_arrays;
27+
use crate::spill::gc_nested_arrays;
2828
use crate::spill::spill_manager::SpillManager;
2929
use crate::stream::RecordBatchStreamAdapter;
3030

@@ -314,12 +314,13 @@ impl AggregateSpill {
314314
.with_intermediate_merge_sizing(Some(min_spill_batch_rows))
315315
.build()?;
316316
// A merged batch references the whole data buffers of the spilled
317-
// batches its rows come from. Replay keeps the list states of a group,
318-
// such as those of `array_agg`, until the group completes, so compact
319-
// them to hold only the bytes of their own rows.
317+
// batches its rows come from, and can hold all their dictionary
318+
// values. Replay keeps the list states of a group, such as those of
319+
// `array_agg`, until the group completes, so compact them to hold only
320+
// the bytes of their own rows.
320321
let merged: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
321322
merged.schema(),
322-
merged.map(|batch| gc_nested_view_arrays(&batch?)),
323+
merged.map(|batch| gc_nested_arrays(&batch?)),
323324
));
324325
let replay = ClusteredFinalAggregateStream::new_with_input_and_metrics(
325326
&replay_agg,

‎datafusion/physical-plan/src/sorts/stream.rs‎

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// SPDX-FileContributor: Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements
44

55
use crate::sorts::cursor::{ArrayValues, CursorArray, RowValues};
6+
use crate::spill::gc_columns;
67
use crate::spill::spilled_row_sizes::SpilledRowSizes;
78
use crate::{EmptyRecordBatchStream, SendableRecordBatchStream};
89
use crate::{PhysicalExpr, PhysicalSortExpr};
@@ -370,7 +371,9 @@ impl IncrementalSortIterator {
370371

371372
/// Also limits each chunk to `max_batch_bytes`, by the size its rows take
372373
/// once spilled (see [`SpilledRowSizes`]). A row larger than that is
373-
/// returned on its own.
374+
/// returned on its own. Chunks hold only the dictionary values of their
375+
/// own rows in the columns where that size counts them (see
376+
/// [`SpilledRowSizes::compacted_columns`]).
374377
pub(crate) fn with_max_batch_bytes(mut self, max_batch_bytes: usize) -> Self {
375378
self.max_batch_bytes = Some(max_batch_bytes);
376379
self
@@ -446,6 +449,17 @@ impl Iterator for IncrementalSortIterator {
446449
Ok(batch) => batch,
447450
Err(e) => return Some(Err(e.into())),
448451
};
452+
// `take` keeps every dictionary value, while the row sizes
453+
// count only the values of the rows of `compacted_columns`
454+
let new_batch = match &self.row_sizes {
455+
Some(row_sizes) if !row_sizes.compacted_columns().is_empty() => {
456+
match gc_columns(&new_batch, row_sizes.compacted_columns()) {
457+
Ok(batch) => batch,
458+
Err(e) => return Some(Err(e)),
459+
}
460+
}
461+
_ => new_batch,
462+
};
449463

450464
self.cursor += batch_size;
451465

@@ -485,7 +499,7 @@ impl FusedIterator for IncrementalSortIterator {}
485499
mod tests {
486500
use super::*;
487501
use crate::memory::MemoryStream;
488-
use arrow::array::{ArrayRef, AsArray, Int32Array, StringArray};
502+
use arrow::array::{ArrayRef, AsArray, DictionaryArray, Int32Array, StringArray};
489503
use arrow::datatypes::{DataType, Field, Int32Type};
490504
use arrow_schema::SchemaRef;
491505
use datafusion_common::DataFusionError;
@@ -696,6 +710,48 @@ mod tests {
696710
Ok(())
697711
}
698712

713+
#[test]
714+
fn incremental_sort_iterator_compacts_unique_dictionaries() -> Result<()> {
715+
// Keys 99 down to 0, each with its own 100 byte value in `unique`, and
716+
// one of four values, each of 25 keys, in `shared`
717+
let keys: Vec<i32> = (0..100).rev().collect();
718+
let values: Vec<String> = keys.iter().map(|key| format!("{key:0>100}")).collect();
719+
let unique: DictionaryArray<Int32Type> =
720+
values.iter().map(String::as_str).collect();
721+
let shared: DictionaryArray<Int32Type> = keys
722+
.iter()
723+
.map(|key| ["a", "b", "c", "d"][*key as usize / 25])
724+
.collect();
725+
let shared_values = shared.values().to_data();
726+
let batch = RecordBatch::try_from_iter(vec![
727+
("key", Arc::new(Int32Array::from(keys)) as ArrayRef),
728+
("unique", Arc::new(unique) as ArrayRef),
729+
("shared", Arc::new(shared) as ArrayRef),
730+
])?;
731+
let expressions = LexOrdering::new(vec![PhysicalSortExpr::new_default(col(
732+
"key",
733+
&batch.schema(),
734+
)?)])
735+
.unwrap();
736+
737+
// A row takes 4 bytes of key and 4 of `shared` key, plus a 4 byte key,
738+
// a 4 byte offset and 100 bytes of value in `unique`, so 10 rows fit
739+
// in 1200 bytes
740+
let chunks = IncrementalSortIterator::new(batch, expressions, 50)
741+
.with_max_batch_bytes(1200)
742+
.collect::<Result<Vec<_>>>()?;
743+
assert_eq!(chunks.len(), 10);
744+
for chunk in &chunks {
745+
// Each chunk holds the unique values of its own rows, and shares
746+
// the values of the other dictionary
747+
let unique = chunk.column(1).as_any_dictionary();
748+
assert_eq!(unique.values().len(), chunk.num_rows());
749+
let shared = chunk.column(2).as_any_dictionary();
750+
assert!(shared.values().to_data().ptr_eq(&shared_values));
751+
}
752+
Ok(())
753+
}
754+
699755
fn assert_iterator_size_hint(iter: &IncrementalSortIterator, expected_len: usize) {
700756
assert_eq!(iter.size_hint(), (expected_len, Some(expected_len)));
701757
}

‎datafusion/physical-plan/src/spill/mod.rs‎

Lines changed: 112 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ use std::sync::{Arc, LazyLock};
2222
use std::task::{Context, Poll};
2323

2424
use arrow::array::{
25-
Array, ArrayRef, BinaryViewArray, BufferSpec, GenericByteViewArray, StringViewArray,
26-
layout, make_array,
25+
Array, ArrayRef, AsArray, BinaryViewArray, BufferSpec, GenericByteViewArray,
26+
StringViewArray, layout, make_array,
2727
};
2828
use arrow::buffer::{Buffer, ScalarBuffer};
2929
use arrow::datatypes::DataType;
@@ -38,6 +38,7 @@ use arrow_data::{ArrayDataBuilder, ByteView, MAX_INLINE_VIEW_LEN};
3838
#[cfg(test)]
3939
use arrow_ipc::writer::StreamWriter;
4040
use arrow_ipc::{CompressionType, root_as_message};
41+
use arrow_select::dictionary::garbage_collect_any_dictionary;
4142

4243
use datafusion_common::config::SpillCompression;
4344
use datafusion_common::{Result, internal_datafusion_err};
@@ -772,33 +773,45 @@ const VIEW_SIZE_BYTES: usize = 16;
772773
/// `List`, `Map`, `Union`, `Dictionary`, and other child-bearing arrays are compacted too
773774
/// - The Arrow `gc()` method itself is optimized and only copies referenced data
774775
pub(crate) fn gc_view_arrays(batch: &RecordBatch) -> Result<RecordBatch> {
775-
gc_view_arrays_of_columns(batch, |_| true)
776+
gc_arrays_of_columns(batch, |_, _| true, false)
776777
}
777778

778-
/// Like [`gc_view_arrays`], but only compacts the view arrays of nested
779-
/// columns, such as lists of strings.
779+
/// Like [`gc_view_arrays`], but only compacts `columns`, and also removes the
780+
/// dictionary values that no key of theirs references.
781+
///
782+
/// `take` and `slice` keep every value of a dictionary, so a few rows taken
783+
/// from a large batch carry the values of all its rows.
784+
pub(crate) fn gc_columns(batch: &RecordBatch, columns: &[usize]) -> Result<RecordBatch> {
785+
gc_arrays_of_columns(batch, |index, _| columns.contains(&index), true)
786+
}
787+
788+
/// Like [`gc_view_arrays`], but only compacts nested columns, such as lists of
789+
/// strings, and also removes the dictionary values that no key of theirs
790+
/// references.
780791
///
781792
/// A batch built by `interleave`, such as the output of a sort-preserving
782-
/// merge, references the whole data buffers of the batches it takes rows from.
783-
/// An accumulator that keeps the arrays it merges, such as `array_agg`, keeps
784-
/// those buffers alive with each group. Top-level columns are left as they
785-
/// are, since group keys and the accumulators of top-level states typically
786-
/// copy the values they keep.
787-
pub(crate) fn gc_nested_view_arrays(batch: &RecordBatch) -> Result<RecordBatch> {
788-
gc_view_arrays_of_columns(batch, |array| array.data_type().is_nested())
793+
/// merge, references the whole data buffers of the batches it takes rows
794+
/// from, and can hold all their dictionary values. An accumulator that keeps
795+
/// the arrays it merges, such as `array_agg`, keeps them alive with each
796+
/// group. Top-level columns are left as they are, since group keys and the
797+
/// accumulators of top-level states typically copy the values they keep.
798+
pub(crate) fn gc_nested_arrays(batch: &RecordBatch) -> Result<RecordBatch> {
799+
gc_arrays_of_columns(batch, |_, array| array.data_type().is_nested(), true)
789800
}
790801

791-
/// Compacts the view arrays of the columns of `batch` that `include` selects.
792-
fn gc_view_arrays_of_columns(
802+
/// Compacts the view arrays of the columns of `batch` that `include` selects
803+
/// by index, and their dictionary arrays if `dictionaries` is set.
804+
fn gc_arrays_of_columns(
793805
batch: &RecordBatch,
794-
include: impl Fn(&ArrayRef) -> bool,
806+
include: impl Fn(usize, &ArrayRef) -> bool,
807+
dictionaries: bool,
795808
) -> Result<RecordBatch> {
796809
let mut mutated = false;
797810
let mut new_columns: Vec<Arc<dyn Array>> = Vec::with_capacity(batch.num_columns());
798811

799-
for array in batch.columns() {
800-
let (gc_array, array_mutated) = if include(array) {
801-
gc_array(array)?
812+
for (index, array) in batch.columns().iter().enumerate() {
813+
let (gc_array, array_mutated) = if include(index, array) {
814+
gc_array(array, dictionaries)?
802815
} else {
803816
(Arc::clone(array), false)
804817
};
@@ -813,7 +826,9 @@ fn gc_view_arrays_of_columns(
813826
}
814827
}
815828

816-
fn gc_array(array: &ArrayRef) -> Result<(ArrayRef, bool)> {
829+
/// Compacts the view arrays in `array`, and its dictionary arrays if
830+
/// `dictionaries` is set. Also returns whether anything was compacted.
831+
fn gc_array(array: &ArrayRef, dictionaries: bool) -> Result<(ArrayRef, bool)> {
817832
match array.data_type() {
818833
DataType::Utf8View => {
819834
let string_view = array
@@ -835,7 +850,18 @@ fn gc_array(array: &ArrayRef) -> Result<(ArrayRef, bool)> {
835850
None => (Arc::clone(array), false),
836851
})
837852
}
838-
_ => gc_array_children(array),
853+
DataType::Dictionary(_, _) if dictionaries => {
854+
let dictionary = array.as_any_dictionary();
855+
let gc = garbage_collect_any_dictionary(dictionary)?;
856+
let gc = gc.as_any_dictionary();
857+
let shrunk = gc.values().len() < dictionary.values().len();
858+
let (values, values_mutated) = gc_array(gc.values(), dictionaries)?;
859+
if !shrunk && !values_mutated {
860+
return Ok((Arc::clone(array), false));
861+
}
862+
Ok((gc.with_values(values), true))
863+
}
864+
_ => gc_array_children(array, dictionaries),
839865
}
840866
}
841867

@@ -920,7 +946,7 @@ fn dedup_buffer_entries(buffers: &[Buffer]) -> (Vec<u32>, Vec<Buffer>) {
920946
(remap, unique)
921947
}
922948

923-
fn gc_array_children(array: &ArrayRef) -> Result<(ArrayRef, bool)> {
949+
fn gc_array_children(array: &ArrayRef, dictionaries: bool) -> Result<(ArrayRef, bool)> {
924950
let data = array.to_data();
925951
if data.child_data().is_empty() {
926952
return Ok((Arc::clone(array), false));
@@ -930,7 +956,7 @@ fn gc_array_children(array: &ArrayRef) -> Result<(ArrayRef, bool)> {
930956
let mut child_data = Vec::with_capacity(data.child_data().len());
931957
for child in data.child_data() {
932958
let child_array = make_array(child.clone());
933-
let (gc_child, child_mutated) = gc_array(&child_array)?;
959+
let (gc_child, child_mutated) = gc_array(&child_array, dictionaries)?;
934960
mutated |= child_mutated;
935961
child_data.push(gc_child.to_data());
936962
}
@@ -2719,7 +2745,7 @@ mod tests {
27192745
}
27202746

27212747
#[test]
2722-
fn test_gc_nested_view_arrays_skips_top_level_columns() -> Result<()> {
2748+
fn test_gc_nested_arrays_skips_top_level_columns() -> Result<()> {
27232749
use arrow::buffer::Buffer;
27242750

27252751
let strings: Vec<String> = (0..400)
@@ -2749,9 +2775,9 @@ mod tests {
27492775
let batch =
27502776
RecordBatch::try_new(schema, vec![Arc::clone(&top_level), list_array])?;
27512777

2752-
// `gc_view_arrays` compacts the top-level column, `gc_nested_view_arrays` does not
2778+
// `gc_view_arrays` compacts the top-level column, `gc_nested_arrays` does not
27532779
assert!(!Arc::ptr_eq(gc_view_arrays(&batch)?.column(0), &top_level));
2754-
let gc_batch = gc_nested_view_arrays(&batch)?;
2780+
let gc_batch = gc_nested_arrays(&batch)?;
27552781
assert!(Arc::ptr_eq(gc_batch.column(0), &top_level));
27562782

27572783
let gc_list_values =
@@ -2768,6 +2794,67 @@ mod tests {
27682794
Ok(())
27692795
}
27702796

2797+
#[test]
2798+
fn test_gc_columns_compacts_dictionaries() -> Result<()> {
2799+
use arrow::array::{DictionaryArray, ListArray};
2800+
use arrow::buffer::OffsetBuffer;
2801+
use arrow::datatypes::Int32Type;
2802+
2803+
// Every column references a few of the same 100 values
2804+
let values: ArrayRef = Arc::new(StringArray::from_iter_values(
2805+
(0..100).map(|i| format!("value {i}")),
2806+
));
2807+
let dictionary = |keys: Vec<i32>| {
2808+
DictionaryArray::new(Int32Array::from(keys), Arc::clone(&values))
2809+
};
2810+
let decode = |array: &ArrayRef| -> Vec<String> {
2811+
let dictionary = array.as_dictionary::<Int32Type>();
2812+
let strings = dictionary.downcast_dict::<StringArray>().unwrap();
2813+
strings
2814+
.into_iter()
2815+
.map(|s| s.unwrap().to_string())
2816+
.collect()
2817+
};
2818+
let list = ListArray::new(
2819+
Arc::new(Field::new_list_field(
2820+
DataType::Dictionary(Box::new(DataType::Int32), Box::new(DataType::Utf8)),
2821+
true,
2822+
)),
2823+
OffsetBuffer::from_lengths([2, 1]),
2824+
Arc::new(dictionary(vec![7, 3, 7])),
2825+
None,
2826+
);
2827+
let batch = RecordBatch::try_from_iter(vec![
2828+
("compacted", Arc::new(dictionary(vec![5, 9])) as ArrayRef),
2829+
("other", Arc::new(dictionary(vec![1, 2])) as ArrayRef),
2830+
("list", Arc::new(list) as ArrayRef),
2831+
])?;
2832+
2833+
// `gc_view_arrays` leaves dictionaries as they are
2834+
let gc_batch = gc_view_arrays(&batch)?;
2835+
for (column, gc_column) in batch.columns().iter().zip(gc_batch.columns()) {
2836+
assert!(Arc::ptr_eq(column, gc_column));
2837+
}
2838+
2839+
// `gc_columns` keeps only the values that the given columns reference
2840+
let gc_batch = gc_columns(&batch, &[0, 2])?;
2841+
let compacted = gc_batch.column(0);
2842+
assert_eq!(compacted.as_any_dictionary().values().len(), 2);
2843+
assert_eq!(decode(compacted), ["value 5", "value 9"]);
2844+
assert!(Arc::ptr_eq(gc_batch.column(1), batch.column(1)));
2845+
let list_values = gc_batch.column(2).as_list::<i32>().values();
2846+
assert_eq!(list_values.as_any_dictionary().values().len(), 2);
2847+
assert_eq!(decode(list_values), ["value 7", "value 3", "value 7"]);
2848+
2849+
// `gc_nested_arrays` compacts only the nested column
2850+
let gc_batch = gc_nested_arrays(&batch)?;
2851+
assert!(Arc::ptr_eq(gc_batch.column(0), batch.column(0)));
2852+
let list_values = gc_batch.column(2).as_list::<i32>().values();
2853+
assert_eq!(list_values.as_any_dictionary().values().len(), 2);
2854+
2855+
Ok(())
2856+
}
2857+
27712858
#[tokio::test]
27722859
async fn test_spill_file_size_gc_verification_string_view() -> Result<()> {
27732860
use arrow::array::StringViewArray;

0 commit comments

Comments
 (0)