viirya commented on code in PR #26214:
URL: https://github.com/apache/datafusion/pull/26214#discussion_r4438724928
##########
datafusion/physical-plan/src/joins/hash_join/not_in.rs:
##########
@@ -1136,25 +1233,56 @@ where
build_lookups: 0,
#[cfg(test)]
mark_checks: 0,
+ #[cfg(test)]
+ max_decided_rows: 0,
f,
}
}
- /// Pairs each of `probe_rows` with each of `build_rows`. The pairing stops
- /// when every row that the pairs decide is marked.
- fn cross_product(
+ /// Pairs each of the rows that `probe_rows` yields with each of
+ /// `build_rows`. The pairing stops when every row that the pairs decide
+ /// is marked.
+ ///
+ /// When the pairs decide the build rows, the unmarked build rows are
+ /// paired in chunks of `batch_size` rows, and `probe_rows` is called
+ /// again for each chunk. Neither side is then held whole: a large build
+ /// side, as when this pairing stands in for a scope map that the memory
+ /// pool declined, or a probe batch larger than the batch size. A chunk is
+ /// filled with unmarked rows only, so a build side with few unmarked rows
+ /// left pairs them all at once, as a single chunk would.
+ fn cross_product<P: Iterator<Item = u32>>(
&mut self,
build_rows: impl Iterator<Item = u64>,
- probe_rows: impl Iterator<Item = u32>,
+ probe_rows: impl Fn() -> P,
) -> Result<()> {
- // Drop the rows that are already marked before the first pair.
- self.needs_refresh = true;
match self.marks {
- UnknownMarks::Build(_) => {
- self.pair_unmarked(&mut build_rows.collect(), probe_rows, |b,
p| (b, p))
+ UnknownMarks::Build(bitmap) => {
+ if probe_rows().next().is_none() {
+ return Ok(());
+ }
+ let mut build_rows = build_rows.peekable();
+ let mut chunk = Vec::with_capacity(self.batch_size);
+ while build_rows.peek().is_some() {
+ chunk.clear();
+ {
+ let bitmap = bitmap.lock();
+ chunk.extend(
+ build_rows
+ .by_ref()
+ .filter(|row| !bitmap.get_bit(*row as usize))
+ .take(self.batch_size),
+ );
+ }
+ // The chunk holds no row marked so far.
+ self.needs_refresh = false;
+ self.pair_unmarked(&mut chunk, probe_rows(), |b, p| (b,
p))?;
Review Comment:
Confirmed, and fixed in a40e07c41 as you suggest. `cross_product` first
takes up to `batch_size + 1` probe rows. If there are at most `batch_size`, it
keeps them and pairs every chunk with them, so your case walks the bitmap once,
as the merge base does. With more, it drops them and produces the probe rows
again for each chunk, which keeps the memory bound. That walk then costs less
than pairing those rows with the chunk, which is at least `batch_size *
batch_size` pairs.
The fix sits in `cross_product`, so it also covers the NULL-valued build
rows paired with the in-scope probe rows, where a few in-scope rows in a large
batch had the same problem. `cross_product_chunks_the_build_rows` now counts
the calls of `probe_rows`: one for three probe rows over 157 chunks (158
without the fix), and one per chunk plus one for `batch_size + 1` probe rows.
##########
datafusion/physical-plan/src/joins/hash_join/not_in.rs:
##########
@@ -1245,33 +1379,46 @@ where
self.batch_size,
)
};
- let rest_hashes = &hashes[group_from..];
// Estimate the groups from a sample first, because a sort of all the
// rest that finds no group worth it costs as much as the wasted
// matches did.
- let worth_sorting = saves_work(rest_hashes.len(),
estimate_groups(rest_hashes));
- let mut rest: Vec<(u64, u32)> = vec![];
- let mut groups = vec![];
- if worth_sorting {
- rest = rest_hashes
- .iter()
- .copied()
- .zip(probe_rows[group_from..].iter().copied())
- .collect();
- rest.sort_unstable_by_key(|(hash, _)| *hash);
- groups = rest.chunk_by(|a, b| a.0 == b.0).collect();
+ let mut rest = vec![];
+ if saves_work(
+ probe_rows.len() - group_from,
+ estimate_groups(&hashes[group_from..]),
+ ) {
+ rest = sort_by_hash(&hashes[group_from..],
&probe_rows[group_from..]);
}
- if !worth_sorting || !saves_work(rest.len(), groups.len()) {
- self.lookup_each_row(
+ let mut groups = hash_groups(&rest);
+ if rest.is_empty() || !saves_work(rest.len(), groups.len()) {
+ self.wasted_matches = 0;
+ let Some((offset, _)) = self.lookup_each_row(
scope_map,
hashes,
(!all_rows).then_some(probe_rows.as_slice()),
offset,
&to_build_row,
- usize::MAX,
+ max_wasted_matches,
lookup,
- )?;
- return Ok(());
+ )?
+ else {
+ return Ok(());
+ };
+ let group_from = match offset {
+ AtRow { row } | MidChain { row, .. } => row,
+ };
+ if rest.is_empty() {
+ rest = sort_by_hash(&hashes[group_from..],
&probe_rows[group_from..]);
+ } else {
+ // `probe_rows` is increasing, so the rows from `group_from` on
+ // are those not below its row, and keeping them keeps the
+ // order by hash.
+ match probe_rows.get(group_from) {
+ Some(&first) => rest.retain(|(_, row)| *row >= first),
+ None => rest.clear(),
+ }
+ }
+ groups = hash_groups(&rest);
Review Comment:
Right, done in a40e07c41: the retry starts with `drop(groups)`, so the
rejected groups are freed before the lookups resume rather than when the new
groups replace them.
##########
docs/source/library-user-guide/upgrading/56.0.0.md:
##########
@@ -934,9 +934,15 @@ Wire compatibility is directional:
### Null-aware joins record their `NOT IN` value key count
Multi-column `(a, b) NOT IN (SELECT x, y ...)` is now planned as a null-aware
-join. Its first equi-join keys are the tuple elements, followed by any
-correlation keys, so a null-aware join now records how many of its leading keys
-are `NOT IN` value keys:
+join. Its first equi-join keys are the tuple elements that can be NULL. A
+nullable element cannot be NULL here when a correlated filter of the subquery
+rejects NULL for it, such as `y > o.c` for `y`. The elements that cannot be
+NULL and any equality correlations follow as further equi-join keys when their
+types can be hashed, and otherwise stay in the join filter. `Null`,
+dictionary, run-end encoded and union elements stay among the first keys,
Review Comment:
Reworded in a40e07c41. The guide now says that these types, and elements
that read a column of them, count as elements that can be NULL. Like any such
element, they must be hashable equi-join keys, so the planner rejects a run-end
encoded one.
--
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]