Available in versions: Dev (3.20) | Latest (3.19) | 3.18 | 3.17 | 3.16 | 3.15 | 3.14 | 3.13 | 3.12 | 3.11 | 3.10
ANTI JOIN
Applies to ✅ Open Source Edition ✅ Express Edition ✅ Professional Edition ✅ Enterprise Edition
Relational algebra defines a ANTI JOIN
operation that regrettably didn't make it into standard SQL (yet), though it is easy to emulate using the NOT EXISTS predicate. Unlike SEMI JOIN, it is not advised to use the NOT IN predicate to emulate ANTI JOIN
, because that risks being incorrect in the presence of NULL
values, a mistake that can be very subtle and thus hard to find.
jOOQ offers a convenient LEFT ANTI JOIN
operator to match the relational algebra semantics. The following query will produce all authors that have no books:
SELECT AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME FROM AUTHOR WHERE NOT EXISTS ( SELECT * FROM BOOK WHERE BOOK.AUTHOR_ID = AUTHOR.ID )
create.select( AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME ) .from(AUTHOR) .leftAntiJoin(BOOK).on(BOOK.AUTHOR_ID.eq(AUTHOR.ID)) .fetch();
The result might look like this, i.e. we might have an author Jane Austen in our database, but we don't have any books for her yet:
+------------+-----------+ | FIRST_NAME | LAST_NAME | +------------+-----------+ | Jane | Austen | +------------+-----------+
Of course, you can form an equivalent query using NOT EXISTS
as well in jOOQ. It is also possible to achieve ANTI JOIN
semantics by using an LEFT JOIN and a NULL predicate on the anti joined table's primary key placed outside of the ON clause, though that might be a bit esoteric and hard to read:
SELECT AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME FROM AUTHOR LEFT JOIN BOOK ON BOOK.AUTHOR_ID = AUTHOR.ID WHERE BOOK.ID IS NULL
create.select( AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME) .from(AUTHOR) .leftJoin(BOOK).on(BOOK.AUTHOR_ID.eq(AUTHOR.ID)) .where(BOOK.ID.isNull()) .fetch();
Think of the LEFT JOIN example result:
+------------+-----------+--------------+ | FIRST_NAME | LAST_NAME | TITLE | +------------+-----------+--------------+ | George | Orwell | 1984 | | George | Orwell | Animal Farm | | Paulo | Coelho | O Alquimista | | Paulo | Coelho | Brida | <-- Reject all of the above where we have BOOK.ID IS NOT NULL | Jane | Austen | | <-- Keep only this row, where BOOK.ID IS NULL +------------+-----------+--------------+
As can be seen, no DISTINCT
is required to remove duplicates, because there's always only 1 row for an author without books.
ANTI JOIN
is the inverse of the SEMI JOIN operator.
Dialect support
This example using jOOQ:
select(AUTHOR.ID).from(AUTHOR).leftAntiJoin(BOOK).on(BOOK.AUTHOR_ID.eq(AUTHOR.ID))
Translates to the following dialect specific expressions:
ASE, Access, Aurora MySQL, Aurora Postgres, BigQuery, CockroachDB, DB2, Derby, Exasol, Firebird, H2, HSQLDB, Hana, Informix, MariaDB, MemSQL, MySQL, Oracle, Postgres, Redshift, SQLDataWarehouse, SQLServer, SQLite, Snowflake, Sybase, Teradata, Trino, Vertica, YugabyteDB
SELECT AUTHOR.ID FROM AUTHOR WHERE NOT EXISTS ( SELECT 1 one FROM BOOK WHERE BOOK.AUTHOR_ID = AUTHOR.ID )
ClickHouse
SELECT AUTHOR.ID FROM AUTHOR LEFT ANTI JOIN BOOK ON BOOK.AUTHOR_ID = AUTHOR.ID
DuckDB
SELECT AUTHOR.ID FROM AUTHOR ANTI JOIN BOOK ON BOOK.AUTHOR_ID = AUTHOR.ID
Generated with jOOQ 3.20. Translate your own SQL on our website
Feedback
Do you have any feedback about this page? We'd love to hear it!