{"slug": "implicit-outer-joins-in-join-to-one-feature-or-flaw", "title": "Implicit Outer Joins in Join-to-One: Feature or Flaw?", "summary": "Oracle AI Database 26.2 introduced the join-to-one clause, which defaults to outer join semantics, a first for SQL joins, and the feature has been accepted into the next SQL standard. The author, a parser maintainer, criticizes the implicit outer join as potentially introducing null values and argues that the join type should depend on foreign key optionality, though he acknowledges the feature's usefulness.", "body_md": "## Background\n\nAs 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.\n\nThe 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](https://docs.oracle.com/en/database/oracle/oracle-database/26/adfns/building-queries-correct-joins-more-easily.html#GUID-FEB8218C-8FD5-47BC-BB14-4E43DDD32F35) while documenting the changes required for IslandSQL in a [GitHub issue](https://github.com/IslandSQL/IslandSQL/issues/315). I tried the feature in an OCI instance and posted about it on Bluesky.\n\nThis led to some discussions in private channels.\n\nBack 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.\n\nIn 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.\n\n## Join-To-One Will Make It Into the SQL Standard\n\nAccording to [Peter Eisentraut’s blog post](https://peter.eisentraut.org/blog/2026/06/30/waiting-for-sql-202y-stockholm-meeting-report#join-to-one), 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.\n\nAnyway, 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](https://docs.oracle.com/en/database/oracle/oracle-database/26/refrn/GROUP_BY_POSITION_ENABLED.html) to keep backward compatibility. I doubt that something like this will happen.\n\n## What Is the Problem?\n\nIt 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.\n\nChanging such a long-established convention is not necessarily a bad thing. However, there should be a convincing reason for doing so.\n\nThe [SQL Language Reference](https://docs.oracle.com/en/database/oracle/oracle-database/26/sqlrf/SELECT.html#GUID-CFA006CA-6FF1-4972-821E-6996142A51C6__GUID-9388EBCC-9ADE-4668-AAF4-D6193B8395F2) provides this reason:\n\n“The database chose this default because\n\n`INNER JOIN`\n\ns often filter rows unintentionally.”\n\nSo, the intention is to avoid accidental filtering. That sounds reasonable. But is silently preserving rows really better than silently filtering them?\n\nIn 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.\n\nIn 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?\n\nOf 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?\n\nHowever, 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.\n\nOuch. 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](https://jspecify.dev/docs/user-guide/)). Therefore, it’s probably better not to derive the join type automatically from a foreign key’s optionality. Being explicit has its value.\n\nFurthermore, 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](https://blog.sqlora.com/en/oracle-26ai-an-even-closer-look-at-join-to-one/).\n\nThese performance aspects may be addressed in upcoming versions. But the unfamiliar implicit default join strategy remains.\n\n## An Example\n\nLet’s compare the following two queries:\n\n```\nselect count(*)\n  from employees e\n  join to one (\n          departments d,\n          jobs j,\n          employees mgr on e.manager_id \n                       = mgr.employee_id\n       )\n where e.salary > mgr.salary;\nselect count(*)\n  from employees e\n  join to one (\n          outer join departments d\n          inner join jobs j\n          outer join employees mgr on e.manager_id \n                                  = mgr.employee_id\n       )\n where e.salary > mgr.salary;\n```\n\nThe queries are based on the HR example schema and produce the same result.\n\nWhich one is easier to read and understand?\n\nWhich one reveals information that might be helpful when crafting or reviewing the WHERE clause?\n\nIMO, the second example. For both questions.\n\nThe second example shows that:\n\n- Employees do not necessarily belong to a department. Yes, the\n`department_id`\n\ncolumn in the`employees`\n\ntable is optional. - Not every employee has a manager.\n- Every employee has a job.\n\nThis information helps us filter data using the department or manager columns. We have to handle null values there.\n\nBased 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.\n\nSo, we should change the query to something like the following:\n\n```\nselect count(*)\n  from employees e\n  join to one (\n          outer join departments d\n          inner join jobs j\n          inner join employees mgr on e.manager_id \n                                  = mgr.employee_id\n       )\n where e.salary > mgr.salary;\nselect count(*)\n  from employees e\n  join to one (\n          outer join departments d\n          inner join jobs j\n          outer join employees mgr on e.manager_id \n                                  = mgr.employee_id\n       )\n where e.salary > mgr.salary\n    or mgr.employee_id is null;\n```\n\nQuery 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.\n\nQuery 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.\n\n## Summary\n\nIMO, 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.\n\nIf you are using [dbLinter](https://marketplace.visualstudio.com/items?itemName=Grisselbav.dblinter) – which you should – then you can enable the rule [G-3194](https://dblinter.app/ords/r/dblinter/dblinter-console/rules#P1000_SHOW_RULE=core%20g-3194) to improve the maintainability of your join-to-one clauses.", "url": "https://wpnews.pro/news/implicit-outer-joins-in-join-to-one-feature-or-flaw", "canonical_source": "https://www.salvis.com/blog/2026/08/29/implicit-outer-joins-in-join-to-one-feature-or-flaw/", "published_at": "2026-08-29 18:19:26+00:00", "updated_at": "2026-08-29 18:48:32.145922+00:00", "lang": "en", "topics": ["artificial-intelligence", "ai-products"], "entities": ["Oracle AI Database", "PostgreSQL", "IslandSQL", "Peter Eisentraut"], "alternates": {"html": "https://wpnews.pro/news/implicit-outer-joins-in-join-to-one-feature-or-flaw", "markdown": "https://wpnews.pro/news/implicit-outer-joins-in-join-to-one-feature-or-flaw.md", "text": "https://wpnews.pro/news/implicit-outer-joins-in-join-to-one-feature-or-flaw.txt", "jsonld": "https://wpnews.pro/news/implicit-outer-joins-in-join-to-one-feature-or-flaw.jsonld"}}