Join / keyed · All operations
Join tables on a key
Inner, left, right, full join on equal keys.
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
Left join on a key
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 o.order_id, o.customer, c.segment FROM orders o LEFT JOIN customers c USING (customer) ORDER BY o.order_id;
SELECT * FROM result;
SELECT CASE WHEN ((SELECT count(*) = 10 AND count(segment) = 4 AND max(segment) FILTER (WHERE order_id = 5) = 'wholesale' FROM result)) THEN 'as expected' ELSE error('the join did not keep 10 orders and match 4 of them') 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 join did not keep 10 orders and match 4 of them”.
- It passed on DuckDB
v2.0.0 (Cyanoptera) d54cbbe3df.
Variants
- Keep every row of both tables
Join on equal keys and keep every row of both tables, with nulls wherever either side has no match.
- Join more than two tables
Join three or more tables on their shared keys in one step.
- Nest the matching rows in a column
Join on equal keys and put each row's matches from the other table into one nested-table column.
- Keep every row of one table
Join on equal keys and keep every row of the left or right table, with nulls where the other table has no match.
Choose the join type
Join two tables on equal keys with one operator whose setting picks inner, left, right or full outer.
Keep only matching rows
Join two tables on equal keys and keep only the rows whose key is found in both.
Join on shared column names
Join two tables on every column whose name they share, without naming the keys.
What other tools call it
Meridian's classification; an independent blind rating agreed on 86.2% of 312 rows (exact class, version 2).
Alteryx Designer (4)
Calgary Joinread 2026-09-27Join an input stream to a Calgary database through its indexes
Joinread 2026-09-27Combine two inputs on common fields or on record position; outputs left, inner and right
Join In-DBread 2026-09-27Inner or outer join of two in-DB streams on common fields, run inside the database
Join Multipleread 2026-09-27Combine two or more inputs on a common field or position; full outer join by default
Apache DataFusion (9)
FULL OUTER JOINread 2026-09-27All rows of both sides, with NULLs where either has no match
INNER JOINread 2026-09-27Rows with a match in both tables
JOINread 2026-09-27Pipe: join another relation, BigQuery-style |> syntax
Joins (INNER, LEFT, RIGHT, FULL, CROSS)read 2026-09-27Combine two relations on a condition or as a product
LEFT OUTER JOINread 2026-09-27All left rows, with NULLs where the right has no match
NATURAL JOINread 2026-09-27Inner join on the column names the inputs share
RIGHT OUTER JOINread 2026-09-27All right rows, with NULLs where the left has no match
joinread 2026-09-27DataFrame method: Join with another DataFrame on named columns and an optional filter
join_onread 2026-09-27DataFrame method: Join with another DataFrame on arbitrary expressions
Apache Hop (2)
Merge Joinread 2026-09-27Inner, left, right or full outer join of two key-sorted streams
Multiway Merge Joinread 2026-09-27Inner or full outer join of more than two key-sorted streams
AWS Glue (2)
Azure Data Factory (2)
Joinread 2026-09-27Combine two streams on a join condition
Merging/Joining tablesread 2026-09-27Table.Join and nested join with expansion: inner, left outer, right outer, full outer
Bruin (1)
Joinsread 2026-09-27Declared relationships between semantic models by foreign key or custom SQL
DuckDB (1)
FROM and JOIN (clause)read 2026-09-27Name the tables read and how they join: inner, outer, cross, semi, anti, as-of, positional, lateral
Duckle (1)
Joinread 2026-09-25inner, left, right or full outer join on keys, chosen by a type setting
EasyMorph (2)
Merge another tableread 2026-09-27Join columns from another table on matching keys: lookup, left, inner or full
Natural mergeread 2026-09-27Join two tables on the columns that share a name
Ibis (2)
KNIME Analytics Platform (2)
Pentaho Data Integration (2)
Merge Joinread 2026-09-27Join two key-sorted streams on a key
Multiway Merge Joinread 2026-09-27Inner or full outer join of several streams
Polars (2)
DataFrame.joinread 2026-09-27Join two frames on key columns: inner, left, right, full, semi, anti or cross
align_framesread 2026-09-27Align several frames on shared key values, adding null rows for missing keys, sorted by key
Power Query M (3)
Table.AddJoinColumnread 2026-09-27Left outer join to another table on equal keys, with the matching rows put in a new nested-table column
Table.Joinread 2026-09-27Join two tables on equal keys: inner, left, right or full outer, left or right anti, left or right semi
Table.NestedJoinread 2026-09-27Join two tables on equal keys and put the matching rows in a new nested-table column
RapidMiner Studio (2)
Joinread 2026-09-27Inner, left, right or outer join of two tables on key columns
Join (In Database)read 2026-09-27Join two in-database inputs on key columns
SQLMesh (1)
@JOINread 2026-09-27Include a JOIN clause, of any join type, only when the argument is true
Tableau Prep (6)
Fullread 2026-09-27All rows of both tables; nulls where a row has no match
Innerread 2026-09-27Rows with matches in both tables
Joinread 2026-09-27Combine two tables on join clauses; join type is picked on a Venn diagram
Join data in the Input stepread 2026-09-27Join a table to a related table using the key relationships defined in the database
Leftread 2026-09-27All rows of the left table with matches from the right; nulls where none
Rightread 2026-09-27All rows of the right table with matches from the left; nulls where none
Talend (1)
tJoinread 2026-09-27Inner or outer join of a main flow to a lookup flow on exact key matches
Trifacta (1)
Join datasetsread 2026-09-27Add columns from another dataset by joining on keys
VisiData (16)
join-cols-fullread 2026-09-23Join on selected columns: keep all rows from all sheets (full outer)
join-cols-innerread 2026-09-23Join on selected columns: keep rows whose key is present in every sheet
join-cols-outerread 2026-09-23Join on selected columns: keep all rows of the first sheet (left outer)
join-selectedread 2026-09-23Join selected sheets on their key columns with a prompted jointype
join-selected-fullread 2026-09-23Join selected sheets: keep all rows from all sheets (full outer)
join-selected-innerread 2026-09-23Join selected sheets: keep rows whose key is present in every sheet
join-selected-outerread 2026-09-23Join selected sheets: keep all rows of the first sheet (left outer)
join-sheets-allread 2026-09-23Join all sheets on the stack on their key columns with a prompted jointype
join-sheets-all-fullread 2026-09-23Join all stacked sheets: keep all rows from all sheets (full outer)
join-sheets-all-innerread 2026-09-23Join all stacked sheets: keep rows whose key is present in every sheet
join-sheets-all-outerread 2026-09-23Join all stacked sheets: keep all rows of the first sheet (left outer)
join-sheets-colsread 2026-09-23Join sheets using the selected Columns Sheet rows as join keys, prompted jointype
join-sheets-top2read 2026-09-23Join top two sheets on the stack on their key columns with a prompted jointype
join-sheets-top2-fullread 2026-09-23Join top two sheets: keep all rows from all sheets (full outer)
join-sheets-top2-innerread 2026-09-23Join top two sheets: keep rows whose key is present in every sheet
join-sheets-top2-outerread 2026-09-23Join top two sheets: keep all rows of the first sheet (left outer)
Background
A join in Structured Query Language (SQL) combines columns from one or more tables into a new table, putting records with matching fields on the same row.[1] An equi-join is a join that uses only equality comparisons in its join predicate.[1] An inner join keeps the pairs of rows that satisfy the join predicate, while outer joins also retain unmatched rows of the left table, the right table or both.[1]
When to use it
An inner join leaves out rows that have no match in the other table, and depending on the result wanted that can be a subtle bug, avoided by using an outer join.[1] Reach for an inner join when a row without a partner means nothing to the question, and for an outer join whenever the unmatched rows are part of the answer. (Meridian's view) Count the rows of each input and of the result: a total you did not expect is the quickest sign of a dropped or repeated key. (Meridian's view)
In PostgreSQL, an inner join gives, for each row of the first table, one row for each row of the second table that satisfies the join condition with it.[2] In dplyr, a many-to-many relationship, where a row on each side matches several rows on the other, can cause a Cartesian explosion in the number of rows returned.[3] dplyr warns by default when an equality join meets a many-to-many relationship.[3] dplyr's relationship argument throws an error when the data breaks the expected relationship, such as one-to-one or many-to-one.[3] State the relationship you expect between the keys and let the tool check it, rather than finding the fan-out later in a total. (Meridian's view)
In SQL, NULL never matches any other value in a join condition, not even another NULL.[1] pandas merge matches rows whose keys are both null against each other, which differs from usual SQL join behaviour.[4] In dplyr, joins treat two NA values as equal by default, and na_matches = "never" stops them matching, which is similar to joins for database sources.[3] Decide what a missing key should mean before you join, and check how your tool treats it. (Meridian's view)
Tableau Prep reads values that differ in capitalisation, spelling or extra spaces as not matching, and excludes them from the join.[5] Standardise the key columns, their case, spacing and type, before the join rather than after it. (Meridian's view)
A natural join compares all the columns the two tables share by name, so the join columns are not written in the query and a change in column names may change the results.[1] Name the key columns with USING or ON, so that adding a column to either table cannot change what the join matches on. (Meridian's view)
The method
The SQL join corresponds to the join of relational algebra, a theory introduced by Edgar F. Codd as part of his relational model of data in 1970.[1][6] Outer joins are not considered part of the classical relational algebra, and they assume a null value to fill the attributes of an unmatched row.[6]
The result of an inner join can be defined as the Cartesian product of the tables, keeping only the rows that satisfy the join predicate.[1] Actual SQL implementations normally use other approaches, such as hash joins or sort-merge joins, since computing the Cartesian product is slower and would often need a prohibitively large amount of memory.[1] Three fundamental algorithms perform a binary join: the nested loop join, the sort-merge join and the hash join.[1] A hash join builds a hash table from one input, ideally the smaller, then probes it with the rows of the other, so only rows with the same hash code are compared for equality.[7] A hash join needs an equijoin predicate, a conjunction of equality comparisons on one or more columns.[7] A sort-merge join first sorts both inputs on the join attribute, so that interleaved linear scans meet the matching rows of each value at the same time.[8]
In PostgreSQL, USING (a, b) forms an equality comparison for each listed column, and its output shows each listed column once rather than once per table.[2]
Compared with the alternatives
A cross join returns every combination of a row of the first table with a row of the second, and filtering it with a WHERE clause can give the equivalent of an inner join.[1] In PostgreSQL, T1 CROSS JOIN T2 is equivalent to T1 INNER JOIN T2 ON TRUE.[2] In DuckDB, a semi join returns the rows of the left table that have at least one match in the right table, and its result never has more rows than the left table.[9] Where the question is only whether a match exists, reach for a semi join rather than a keyed join followed by DISTINCT. (Meridian's view)
Sources
- [1]Join (SQL) - Wikipediasecondary source
- [2]PostgreSQL: Documentation: 18: 7.2. Table Expressionsdocumentation
- [3]Mutating joins — mutate-joins • dplyrdocumentation
- [4]pandas.merge — pandas 3.1.1 documentationdocumentation
- [5]Aggregate, Join, or Union Data - Tableaudocumentation
- [6]Relational algebra - Wikipediasecondary source
- [7]Hash join - Wikipediasecondary source
- [8]Sort-merge join - Wikipediasecondary source
- [9]FROM and JOIN Clauses – DuckDBdocumentation
Types it works with
IBAN: dovetail's relate treats it as a join key.
Evidence: crates/dovetail-core/src/identifiers.rs in dovetail at c5c1a4f
CUSIP: dovetail's relate treats it as a join key.
Evidence: crates/dovetail-core/src/identifiers.rs in dovetail at c5c1a4f
FIGI: dovetail's relate treats it as a join key.
Evidence: crates/dovetail-core/src/identifiers.rs in dovetail at c5c1a4f
ISIN: dovetail's relate treats it as a join key.
Evidence: crates/dovetail-core/src/identifiers.rs in dovetail at c5c1a4f
LEI: dovetail's relate treats it as a join key.
Evidence: crates/dovetail-core/src/identifiers.rs in dovetail at c5c1a4f
SEDOL: dovetail's relate treats it as a join key.
Evidence: crates/dovetail-core/src/identifiers.rs in dovetail at c5c1a4f
ORCID: dovetail's relate treats it as a join key.
Evidence: crates/dovetail-core/src/identifiers.rs in dovetail at c5c1a4f
ISBN: dovetail's relate treats it as a join key.
Evidence: crates/dovetail-core/src/identifiers.rs in dovetail at c5c1a4f
ISSN: dovetail's relate treats it as a join key.
Evidence: crates/dovetail-core/src/identifiers.rs in dovetail at c5c1a4f
Australian Business Number: dovetail's relate treats it as a join key.
Evidence: crates/dovetail-core/src/identifiers.rs in dovetail at c5c1a4f
NPI (National Provider Identifier): dovetail's relate treats it as a join key.
Evidence: crates/dovetail-core/src/identifiers.rs in dovetail at c5c1a4f
Ethereum Address: dovetail's relate treats it as a join key.
Evidence: crates/dovetail-core/src/identifiers.rs in dovetail at c5c1a4f
Email Address: dovetail's relate treats it as a join key.
Evidence: crates/dovetail-core/src/identifiers.rs in dovetail at c5c1a4f
UUID: dovetail's relate treats it as a join key.
Evidence: crates/dovetail-core/src/identifiers.rs in dovetail at c5c1a4f
DOI: dovetail's relate treats it as a join key.
Evidence: crates/dovetail-core/src/identifiers.rs in dovetail at c5c1a4f
Found by dovetail
dovetail’s
dovetail relateproposes joins between tables on equal keys, each graded, with FineType types among its evidence (key credit).
Used in
One Protocol uses this operation:
- EDGAR ↔ GLEIF — Company-to-LEI Crosswalk, steps
build_sec_entities,package