Join / semi anti · All operations
Keep rows found in another table
Keep or drop rows by existence in another table.
Status
- In arcform
Runs as a SQL step
arcform carries it as a
sql:step today.- Shape of the result
- one table from two or more
- Level
- one clause of SQL
The DuckDB step
Customers with no orders
It reads the orders and customers tables the proofs share. Create them first:
CREATE TABLE orders AS SELECT * FROM (VALUES
(1, 'Alice Ng', 'north', DATE '2026-01-03', 120.00, 'widget', 'a|b', '{"channel":"web","items":[1,2]}'),
(2, 'alice ng', 'north', DATE '2026-01-05', 80.50, 'gadget', 'b', '{"channel":"shop","items":[3]}'),
(3, 'Bob Okafor', 'south', DATE '2026-01-05', 300.00, 'widget', 'a|c', '{"channel":"web","items":[4,5,6]}'),
(4, 'Carla Díaz', 'east', DATE '2026-01-09', NULL, 'gizmo', NULL, '{"channel":"web","items":[]}'),
(5, 'Bob Okafor', 'south', DATE '2026-01-12', 45.25, 'gadget', 'c', '{"channel":"phone","items":[7]}'),
(6, 'Dev Patel', 'west', DATE '2026-01-12', 510.00, 'widget', 'a', '{"channel":"web","items":[8,9]}'),
(7, 'Carla Diaz', 'east', DATE '2026-01-20', 99.99, 'gizmo', 'b|c', '{"channel":"shop","items":[10]}'),
(8, 'Dev Patel', 'west', DATE '2026-02-02', 510.00, 'widget', 'a', '{"channel":"web","items":[8,9]}'),
(9, 'Eve Larsen', NULL, DATE '2026-02-03', 15.00, 'gadget', NULL, '{"channel":"web","items":[11]}'),
(10, 'Alice Ng', 'north', DATE '2026-02-10', 220.00, 'gizmo', 'a|b|c', '{"channel":"phone","items":[12,13]}')
) t(order_id, customer, region, order_date, amount, product, tags, meta);
CREATE TABLE customers AS SELECT * FROM (VALUES
('Alice Ng', 'retail', DATE '2025-06-01', 52.52, 13.40),
('Bob Okafor', 'wholesale', DATE '2025-09-15', 48.85, 2.35),
('Carla Diaz', 'retail', DATE '2025-11-30', 40.42, -3.70),
('Frank Moore', 'retail', DATE '2026-01-01', 51.51, -0.13)
) t(customer, segment, joined, lat, lon);Then run the statement:
CREATE TEMP TABLE result AS
SELECT * FROM customers ANTI JOIN orders USING (customer);
SELECT * FROM result;
SELECT CASE WHEN ((SELECT list(customer) = ['Frank Moore'] FROM result) AND (SELECT count(*) = 3 FROM customers SEMI JOIN orders USING (customer))) THEN 'as expected' ELSE error('the customers without and with orders are not Frank Moore and the other three') END AS verdict;- It needs nothing installed beyond DuckDB itself.
- The statement checks its own result and raises an error when it is wrong: “the customers without and with orders are not Frank Moore and the other three”.
- It passed on DuckDB
v2.0.0 (Cyanoptera) d54cbbe3df.
Variants
- Keep rows without a match
Keep the rows whose key does not appear in another table.
- Keep unmatched rows from both tables
Keep the rows of either table whose key is missing from the other.
Keep rows with a match
Keep the rows whose key appears in another table, with their own columns only.
Split rows by whether they match
Send matching rows to one output and the rest to another.
What other tools call it
Meridian's classification; an independent blind rating agreed on 86.2% of 312 rows (exact class, version 2).
Apache DataFusion (6)
LEFT ANTI JOINread 2026-09-27Left rows that have no match on the right
LEFT SEMI JOINread 2026-09-27Left rows that have at least one match on the right; left columns only
RIGHT ANTI JOINread 2026-09-27Right rows that have no match on the left
RIGHT SEMI JOINread 2026-09-27Right rows that have at least one match on the left; right columns only
[ NOT ] EXISTSread 2026-09-27Keep rows for which a correlated subquery returns, or does not return, any row
[ NOT ] INread 2026-09-27Keep rows whose value is, or is not, found in the subquery result
Apache NiFi (2)
ScanAttributeread 2026-09-27Check attribute values against a dictionary of terms and route by presence
ScanContentread 2026-09-27Search content for terms from a user-supplied dictionary and route by match
Azure Data Factory (1)
Existsread 2026-09-27Keep rows by whether they exist, or do not exist, in another stream
Duckle (2)
EasyMorph (1)
Keep/remove matchingread 2026-09-27Keep or remove rows whose value exists in a column of another table
Ibis (2)
ExistsSubqueryread 2026-09-27Test whether a subquery returns any row
InSubqueryread 2026-09-27Test whether a value appears in the result of a subquery
KNIME Analytics Platform (2)
Reference Row Filterread 2026-09-27Keep or drop rows whose key appears in a second reference table
Reference Row Splitterread 2026-09-27Split rows by whether their key appears in a second reference table
Power Query M (1)
Table.RemoveMatchingRowsread 2026-09-27Drop every row that matches one of a given list of rows
RapidMiner Studio (2)
Tableau Prep (3)
VisiData (4)
join-cols-diffread 2026-09-23Join on selected columns: keep rows whose key is missing from at least one sheet
join-selected-diffread 2026-09-23Join selected sheets: keep rows whose key is missing from at least one sheet
join-sheets-all-diffread 2026-09-23Join all stacked sheets: keep rows whose key is missing from at least one sheet
join-sheets-top2-diffread 2026-09-23Join top two sheets: keep rows whose key is missing from at least one sheet
Background
A semijoin keeps the rows of one table that match a row of another on their common columns, and an antijoin keeps the rows that match none.[1] A semijoin differs from a natural join in that the other table's columns do not appear in its result.[1] In DuckDB, the result of a semi or anti join never has more rows than the left-hand table.[2]
When to use it
Reach for a semi join when the other table is there only to say which rows count, and for an ordinary join when its columns are wanted as well. (Meridian's view)
KNIME's Reference Row Splitter writes the rows of the first table that are also in a reference table to one output, and all other rows to a second output.[3] Split the rows when both halves are needed, rather than running a keep filter and a drop filter that could be written with different conditions. (Meridian's view)
The method
A semijoin can be simulated with a natural join followed by a projection onto the first table's columns.[1] In E.F. Codd's 1970 paper, the semijoin is called restriction.[1] In DuckDB, semi joins provide the same logic as the IN operator.[2] In PostgreSQL, an EXISTS subquery is generally executed only long enough to determine whether at least one row is returned.[4]
Compared with the alternatives
In PostgreSQL, a WHERE EXISTS test on a key is like an inner join on that key, but produces at most one output row for each outer row, even if several rows match.[4] Where the other table may hold a key more than once, prefer a semi join to an inner join used as a filter. (Meridian's view)
Sources
- [1]Join (relational algebra) - Wikipediasecondary source
- [2]FROM and JOIN Clauses – DuckDBdocumentation
- [3]Reference Row Splitterdocumentation
- [4]PostgreSQL: Documentation: 18: 9.24. Subquery Expressionsdocumentation
Used in
2 Protocols use this operation:
- EDGAR ↔ GLEIF — Company-to-LEI Crosswalk, steps
load,tier,package - SuperGPQA dataset, step
corpus