| Lesson 12 | Using Subqueries |
| Objective | Use subqueries with your INSERT, UPDATE, and DELETE statements. |
Using Subqueries with INSERT, UPDATE, and DELETE
The SELECT statement module covered using a subquery to filter or calculate values inside a SELECT. This lesson covers the same technique applied to the three Data Manipulation Language (DML) statements: using a subquery to filter, populate, or recalculate data inside an INSERT, UPDATE, or DELETE.
When to Use a Subquery: A Quick Review
A subquery can appear:
- After an
IN keyword
- After a
NOT IN keyword
- After an
EXISTS keyword
- After a
NOT EXISTS keyword
- After an
ANY keyword
- After a
SOME keyword
- After an
ALL keyword
- When comparing values
- Anywhere an expression is allowed
A subquery is most commonly used in place of joining two tables together, and shown as a subset of
DELETE syntax, the general pattern looks like this:
DELETE FROM table
[FROM table [, ...]]
WHERE column [NOT] IN | [NOT] EXISTS | ANY | SOME | ALL | comparison (subquery)
The important thing to remember is that a subquery returns one or more values, and which form you use (
IN,
EXISTS, a scalar comparison) depends on how many rows and columns it returns. Testing a subquery on its own first, before wrapping it in the outer statement, is the easiest way to confirm what it actually returns before trusting it inside a larger
INSERT,
UPDATE, or
DELETE.
As for performance, SQL Server 2025's optimizer, Intelligent Query Processing, active under the default compatibility level of 170, routinely rewrites a correlated
EXISTS subquery into an efficient semi-join or anti-semi-join plan behind the scenes. In practice, there is little reason to prefer a hand-written join over a well-formed
EXISTS subquery purely for performance; pick whichever form reads more clearly for the situation.
A subquery can appear anywhere an expression is allowed in an
INSERT,
UPDATE, or
DELETE statement, with one exception: it cannot appear in an
ORDER BY clause.
Subqueries with INSERT
Table-Level Subquery: INSERT INTO ... SELECT
The standard pattern for bulk-loading rows from one or more source tables into a target table does not wrap the
SELECT in parentheses:
INSERT INTO Sales.OrdersArchive (OrderID, CustomerID, OrderDate, TotalAmount)
SELECT
o.OrderID,
o.CustomerID,
o.OrderDate,
o.TotalAmount
FROM Sales.Orders AS o
WHERE o.Status = 'Completed'
AND o.OrderDate >= '2025-01-01'
AND o.OrderDate < '2026-01-01';
Scalar Subquery Inside a VALUES Clause
When inserting a single row, a scalar subquery (one that returns at most one value) can sit inside the
VALUES list to look up a foreign key or other value dynamically:
INSERT INTO Production.Products (ProductName, CategoryID, Price, CreatedDate)
VALUES (
'Quantum SSD 4TB',
(SELECT CategoryID FROM Production.Categories WHERE CategoryName = 'Storage'),
349.99,
SYSDATETIME()
);
Worked Example: Guarding Against a Duplicate WSS SalaryHistory Row
A
NOT EXISTS subquery inside an
INSERT is a common guard against inserting a row that is already there. Before giving Mickey Mouse a raise record, WSS can check whether that exact raise has already been logged:
INSERT INTO SalaryHistory (LastName, FirstName, PriorSalary)
SELECT LastName, FirstName, CurrentSalary
FROM Employee
WHERE LastName = 'Mouse'
AND FirstName = 'Mickey'
AND NOT EXISTS (
SELECT *
FROM SalaryHistory
WHERE LastName = 'Mouse'
AND FirstName = 'Mickey'
AND PriorSalary = 20000
);
The subquery runs once per candidate row from
Employee. If a
SalaryHistory row already exists for Mickey Mouse at a
PriorSalary of
20000,
NOT EXISTS evaluates false and the insert is skipped, so running this statement twice by accident does not create a duplicate record.
Subqueries with UPDATE
Worked Example: A Scalar Subquery in the SET Clause
A scalar subquery, one that is guaranteed to return exactly one value, can sit directly in the
SET clause. Here, WSS wants to give Mickey Mouse a raise equal to 150% of the current highest salary at the company:
UPDATE Employee
SET CurrentSalary = (
SELECT MAX(CurrentSalary) * 1.5
FROM Employee
)
WHERE LastName = 'Mouse'
AND FirstName = 'Mickey';
The inner
SELECT returns a single value, the highest
CurrentSalary in the table times 1.5, and that value is applied only to the row matching Mickey Mouse.
Correlated Subquery in the SET Clause
Filtering Rows with WHERE EXISTS
When a condition in another table should decide which rows get updated, but nothing from that table needs to be pulled into the
SET clause,
EXISTS (or
NOT EXISTS) in the
WHERE clause is the right tool:
UPDATE p
SET p.UnitPrice = p.UnitPrice * 0.90
FROM Production.Products AS p
WHERE NOT EXISTS (
SELECT 1
FROM Sales.OrderDetails AS od
JOIN Sales.Orders AS o ON od.OrderID = o.OrderID
WHERE od.ProductID = p.ProductID
AND o.OrderDate >= DATEADD(DAY, -90, SYSDATETIME())
);
Updating Multiple Columns via a Derived Table
Writing a separate scalar subquery for each column in the
SET clause means recalculating the same thing more than once. A derived table in the
FROM clause computes it once and updates every column from it in a single pass:
UPDATE c
SET
c.OrderCount = summary.TotalOrders,
c.LastOrderDate = summary.MaxDate
FROM Sales.Customers AS c
JOIN (
SELECT
CustomerID,
COUNT(OrderID) AS TotalOrders,
MAX(OrderDate) AS MaxDate
FROM Sales.Orders
GROUP BY CustomerID
) AS summary ON c.CustomerID = summary.CustomerID;
Subqueries with DELETE
Worked Example: Deleting with a Multi-Row IN Subquery
A subquery used with
IN can return more than one value, which is useful when the condition does not uniquely identify a single row. WSS wants to remove every
SalaryHistory record belonging to any employee named Mouse:
DELETE FROM SalaryHistory
WHERE EmployeeID IN (
SELECT EmployeeID
FROM Employee
WHERE LastName = 'Mouse'
);
Because the subquery filters only on
LastName, it can return more than one
EmployeeID (Mickey's and Minnie's both), and every value it returns is honored by the
IN list.
Prefer EXISTS / NOT EXISTS over IN / NOT IN
The lesson on deleting data from another table covered this in depth: if the subquery inside a
NOT IN list returns even a single
NULL, the entire expression evaluates to
UNKNOWN and the
DELETE removes nothing at all.
EXISTS and
NOT EXISTS test for the presence of a matching row instead of comparing against a list, so they do not carry that risk:
DELETE FROM Staging.CustomerImports
WHERE EXISTS (
SELECT 1
FROM Sales.Customers AS c
WHERE c.EmailAddress = Staging.CustomerImports.EmailAddress
);
Best Practices and Pitfalls in SQL Server 2025
A few patterns are worth keeping in mind whenever a subquery drives an INSERT, UPDATE, or DELETE:
| Pattern / Feature | Recommendation | Why It Matters |
EXISTS vs. IN | Prefer EXISTS / NOT EXISTS | EXISTS short-circuits as soon as a match is found and handles NULL safely, where NOT IN fails silently if the subquery's result set contains a NULL. |
| Error 512 | Guard scalar subqueries with TOP (1) or an aggregate | If a scalar subquery in a SET or VALUES clause unexpectedly returns more than one row, SQL Server stops the statement with Msg 512, "Subquery returned more than 1 value." |
| 2025 Optimizer (IQP) | Write natural EXISTS and correlated patterns | SQL Server 2025's Intelligent Query Processing can automatically un-nest many correlated subqueries into efficient semi-join or anti-semi-join plans under compatibility level 170. |
| Auditing | Pair with the OUTPUT clause | When a subquery-driven UPDATE or DELETE touches rows across tables, an OUTPUT clause reading deleted.* and, for an UPDATE, inserted.*, records exactly which rows changed. |
Create Delete Statement using Subquery - Exercise
Ready to practice? The exercise below gives you another scenario for deleting data using a subquery, so you can work through the
IN and
EXISTS questions on your own.
Create Delete Statement using Subquery - Exercise
In the next lesson, you will learn about indexing strategies.
