Subqueries: scalar, list and correlated
A query inside a query: single-value subqueries in WHERE and SELECT, lists with IN, derived tables in FROM, and correlated subqueries that run once for each outer row.
SELECT Name, PriceFROM dbo.ProductsWHERE Price > (SELECT AVG(Price) FROM dbo.Products)ORDER BY Price DESC;Name Price ------------------- ----- Cordless drill 89.00 Masonry paint 10 L 54.00 Plywood sheet 18 mm 42.00
WHERE Price > AVG(Price): WHERE cannot use aggregates. The subquery works out the average first (22.254166) as a separate query, and the outer query compares each row with that number.
| Kind | Returns | Used with |
|---|---|---|
=, <, >, in SELECT, anywhere a value fits | ||
IN, NOT IN | ||
FROM (…) AS alias | ||
Scalar subqueries
SELECT Name, Price, (SELECT MAX(Price) FROM dbo.Products) AS Dearest, CAST(Price * 100 / (SELECT MAX(Price) FROM dbo.Products) AS decimal(5,1)) AS PctOfDearestFROM dbo.ProductsWHERE CategoryId = 1;Name Price Dearest PctOfDearest ---------------- ----- ------- ------------ Claw hammer 18.50 89.00 20.8 Cordless drill 89.00 89.00 100.0 Tape measure 8 m 9.75 89.00 11.0
SELECT Name, PriceFROM dbo.ProductsWHERE Price = (SELECT MAX(Price) FROM dbo.Products);Name Price -------------- ----- Cordless drill 89.00
= returns more than one row, the statement fails when it runs:
SELECT NameFROM dbo.ProductsWHERE CategoryId = (SELECT CategoryId FROM dbo.Categories WHERE Name LIKE N'T%');Msg 512, Level 16, State 1, Line 1 Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
Lists with IN
IN. Which customers have bought a Tools product?
SELECT FirstName, LastNameFROM dbo.CustomersWHERE CustomerId IN ( SELECT o.CustomerId FROM dbo.Orders AS o JOIN dbo.OrderItems AS oi ON oi.OrderId = o.OrderId JOIN dbo.Products AS p ON p.ProductId = oi.ProductId WHERE p.CategoryId = 1)ORDER BY FirstName;FirstName LastName --------- -------- Asha Patel Farid Khan Zoë Müller
Derived tables: a subquery in FROM
SELECT COUNT(*) AS Orders, AVG(t.OrderTotal) AS AvgOrderValue, MAX(t.OrderTotal) AS BiggestOrderFROM ( SELECT OrderId, SUM(Quantity * UnitPrice) AS OrderTotal FROM dbo.OrderItems GROUP BY OrderId) AS t;Orders AvgOrderValue BiggestOrder
------ ------------- ------------
10 133.325000 524.00SELECT AVG(t.OrderTotal)FROM ( SELECT OrderId, SUM(Quantity * UnitPrice) FROM dbo.OrderItems GROUP BY OrderId) AS t;Msg 8155, Level 16, State 2, Line 6 No column name was specified for column 2 of 't'. Msg 207, Level 16, State 1, Line 1 Invalid column name 'OrderTotal'.
Correlated subqueries
SELECT p.Name, p.CategoryId, p.PriceFROM dbo.Products AS pWHERE p.Price > ( SELECT AVG(p2.Price) FROM dbo.Products AS p2 WHERE p2.CategoryId = p.CategoryId)ORDER BY p.CategoryId;Name CategoryId Price ------------------- ---------- ----- Cordless drill 1 89.00 Cement 25 kg 2 7.20 Plywood sheet 18 mm 3 42.00 Wood screws (200) 4 11.90 Masonry paint 10 L 5 54.00
p is the product being tested and p2 ranges over the products in its category. Without them, CategoryId = CategoryId would compare p2 with itself and always be true.
SELECT c.FirstName, (SELECT COUNT(*) FROM dbo.Orders AS o WHERE o.CustomerId = c.CustomerId) AS Orders, (SELECT MAX(OrderDate) FROM dbo.Orders AS o WHERE o.CustomerId = c.CustomerId) AS LastOrderFROM dbo.Customers AS cORDER BY c.CustomerId;FirstName Orders LastOrder --------- ------ ---------- Asha 2 2024-03-01 Ben 2 2024-06-01 Chloe 1 2024-04-20 Dev 2 2024-05-05 Ella 0 NULL Farid 2 2024-06-12 Grace 0 NULL Zoë 1 2024-05-18
Show a hint
Show the solution
SELECT t.OrderId, t.OrderTotalFROM ( SELECT OrderId, SUM(Quantity * UnitPrice) AS OrderTotal FROM dbo.OrderItems GROUP BY OrderId) AS tWHERE t.OrderTotal > ( SELECT AVG(x.OrderTotal) FROM ( SELECT SUM(Quantity * UnitPrice) AS OrderTotal FROM dbo.OrderItems GROUP BY OrderId ) AS x)ORDER BY t.OrderTotal DESC;OrderId OrderTotal
------- ----------
4 524.00
7 199.50Remember
A scalar subquery returns one value and fits anywhere a value does. With =, more than one row is error 512; no rows gives NULL. IN (subquery) tests membership and never duplicates outer rows. A derived table needs an alias and a name for every column. A correlated subquery refers to the outer row and is evaluated per row (logically). Aliases keep the inner and outer copies of a table apart.