Implicit Outer Joins in Join-to-One: Feature or Flaw?

Background

As you probably know, I’m maintaining a parser for grammars used in SQL files for Oracle AI Database and PostgreSQL. As a result, I check for new statements and clauses in every version of those DBMSs. PostgreSQL typically adds new features only in major versions; this means yearly. Oracle, on the other hand, documents new features in every new RU; this means quarterly.

The RU for 26.2 was a bit delayed. So, on 1 May 2026, I went through the new features in 26.2 and discovered the join-to-one clause while documenting the changes required for IslandSQL in a GitHub issue. I tried the feature in an OCI instance and posted about it on Bluesky.

Join to one post on Bluesky

This led to some discussions in private channels.

Back then, I hadn’t made up my mind regarding the usefulness of this feature, but the fact that a join uses outer-join semantics by default irritated me from the very first moment.

In the meantime, I believe that this feature is useful for various use cases. I’m not going to talk about that in this blog post. However, the longer I thought about the implicit outer join in join-to-one, the more it irritated me.

Join-To-One Will Make It Into the SQL Standard

According to Peter Eisentraut’s blog post, this feature has been accepted into the next SQL standard and is now part of its working draft. I wonder whether they also discussed the default join behaviour of a join-to-one and what the consensus was.

Anyway, since version 26.2, this feature is part of Oracle AI Database. Changing the behaviour at that stage is only feasible with a database parameter similar to group_by_position_enabled to keep backward compatibility. I doubt that something like this will happen.

What Is the Problem?

It is the first time an outer join has become the default. For every other join variant since ANSI SQL-86, an inner join has been the default. We are used to that.

Changing such a long-established convention is not necessarily a bad thing. However, there should be a convincing reason for doing so.

The SQL Language Reference provides this reason:

“The database chose this default because INNER JOINs often filter rows unintentionally.”

So, the intention is to avoid accidental filtering. That sounds reasonable. But is silently preserving rows really better than silently filtering them?

In both cases, the omitted join type affects the result. An implicit inner join may remove rows. An implicit outer join may introduce null values that have to be considered in subsequent expressions and predicates.

In any case, the join-to-one clause relies on integrity constraints. These constraints are already used to derive the join condition. So why not consider them fully and choose an inner join for mandatory foreign keys and an outer join for optional foreign keys?

Of course, this would make the join type depend on the schema definition. A change to the optionality of a foreign key could then change the semantics of an existing query. That would have been a very good reason not to choose a fixed default join type, right?

However, while the optimiser could deal with a dynamically determined join type, the query would be more challenging to read and understand. We would need to check the current optionality of each foreign key involved to determine whether nulls need to be handled.

Ouch. This would be cumbersome without implementing an additional concept that forces us to handle null values in SQL expressions and predicates (something similar to JSpecify). Therefore, it’s probably better not to derive the join type automatically from a foreign key’s optionality. Being explicit has its value.

Furthermore, an outer join limits the optimiser’s solution space and can affect performance, as Andrej Pashchenko demonstrated for join-to-one in this blog post.

These performance aspects may be addressed in upcoming versions. But the unfamiliar implicit default join strategy remains.

An Example

Let’s compare the following two queries:

1) Implicit outer joins
select count(*)
  from employees e
  join to one (
          departments d,
          jobs j,
          employees mgr on e.manager_id 
                       = mgr.employee_id
       )
 where e.salary > mgr.salary;
2) Explict inner and outer joins
select count(*)
  from employees e
  join to one (
          outer join departments d
          inner join jobs j
          outer join employees mgr on e.manager_id 
                                  = mgr.employee_id
       )
 where e.salary > mgr.salary;

The queries are based on the HR example schema and produce the same result.

Which one is easier to read and understand?

Which one reveals information that might be helpful when crafting or reviewing the WHERE clause?

IMO, the second example. For both questions.

The second example shows that:

  • Employees do not necessarily belong to a department. Yes, the department_id column in the employees table is optional.
  • Not every employee has a manager.
  • Every employee has a job.

This information helps us filter data using the department or manager columns. We have to handle null values there.

Based on that, we can see a flaw in the query. We can either use an inner join for managers because we are not interested in counting employees without a manager, or we have to address employees without a manager in the WHERE clause.

So, we should change the query to something like the following:

2a) Excluding emps without mgr
select count(*)
  from employees e
  join to one (
          outer join departments d
          inner join jobs j
          inner join employees mgr on e.manager_id 
                                  = mgr.employee_id
       )
 where e.salary > mgr.salary;
2b) Including emps without mgr
select count(*)
  from employees e
  join to one (
          outer join departments d
          inner join jobs j
          outer join employees mgr on e.manager_id 
                                  = mgr.employee_id
       )
 where e.salary > mgr.salary
    or mgr.employee_id is null;

Query 2a produces the same result as queries 1 and 2, but the inner join on employees makes it clear that excluding employees without a manager was intended.

Query 2b produces additional results for employees without a manager. The outer join on employees and the null handling in the WHERE clause make it clear that this was intentional.

Summary

IMO, the implicit outer joins in the new join-to-one clause are clearly a flaw. You should always specify inner or outer joins explicitly in a join-to-one clause. This communicates the intended semantics and highlights where downstream null handling may be required.

If you are using dbLinter – which you should – then you can enable the rule G-3194 to improve the maintainability of your join-to-one clauses.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.