AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 07 — Subqueries: A Query Inside a Query

07 — Subqueries: A Query Inside a Query

August 13, 20266 min read
Download as Markdown

Assembling subqueries by trial and error until they ran was my method, and it never explained why. Writing them down produced one model: a subquery is a query whose result feeds another query, and the slot it occupies (scalar, column, row, or table) decides what shape it has to return. [1][2]

The framing that clicked is shape-driven. A subquery isn't a different kind of query — it's a regular SELECT placed inside another statement, returning a value, a row, or a set of rows. Once I knew which slot I was filling, the shape requirement became obvious: a scalar slot needs one column and one row, a WHERE col IN (...) slot needs one column and many rows, a FROM slot needs a whole table. The error messages ("scalar subquery returned more than one row") stopped being cryptic.

non-correlated (nested) outer: SELECT ... WHERE total > inner: SELECT AVG(total) FROM orders inner runs ONCE → one value feeds the outer WHERE correlated outer: per customer c inner: WHERE customer_id = c.id (outer's row) inner references outer → re-evaluated per outer row slot decides shape: scalar = 1 value · IN = 1 column · FROM = whole table

Where a subquery can appear

A subquery is a SELECT embedded in another statement. It can sit in several slots, and each slot demands a specific return shape [1]:

  • SELECT list — needs a scalar (one column, one row). Useful for attaching a computed benchmark to each output row.
  • WHERE with =, >, etc. — needs a scalar too.
  • WHERE col IN (...) — needs one column, many rows.
  • FROM — needs a whole table (any columns/rows); this is the "derived table" or inline view.
-- scalar in WHERE: compare each row to the overall average
SELECT name, total FROM orders
WHERE total > (SELECT AVG(total) FROM orders);

-- one-column set in WHERE: IN
SELECT name FROM customers
WHERE id IN (SELECT customer_id FROM orders WHERE total > 1000);

-- table in FROM: a derived table
SELECT t.customer_id, t.spent
FROM (SELECT customer_id, SUM(total) AS spent FROM orders GROUP BY customer_id) t
WHERE t.spent > 500;

Matching the subquery's shape to the slot is the whole skill. A scalar subquery that returns two rows in a WHERE total > (...) is a runtime error, not a logical one.

Nested subqueries — independent and run once

A nested (non-correlated) subquery doesn't refer to the outer query at all — it can run on its own, so the engine evaluates it once and feeds the result up [3][4]. The AVG(total) example above is nested: it computes one average over the whole orders table, independent of whichever outer row is being tested.

That independence is what makes nested subqueries cheap: one evaluation, reused for every outer row. If a subquery can be written without referencing the outer query, it should be.

Correlated subqueries — re-evaluated per row

A correlated subquery does reference the outer query's columns, so it cannot run standalone — the engine has to re-evaluate it once for each outer row [5][6].

SELECT c.name,
(SELECT SUM(o.total) FROM orders o WHERE o.customer_id = c.id) AS spent
FROM customers c;

Here the inner query references c.id, so for each customer the engine runs the sum over that customer's orders. Correlated subqueries read clearly — "for each customer, their total spend" — but the per-row re-evaluation is the classic performance trap. On large tables, the same logic rewritten as a JOIN against a grouped subquery usually runs dramatically faster, because the grouping happens once instead of per row [5].

When subqueries, when joins

This is the decision I had to make peace with. Subqueries and joins often express the same logic; the question is which reads more clearly and which the optimizer handles better:

  • Use a scalar subquery when I need one computed value plugged into a comparison or a SELECT list — the intent ("compare to the average") is obvious.
  • Use a JOIN when I need columns from another table on every output row — joins are what the engine is built to optimize.
  • Use a derived table in FROM when I need to aggregate first and filter the aggregate second, which is also what CTEs make readable (covered later).

Most "subquery vs join" debates collapse to this: prefer the join when performance matters at scale, prefer the subquery when it makes the logic clearer and the table isn't huge.

How I use this

The habit I keep is reading a subquery aloud and asking does the inner query mention the outer row? If yes, it's correlated, and the per-row cost is a flag — I'll check whether a grouped JOIN expresses the same thing once. If no, it's nested and cheap, and I leave it because it reads cleanly. And before writing any subquery I name the slot it's filling (scalar, IN-set, FROM-table); that single decision determines the shape requirement and catches the "returned more than one row" class of error at write time.

References

[1] TutorialsPoint, "SQL Sub Queries," tutorialspoint.com, 2024. [Online]. Available: https://www.tutorialspoint.com/sql/sql-sub-queries.htm

[2] YouTube, "Advanced SQL Tutorial | Subqueries," 2023. [Online]. Available: https://www.youtube.com/watch?v=m1KcNV-Zhmc

[3] StudySmarter, "Nested Subqueries in SQL," studysmarter.co.uk, 2024. [Online]. Available: https://www.studysmarter.co.uk/explanations/computer-science/databases/nested-subqueries-in-sql/

[4] YouTube, "MySQL Subqueries," 2023. [Online]. Available: https://www.youtube.com/watch?v=i5acg3Hvu6g

[5] MySQL, "Correlated Subqueries," dev.mysql.com, 2024. [Online]. Available: https://dev.mysql.com/doc/refman/8.4/en/correlated-subqueries.html

[6] YouTube, "Intro To Subqueries," 2023. [Online]. Available: https://www.youtube.com/watch?v=TUxadt94L0M

Knowledge check · Question 1 of 5

A subquery placed in WHERE total > (...) must return…

Comments

Leave a Comment

You must be signed in to comment

0 Comments

No comments yet. Be the first to comment!