Skip to content

Sort-merge join ignores a join filter that references no columns (wrong FULL JOIN results) #26002

Closed

Description

@jayzhan211

Describe the bug

SortMergeJoinExec skips a join filter whose expression references no columns, so a FULL JOIN keeps pairs that the filter rejects. Hash join returns the correct result.

A common way to end up with such a filter is a prepared statement with a parameter-only condition in the ON clause: once the parameter is bound, the condition (e.g. 3 > 5) no longer references any column.

To Reproduce

SET datafusion.optimizer.prefer_hash_join = false;
SET datafusion.execution.target_partitions = 2;

CREATE TABLE a AS SELECT * FROM (VALUES (1, 10), (2, 20), (3, 30)) t(k, v);
CREATE TABLE b AS SELECT * FROM (VALUES (1, 100), (2, 200), (4, 400)) t(k, w);

PREPARE q(INT) AS SELECT * FROM a FULL JOIN b ON a.k = b.k AND $1 > 5 ORDER BY a.k, b.k;
EXECUTE q(3);
+------+------+------+------+
| k    | v    | k    | w    |
+------+------+------+------+
| 1    | 10   | 1    | 100  |
| 2    | 20   | 2    | 200  |
| 3    | 30   | NULL | NULL |
| NULL | NULL | 4    | 400  |
+------+------+------+------+

The same happens with a boolean parameter, and with a volatile function that takes no columns:

PREPARE qb(BOOLEAN) AS SELECT count(*) FROM a FULL JOIN b ON a.k = b.k AND $1;
EXECUTE qb(false);
-- 4, expected 6

SELECT * FROM a FULL JOIN b ON a.k = b.k AND random() > 2 ORDER BY a.k, b.k;
-- the same 4 rows as above
-- EXPLAIN: SortMergeJoinExec: join_type=Full, on=[(k@0, k@0)], filter=random() > 2

(With target_partitions = 1 the planner picks HashJoinExec and the results are correct.)

Expected behavior

3 > 5 (and random() > 2) is false for every pair, so nothing matches and every row is NULL-padded, as with SET datafusion.optimizer.prefer_hash_join = true:

+------+------+------+------+
| k    | v    | k    | w    |
+------+------+------+------+
| 1    | 10   | NULL | NULL |
| 2    | 20   | NULL | NULL |
| 3    | 30   | NULL | NULL |
| NULL | NULL | 1    | 100  |
| NULL | NULL | 2    | 200  |
| NULL | NULL | 4    | 400  |
+------+------+------+------+

Additional context

Reproduced on current main (Oct 3 2026). LEFT, RIGHT and INNER joins with the same conditions return correct results.

In sort_merge_join/materializing_stream.rs the filter is only evaluated under if !filter_columns.is_empty(), so a filter whose expression reads no columns is skipped and every candidate pair is kept. A possible fix is to evaluate the filter even when it has no columns, building the filter batch with RecordBatchOptions::with_row_count(...) so its row count matches the candidate pairs.

Related: #26001 (same code path, filter batch construction for outer joins).

Activity

  1. added a commit that references this issue on Oct 24, 2026
    201099b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions