Repository navigation
Closed
Labels
bugSomething isn't workingSomething isn't working
Description
Describe the bug
When the matching condition is on columns and combined with OR, the result retuned by LEFT JOIN is similar to INNER JOIN, which is incorrect.
LEFT JOIN should not reduce the number of rows returned, i.e. it should return all the records from the left table, and the matched records from the right table. When there is no match, the result is a NULL, i.e. display as empty.
INNER JOIN reduces the number of rows, i.e. it only returns the records that have matching values in both tables.
To Reproduce
- Use DataFusion CLI:
# in dir datafusion/datafusion-cli
$ cargo build
$ ./target/debug/datafusion-cli- Write sample data
CREATE OR REPLACE TABLE employees(emp_id INT, name VARCHAR) AS VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Carol');
CREATE OR REPLACE TABLE department(emp_id INT, department VARCHAR) AS VALUES (1, 'HR'), (3, 'Engineering'), (4, 'Sales');- This is how
employeestable anddepartmenttable look like
> SELECT * FROM employees;
+--------+-------+
| emp_id | name |
+--------+-------+
| 1 | Alice |
| 2 | Bob |
| 3 | Carol |
+--------+-------+
3 row(s) fetched.
Elapsed 0.009 seconds.
> SELECT * FROM department;
+--------+-------------+
| emp_id | department |
+--------+-------------+
| 1 | HR |
| 3 | Engineering |
| 4 | Sales |
+--------+-------------+
3 row(s) fetched.
Elapsed 0.006 seconds.- Query
> SELECT e.emp_id, e.name, d.department
FROM employees e
LEFT JOIN department d
ON (e.name = 'Alice' OR e.name = 'Bob');
+--------+-------+-------------+
| emp_id | name | department |
+--------+-------+-------------+
| 1 | Alice | HR |
| 1 | Alice | Engineering |
| 1 | Alice | Sales |
| 2 | Bob | HR |
| 2 | Bob | Engineering |
| 2 | Bob | Sales |
+--------+-------+-------------+ <-- should have one more row for Carol
6 row(s) fetched.
Elapsed 0.013 seconds.
> SELECT e.emp_id, e.name, d.department
FROM employees e
LEFT JOIN department d
ON (e.name = 'NotExist1' OR e.name = 'NotExist2');
+--------+------+------------+
| emp_id | name | department |
+--------+------+------------+
+--------+------+------------+ <-- should have three rows, 1 row for Alice, 1 row for Bob, and 1 row for Carol
0 row(s) fetched.
Elapsed 0.014 seconds.
> SELECT e.emp_id, e.name, d.department
FROM employees e
LEFT JOIN department d
ON (e.name = 'Alice' OR e.name = 'NotExist');
+--------+-------+-------------+
| emp_id | name | department |
+--------+-------+-------------+
| 1 | Alice | HR |
| 1 | Alice | Engineering |
| 1 | Alice | Sales |
+--------+-------+-------------+ <-- should have two more rows, 1 row for Bob and 1 row for Carol
3 row(s) fetched.
Elapsed 0.014 seconds.Expected behavior
Postgres shows the expected results.
- In psql cli, drop tables and create new tables with data
DROP TABLE IF EXISTS employees;
DROP TABLE IF EXISTS department;
# create tables
CREATE TABLE employees(emp_id INT, name VARCHAR);
CREATE TABLE department(emp_id INT, dept_name VARCHAR);
# write sample data
INSERT INTO employees (emp_id, name) VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Carol');
INSERT INTO department (emp_id, dept_name) VALUES (1, 'HR'), (3, 'Engineering'), (4, 'Sales');- This is how two tables look like:
chunchun=# SELECT * FROM employees;
emp_id | name
--------+-------
1 | Alice
2 | Bob
3 | Carol
(3 rows)
chunchun=# SELECT * FROM department;
emp_id | dept_name
--------+-------------
1 | HR
3 | Engineering
4 | Sales
(3 rows)- Query
SELECT e.emp_id, e.name, d.department
FROM employees AS e
LEFT JOIN department AS d
ON (e.name = 'Alice' OR e.name = 'Bob');
emp_id | name | department
--------+-------+-------------
1 | Alice | HR
1 | Alice | Engineering
1 | Alice | Sales
2 | Bob | HR
2 | Bob | Engineering
2 | Bob | Sales
3 | Carol |
(7 rows)
SELECT e.emp_id, e.name, d.department
FROM employees e
LEFT JOIN department d
ON (e.name = 'NotExist1' OR e.name = 'NotExist2');
emp_id | name | department
--------+-------+------------
1 | Alice |
2 | Bob |
3 | Carol |
(3 rows)
SELECT e.emp_id, e.name, d.department
FROM employees e
LEFT JOIN department d
ON (e.name = 'Alice' OR e.name = 'NotExist');
emp_id | name | department
--------+-------+-------------
1 | Alice | HR
1 | Alice | Engineering
1 | Alice | Sales
2 | Bob |
3 | Carol |
(5 rows)Additional context
No response
Reacted by Jonah Gao, Maxim Ivanov, Liang-Chi Hsieh, Jay Han and Kun Liu
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't working
Activity
viirya commentedon Jun 13, 2024
Hmm, as the join filter is only on
employees, I think it is pushdown through the Join. The results seem correct to me. I will verify it on Spark later.redbaron commentedon Jun 14, 2024
No, what essentially this query is:
which by definition of
LEFT JOINshould not reduce number of rows on the LEFTviirya commentedon Jun 14, 2024
Yea, just verified with Spark:
viirya commentedon Jun 14, 2024
This will cause correctness issue on Comet too. I will take a look.
ozankabak commentedon Jun 15, 2024
Could this be related to a bug in the optimizer (pushing down filter to the left where it shouldn't)?
I tried adding a trivially false term to the OR clause that references the
departmenttable. This shouldn't change the result but would inhibit pushdown to theemployeestable. I got the expected results:ozankabak commentedon Jun 15, 2024
If we look at the following plan:
> EXPLAIN SELECT e.emp_id, e.name, d.department FROM employees e LEFT JOIN department d ON (e.name = 'Alice' OR e.name = 'Bob'); +---------------+----------------------------------------------------------------------------+ | plan_type | plan | +---------------+----------------------------------------------------------------------------+ | logical_plan | Left Join: Filter: e.name = Utf8("Alice") OR e.name = Utf8("Bob") | | | SubqueryAlias: e | | | Filter: employees.name = Utf8("Alice") OR employees.name = Utf8("Bob") | | | TableScan: employees projection=[emp_id, name] | | | SubqueryAlias: d | | | TableScan: department projection=[department] | | physical_plan | NestedLoopJoinExec: join_type=Left, filter=name@0 = Alice OR name@0 = Bob | | | CoalesceBatchesExec: target_batch_size=8192 | | | FilterExec: name@1 = Alice OR name@1 = Bob | | | MemoryExec: partitions=1, partition_sizes=[1] | | | MemoryExec: partitions=1, partition_sizes=[1] | | | | +---------------+----------------------------------------------------------------------------+The filters in italics seem wrong. They indeed disappear in the example I gave above:
> EXPLAIN SELECT e.emp_id, e.name, d.department FROM employees e LEFT JOIN department d ON (e.name = 'Alice' OR e.name = 'Bob' OR d.department = 'FOOBAR'); +---------------+-------------------------------------------------------------------------------------------------------+ | plan_type | plan | +---------------+-------------------------------------------------------------------------------------------------------+ | logical_plan | Left Join: Filter: e.name = Utf8("Alice") OR e.name = Utf8("Bob") OR d.department = Utf8("FOOBAR") | | | SubqueryAlias: e | | | TableScan: employees projection=[emp_id, name] | | | SubqueryAlias: d | | | TableScan: department projection=[department] | | physical_plan | ProjectionExec: expr=[emp_id@1 as emp_id, name@2 as name, department@0 as department] | | | NestedLoopJoinExec: join_type=Right, filter=name@0 = Alice OR name@0 = Bob OR department@1 = FOOBAR | | | MemoryExec: partitions=1, partition_sizes=[1] | | | MemoryExec: partitions=1, partition_sizes=[1] | | | | +---------------+-------------------------------------------------------------------------------------------------------+ 2 row(s) fetched. Elapsed 0.016 seconds.These filters are also not present if have an AND clause instead:
> EXPLAIN SELECT e.emp_id, e.name, d.department FROM employees e LEFT JOIN department d ON (e.name = 'Alice' AND e.name = 'Bob'); +---------------+---------------------------------------------------------------------------------------+ | plan_type | plan | +---------------+---------------------------------------------------------------------------------------+ | logical_plan | Left Join: Filter: e.name = Utf8("Alice") AND e.name = Utf8("Bob") | | | SubqueryAlias: e | | | TableScan: employees projection=[emp_id, name] | | | SubqueryAlias: d | | | TableScan: department projection=[department] | | physical_plan | ProjectionExec: expr=[emp_id@1 as emp_id, name@2 as name, department@0 as department] | | | NestedLoopJoinExec: join_type=Right, filter=name@0 = Alice AND name@0 = Bob | | | MemoryExec: partitions=1, partition_sizes=[1] | | | MemoryExec: partitions=1, partition_sizes=[1] | | | | +---------------+---------------------------------------------------------------------------------------+ 2 row(s) fetched. Elapsed 0.013 seconds.doki23 commentedon Jun 26, 2024
The doc of
push_down_filtersays:So this definition is debatable.
We could distinguish between
allandsubsetsituations.For example,
left joinreturnsallof the rows from the left side, while returnssubsetof the rows from the right side.And
inner joinreturnssubsetrows of both sides.When the join returns a subset of the relevant side, we can push the filter down to it.
alamb commentedon Jul 1, 2024
I agree with @ozankabak in #10881 (comment) that the problem is that the filter should not actually be pushed below the join
I also agree with @doki23 in #10881 (comment) that the problem is related to the pushdown optimization
Different semantics of predicates in
ONandWHEREI think the core issue is that for
OUTERjoins, the semantics of predicates in theONclause are different than predicates in theWHEREclausePutting
(e.name = 'Alice' OR e.name = 'Bob')in theONclause means the predicate happens during the join, and thus for OUTER joins can't filter non-preserved rows, which is why theCarolrow still appears:However, when you put the same predicate in the
WHEREclause it does filter the row, as expectedProposed Fix
Thus I propose to fix this issue by changing predicate pushdown to NOT push predicates from the
ONclause belowOUTERjoins. We can and should still push predicates forINNERjoins or predicates from theWHEREclause (aka in Filters)viirya commentedon Jul 1, 2024
Agreed. This is Spark query plan:
The filter predicate is not pushdown.