@@ -119,6 +119,39 @@ fn unsliced_hash_len(data: &ArrayData) -> usize {
119119 . fold ( own, usize:: max)
120120}
121121
122+ /// A build slice is this many times longer than the shared values beneath a
123+ /// dictionary or list view that it rehashes, so rehashing them adds at most
124+ /// about 1/16 to the hashing work.
125+ const SHARED_VALUES_SLICE_FACTOR : usize = 16 ;
126+
127+ /// The headroom from [`SHARED_VALUES_SLICE_FACTOR`] stops at this many rows
128+ /// (8 MiB of hashes), so a large merged dictionary cannot grow the hashes
129+ /// buffer and scratch space to the whole build side. Past it only the
130+ /// rehashing overhead grows, and it stays linear in the build rows.
131+ const MAX_HASH_CHUNK_ROWS : usize = 1 << 20 ;
132+
133+ /// The number of rows in each slice the hash map is filled from.
134+ ///
135+ /// A slice is never shorter than the largest build batch or the longest
136+ /// shared values under any key, which keeps hashing linear in the build rows.
137+ /// The overhead ratio is per key column, so the longest key counts, not the sum.
138+ /// Beyond that, a slice is [`SHARED_VALUES_SLICE_FACTOR`] times the shared
139+ /// values, up to [`MAX_HASH_CHUNK_ROWS`], and never longer than the build side.
140+ fn hash_chunk_rows (
141+ left_values : & [ ArrayRef ] ,
142+ max_batch_rows : usize ,
143+ num_rows : usize ,
144+ ) -> usize {
145+ let shared = left_values
146+ . iter ( )
147+ . map ( |values| unsliced_hash_len ( & values. to_data ( ) ) )
148+ . max ( )
149+ . unwrap_or ( 0 ) ;
150+ let base = max_batch_rows. max ( shared) ;
151+ let amortized = base. max ( shared. saturating_mul ( SHARED_VALUES_SLICE_FACTOR ) ) ;
152+ amortized. min ( base. max ( MAX_HASH_CHUNK_ROWS ) ) . min ( num_rows)
153+ }
154+
122155/// The longest array anywhere in `data`, including `data` itself.
123156fn subtree_max_len ( data : & ArrayData ) -> usize {
124157 data. child_data ( )
@@ -704,10 +737,11 @@ impl From<&HashJoinExec> for HashJoinExecBuilder {
704737/// hash table is updated by iterating the build-side rows in reverse order, which keeps rows
705738/// with smaller indices "on the top" of hash table. The build-side batches are first
706739/// concatenated in their input order. The hash table is then filled from slices of the
707- /// concatenated batch, at most as long as the largest build-side batch (or the longest
708- /// values under a dictionary or list view in a key, when that is longer), starting
709- /// from the last slice. Each slice is inserted with the offset of its first row in
710- /// the concatenated batch.
740+ /// concatenated batch, as long as the largest build-side batch. When a key has values
741+ /// under a dictionary or list view, which every slice hashes in full, slices grow to 16
742+ /// times the length of those values, capped at 2^20 rows unless the values alone are
743+ /// longer. Filling starts from the last slice, and each slice is inserted with the offset
744+ /// of its first row in the concatenated batch.
711745///
712746/// Example of build phase for 3 record batches, filled from slices of 3 rows:
713747///
@@ -3242,14 +3276,11 @@ fn build_in_memory(
32423276 let mut hashmap = new_join_hashmap ( num_rows, & mut reservation, metrics) ?;
32433277
32443278 // Every slice shares everything beneath a dictionary or list view,
3245- // at any depth of a key, which `create_hashes` hashes on each call: a
3246- // slice at least as long as the longest of those arrays keeps that
3247- // cost linear in the build rows. No slice is longer than the build side.
3248- let chunk_rows = left_values
3249- . iter ( )
3250- . map ( |values| unsliced_hash_len ( & values. to_data ( ) ) )
3251- . fold ( max_batch_rows, usize:: max)
3252- . min ( num_rows) ;
3279+ // at any depth of a key, which `create_hashes` hashes on each call:
3280+ // slices `SHARED_VALUES_SLICE_FACTOR` times longer than those values
3281+ // (up to `MAX_HASH_CHUNK_ROWS`, never shorter than the values or the
3282+ // largest batch) keep that cost small.
3283+ let chunk_rows = hash_chunk_rows ( & left_values, max_batch_rows, num_rows) ;
32533284
32543285 let scratch_reservation = reservation. new_empty ( ) ;
32553286 if prepared {
@@ -5233,9 +5264,52 @@ mod tests {
52335264 Ok ( ( ) )
52345265 }
52355266
5236- /// Build batches sharing one key dictionary that is longer than each batch
5237- /// fill the hash map from slices longer than the batches, which must join
5238- /// as if the build side were a single batch.
5267+ /// A key with shared values gets slices `SHARED_VALUES_SLICE_FACTOR` times
5268+ /// longer than those values, up to `MAX_HASH_CHUNK_ROWS` unless the values
5269+ /// alone are longer, and never longer than the build side.
5270+ #[ test]
5271+ fn hash_chunk_rows_amortizes_shared_values ( ) -> Result < ( ) > {
5272+ fn dict ( values : i32 ) -> Result < ArrayRef > {
5273+ Ok ( Arc :: new ( DictionaryArray :: < Int32Type > :: try_new (
5274+ Int32Array :: from ( vec ! [ 0 ] ) ,
5275+ Arc :: new ( Int32Array :: from_iter_values ( 0 ..values) ) ,
5276+ ) ?) )
5277+ }
5278+ let plain: ArrayRef = Arc :: new ( Int32Array :: from ( vec ! [ 1 , 2 , 3 ] ) ) ;
5279+ let cases = [
5280+ (
5281+ vec ! [ dict( 8192 ) ?] ,
5282+ 8192 ,
5283+ 2_097_152 ,
5284+ SHARED_VALUES_SLICE_FACTOR * 8192 ,
5285+ ) ,
5286+ ( vec ! [ Arc :: clone( & plain) ] , 8192 , 2_097_152 , 8192 ) ,
5287+ // The largest batch is longer than 16 times the dictionary
5288+ ( vec ! [ dict( 100 ) ?] , 8192 , 2_097_152 , 8192 ) ,
5289+ ( vec ! [ Arc :: clone( & plain) , dict( 8192 ) ?] , 8192 , 50_000 , 50_000 ) ,
5290+ // Values longer than the cap are the slice length, not 16 times them
5291+ ( vec ! [ dict( 2_097_152 ) ?] , 8192 , 4_194_304 , 2_097_152 ) ,
5292+ ( vec ! [ dict( 100_000 ) ?] , 8192 , 4_194_304 , MAX_HASH_CHUNK_ROWS ) ,
5293+ // A batch longer than the cap is still the slice length
5294+ ( vec ! [ dict( 1000 ) ?] , 2_097_152 , 4_194_304 , 2_097_152 ) ,
5295+ ( vec ! [ ] , 8192 , 0 , 0 ) ,
5296+ ( vec ! [ dict( 8 ) ?] , 8192 , 0 , 0 ) ,
5297+ ] ;
5298+ for ( case, ( keys, max_batch_rows, num_rows, expected) ) in
5299+ cases. into_iter ( ) . enumerate ( )
5300+ {
5301+ assert_eq ! (
5302+ hash_chunk_rows( & keys, max_batch_rows, num_rows) ,
5303+ expected,
5304+ "case {case}"
5305+ ) ;
5306+ }
5307+ Ok ( ( ) )
5308+ }
5309+
5310+ /// Build batches sharing one key dictionary fill the hash map from slices
5311+ /// longer than the batches, which must join as if the build side were a
5312+ /// single batch.
52395313 #[ tokio:: test]
52405314 async fn join_keeps_build_order_with_dictionary_keys ( ) -> Result < ( ) > {
52415315 let dict_type =
@@ -5245,38 +5319,41 @@ mod tests {
52455319 Field :: new( "b1" , dict_type. clone( ) , true ) ,
52465320 ] ) ) ;
52475321 // Concatenation keeps a dictionary shared by every batch as is, so the
5248- // 12 build rows are filled from slices of 5 rows: [0, 5), [5, 10) and
5249- // [10, 12), each crossing the 2-row batch boundaries.
5250- let values: ArrayRef = Arc :: new ( StringArray :: from ( vec ! [ "x" , "y" , "z" , "w" , "v" ] ) ) ;
5251- let batch = |a : Vec < i32 > , keys : Vec < Option < i32 > > | -> Result < RecordBatch > {
5252- let b1 = DictionaryArray :: < Int32Type > :: try_new (
5253- Int32Array :: from ( keys) ,
5254- Arc :: clone ( & values) ,
5255- ) ?;
5256- Ok ( RecordBatch :: try_new (
5257- Arc :: clone ( & schema) ,
5258- vec ! [ Arc :: new( Int32Array :: from( a) ) , Arc :: new( b1) ] ,
5259- ) ?)
5260- } ;
5261- let batches = vec ! [
5262- batch( vec![ 1 , 2 ] , vec![ Some ( 0 ) , None ] ) ?,
5263- batch( vec![ 3 , 4 ] , vec![ Some ( 1 ) , Some ( 2 ) ] ) ?,
5264- batch( vec![ 5 , 6 ] , vec![ Some ( 0 ) , Some ( 1 ) ] ) ?,
5265- batch( vec![ 7 , 8 ] , vec![ Some ( 3 ) , Some ( 0 ) ] ) ?,
5266- batch( vec![ 9 , 10 ] , vec![ None , Some ( 1 ) ] ) ?,
5267- batch( vec![ 11 , 12 ] , vec![ Some ( 0 ) , Some ( 1 ) ] ) ?,
5268- ] ;
5322+ // 45 build rows, in 3-row batches over 2 dictionary values, are filled
5323+ // from slices of 32 rows: [0, 32) and [32, 45). The first boundary is
5324+ // inside the batch [30, 33), and "x" has matches in both slices.
5325+ let values: ArrayRef = Arc :: new ( StringArray :: from ( vec ! [ "x" , "y" ] ) ) ;
5326+ // Every fifth row has a null key, the others alternate "x" and "y"
5327+ let key = |a1 : i32 | ( a1 % 5 != 0 ) . then_some ( ( a1 + 1 ) % 2 ) ;
5328+ let batches = ( 1 ..=45 )
5329+ . step_by ( 3 )
5330+ . map ( |first| {
5331+ let a1: Vec < i32 > = ( first..first + 3 ) . collect ( ) ;
5332+ let b1 = DictionaryArray :: < Int32Type > :: try_new (
5333+ a1. iter ( ) . map ( |& a1| key ( a1) ) . collect ( ) ,
5334+ Arc :: clone ( & values) ,
5335+ ) ?;
5336+ Ok ( RecordBatch :: try_new (
5337+ Arc :: clone ( & schema) ,
5338+ vec ! [ Arc :: new( Int32Array :: from( a1) ) , Arc :: new( b1) ] ,
5339+ ) ?)
5340+ } )
5341+ . collect :: < Result < Vec < _ > > > ( ) ?;
5342+ assert_eq ! (
5343+ hash_chunk_rows( & [ Arc :: clone( batches[ 0 ] . column( 1 ) ) ] , 3 , 45 ) ,
5344+ 32
5345+ ) ;
52695346 let right_schema = Arc :: new ( Schema :: new ( vec ! [
52705347 Field :: new( "a2" , DataType :: Int32 , false ) ,
52715348 Field :: new( "b2" , dict_type, true ) ,
52725349 ] ) ) ;
52735350 let right = RecordBatch :: try_new (
52745351 Arc :: clone ( & right_schema) ,
52755352 vec ! [
5276- Arc :: new( Int32Array :: from( vec![ 10 , 30 ] ) ) ,
5353+ Arc :: new( Int32Array :: from( vec![ 10 ] ) ) ,
52775354 Arc :: new( DictionaryArray :: <Int32Type >:: try_new(
5278- Int32Array :: from( vec![ 0 , 1 ] ) ,
5279- Arc :: new( StringArray :: from( vec![ "x" , "y" ] ) ) ,
5355+ Int32Array :: from( vec![ 0 ] ) ,
5356+ Arc :: new( StringArray :: from( vec![ "x" ] ) ) ,
52805357 ) ?) ,
52815358 ] ,
52825359 ) ?;
@@ -5301,13 +5378,23 @@ mod tests {
53015378 | a1 | b1 | a2 | b2 |
53025379 +----+----+----+----+
53035380 | 1 | x | 10 | x |
5304- | 5 | x | 10 | x |
5305- | 8 | x | 10 | x |
5381+ | 3 | x | 10 | x |
5382+ | 7 | x | 10 | x |
5383+ | 9 | x | 10 | x |
53065384 | 11 | x | 10 | x |
5307- | 3 | y | 30 | y |
5308- | 6 | y | 30 | y |
5309- | 10 | y | 30 | y |
5310- | 12 | y | 30 | y |
5385+ | 13 | x | 10 | x |
5386+ | 17 | x | 10 | x |
5387+ | 19 | x | 10 | x |
5388+ | 21 | x | 10 | x |
5389+ | 23 | x | 10 | x |
5390+ | 27 | x | 10 | x |
5391+ | 29 | x | 10 | x |
5392+ | 31 | x | 10 | x |
5393+ | 33 | x | 10 | x |
5394+ | 37 | x | 10 | x |
5395+ | 39 | x | 10 | x |
5396+ | 41 | x | 10 | x |
5397+ | 43 | x | 10 | x |
53115398 +----+----+----+----+
53125399 " ) ;
53135400 Ok ( ( ) )
0 commit comments