Repository navigation
Full join on dataframe with only index yields dropped rows #1305
Copy link
Copy link
Closed
Labels
bugSomething isn't workingSomething isn't working
Description
Describe the bug
If I do a full join between a dataframe with content and one only consisting of an index column that index column only bits get dropped.
To Reproduce
See commented out empty additional column. When that is included then we see results in the final dataframe.
ctx = dfn.SessionContext()
key_frame = ctx.from_pydict(
{
"log_time": [1, 3, 5, 7, 9, 11, 13, 15, 17, 19],
"key_frame": [True, True, True, True, True, True, True, True, True, True]
}
)
query_times = ctx.from_pydict(
{
"log_time": [2, 4, 6, 8, 10],
#"empty": [0, 0, 0, 0, 0]
}
)
print(key_frame)
print(query_times)
merged = query_times.join(key_frame, left_on="log_time", right_on="log_time", how="full")
print(merged)DataFrame()
+----------+-----------+
| log_time | key_frame |
+----------+-----------+
| 1 | true |
| 3 | true |
| 5 | true |
| 7 | true |
| 9 | true |
| 11 | true |
| 13 | true |
| 15 | true |
| 17 | true |
| 19 | true |
+----------+-----------+
DataFrame()
+----------+
| log_time |
+----------+
| 2 |
| 4 |
| 6 |
| 8 |
| 10 |
+----------+
DataFrame()
+----------+----------+-----------+
| log_time | log_time | key_frame |
+----------+----------+-----------+
| | 1 | true |
| | 3 | true |
| | 5 | true |
| | 7 | true |
| | 9 | true |
| | 11 | true |
| | 13 | true |
| | 15 | true |
| | 17 | true |
| | 19 | true |
+----------+----------+-----------+Expected behavior
When doing a full join I get back all rows. Effectively merging the dataframes.
Here is a somewhat equivalent in pandas
key_frame_df = pd.DataFrame({
"log_time": [1, 3, 5, 7, 9, 11, 13, 15, 17, 19],
"key_frame": [True, True, True, True, True, True, True, True, True, True]
})
query_times_df = pd.DataFrame({
"log_time": [2, 4, 6, 8, 10],
# "empty": [0, 0, 0, 0, 0] # commented out like in original
})
# Perform full outer join (equivalent to DataFusion's "full" join)
merged_df = pd.merge(query_times_df, key_frame_df, on="log_time", how="outer")
print("\nMerged DataFrame (full outer join):")
print(merged_df)Merged DataFrame (full outer join):
log_time key_frame
0 1 True
1 2 NaN
2 3 True
3 4 NaN
4 5 True
5 6 NaN
6 7 True
7 8 NaN
8 9 True
9 10 NaN
10 11 True
11 13 True
12 15 True
13 17 True
14 19 TrueActually the behavior in pyarrow is maybe a more direct comparison
key_table = pa.table(key_frame)
query_table = pa.table(query_times)
merged_table = query_table.join(key_table, keys="log_time", join_type="full outer")
print(ctx.from_arrow(merged_table))Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't working
Activity
IshaGudewar commentedon Dec 4, 2025
Thanks for the detailed report! I investigated the behavior and confirmed that the root cause is the schema merging during full outer joins.
When one of the input DataFrames contains only the join key (no extra columns), the join schema builder drops the duplicated join key column from that side. Since that side contributes no additional fields, the corresponding rows end up empty and are removed from the final output. This leads to incorrect results for full joins.
I can contribute by adding:
A regression test showing that a full join between
df(log_time)
df(log_time, key_frame)
must return all rows from both sides.
A schema validation test verifying that join key rows from the “index-only” DataFrame are preserved.
If helpful, I can also explore patching the join schema builder to ensure a full join retains rows from both sides even when one side has only the join key.
I'll prepare a PR with the regression test shortly.
renato2099 commentedon Dec 7, 2025
I tried this in directly on datafusion, and it works as expected ... which is good 😅
results
also following your example, I see that all rows are there, but the values from the left side are dropped
and when using the
emptycolumn we get the followingI am thinking that the left
log_timecolumn values are just getting droppedI will take a closer look in the datafusion-python side :)
fix: use coalesce instead of drop_duplicate_keys for join
fix: use coalesce instead of drop_duplicate_keys for join
renato2099 commentedon Dec 13, 2025
thinking more about this , I think we should just not allow
drop_duplicate_keyswhen doing a full outer join.I think that would be the right behavior because
(1) For a FULL OUTER JOIN, the left and right join keys are NOT equivalent columns
(2) That would resemble the expected from upstream and what other databases do. For example, postgresql https://www.postgresql.org/docs/8.2/queries-table-expressions.html
(3) User can decide on how to merge columns afterwards.
then we can allow something like this:
then user can do as they want, like coalescing
then no weird semantics + no workarounds + give user the power to decide what to do.
Also created PR for it here #1320
renato2099 commentedon Dec 13, 2025
hey @timsaucer @ntjohnson1 , would you mind sharing your thoughts on this?
ntjohnson1 commentedon Dec 13, 2025
I'm traveling all day today so I can't think carefully on this. I'd be fine with this approach if it didn't require the select_exprs since that feels a little unergonomic. The need to track the data frame names separately from the variable seems unfortunate.
I'd like to instead do
df.select(F.coalesce(df1.col("num"), df2.col("num")).alias("num")unfortunately I don't think we currently have a syntax like that (or a nice alternative) to dedup columns that have the same name.So if there was a way to resolve the columns without a specially crafted string I think the proposed direction seems reasonable.
renato2099 commentedon Dec 14, 2025
that is the case because of the way we are registering the dataframes with the
SessionContextIn the example, we are doing this because we need it to disambiguate columns with the same name ... which is also our case 😅 (we have two columns that don't represent the same values but have the same name)
right, unfortunately datafusion-python doesn't support that syntax atm (what is that btw? polars? spark-api?), but that is a good feature request though
we can do the following and no crafted string :)
then we get
fix: use coalesce instead of drop_duplicate_keys for join
mesejo commentedon Dec 17, 2025
I've opened a PR that uses coalesce instead of dropping the left column. Using coalesce seems the right approach since:
renato2099 commentedon Dec 17, 2025
cool! thanks @mesejo ! yeah it is definitely an option :)
For me, it depends on how much SQL like we want/need to be + let the user decide the next operation (and hopefully an optimizer fuse operators together where it makes sense)
looking at how polars does it, it is interesting to see that columns get properly "renamed" such that users can also decide afterwards
I haven't checked your PR yet @mesejo but if we are going that direction, we might as well consider column renaming (?)
ntjohnson1 commentedon Dec 29, 2025
From an end result perspective I prefer the coalesce direction. I think
resolve_duplicatesmight be a nicer keyword. With my user hat on I don't care too much about HOW that happens under the hood so maybe this also makes things more flexible if we change later. Additionally, special caseing kwargs to not manage certain situations adds complexity.I think if the user wants to decide the details of how things are handled they can opt out of
resolve_duplicates. I just opened a PR for datafusion to try and make resolving ambiguous column dropping and reference clearer so they can actually do so without just doing it in SQL.ntjohnson1 commentedon Dec 29, 2025
I saw you took a look at one of the PRs @kosiew so not sure if you have opinions.
kosiew commentedon Dec 31, 2025
The current Python join wrapper drops mutually named keys by default for all join types, including full, by projecting away one side’s qualified key column when drop_duplicate_keys is true.
datafusion-python/src/dataframe.rs
Lines 650 to 715 in fcd7056
The user guide also documents this default key-dropping behavior without noting any join-type exceptions.
datafusion-python/docs/source/user-guide/common-operations/joins.rst
Lines 109 to 136 in fcd7056
Given that, I agree with @renato’s concern: for full outer joins the two key columns are not equivalent, so dropping one of them can remove the only way to represent unmatched rows. Disallowing drop_duplicate_keys=True (or forcing it to False) for how="full" would avoid silent data loss and keep both key columns available for user-controlled coalescing or renaming afterward, aligning with SQL-style expectations.
@mesejo’s coalesce-based approach also addresses the correctness gap and matches behaviors in other libraries. If we keep drop_duplicate_keys=True as the default, applying coalesce for outer joins instead of dropping one side would preserve rows while still returning a single key column; the trade-off is slightly higher compute and less user control over per-column treatment. Either way, documenting the chosen semantics explicitly for full joins will help set user expectations.
fix: use coalesce instead of drop_duplicate_keys for join (#1318)
Allow dropping qualified columns (#19549)
Allow dropping qualified columns (apache#19549)