Joins, MySQL style: FULL OUTER emulation and multi-table writes
MySQL has no FULL OUTER JOIN, lets UPDATE and DELETE join other tables, and refuses a subquery that reads the table being changed. Here is how to work with all three.
After this lesson you can
- Emulate a FULL OUTER JOIN with two outer joins and UNION ALL
- Write UPDATE ... JOIN and DELETE ... JOIN statements
- Explain error 1093 and the two ways around it
The SQL course covers inner and outer joins. This lesson is about the places where MySQL differs from the standard and from other databases.
No FULL OUTER JOIN
MySQL does not implement FULL [OUTER] JOIN. Writing it is a syntax error,
as the first attempt further down shows. The standard emulation is two
queries glued together:
- a
LEFT JOINfrom A to B, which gives every row of A, matched or not; UNION ALLthe rows of B that have no match in A (aLEFT JOINfrom B to A, keepingWHERE a.key IS NULL).
CREATE TABLE stock (sku VARCHAR(5) PRIMARY KEY, qty INT NOT NULL);CREATE TABLE counted (sku VARCHAR(5) PRIMARY KEY, qty INT NOT NULL);INSERT INTO stock VALUES ('A', 10), ('B', 5), ('C', 0);INSERT INTO counted VALUES ('B', 4), ('C', 0), ('D', 2);SELECT s.sku, s.qty AS stock_qty, c.qty AS counted_qtyFROM stock sLEFT JOIN counted c ON c.sku = s.skuUNION ALLSELECT c.sku, NULL, c.qtyFROM counted cLEFT JOIN stock s ON s.sku = c.skuWHERE s.sku IS NULLORDER BY sku;Every SKU appears once: A exists only in stock, D only in the count,
B and C in both. The second half of the query is an anti-join, and
its IS NULL test must be on a column that cannot be NULL in a real
match, normally the join key.
Use UNION ALL, not UNION. Plain UNION removes duplicate rows, which
hides genuine duplicates in your data and adds a deduplication step to do it.
The anti-join already guarantees the two halves do not overlap.
UPDATE and DELETE can join
MySQL lets a data-changing statement join other tables, which is often clearer than a correlated subquery:
UPDATE products p
JOIN price_updates u ON u.product_id = p.id
SET p.price = u.new_price;
DELETE c -- name the table(s) to delete from
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL; -- customers with no orders
CREATE TABLE products (id INT PRIMARY KEY, name VARCHAR(20), price DECIMAL(8,2));CREATE TABLE price_updates (product_id INT PRIMARY KEY, new_price DECIMAL(8,2));INSERT INTO products VALUES (1, 'Pen', 1.00), (2, 'Ink', 5.00), (3, 'Pad', 3.00);INSERT INTO price_updates VALUES (1, 1.20), (3, 2.50), (9, 9.99);CREATE TABLE customers (id INT PRIMARY KEY, name VARCHAR(20));CREATE TABLE orders (id INT PRIMARY KEY, customer_id INT);INSERT INTO customers VALUES (1, 'Ann'), (2, 'Bob'), (3, 'Cy');INSERT INTO orders VALUES (10, 1), (11, 1), (12, 3);UPDATE products pJOIN price_updates u ON u.product_id = p.idSET p.price = u.new_price; DELETE cFROM customers cLEFT JOIN orders o ON o.customer_id = c.idWHERE o.id IS NULL; SELECT (SELECT GROUP_CONCAT(CONCAT(name, '=', price) ORDER BY id) FROM products) AS prices, (SELECT GROUP_CONCAT(name ORDER BY id) FROM customers) AS customers_left;The update changed the two products that have a pending price, and the
delete removed the one customer with no orders. In a multi-table DELETE
the names between DELETE and FROM say which tables lose rows; the
others are only read.
Error 1093: reading the table you are changing
MySQL refuses an UPDATE or DELETE whose own subquery selects from the
target table.
CREATE TABLE customers (id INT PRIMARY KEY, name VARCHAR(20));INSERT INTO customers VALUES (1, 'Ann'), (2, 'Bob');-- A helper that runs a statement and records the error instead of stopping.CREATE TABLE attempts (n INT AUTO_INCREMENT PRIMARY KEY, statement TEXT, outcome TEXT);CREATE PROCEDURE try_sql(IN stmt TEXT)BEGIN DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN GET DIAGNOSTICS CONDITION 1 @err_no = MYSQL_ERRNO, @err_msg = MESSAGE_TEXT; INSERT INTO attempts (statement, outcome) VALUES (stmt, CONCAT('error ', @err_no, ': ', REPLACE(@err_msg, CONCAT(DATABASE(), '.'), ''))); END; SET @sql = stmt; PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; INSERT INTO attempts (statement, outcome) VALUES (stmt, 'ok');END;CALL try_sql('SELECT * FROM customers x FULL OUTER JOIN customers y ON x.id = y.id');CALL try_sql('UPDATE customers SET name = ''Max'' WHERE id = (SELECT MAX(id) FROM customers)');CALL try_sql('UPDATE customers SET name = ''Max'' WHERE id = (SELECT m FROM (SELECT MAX(id) AS m FROM customers) AS t)');CALL try_sql('UPDATE customers c JOIN (SELECT MIN(id) AS m FROM customers) t ON c.id = t.m SET c.name = ''Min'''); SELECT n, outcome, IF(n = 4, (SELECT GROUP_CONCAT(name ORDER BY id) FROM customers), NULL) AS names_nowFROM attemptsORDER BY n;Two ways around it, both shown: wrap the subquery in a derived table
(SELECT ... FROM (SELECT ...) AS t), which MySQL materialises before the
update starts, or turn the statement into a multi-table UPDATE ... JOIN.
Common mistakes
- Writing
FULL OUTER JOIN. It does not exist in MySQL; the attempt above fails with error 1064. - Emulating it with
UNIONinstead ofUNION ALL. That silently collapses rows that are legitimately identical. - Testing the anti-join on a nullable column.
WHERE b.note IS NULLalso keeps matched rows whosenotehappens to beNULL; test the key. - Using a self-referencing subquery in
DELETE. Error 1093; use a derived table or a join.
What interviewers ask
- How do you get a full outer join in MySQL?
LEFT JOINone way,UNION ALLthe unmatched rows of aLEFT JOINthe other way. - How do you delete the rows of A that have no match in B?
DELETE a FROM a LEFT JOIN b ON ... WHERE b.key IS NULL, or .
Exercise · Pro
Reconcile stock with a count
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade