jayzhan211 opened a new issue, #11901:
URL: https://github.com/apache/arrow-rs/issues/11901

   ### Is your feature request related to a problem or challenge?
   
   `RowConverter::convert_columns` on a dictionary array row-encodes the 
dictionary's whole values array, then copies each row's bytes out of it 
(`Codec::encoder`, `arrow-row/src/lib.rs:777` on main):
   
   ```rust
   Codec::Dictionary(converter, nulls) => {
       let values = array.as_any_dictionary().values().clone();
       let rows = converter.convert_columns(&[values])?;
   ```
   
   So converting a short slice of a large dictionary costs time proportional to 
the dictionary, not to its rows. #7996 fixed the same problem for lists by 
encoding only the referenced list values.
   
   The repro below (arrow 60.0.0) converts 8192 rows taken from the middle of a 
`Dictionary(Int32, Utf8)` whose values are all distinct:
   
   ```rust
   use arrow_array::types::Int32Type;
   use arrow_array::{Array, ArrayRef, DictionaryArray, Int32Array, StringArray};
   use arrow_cast::cast;
   use arrow_row::{RowConverter, SortField};
   use arrow_schema::DataType;
   use std::sync::Arc;
   use std::time::Instant;
   
   fn dictionary(values: usize) -> DictionaryArray<Int32Type> {
       let v = StringArray::from_iter_values((0..values).map(|i| 
format!("value_{i}")));
       let keys = Int32Array::from_iter_values(0..values as i32);
       DictionaryArray::try_new(keys, Arc::new(v)).unwrap()
   }
   
   fn per_convert(column: &ArrayRef) -> std::time::Duration {
       let converter = 
RowConverter::new(vec![SortField::new(column.data_type().clone())]).unwrap();
       converter.convert_columns(&[Arc::clone(column)]).unwrap(); // warm up
       let start = Instant::now();
       for _ in 0..20 {
           converter.convert_columns(&[Arc::clone(column)]).unwrap();
       }
       start.elapsed() / 20
   }
   
   fn main() {
       let rows = 8192;
       for values in [8_192, 100_000, 1_000_000] {
           // 8192 rows from the middle of a dictionary with `values` distinct 
strings
           let slice: ArrayRef = Arc::new(dictionary(values).slice((values - 
rows) / 2, rows));
           let utf8 = cast(&slice, &DataType::Utf8).unwrap();
           println!(
               "{values} values: Dictionary {:?}, same rows as Utf8 {:?}",
               per_convert(&slice),
               per_convert(&utf8)
           );
       }
   }
   ```
   
   | values in the dictionary | `convert_columns` of the 8192 dictionary rows 
(µs) | the same rows as `Utf8` (µs) |
   |---|---|---|
   | 8,192 | 80–87 | 40–42 |
   | 100,000 | 568–688 | 44–55 |
   | 1,000,000 | 6,753–7,292 | 55–68 |
   
   The input always has 8192 rows, but the time grows with the size of the 
dictionary. The table shows the range over 5 runs of the snippet (release 
build, Apple M4 Pro).
   
   In DataFusion this makes a spilling GROUP BY on a high-cardinality 
dictionary column effectively never finish. ClickBench Q33 (`GROUP BY "URL"` 
with Parquet dictionary reads enabled) takes about 15 s without a memory limit. 
With an 8 GB limit, a three-iteration run was still going after 5 hours, when I 
stopped it. All 12 worker threads were in this encoder, called from the 
sort-preserving merge of the aggregation's spilled runs.
   
   ### Describe the solution you'd like
   
   When a dictionary has more values than the array has rows, encode only the 
values its keys reference, as #7996 does for lists. For example, collect the 
referenced keys, encode just those values, and map each key to its encoded row. 
Arrays whose dictionary isn't larger than their row count keep the current path.
   
   ### Describe alternatives you've considered
   
   - **Compacting in each caller** (`take(values, keys)` with keys `0..len`) 
before converting. This works, but every caller that converts slices of 
dictionaries would have to do it.
   - **`garbage_collect_dictionary` first.** It still allocates a 
`values.len()` remap for each batch (`arrow-select/src/dictionary.rs:59`).
   
   ### Additional context
   
   - #11885 is the same kind of problem in `concat_dictionaries`.
   - Converting a dictionary is slower than converting the equivalent `Utf8` 
even when the dictionary has no extra values (about 2x at 8,192 values above). 
This issue is only about the part that grows with the dictionary size.
   
   I'm not planning to fix this myself, so anyone is welcome to pick it up. 
Feel free to ping me with questions about the cause or when you'd like a review.
   


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

Reply via email to