I keep the join variants straight by remembering these tables

> select * from lefty;
+---+---+
| k | v |
|---+---|
| 1 | a |
| 2 | b |
| 2 | c |
| 3 | d |
+---+---+

> select * from righty;
+---+---+
| k | v |
|---+---|
| 1 | e |
| 2 | f |
| 2 | g |
| 4 | h |
+---+---+

and asking myself how many rows the join table has.

A join table row is a lefty row l or null row concatenated to a righty row r or null row. The difference is: which rows get concatenated?

A cross join concatenates all l-r. So, 16.

> select * from lefty cross join righty;
+---+---+---+---+
| k | v | k | v |
|---+---+---+---|
| 1 | a | 1 | e |
| 1 | a | 2 | f |
| 1 | a | 2 | g |
| 1 | a | 4 | h |
| 2 | b | 1 | e |
| 2 | b | 2 | f |
| 2 | b | 2 | g |
| 2 | b | 4 | h |
| 2 | c | 1 | e |
| 2 | c | 2 | f |
| 2 | c | 2 | g |
| 2 | c | 4 | h |
| 3 | d | 1 | e |
| 3 | d | 2 | f |
| 3 | d | 2 | g |
| 3 | d | 4 | h |
+---+---+---+---+

An inner join concatenates matching l-r. So, 5.

> select * from lefty inner join righty on lefty.k = righty.k;
+---+---+---+---+
| k | v | k | v |
|---+---+---+---|
| 1 | a | 1 | e |
| 2 | b | 2 | f |
| 2 | b | 2 | g |
| 2 | c | 2 | f |
| 2 | c | 2 | g |
+---+---+---+---+

A left join concatenates matching l-r, plus l-null for unmatched l. So, 6.

> select * from lefty left join righty on lefty.k = righty.k;
+---+---+--------+--------+
| k | v | k      | v      |
|---+---+--------+--------|
| 1 | a | 1      | e      |
| 2 | b | 2      | f      |
| 2 | b | 2      | g      |
| 2 | c | 2      | f      |
| 2 | c | 2      | g      |
| 3 | d | <null> | <null> |
+---+---+--------+--------+

A right join concatenates matching l-r, plus null-r for unmatched r. So, 6.

> select * from lefty right join righty on lefty.k = righty.k;
+--------+--------+---+---+
| k      | v      | k | v |
|--------+--------+---+---|
| 1      | a      | 1 | e |
| 2      | b      | 2 | f |
| 2      | c      | 2 | f |
| 2      | b      | 2 | g |
| 2      | c      | 2 | g |
| <null> | <null> | 4 | h |
+--------+--------+---+---+

A full join concatenates matching l-r, plus l-null for unmatched l and null-r for unmatched r. So, 7.

> select * from lefty full join righty on lefty.k = righty.k;
+--------+--------+--------+--------+
| k      | v      | k      | v      |
|--------+--------+--------+--------|
| 1      | a      | 1      | e      |
| 2      | b      | 2      | f      |
| 2      | b      | 2      | g      |
| 2      | c      | 2      | f      |
| 2      | c      | 2      | g      |
| 3      | d      | <null> | <null> |
| <null> | <null> | 4      | h      |
+--------+--------+--------+--------+

It’s no different if lefty or righty contained a null k. Just remember that l and r don’t match when either’s k is null.

TODO: lateral join?