Join to one in oracle database 26ai

Join to one in Oracle Database 26ai

Introduction

With the release of Oracle Database 23.26.2.0.0, A new join syntax, JOIN TO ONE, has been introduced. It’s meant to simplify the FROM clause and prevent silent row multiplications that may arise from join conditions. In a relational database, relationships are often one-to-many. When a query requires each row from the subject table (RWT) to map to at most one row in the results, JOIN TO ONE enforces this by raising an error if multiple matching rows are found.

This post explores this new feature and shows some examples of its use.

Case Study

Consider a customer and orders relationship like that in the Justlee schema. A customer could have many orders but an order could only have been placed by one customer. This is a typical one-to-many relationship.

One-to-many relationship between Orders and Customers
Full Justlee Schema

Consider a request to report on all the order numbers for all customers and the order dates. We can resolve this using the query :

  SELECT
         c.customer#
        ,c.firstname || ' ' || c.lastname customer_name
        ,o.order#
        ,o.orderdate
  FROM
                customers c
    INNER JOIN  orders    o    ON    c.customer# = o.customer#
  ORDER BY
         c.customer#
 /

 CUSTOMER# CUSTOMER_NAME             ORDER# ORDERDATE
---------- --------------------- ---------- ---------
      1001 BONITA MORALES              1018 05-APR-09
      1001 BONITA MORALES              1003 01-APR-09
      1003 LEILA SMITH                 1016 04-APR-09
      1003 LEILA SMITH                 1006 01-APR-09
      1004 THOMAS PIERSON              1008 02-APR-09
      1005 CINDY GIRARD                1009 03-APR-09
      1005 CINDY GIRARD                1000 31-MAR-09
      1007 TAMMY GIANA                 1007 02-APR-09
      1007 TAMMY GIANA                 1014 04-APR-09
      1008 KENNETH JONES               1020 05-APR-09
      1010 JAKE LUCAS                  1001 31-MAR-09
      1010 JAKE LUCAS                  1011 03-APR-09
      1011 REESE MCGOVERN              1002 31-MAR-09
      1014 JASMINE LEE                 1013 03-APR-09
      1015 STEVE SCHELL                1017 04-APR-09
      1017 BECCA NELSON                1012 03-APR-09
      1018 GREG MONTIASA               1005 01-APR-09
      1018 GREG MONTIASA               1019 05-APR-09
      1019 JENNIFER SMITH              1010 03-APR-09
      1020 KENNETH FALAH               1004 01-APR-09
      1020 KENNETH FALAH               1015 04-APR-09

21 rows selected.

The “Row Explosion” Problem

In the results above, customer 1001 is returned twice, albeit for two different order numbers. This is fine as in a one-to-many relationship this is expected. However, sometimes this row multiplication is unwanted or unexpected. Typical SQL join syntax doesn’t prevent this from happening; rather, it silently multiplies the rows.

For example, we may want to display the books the customers ordered alongside their authors. We would like to simply attach those 2 columns to our query results from above.

We can do that using a query like:

SELECT
         c.customer#
        ,c.firstname || ' ' || c.lastname customer_name
        ,o.order#
        ,o.orderdate
        ,b.title
        ,a.fname || ' ' || a.lname author_name
FROM
              customers   c
  INNER JOIN  orders      o    ON c.customer# = o.customer#
  INNER JOIN  orderitems oi    ON O.order#    = oi.order#
  INNER JOIN  books       b    ON oi.isbn     = b.isbn
  INNER JOIN  bookauthor ba    ON b.isbn      = ba.isbn
  INNER JOIN  author     a     ON ba.authorid = a.authorid
ORDER BY
         c.customer#
/

  CUSTOMER# CUSTOMER_NAME	     ORDER# ORDERDATE TITLE			     AUTHOR_NAME
---------- --------------------- ---------- --------- ------------------------------ ---------------------
      1001 BONITA MORALES	       1003 01-APR-09 DATABASE IMPLEMENTATION	     JUAN ADAMS
      1001 BONITA MORALES	       1003 01-APR-09 DATABASE IMPLEMENTATION	     JAMES AUSTIN
      1001 BONITA MORALES	       1003 01-APR-09 DATABASE IMPLEMENTATION	     TINA PETERSON
      1001 BONITA MORALES	       1003 01-APR-09 BODYBUILD IN 10 MINUTES A DAY  SAM SMITH
      1001 BONITA MORALES	       1018 05-APR-09 DATABASE IMPLEMENTATION	     TINA PETERSON
      1001 BONITA MORALES	       1003 01-APR-09 COOKING WITH MUSHROOMS	     JACK BAKER
      1001 BONITA MORALES	       1018 05-APR-09 COOKING WITH MUSHROOMS	     JACK BAKER
      1001 BONITA MORALES	       1018 05-APR-09 DATABASE IMPLEMENTATION	     JAMES AUSTIN
      1001 BONITA MORALES	       1018 05-APR-09 DATABASE IMPLEMENTATION	     JUAN ADAMS
      1001 BONITA MORALES	       1003 01-APR-09 BODYBUILD IN 10 MINUTES A DAY  LISA PORTER
      1003 LEILA SMITH		       1016 04-APR-09 PAINLESS CHILD-REARING	     JACK BAKER
...
      1020 KENNETH FALAH	       1015 04-APR-09 COOKING WITH MUSHROOMS	     JACK BAKER
      1020 KENNETH FALAH	       1004 01-APR-09 PAINLESS CHILD-REARING	     ROBERT ROBINSON

56 rows selected.


Notice that order 1003 now appears multiple times because several purchased books have multiple authors. With JOIN TO ONE, this query would instead return an error because the join multiplies rows in the subject (or Row-Widened) table.

Join to one

Syntax

The basic syntax of JOIN TO ONE is

FROM t1 JOIN TO ONE ( t2, t3, ... )

Benefits

There are various benefits with using this syntax

  • We do not need to specify a join condition (Although we can).
  • The query is guaranteed to widen the subject table (t1) without multiplying its rows.
  • The syntax for complex multi-table queries is simplified.

Guaranteed Row Cardinality

Using this new clause, any time a join results in multiple rows for the subject table, Oracle returns an error.

For example, if we attempt the exact same query above using join to one, we get an error ORA-18641.

SELECT
         c.customer#
        ,c.firstname || ' ' || c.lastname customer_name
        ,o.order#
        ,o.orderdate
        ,b.title
        ,a.fname || ' ' || a.lname author_name
  FROM
        customers c
  JOIN TO ONE (
                orders o
               ,orderitems oi
               ,books b
               ,bookauthor ba
               ,author a
              )
  ORDER BY
         c.customer#
/

ERROR at line 11:
ORA-18641: No join key found for "ORDERS"
Help: https://docs.oracle.com/error-help/db/ora-18641/

Notice the cleaner syntax and no join types or conditions specified with this new clause.

Despite the error message returned, ORA-18641 the underlying issue is that each customer can have many orders. If we provide the join condition explicitly, the query instead raises an ORA-18640 when it detects that a customer matches multiple orders. Additionally, there is no foreign key constraint on the Customers table referencing the Orders table, valuable information needed to infer the join conditions. This is talked about in more detail later on in this post.

Using join to one, the first query in this post fails as well.

SELECT
         c.customer#
        ,c.firstname || ' ' || c.lastname customer_name
        ,o.order#
        ,o.orderdate
  FROM
        customers c
       JOIN TO ONE (orders o)
  ORDER BY
         c.customer#
/
ERROR at line 8:
ORA-18641: No join key found for "ORDERS"
Help: https://docs.oracle.com/error-help/db/ora-18641/

The reason is the same; For each customer, there is more than one order.

The Join order matters

If we reverse the order of the tables, the query succeeds as the Orders table, the many side in the one-to-many relationship, now becomes the subject or “Row-widened” Table.

SELECT
         c.customer#
        ,c.firstname || ' ' || c.lastname customer_name
        ,o.order#
        ,o.orderdate
  FROM
        orders o
       JOIN TO ONE (customers c)
  ORDER BY
         c.customer#
/

 CUSTOMER# CUSTOMER_NAME	     ORDER# ORDERDATE
---------- --------------------- ---------- ---------
      1001 BONITA MORALES	       1003 01-APR-09
      1001 BONITA MORALES	       1018 05-APR-09
      1003 LEILA SMITH		       1006 01-APR-09
      1003 LEILA SMITH		       1016 04-APR-09
      1004 THOMAS PIERSON	       1008 02-APR-09
      1005 CINDY GIRARD 	       1000 31-MAR-09
      1005 CINDY GIRARD 	       1009 03-APR-09
      1007 TAMMY GIANA		       1007 02-APR-09
      1007 TAMMY GIANA		       1014 04-APR-09
      1008 KENNETH JONES	       1020 05-APR-09
      1010 JAKE LUCAS		       1001 31-MAR-09
      1010 JAKE LUCAS		       1011 03-APR-09
      1011 REESE MCGOVERN	       1002 31-MAR-09
      1014 JASMINE LEE		       1013 03-APR-09
      1015 STEVE SCHELL 	       1017 04-APR-09
      1017 BECCA NELSON 	       1012 03-APR-09
      1018 GREG MONTIASA	       1005 01-APR-09
      1018 GREG MONTIASA	       1019 05-APR-09
      1019 JENNIFER SMITH	       1010 03-APR-09
      1020 KENNETH FALAH	       1004 01-APR-09
      1020 KENNETH FALAH	       1015 04-APR-09

21 rows selected.

We can see that using JOIN TO ONE, the join order is very important.

Conditions can be specified

We are also able to specify join conditions. If we specify a join condition on the earlier query, we get a different error

SELECT
         c.customer#
        ,c.firstname || ' ' || c.lastname customer_name
        ,o.order#
        ,o.orderdate
  FROM
        customers c
       JOIN TO ONE (orders o
                       ON c.customer# = o.customer#)
  ORDER BY
         c.customer#
/
                    *
ERROR at line 8:
ORA-18640: JOIN TO ONE reached multiple rows joining to "O", resulting in a non-unique join
Help: https://docs.oracle.com/error-help/db/ora-18640/

This time we specified the condition and got an error ORA-18640 because the join is “non-unique”, meaning it does not strictly widen the results going from table a to table b (i.e. customers to orders).

How does Oracle know what columns to join on?

You may wonder about this; well, it makes use of the foreign key constraints. This is another reason why defining these constraints in your database schemas is very helpful. I talk about how important database integrity constraints are in a previous post about database constraints. If no foreign keys are defined, the conditions will need to be explicitly specified using the ON clause.

In the example below, I drop the foreign key constraint between the customers and orders tables, attempt the query without the join condition, and attempt it again with a join condition.

ALTER TABLE orders DROP CONSTRAINT orders_customer#_fk ; 

Table altered.

SELECT
         c.customer#
        ,c.firstname || ' ' || c.lastname customer_name
        ,o.order#
        ,o.orderdate
  FROM
        orders o
       JOIN TO ONE (customers c)
  ORDER BY
         c.customer#
/
ERROR at line 8:
ORA-18641: No join key found for "CUSTOMERS"
Help: https://docs.oracle.com/error-help/db/ora-18641/

...

SELECT
         c.customer#
        ,c.firstname || ' ' || c.lastname customer_name
        ,o.order#
        ,o.orderdate
  FROM
        orders o
       JOIN TO ONE (customers c ON o.customer# = c.customer#)
  ORDER BY
         c.customer#
/

 CUSTOMER# CUSTOMER_NAME	     ORDER# ORDERDATE
---------- --------------------- ---------- ---------
      1001 BONITA MORALES	       1001 31-MAR-09
      1002 RYAN THOMPSON	       1002 31-MAR-09
      1003 LEILA SMITH		       1003 01-APR-09
      1004 THOMAS PIERSON	       1004 01-APR-09
      1005 CINDY GIRARD 	       1005 01-APR-09
      1006 MESHIA CRUZ		       1006 01-APR-09
      1007 TAMMY GIANA		       1007 02-APR-09
      1008 KENNETH JONES	       1008 02-APR-09
      1009 JORGE PEREZ		       1009 03-APR-09
      1010 JAKE LUCAS		       1010 03-APR-09
      1011 REESE MCGOVERN	       1011 03-APR-09
      1012 WILLIAM MCKENZIE	       1012 03-APR-09
      1013 NICHOLAS NGUYEN	       1013 03-APR-09
      1014 JASMINE LEE		       1014 04-APR-09
      1015 STEVE SCHELL 	       1015 04-APR-09
      1016 MICHELL DAUM 	       1016 04-APR-09
      1017 BECCA NELSON 	       1017 04-APR-09
      1018 GREG MONTIASA	       1018 05-APR-09
      1019 JENNIFER SMITH	       1019 05-APR-09
      1020 KENNETH FALAH	       1020 05-APR-09
				       1000 31-MAR-09

21 rows selected.

Notice that after deleting the foreign key, Oracle can no longer infer the join condition automatically, so we must specify it explicitly. Also worth noting is that it appears the constraints only have to exist; they can be disabled or invalidated, and Oracle will still make use of them. This is explored further in Dani Schnider’s blog.

Re-adding the constraint, we no longer need to specify the condition.

SQL> ALTER TABLE ORDERS ADD CONSTRAINT orders_customer#_fk FOREIGN KEY (customer#) REFERENCES customers(customer#) ;

Table ORDERS altered.

SELECT
         c.customer#
        ,COUNT(o.order#) number_of_orders
  FROM
        orders o
       JOIN TO ONE (INNER JOIN customers c)
GROUP BY 
       c.customer#
  ORDER BY
         c.customer#
/
   CUSTOMER#    NUMBER_OF_ORDERS 
____________ ___________________ 
        1001                   2 
        1003                   2 
        1004                   1 
        1005                   2 
        1007                   2 
        1008                   1 
        1010                   2 
        1011                   1 
        1014                   1 
        1015                   1 
        1017                   1 
        1018                   2 
        1019                   1 
        1020                   2 

14 rows selected. 

The Join Type can be specified

Using JOIN TO ONE, the join type defaults to a LEFT OUTER JOIN. However, an INNER JOIN can be specified for deliberately filtering joins. Additionally, when performing a join on multiple tables, the join type can be specified explicitly for each table.

For example, we may want to see a list of books and their publishers. If a book has no publisher, using the default JOIN TO ONE syntax, that book shows up in the results anyway. By specifying an INNER JOIN, we only get a list of only books with an associated publisher.

In order to demonstrate this, we add a new row to the books table with only values for the ISBN and Title.

INSERT INTO books (isbn, title)
values (1010101010, 'NOT A REAL BOOK') ;

1 row inserted.

We then query for a list of books and their publishers using JOIN TO ONE, specifying the join type as INNER JOIN.

SELECT
      b.title
     ,p.name
  FROM
        books b
       JOIN TO ONE (INNER JOIN publishers p ON b.pubid = p.pubid)
/

TITLE                            NAME                     
-------------------------------- ------------------------ 
BODYBUILD IN 10 MINUTES A DAY    READING MATERIALS INC.   
REVENGE OF MICKEY                PRINTING IS US           
BUILDING A CAR WITH TOOTHPICKS   PUBLISH OUR WAY          
DATABASE IMPLEMENTATION          AMERICAN PUBLISHING      
COOKING WITH MUSHROOMS           READING MATERIALS INC.   
HOLY GRAIL OF ORACLE             AMERICAN PUBLISHING      
HANDCRANKED COMPUTERS            AMERICAN PUBLISHING      
E-BUSINESS THE EASY WAY          PUBLISH OUR WAY          
PAINLESS CHILD-REARING           REED-N-RITE              
THE WOK WAY TO COOK              READING MATERIALS INC.   
BIG BEAR AND LITTLE DOVE         REED-N-RITE              
HOW TO GET FASTER PIZZA          READING MATERIALS INC.   
HOW TO MANAGE THE MANAGER        PRINTING IS US           
SHORTEST POEMS                   REED-N-RITE              

14 rows selected. 

Notice that the newly added book doesn’t show up in the results.

If we attempt the query again without specifying the INNER JOIN the book without a publisher is displayed.

SELECT
      b.title
     ,p.name
  FROM
        books b
       JOIN TO ONE (publishers p ON b.pubid = p.pubid)
/

TITLE                            NAME                     
-------------------------------- ------------------------ 
REVENGE OF MICKEY                PRINTING IS US           
HOW TO MANAGE THE MANAGER        PRINTING IS US           
BUILDING A CAR WITH TOOTHPICKS   PUBLISH OUR WAY          
E-BUSINESS THE EASY WAY          PUBLISH OUR WAY          
DATABASE IMPLEMENTATION          AMERICAN PUBLISHING      
HOLY GRAIL OF ORACLE             AMERICAN PUBLISHING      
HANDCRANKED COMPUTERS            AMERICAN PUBLISHING      
BODYBUILD IN 10 MINUTES A DAY    READING MATERIALS INC.   
COOKING WITH MUSHROOMS           READING MATERIALS INC.   
THE WOK WAY TO COOK              READING MATERIALS INC.   
HOW TO GET FASTER PIZZA          READING MATERIALS INC.   
PAINLESS CHILD-REARING           REED-N-RITE              
BIG BEAR AND LITTLE DOVE         REED-N-RITE              
SHORTEST POEMS                   REED-N-RITE              
NOT A REAL BOOK                                           

15 rows selected. 

This shows us that JOIN TO ONE defaults to a [LEFT] OUTER JOIN.

It is important to note that if we specify an INNER JOIN on any table in the query, even one not joined directly to the subject table, it makes the entire chain of joins leading back to the subject table INNER joins as well. In other words, an INNER JOIN cascades backwards.

Resolving Chasm Traps

A chasm trap occurs when a query attempts to join two or more independent child tables through a single, shared parent table, resulting in a many-to-one-to-many join structure. Essentially, a many-to-one join is occurring twice within the same query, resulting in a many-to-many join.

child table A -> Parent table <- Child table B

In the Justlee Schema, consider the relationship between the books, orderitems and bookauthor tables.

A book can have multiple authors and at the same time appear on many order items. The relationship looks like :

Orderitems -> Books <- Bookauthor

This is the classic condition for a chasm trap as a standard SQL join causes a Cartesian-like row explosion. This silently multiplies your query results and produces severely inflated totals.

Example

Consider the below query displaying the number of times a book has been sold and the number of authors for the book:

SELECT  
      b.title, 
      SUM(oi.quantity) AS total_sold, 
      count(ba.authorid) number_of_authors
FROM        
            orderitems  oi
  INNER JOIN books      b  ON oi.isbn = b.isbn
  INNER JOIN bookauthor ba ON b.isbn  = ba.isbn
GROUP BY 
         b.title;

TITLE                               TOTAL_SOLD    NUMBER_OF_AUTHORS 
________________________________ _____________ ____________________ 
COOKING WITH MUSHROOMS                       8                    7 
HOW TO MANAGE THE MANAGER                    1                    1 
PAINLESS CHILD-REARING                      18                   15 
DATABASE IMPLEMENTATION                     21                   18 
BODYBUILD IN 10 MINUTES A DAY                2                    2 
SHORTEST POEMS                               1                    1 
E-BUSINESS THE EASY WAY                      2                    2 
HOLY GRAIL OF ORACLE                         3                    1 
BIG BEAR AND LITTLE DOVE                     4                    3 
REVENGE OF MICKEY                            5                    4 
HANDCRANKED COMPUTERS                        4                    2 

11 rows selected. 

If a book has 2 authors and has been ordered 3 times, this query matches every order row with every author row, multiplying the row count to 6 and double-counting the total quantity sold. In fact, the book ” PAINLESS CHILD-REARING” has only been ordered 6 times and has 3 authors but the results show a cartesian product of 3 and 6 (18) as the total sold.

See the below query showing the actual number of books sold.

SELECT 
         b.title, 
         SUM(oi.quantity) AS total_sold 
FROM         orderitems oi
JOIN TO ONE (books b ON oi.isbn=b.isbn )
GROUP BY b.title;

TITLE                               TOTAL_SOLD 
________________________________ _____________ 
BODYBUILD IN 10 MINUTES A DAY                1 
REVENGE OF MICKEY                            5 
DATABASE IMPLEMENTATION                      7 
COOKING WITH MUSHROOMS                       8 
HOLY GRAIL OF ORACLE                         3 
HANDCRANKED COMPUTERS                        2 
E-BUSINESS THE EASY WAY                      2 
PAINLESS CHILD-REARING                       6 
BIG BEAR AND LITTLE DOVE                     4 
HOW TO MANAGE THE MANAGER                    1 
SHORTEST POEMS                               1 

11 rows selected. 

How Join to one helps

If we apply the JOIN TO ONE clause, Oracle treats the leftmost table (Orderitems) as the authoritative Row-Widened Table (RWT) and enforces a strict many-to-one hierarchy. If you attempt to traverse downward into a one-to-many sibling path (like Bookauthor), Oracle will actively block the query or throw a runtime error rather than returning silently corrupted data.

SELECT 
         b.title, 
         SUM(oi.quantity) AS total_sold, 
         count(ba.authorid) number_of_authors
FROM          orderitems oi
 JOIN TO ONE (books b ON oi.isbn=b.isbn , bookauthor ba ON b.isbn = ba.isbn )
GROUP BY b.title;

Error starting at line : 1 in command -
...
Error at Command Line : 7 Column : 42
Error report -
SQL Error: ORA-18640: JOIN TO ONE reached multiple rows joining to "BA", resulting in a non-unique join
Help: https://docs.oracle.com/error-help/db/ora-18640/

Solution

The solution is to make use of CTEs. Using a CTE, we can first compute the total number of books sold and then the number of authors per book and join the results of both CTEs together in order to avoid this duplication.

WITH total_books_sold as 
   ( SELECT 
            b.title
           ,sum(oi.quantity) total_sold
     FROM 
          orderitems oi
     JOIN books b ON oi.isbn = b.isbn
     GROUP BY b.title ),
  number_of_authors_per_book as
  ( SELECT
          b.title
          ,count(ba.authorid) number_of_authors 
    FROM
         bookauthor ba
    JOIN books b ON ba.isbn = b.isbn
    GROUP BY b.title )
  SELECT 
         tbs.title
        ,tbs.total_sold
        ,nab.number_of_authors
  FROM
       total_books_sold tbs
JOIN number_of_authors_per_book nab ON tbs.title = nab.title ;

TITLE                               TOTAL_SOLD    NUMBER_OF_AUTHORS 
________________________________ _____________ ____________________ 
REVENGE OF MICKEY                            5                    1 
BODYBUILD IN 10 MINUTES A DAY                1                    2 
HANDCRANKED COMPUTERS                        2                    2 
SHORTEST POEMS                               1                    1 
PAINLESS CHILD-REARING                       6                    3 
COOKING WITH MUSHROOMS                       8                    1 
HOLY GRAIL OF ORACLE                         3                    1 
BIG BEAR AND LITTLE DOVE                     4                    1 
DATABASE IMPLEMENTATION                      7                    3 
HOW TO MANAGE THE MANAGER                    1                    1 
E-BUSINESS THE EASY WAY                      2                    1 

11 rows selected. 

The results now show correctly, that the book “PAINLESS CHILD-REARING” was sold 6 times and has 3 authors. In this example, JOIN TO ONE while not being the solution to the query itself, prevented us from having multiple rows and alerted us to the chasm trap.

Main takeaways about join to one

  • It helps prevent unwanted duplication of rows, chasm traps and many-to-many joins.
  • There is not an absolute need to specify join conditions if foreign key constraints are defined.
  • The order of the join matters, the first table is the subject of the query or the Row widened Table(RWT).
  • RWT rows never multiply during joins (though rows may disappear through filters or inner joins), always maintaining a one-to-one mapping from some subset of the RWT to joined rows reached by the query.
  • [LEFT] OUTER is the default join type using this clause.
  • Explicitly specifying the Join type can be helpful for filtering out unwanted rows.
  • It’s useful to explicitly define the ON clause when multiple foreign key columns may qualify for a join or when you need a join that is not derivable from declared keys, alone.

References and Further reading

There are many other resources and blog posts to learn more about this new feature, a lot of which I consulted prior to writing this post. Here are some of them:

Hope this is helpful.
Cheers, Harris.

Leave a Comment

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