Outer Join

An inner join keeps only the rows that found a match in the other table. An outer join works differently: it always returns every row of one table or of both, filling the missing half with NULL.

There are three kinds of outer join: left (LEFT), right (RIGHT) and full (FULL). The kind is mandatory — a bare OUTER JOIN is a syntax error. The word OUTER itself is optional: LEFT JOIN and LEFT OUTER JOIN mean exactly the same, and the shorter form is used below.

LEFT OUTER JOIN

Returns every row of the left table. If a row finds a match in the right table, the two rows are glued together; if it does not, the right table columns are filled with NULL.

For example, let's get the schedule of calls from the database, joined with the corresponding classes in the schedule.

Data in the Timepair table (schedule of calls):

idstart_pairend_pair
108:30:0009:15:00
209:20:0010:05:00
310:15:0011:00:00
411:05:0011:50:00
512:50:0013:35:00
613:40:0014:25:00
714:35:0015:20:00
815:25:0016:10:00

Data in the Schedule table (schedule of classes):

iddateclassnumber_pairteachersubjectclassroom
12019-09-01T00:00:00.000Z9111147
22019-09-01T00:00:00.000Z928213
32019-09-01T00:00:00.000Z934313
42019-09-02T00:00:00.000Z914313
52019-09-02T00:00:00.000Z922434
62019-09-02T00:00:00.000Z936535
72019-09-03T00:00:00.000Z915636
82019-09-03T00:00:00.000Z9213737
92019-09-03T00:00:00.000Z936838
102019-09-04T00:00:00.000Z919939
112019-09-04T00:00:00.000Z92101040
122019-09-04T00:00:00.000Z9331141
132019-09-05T00:00:00.000Z9131343
142019-09-05T00:00:00.000Z9211147
152019-09-05T00:00:00.000Z935636
162019-08-30T00:00:00.000Z912434
172019-08-30T00:00:00.000Z928213
182019-08-30T00:00:00.000Z936535
192019-08-30T00:00:00.000Z9410147
202019-09-03T00:00:00.000Z94101040
212019-08-30T00:00:00.000Z817953
222019-08-30T00:00:00.000Z827953
232019-08-30T00:00:00.000Z838238
242019-08-30T00:00:00.000Z8411143
252019-08-30T00:00:00.000Z858339
262019-09-01T00:00:00.000Z822434
272019-09-01T00:00:00.000Z836535
282019-09-01T00:00:00.000Z8412636
292019-09-01T00:00:00.000Z8513737
302019-09-02T00:00:00.000Z836838
312019-09-02T00:00:00.000Z847953
322019-09-03T00:00:00.000Z81101040
332019-09-03T00:00:00.000Z827953
342019-09-03T00:00:00.000Z837953
352019-09-04T00:00:00.000Z811114
362019-09-04T00:00:00.000Z8211242
372019-09-04T00:00:00.000Z8331343
382019-09-04T00:00:00.000Z848242
392019-09-04T00:00:00.000Z8511143
402019-09-05T00:00:00.000Z8211143
MySQL 8.1
SELECT Timepair.id "timepair.id", start_pair, end_pair,
    Schedule.id "schedule.id", date, class, number_pair, teacher, subject, classroom
FROM Timepair
    LEFT JOIN Schedule ON Schedule.number_pair = Timepair.id;
timepair.idstart_pairend_pairschedule.iddateclassnumber_pairteachersubjectclassroom
108:30:0009:15:00352019-09-04T00:00:00.000Z811114
108:30:0009:15:00322019-09-03T00:00:00.000Z81101040
108:30:0009:15:00212019-08-30T00:00:00.000Z817953
108:30:0009:15:00162019-08-30T00:00:00.000Z912434
108:30:0009:15:00132019-09-05T00:00:00.000Z9131343
108:30:0009:15:00102019-09-04T00:00:00.000Z919939
108:30:0009:15:0072019-09-03T00:00:00.000Z915636
108:30:0009:15:0042019-09-02T00:00:00.000Z914313
108:30:0009:15:0012019-09-01T00:00:00.000Z9111147
209:20:0010:05:00402019-09-05T00:00:00.000Z8211143
209:20:0010:05:00362019-09-04T00:00:00.000Z8211242
209:20:0010:05:00332019-09-03T00:00:00.000Z827953
209:20:0010:05:00262019-09-01T00:00:00.000Z822434
209:20:0010:05:00222019-08-30T00:00:00.000Z827953
209:20:0010:05:00172019-08-30T00:00:00.000Z928213
209:20:0010:05:00142019-09-05T00:00:00.000Z9211147
209:20:0010:05:00112019-09-04T00:00:00.000Z92101040
209:20:0010:05:0082019-09-03T00:00:00.000Z9213737
209:20:0010:05:0052019-09-02T00:00:00.000Z922434
209:20:0010:05:0022019-09-01T00:00:00.000Z928213
310:15:0011:00:00372019-09-04T00:00:00.000Z8331343
310:15:0011:00:00342019-09-03T00:00:00.000Z837953
310:15:0011:00:00302019-09-02T00:00:00.000Z836838
310:15:0011:00:00272019-09-01T00:00:00.000Z836535
310:15:0011:00:00232019-08-30T00:00:00.000Z838238
310:15:0011:00:00182019-08-30T00:00:00.000Z936535
310:15:0011:00:00152019-09-05T00:00:00.000Z935636
310:15:0011:00:00122019-09-04T00:00:00.000Z9331141
310:15:0011:00:0092019-09-03T00:00:00.000Z936838
310:15:0011:00:0062019-09-02T00:00:00.000Z936535
310:15:0011:00:0032019-09-01T00:00:00.000Z934313
411:05:0011:50:00382019-09-04T00:00:00.000Z848242
411:05:0011:50:00312019-09-02T00:00:00.000Z847953
411:05:0011:50:00282019-09-01T00:00:00.000Z8412636
411:05:0011:50:00242019-08-30T00:00:00.000Z8411143
411:05:0011:50:00202019-09-03T00:00:00.000Z94101040
411:05:0011:50:00192019-08-30T00:00:00.000Z9410147
512:50:0013:35:00392019-09-04T00:00:00.000Z8511143
512:50:0013:35:00292019-09-01T00:00:00.000Z8513737
512:50:0013:35:00252019-08-30T00:00:00.000Z858339
613:40:0014:25:00<NULL><NULL><NULL><NULL><NULL><NULL><NULL>
714:35:0015:20:00<NULL><NULL><NULL><NULL><NULL><NULL><NULL>
815:25:0016:10:00<NULL><NULL><NULL><NULL><NULL><NULL><NULL>

All eight calls made it into the result, exactly as a left join promises. But the result has 43 rows, not 8.

A join does not supplement the left table — it goes through every matching pair of rows. The same pair number appears in the schedule many times, on different days and for different classes, and every match produces its own row. When the key is not unique in the right table, the result has more rows than the left table.

At the end of the result there are rows where every class column is NULL. These are the calls with no class at all: there is no match, but a row of the left table is guaranteed to appear in the result.

Rows without a match

Those NULL values are the basis of the most common practical trick — finding records that have no match. It is enough to keep only the rows where the key of the right table is empty:

MySQL 8.1
SELECT Timepair.id, start_pair, end_pair
FROM Timepair
    LEFT JOIN Schedule ON Schedule.number_pair = Timepair.id
WHERE Schedule.number_pair IS NULL;
idstart_pairend_pair
613:40:0014:25:00
714:35:0015:20:00
815:25:0016:10:00

Three calls are left — the ones with no class scheduled.

A join from which only the rows without a match are kept is called an anti join (ANTI JOIN). It has no operator of its own: both in MySQL and in PostgreSQL it is written exactly like this — a join plus an IS NULL condition.

RIGHT OUTER JOIN

The mirror image of the left join: every row of the right table is guaranteed to appear in the result, and the missing columns of the left table are filled with NULL.

MySQL 8.1
SELECT Timepair.id "timepair.id", start_pair, end_pair,
    Schedule.id "schedule.id", date, class, number_pair, teacher, subject, classroom
FROM Timepair
    RIGHT JOIN Schedule ON Schedule.number_pair = Timepair.id;

The result has 40 rows — exactly as many as there are records in Schedule — and not a single row with NULL. In other words, it matches the inner join completely.

That happened because every class refers to an existing call: the right table simply has no unmatched rows. The kind of join sets the rule, but what ends up in the result is decided by the data.

FULL OUTER JOIN

Returns every row of both tables. Rows that found a match are glued together, while unmatched rows of the left and right tables appear in the result with NULL instead of the missing half.

The result of a full join is made of three parts:

  • the rows of the inner join (INNER JOIN);
  • the rows of the left table that found no match;
  • the rows of the right table that found no match.
MySQL 8.1
SELECT Timepair.id "timepair.id", start_pair, end_pair,
    Schedule.id "schedule.id", date, class, number_pair, teacher, subject, classroom
FROM Timepair
    FULL OUTER JOIN Schedule ON Schedule.number_pair = Timepair.id;

On the data of this database the result matches the left join — the same 43 rows: here the unmatched rows exist only on the left.

MySQL does not support FULL OUTER JOIN, but the same result can be assembled by hand: take the left join and add the rows of the right table that found no match.

MySQL 8.1
SELECT table_fields
FROM left_table
    LEFT JOIN right_table ON right_table.key = left_table.key

UNION ALL

SELECT table_fields
FROM left_table
    RIGHT JOIN right_table ON right_table.key = left_table.key
WHERE left_table.key IS NULL;

The condition in the second part is required: without it the matched rows would appear in the result twice.

All types of table joins

All rows of the left table
Left table
key
1
2
3
Right table
key
1
1
2
4
Result: 4 rows
left.key
right.key
1
1
1
1
2
2
3
NULL
SELECT table_fields
FROM left_table
    LEFT JOIN right_table ON right_table.key = left_table.key

Let's check yourself. The left table has 8 rows. How many rows will a LEFT JOIN with the right table return?