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:

SQL
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:

SQL
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

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-27

    Join an input stream to a Calgary database through its indexes

  • Joinread 2026-09-27

    Combine two inputs on common fields or on record position; outputs left, inner and right

  • Join In-DBread 2026-09-27

    Inner or outer join of two in-DB streams on common fields, run inside the database

  • Join Multipleread 2026-09-27

    Combine two or more inputs on a common field or position; full outer join by default

Apache DataFusion (9)
  • FULL OUTER JOINread 2026-09-27

    All rows of both sides, with NULLs where either has no match

  • INNER JOINread 2026-09-27

    Rows with a match in both tables

  • JOINread 2026-09-27

    Pipe: join another relation, BigQuery-style |> syntax

  • Combine two relations on a condition or as a product

  • LEFT OUTER JOINread 2026-09-27

    All left rows, with NULLs where the right has no match

  • NATURAL JOINread 2026-09-27

    Inner join on the column names the inputs share

  • RIGHT OUTER JOINread 2026-09-27

    All right rows, with NULLs where the left has no match

  • joinread 2026-09-27

    DataFrame method: Join with another DataFrame on named columns and an optional filter

  • join_onread 2026-09-27

    DataFrame method: Join with another DataFrame on arbitrary expressions

Apache Hop (2)
  • Merge Joinread 2026-09-27

    Inner, left, right or full outer join of two key-sorted streams

  • Multiway Merge Joinread 2026-09-27

    Inner or full outer join of more than two key-sorted streams

AWS Glue (2)
  • Joinread 2026-09-27

    Equality join of two frames

  • Joinread 2026-09-27

    Join two datasets on keys: inner, outer, left, right, left semi, left anti

Azure Data Factory (2)
  • Joinread 2026-09-27

    Combine two streams on a join condition

  • Merging/Joining tablesread 2026-09-27

    Table.Join and nested join with expansion: inner, left outer, right outer, full outer

Bruin (1)
  • Joinsread 2026-09-27

    Declared relationships between semantic models by foreign key or custom SQL

DuckDB (1)
  • FROM and JOIN (clause)read 2026-09-27

    Name the tables read and how they join: inner, outer, cross, semi, anti, as-of, positional, lateral

Duckle (1)
  • Joinread 2026-09-25

    inner, left, right or full outer join on keys, chosen by a type setting

EasyMorph (2)
  • Merge another tableread 2026-09-27

    Join columns from another table on matching keys: lookup, left, inner or full

  • Natural mergeread 2026-09-27

    Join two tables on the columns that share a name

Ibis (2)
  • JoinChainread 2026-09-27

    A first table joined to a sequence of further tables, with the output columns selected

  • JoinLinkread 2026-09-27

    One step of a join chain: join kind, table and predicates; kind includes asof, semi, anti, cross

KNIME Analytics Platform (2)
  • DB Joinerread 2026-09-27

    In-database: join two DB tables on matching columns as SQL

  • Joinerread 2026-09-27

    Join two tables on equal key columns; inner, outer and unmatched-row outputs

Pentaho Data Integration (2)
Polars (2)
  • DataFrame.joinread 2026-09-27

    Join two frames on key columns: inner, left, right, full, semi, anti or cross

  • align_framesread 2026-09-27

    Align several frames on shared key values, adding null rows for missing keys, sorted by key

Power Query M (3)
  • Table.AddJoinColumnread 2026-09-27

    Left outer join to another table on equal keys, with the matching rows put in a new nested-table column

  • Table.Joinread 2026-09-27

    Join two tables on equal keys: inner, left, right or full outer, left or right anti, left or right semi

  • Table.NestedJoinread 2026-09-27

    Join two tables on equal keys and put the matching rows in a new nested-table column

RapidMiner Studio (2)
  • Joinread 2026-09-27

    Inner, left, right or outer join of two tables on key columns

  • Join (In Database)read 2026-09-27

    Join two in-database inputs on key columns

SQLMesh (1)
  • @JOINread 2026-09-27

    Include a JOIN clause, of any join type, only when the argument is true

Tableau Prep (6)
  • Fullread 2026-09-27

    All rows of both tables; nulls where a row has no match

  • Innerread 2026-09-27

    Rows with matches in both tables

  • Joinread 2026-09-27

    Combine two tables on join clauses; join type is picked on a Venn diagram

  • Join a table to a related table using the key relationships defined in the database

  • Leftread 2026-09-27

    All rows of the left table with matches from the right; nulls where none

  • Rightread 2026-09-27

    All rows of the right table with matches from the left; nulls where none

Talend (1)
  • tJoinread 2026-09-27

    Inner or outer join of a main flow to a lookup flow on exact key matches

Trifacta (1)
  • Join datasetsread 2026-09-27

    Add columns from another dataset by joining on keys

VisiData (16)
  • join-cols-fullread 2026-09-23

    Join on selected columns: keep all rows from all sheets (full outer)

  • join-cols-innerread 2026-09-23

    Join on selected columns: keep rows whose key is present in every sheet

  • join-cols-outerread 2026-09-23

    Join on selected columns: keep all rows of the first sheet (left outer)

  • join-selectedread 2026-09-23

    Join selected sheets on their key columns with a prompted jointype

  • join-selected-fullread 2026-09-23

    Join selected sheets: keep all rows from all sheets (full outer)

  • join-selected-innerread 2026-09-23

    Join selected sheets: keep rows whose key is present in every sheet

  • join-selected-outerread 2026-09-23

    Join selected sheets: keep all rows of the first sheet (left outer)

  • join-sheets-allread 2026-09-23

    Join all sheets on the stack on their key columns with a prompted jointype

  • join-sheets-all-fullread 2026-09-23

    Join all stacked sheets: keep all rows from all sheets (full outer)

  • join-sheets-all-innerread 2026-09-23

    Join all stacked sheets: keep rows whose key is present in every sheet

  • join-sheets-all-outerread 2026-09-23

    Join all stacked sheets: keep all rows of the first sheet (left outer)

  • join-sheets-colsread 2026-09-23

    Join sheets using the selected Columns Sheet rows as join keys, prompted jointype

  • join-sheets-top2read 2026-09-23

    Join top two sheets on the stack on their key columns with a prompted jointype

  • join-sheets-top2-fullread 2026-09-23

    Join top two sheets: keep all rows from all sheets (full outer)

  • join-sheets-top2-innerread 2026-09-23

    Join top two sheets: keep rows whose key is present in every sheet

  • join-sheets-top2-outerread 2026-09-23

    Join 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)

Types it works with

Found by dovetail

Used in

One Protocol uses this operation: