<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. SQL
  3. Grouping and subqueries
All lessons in this course

Lesson 26 of 54 17 minIntermediate

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.

A subquery is a SELECT in brackets, used inside another statement. SQL Server runs it and uses its result as a value, a list or a table. Which products cost more than the average?

Query.sql
SELECT Name, PriceFROM dbo.ProductsWHERE Price > (SELECT AVG(Price) FROM dbo.Products)ORDER BY Price DESC;
Output
Name                 Price
-------------------  -----
Cordless drill       89.00
Masonry paint 10 L   54.00
Plywood sheet 18 mm  42.00

You could not write 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.

KindReturnsUsed with
Scalarone value (one row, one column)=, <, >, in SELECT, anywhere a value fits
Listone column, any number of rowsIN, NOT IN
Table (derived table)rows and columnsFROM (…) AS alias
Correlatedany of the above, once per outer rowrefers to a column of the outer query

Scalar subqueries

A subquery that returns one value can go wherever a value can, including the SELECT list:

Query.sql
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;
Output
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

This is also the answer to the last lesson’s puzzle, “which product is the dearest?”: find the maximum, then the row that has it.

Query.sql
SELECT Name, PriceFROM dbo.ProductsWHERE Price = (SELECT MAX(Price) FROM dbo.Products);
Output
Name            Price
--------------  -----
Cordless drill  89.00

If a subquery used with = returns more than one row, the statement fails when it runs:

Query.sql
SELECT NameFROM dbo.ProductsWHERE CategoryId = (SELECT CategoryId FROM dbo.Categories WHERE Name LIKE N'T%');
Output
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.

“Tools” and “Timber” both start with T. A scalar subquery that returns no rows is not an error: it gives NULL, and a comparison with NULL is never true, so the outer query quietly returns nothing.

Lists with IN

A subquery that returns one column of many rows can feed IN. Which customers have bought a Tools product?

Query.sql
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;
Output
FirstName  LastName
---------  --------
Asha       Patel
Farid      Khan
Zoë        Müller

Asha bought three tools, but she appears once. IN only asks “is this value in the list?”, so duplicates in the list do not repeat rows. A join to the same tables would have returned Asha three times.

Derived tables: a subquery in FROM

A subquery in FROM is treated as a table for the rest of the query. It must have an alias, and every column must have a name. This finds the average order value, which needs two levels of aggregation: first the total of each order, then the average of those totals:

Query.sql
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;
Output
Orders  AvgOrderValue  BiggestOrder
------  -------------  ------------
    10     133.325000        524.00

Leave out the column name and SQL Server stops you:

Query.sql
SELECT AVG(t.OrderTotal)FROM (    SELECT OrderId, SUM(Quantity * UnitPrice)    FROM dbo.OrderItems    GROUP BY OrderId) AS t;
Output
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

The subqueries so far could run on their own. A correlated subquery refers to a column of the outer query, so it has a different answer for each outer row. Conceptually it runs once per row. Which products cost more than the average of their own category?

Query.sql
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;
Output
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
Interactive

One inner query per outer row

Outer row 1 of 12
Outer: p
Inner query for this row
SELECT AVG(p2.Price)
FROM dbo.Products AS p2
WHERE p2.CategoryId = 1  -- p.CategoryId
Claw hammer 18.50Cordless drill 89.00Tape measure 8 m 9.75
AVG = 39.083333
18.50 > 39.083333 is false: not kept
NameCategoryIdPrice
(0 rows)
For Claw hammer the inner query sees only the 3 Tools products, because p2.CategoryId = p.CategoryId uses the outer row’s value (1). Step on: the next category gives a different average.

The two aliases are essential. 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.

A correlated subquery in SELECT adds a calculated value for each row, here each customer’s number of orders and latest order date:

Query.sql
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;
Output
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

Customers with no orders get 0 and NULL, with no LEFT JOIN needed: the subquery simply finds no rows for them.

Quick check

What happens when a subquery compared with = returns three rows?

Quick check

What makes a subquery correlated?

Practice

List the orders whose total value is above the average order value, with the order id and its total. Use a derived table for the order totals and a scalar subquery for the average.

Show a hint

Group OrderItems by OrderId inside FROM (…) AS t. In WHERE, compare t.OrderTotal with (SELECT AVG(…) FROM (…) AS x), which needs the totals a second time.

Show the solution
Solution.sql
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;
Output
OrderId  OrderTotal
-------  ----------
      4      524.00
      7      199.50

Remember

  • 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.
0 of 54 lessons completed