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


##########
datafusion/physical-plan/src/joins/hash_join/shared_bounds.rs:
##########
@@ -18,7 +18,7 @@ use crate::joins::hash_join::exec::HASH_JOIN_SEED;
 use crate::joins::hash_join::inlist_builder::build_struct_fields;
 use crate::joins::hash_join::not_in::NotIn;
 use crate::joins::hash_join::partitioned_hash_eval::{
-    HashExpr, HashTableLookupExpr, PartitionRouter, SeededRandomState,
+    HashTableLookupExpr, PartitionRouter, SeededRandomState,

Review Comment:
   Optional cleanup: this was the last place that built a `HashExpr`, so only 
proto decoding and tests use it now. Could you either open a follow-up to 
remove it, or add a comment saying it stays so older serialized plans still 
decode?



##########
datafusion/physical-plan/src/joins/hash_join/shared_bounds.rs:
##########
@@ -692,175 +692,50 @@ impl SharedBuildAccumulator {
         }
     }
 
-    /// Builds one routed probe-side filter from finalized partitioned build 
data.
-    /// Empty partitions reject their routed rows, while canceled partitions 
stay
-    /// permissive because their build contents are unknown.
+    /// Builds one probe-side filter from finalized partitioned build data.
+    /// Empty and canceled partitions contribute no key.
     fn build_partitioned_filter(&self, partitions: Vec<PartitionStatus>) -> 
Result<()> {
-        let mut partition_filters = Vec::with_capacity(partitions.len());
-        let mut real_partition_ids = Vec::new();
-        let mut empty_partition_ids = Vec::new();
-        let mut has_canceled_unknown = false;
-        let mut keys_have_null = false;
-        // Every non-empty partition's membership strategy and bounds, merged 
into
-        // one membership filter below; `None` once a partition cannot be 
merged.
-        let mut members = Some(Vec::with_capacity(partitions.len()));
+        let num_partitions = partitions.len();
+        let mut members = Vec::with_capacity(num_partitions);
         let mut merged_bounds: Option<PartitionBounds> = None;
+        let mut keys_have_null = false;
 
         for (partition_id, partition) in partitions.into_iter().enumerate() {
             match partition {
-                PartitionStatus::Reported(PartitionData {
+                // A canceled partition's stream emits no row: it is dropped
+                // before its report is delivered, and a stream probes only
+                // after delivery. So no key of its build side can matter.
+                PartitionStatus::CanceledUnknown
+                | PartitionStatus::Reported(PartitionData {
                     pushdown: PushdownStrategy::Empty,
                     ..
-                }) => {
-                    empty_partition_ids.push(partition_id);
-                    partition_filters.push(lit(false));
-                }
+                }) => {}
                 PartitionStatus::Reported(PartitionData {
                     bounds,
                     pushdown,
                     keys_have_null: partition_keys_have_null,
                 }) => {
-                    real_partition_ids.push(partition_id);
                     keys_have_null |= partition_keys_have_null;
-                    let mergeable = match &pushdown {
-                        PushdownStrategy::Map(map, _) => {
-                            let map: &Map = map;
-                            matches!(map, Map::HashMap(_))
-                        }
-                        PushdownStrategy::InList(_) => true,
-                        PushdownStrategy::Empty => false,
-                    };
-                    members = members.filter(|_| mergeable).map(|mut members| {
-                        members.push((partition_id, pushdown.clone(), 
bounds.clone()));
-                        members
-                    });
                     merged_bounds = Some(match merged_bounds {
                         None => bounds.clone(),
                         Some(merged) => merged.union(&bounds),
                     });
-
-                    let membership_expr = create_membership_predicate(
-                        &self.on_right,
-                        pushdown,
-                        &HASH_JOIN_SEED,
-                        self.probe_schema.as_ref(),
-                    )?;
-                    let bounds_expr = create_bounds_predicate(&self.on_right, 
&bounds);
-                    let then_expr =
-                        combine_membership_and_bounds(membership_expr, 
bounds_expr)
-                            .unwrap_or_else(|| lit(true));
-                    partition_filters.push(then_expr);
-                }
-                PartitionStatus::CanceledUnknown => {
-                    has_canceled_unknown = true;
-                    partition_filters.push(lit(true));
-                    // Rows routed here must pass, which a merged filter 
cannot express.
-                    members = None;
-                    // A canceled partition's build content is unknown, so it
-                    // may hold a NULL key.
-                    keys_have_null = true;
+                    members.push((partition_id, pushdown, bounds));
                 }
                 PartitionStatus::Pending => {
-                    return datafusion_common::internal_err!(
+                    return internal_err!(
                         "attempted to finalize dynamic filter with pending 
partition"
                     );
                 }
             }
         }
 
-        let all_partitions_canceled = has_canceled_unknown
-            && real_partition_ids.is_empty()
-            && empty_partition_ids.is_empty();
-        let all_partitions_empty = !has_canceled_unknown && 
real_partition_ids.is_empty();
-        let one_non_empty_partition =
-            !has_canceled_unknown && real_partition_ids.len() == 1;
-
-        let filter_expr = if all_partitions_canceled {
-            // No build data is known, so filtering any probe row could 
discard a match.
-            lit(true)
-        } else if all_partitions_empty {
+        let filter_expr = if members.is_empty() {
             // No build row exists, so no probe row can match.
             lit(false)
-        } else if one_non_empty_partition {
-            // Only one build partition contains rows, so its filter covers 
every
-            // possible probe match without routing.
-            Arc::clone(&partition_filters[real_partition_ids[0]])
-        } else if let Some(members) = members {
-            self.merged_membership_filter(
-                &members,
-                merged_bounds,
-                partition_filters.len(),
-            )?
         } else {
-            // Builds the shared sparse `CASE` for partition filter routing.
-            // Without cancellation, omitted branches are known empty and 
safely fall
-            // through to `ELSE false`. With cancellation, omitted canceled 
partitions
-            // have unknown contents and must fall through to `ELSE true`, so 
known-empty
-            // partitions are emitted explicitly as false branches.
-            let mut branches = if has_canceled_unknown {
-                empty_partition_ids
-                    .iter()
-                    .map(|&partition_id| (lit(partition_id as u64), 
lit(false)))
-                    .collect::<Vec<_>>()
-            } else {
-                vec![]
-            };
-            branches.extend(real_partition_ids.iter().map(|&partition_id| {
-                (
-                    lit(partition_id as u64),
-                    Arc::clone(&partition_filters[partition_id]),
-                )
-            }));
-
-            let routing_expr = if let Some(range_partitioning) =
-                &self.probe_range_partitioning
-            {
-                // Routes probe rows with the partition id selected by 
[`RangeExpr`].
-                // CASE range_partition(keys)
-                //   WHEN empty_partition_id THEN false  -- only when 
cancellation exists
-                //   WHEN real_partition_id THEN F(real_partition_id)
-                //   ...
-                //   ELSE has_canceled_unknown
-                // END
-                assert_or_internal_err!(
-                    partition_filters.len() == 
range_partitioning.partition_count(),
-                    "Dynamic filter partition count {} does not match Range 
partition count {}",
-                    partition_filters.len(),
-                    range_partitioning.partition_count()
-                );
-                Arc::new(RangeExpr::try_new_with_schema(
-                    self.on_right.clone(),
-                    range_partitioning,
-                    &self.probe_schema,
-                )?) as Arc<dyn PhysicalExpr>
-            } else {
-                // Routes probe rows with the same `hash(keys) % 
partition_count` expression used
-                // by Hash repartitioning.
-                // CASE hash(keys) % partition_count
-                //   WHEN empty_partition_id THEN false  -- only when 
cancellation exists
-                //   WHEN real_partition_id THEN F(real_partition_id)
-                //   ...
-                //   ELSE has_canceled_unknown
-                // END
-                let routing_hash_expr = Arc::new(HashExpr::new(
-                    self.on_right.clone(),
-                    self.repartition_random_state.clone(),
-                    "hash_repartition".to_string(),
-                )) as Arc<dyn PhysicalExpr>;
-                Arc::new(BinaryExpr::new(
-                    routing_hash_expr,
-                    Operator::Modulo,
-                    lit(partition_filters.len() as u64),
-                )) as Arc<dyn PhysicalExpr>
-            };
-
-            Arc::new(CaseExpr::try_new(
-                Some(routing_expr),
-                branches,
-                Some(lit(has_canceled_unknown)),
-            )?)
+            self.merged_membership_filter(&members, merged_bounds, 
num_partitions)?
         };

Review Comment:
   Removing the single-partition branch changes behavior, and it costs bloom 
filter pruning. `[None, Some(map), None]` doesn't match `(_, [Some(map)])`, so 
it gets routed, and the merged IN list limit counts duplicate rows, so one key 
with more than 150 build rows becomes `hash_lookup`.
   
   ```sql
   -- default config, plans Partitioned: 500 dim rows pass, all with one key, 
which is absent from fact
   SELECT count(*) FROM dim d JOIN fact ON d.k = fact.k WHERE d.s LIKE 'x%';
   ```
   
   | | main | this PR | with this change |
   |---|---|---|---|
   | Filter | `IN (SET)` | `hash_lookup` | `IN (SET)` |
   | Row groups kept by bloom | 5 of 50 | no bloom pruning | 5 of 50 |
   | Rows scanned | 500K | 5M | 500K |
   | Bytes scanned | 7.9 MB | 16.2 MB | 7.9 MB |
   
   Routing also costs more when the probes land in that partition: filter 
evaluation was about 3.5x slower with a hash table and 7-9x with an `ArrayMap`, 
under a noisy load. I ran this change with the touched tests:
   
   ````suggestion
           let filter_expr = match members.as_slice() {
               // No build row exists, so no probe row can match.
               [] => lit(false),
               // Only one build partition holds rows, so its own filter covers 
every
               // probe match without routing, and an IN list keeps its pruning.
               [(_, pushdown, bounds)] => combine_membership_and_bounds(
                   create_membership_predicate(
                       &self.on_right,
                       pushdown.clone(),
                       &HASH_JOIN_SEED,
                       self.probe_schema.as_ref(),
                   )?,
                   create_bounds_predicate(&self.on_right, bounds),
               )
               .expect("membership predicate is always present"),
               _ => {
                   self.merged_membership_filter(&members, merged_bounds, 
num_partitions)?
               }
           };
   ````
   
   Could you also add this test? It fails without the change, because both 
shapes get routed:
   
   ```rs
   /// One non-empty partition keeps its own filter: no routing, and an IN list
   /// stays an IN list even when its duplicate rows exceed the merged list 
limit.
   #[test]
   fn partitioned_one_non_empty_partition_keeps_its_own_filter() {
       let empty = || reported(PushdownStrategy::Empty, no_bounds());
       let mut acc = make_partitioned_expr_accumulator_for_test(3);
       acc.in_list_max_values = 150;
       acc.build_filter(FinalizeInput::Partitioned(vec![
           empty(),
           reported(in_list(&[2; 1000]), no_bounds()),
           PartitionStatus::CanceledUnknown,
       ]))
       .unwrap();
       in_list_expr(&current_expr(&acc));
   
       let acc = make_partitioned_expr_accumulator_for_test(3);
       acc.build_filter(FinalizeInput::Partitioned(vec![
           empty(),
           reported(hash_map(), bounds(1, 7)),
           PartitionStatus::CanceledUnknown,
       ]))
       .unwrap();
       let expr = format!("{:?}", current_expr(&acc));
       assert!(!expr.contains("routed by"), "{expr}");
   }
   ```



##########
datafusion/physical-plan/src/joins/hash_join/partitioned_hash_eval.rs:
##########
@@ -352,8 +353,7 @@ impl HashTableLookupExpr {
     ///   for pruning only, or `None`
     ///
     /// A single non-empty map is probed directly without routing, so it may be
-    /// any [`Map`] and `router` is ignored. Otherwise `router` is required and
-    /// every map must be a [`Map::HashMap`].
+    /// any [`Map`] and `router` is ignored. Otherwise `router` is required.

Review Comment:
   Optional cleanup: this reads as if one non-empty map skips routing, but only 
a one-entry `maps` does.
   
   ```suggestion
       /// When `maps` has exactly one entry, that map is probed directly 
without
       /// routing and `router` is ignored. Otherwise `router` is required, 
even if
       /// only one entry is non-empty.
   ```



##########
datafusion/physical-plan/src/joins/hash_join/shared_bounds.rs:
##########
@@ -1893,10 +1597,10 @@ mod tests {
         );
     }
 
-    /// A canceled partition's build content is unknown, so it may hold a NULL 
key:
-    /// the aggregated flag must be permissive even when no partition reported 
one.
+    /// A canceled partition holds no key, so it cannot add a NULL key that
+    /// would make a null-equal join keep probe-side NULL rows.
     #[test]
-    fn partitioned_canceled_partition_widens_routed_filter() {
+    fn partitioned_canceled_partition_does_not_widen_filter() {

Review Comment:
   Nothing here covers a canceled partition next to live ones anymore, since 
the range test now has only live and empty partitions. Could you add this one? 
It passes on this branch and fails on main, where those rows are kept:
   
   ```rs
   /// A canceled partition rejects the rows routed to it, and only those: the
   /// live partitions keep their matches.
   #[cfg(not(feature = "force_hash_collisions"))]
   #[test]
   fn partitioned_canceled_partition_rejects_only_its_routed_rows() {
       let acc = make_partitioned_expr_accumulator_for_test(4);
       let keys: Vec<i32> = (0..40).collect();
       let mut partitions =
           routed_partitions(&acc, 4, &keys, |_, held| hash_map_of(held));
       partitions[0] = PartitionStatus::CanceledUnknown;
       acc.build_filter(FinalizeInput::Partitioned(partitions))
           .unwrap();
   
       let array: ArrayRef = Arc::new(Int32Array::from(keys.clone()));
       let mut hashes = vec![0; keys.len()];
       create_hashes(
           [&array],
           acc.repartition_random_state.random_state(),
           &mut hashes,
       )
       .unwrap();
       let expected: Vec<bool> = hashes.iter().map(|hash| *hash % 4 != 
0).collect();
       assert!(expected.contains(&true) && expected.contains(&false));
       assert_matches(&acc, &keys, &expected);
   }
   ```



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