How do you write a DELETE whose target rows are chosen by a match in another table?
answer
- One statement, one target table
- The other table goes in the predicate
- Correlate on the join column inside the subquery
- EXISTS for match, NOT EXISTS for orphans
- JOIN syntax in DELETE is vendor-specific
basics
~20 sKeep one target table in DELETE FROM and put the other table inside a correlated EXISTS or an IN subquery in the WHERE clause. Only the target table's rows are removed; the second table is read, never modified.
solid answer
~50 sStandard `DELETE` names **one** table, so the second table has to appear inside the `WHERE` clause as a subquery: ```sql DELETE FROM order_items oi WHERE EXISTS (SELECT 1 FROM orders o WHERE o.id = oi.order_id AND o.status = 'CANCELLED'); ``` The correlated `EXISTS` asks, per candidate row, "is there a matching cancelled order?" and the row is removed when the answer is yes. `WHERE order_id IN (SELECT id FROM orders WHERE status = 'CANCELLED')` expresses the same thing; `EXISTS` generalises better because the correlation can use several columns and it behaves predictably when the subquery can produce NULLs. Flip it to `NOT EXISTS` to delete non-matching rows, such as orphans whose parent is gone. Note that `orders` is only *read* — one `DELETE` modifies one table. Some engines add non-standard join syntax for this (`DELETE ... USING`, `DELETE t1 FROM t1 JOIN t2`); the subquery form above is the portable one.
code
sql · 8 lines-- delete the line items belonging to cancelled orders
DELETE FROM order_items oi
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.id = oi.order_id
AND o.status = 'CANCELLED'
);go deeper
Know that DELETE targets one table and that a second table is referenced through a subquery in WHERE, and be able to write the IN form for a simple single-column match.
Explain the correlated EXISTS form, why it is the portable choice, how NOT EXISTS expresses the orphan case, and the fact that the second table is only read.
Demonstrate the discipline around it: qualified column names, a counted preview of the target set, deliberate match-versus-anti-match, and a conscious choice when reaching for a dialect-specific join form.
Decide where such cross-table cleanups live at all — reviewed migration, scheduled job, or application code — and whether the codebase commits to portable SQL or accepts engine-specific delete syntax as a lock-in cost.
## One target table, everything else in the WHERE clause A searched `DELETE` in standard SQL has a single target: ```sql DELETE FROM <table> [ [AS] alias ] WHERE <search condition>; ``` There is no `JOIN` in that grammar, and no way to remove rows from two tables at once. So the everyday requirement "delete the line items of cancelled orders" is expressed by putting the *other* table inside the search condition, as a subquery. ## The EXISTS form ```sql DELETE FROM order_items oi WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.id = oi.order_id AND o.status = 'CANCELLED' ); ``` This is a **correlated** subquery: it mentions `oi`, a name that belongs to the outer statement, so conceptually it is re-evaluated for each candidate row of `order_items`. `EXISTS` yields TRUE as soon as one matching row is found and FALSE otherwise — it never yields UNKNOWN — so the row is removed exactly when a cancelled parent exists. The `SELECT 1` is conventional: `EXISTS` only cares whether rows come back, never what is in them. Many engines accept an alias on the target table, as above. Where yours does not, drop the alias and qualify with the full table name (`orders.id = order_items.order_id`); the meaning is identical. ## The IN form, and why EXISTS is the safer habit ```sql DELETE FROM order_items WHERE order_id IN (SELECT id FROM orders WHERE status = 'CANCELLED'); ``` For a single-column match this is equally correct and often more readable. Two reasons to reach for `EXISTS` by default: it extends naturally to multi-column correlations (`o.tenant_id = oi.tenant_id AND o.id = oi.order_id`), and its negation is well behaved. `NOT IN` over a subquery whose column can be NULL famously stops matching anything at all, whereas `NOT EXISTS` has no such trap — which matters, because the negated form is exactly what you write to delete orphans: ```sql DELETE FROM order_items oi WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.id = oi.order_id); ``` ## Only the target table changes The subquery **reads** the other table; it never modifies it. A statement that deletes line items leaves `orders` completely untouched, even though `orders` is named in the text. If the business operation really is "remove the order and its items", that is two statements in one transaction, children first — or a cascading referential action declared on the foreign key in the schema. A second consequence: the subquery sees the table as it is *during* the statement. Reading the same table you are deleting from is where this gets subtle, and some engines refuse it outright unless you wrap the subquery in a derived table. ## Vendor join syntax is a convenience, not the standard Several engines offer a join-shaped `DELETE` — a `USING` clause, or naming the table to purge before `FROM` in a multi-table join. These are dialect extensions with genuinely different spellings, and code written with one does not move to another engine. Reach for them when you are writing engine-specific code and you have a reason; write the `EXISTS` form when the SQL has to be portable, live in a migration, or be read by someone who works on a different database. ## Getting it right Three checks pay for themselves every time: 1. **Qualify every column** inside the subquery with its table or alias. An unqualified name that does not exist in the inner table silently resolves to the outer one, and the predicate quietly becomes true for everything. 2. **Preview the target set.** Turn the statement into `SELECT COUNT(*) FROM order_items oi WHERE EXISTS (...)` with the identical predicate and see the number before you commit to it. 3. **Decide match versus anti-match deliberately.** `EXISTS` deletes the rows that *have* a partner; `NOT EXISTS` deletes the ones that do not. Inverting that by accident is the most expensive single-keyword mistake in this pattern.
- Can one DELETE statement remove rows from both tables it mentions?No. A `DELETE` has exactly one target table; any other table named in the statement appears inside the search condition and is only read. Removing rows from two tables means two statements in one transaction, ordered children-before-parents, or a cascading action declared on the foreign key in DDL.
- When would you prefer IN over EXISTS in a DELETE predicate?When the match is on a single column, the subquery is uncorrelated and its result is small and clearly non-nullable, `IN` reads more directly. Switch to `EXISTS` for multi-column correlation, and always for the negated form, since `NOT IN` over a nullable subquery column stops matching rows entirely.
- Why should every column inside the subquery be qualified with its table?Name resolution searches the inner query first and then reaches outward. An unqualified column that does not exist in the inner table silently binds to the outer table instead, turning the predicate into a comparison of a row with itself — which is true for essentially every row, so the DELETE removes far more than intended.
saying these in an interview costs you the question
- Believes one DELETE can purge both tables at once
- Writes DELETE ... JOIN and calls it standard SQL
- Uses NOT IN over a nullable column for orphan cleanup
- Leaves subquery columns unqualified, letting them bind outward
- Confuses EXISTS with NOT EXISTS and inverts the target set