Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

SQL (PostgreSQL, MySQL, SQLite)  Module 6 – Subqueries and set operations

Subqueries in SELECT, WHERE and FROM

Use a subquery as one value in SELECT and WHERE or as a derived table in FROM, and see how PostgreSQL, MySQL and SQLite treat extra rows and names.

  • Intermediate
  • 25 minutes
  • Examples run with sql.js 1.14.2
  • By MySmartCoPilot
Free core, Pro depth: 8 of 25 minutes free
Free: Three places for a subquery · A scalar subquery is one value · A derived table is a result you can query · 2 more sections. With a pass: 3 in-depth parts and 3 interview answers, about 17 minutes.

What you will learn

  • Use a scalar subquery as a single value in SELECT and WHERE
  • Use a derived table (a subquery in FROM) and name it
  • Predict what each engine does when a scalar subquery returns several rows or none
  • Aggregate in derived tables before joining, so that totals are not multiplied

Before you start

A subquery is a SELECT written in parentheses inside another statement. What it gives the outer query depends on where it stands. In SELECT or WHERE, a scalar subquery returns a single value, one row with one column, which the outer query uses like a constant: price_paise > (SELECT avg(price_paise) FROM products) compares every product with the average. In FROM, a subquery is a derived table: a result with its own name that the outer query reads like a table, which lets you filter, join or aggregate a result that is itself an aggregate. Subqueries let one statement do what would otherwise take two queries and a value copied between them, and the engines run the inner query for you.

Press Run on any example to try it: the queries execute in sql.js 1.14.2, a build of SQLite 3.49.1 for the browser, and what PostgreSQL 18 and MySQL 8.4 do instead is taken from their manuals.

Three places for a subquery

Subqueries in SELECT, WHERE and FROM differ in what they return, and so in what the outer query may do with them:

A subquery in SELECT or WHERE returns one value; in FROM it is a derived table of rows; IN and EXISTS follow next.One query, three placesIn WHERE with IN or EXISTS:a list, or a yes or no(the next lesson)In SELECT: a scalar subquery(SELECT avg(price_paise) FROM products)one value, shown on every rowIn FROM: a derived table(SELECT city, count(*) ... GROUP BY city) AS per_cityrows, read like a tableIn WHERE: a scalar subqueryprice_paise > (SELECT avg(...) FROM products)one value to compare withalso

Where a subquery can go, and what it returns

Text description of the diagram

The diagram shows one outer query with three places where a subquery can be written, in the order they appear in the query text.

1. In SELECT, a scalar subquery such as (SELECT avg(price_paise) FROM products) returns one value, which is shown on every row of the result. 2. In FROM, a derived table such as (SELECT city, count(*) ... GROUP BY city) AS per_city returns rows, which the outer query reads as if they were a table. 3. In WHERE, a scalar subquery such as price_paise > (SELECT avg(...) FROM products) returns one value that each row is compared with.

A dashed box below, joined by an arrow labelled "also", adds a fourth use that the next lesson covers: in WHERE with IN or EXISTS, a subquery returns a list of values or a yes or no answer.

What each kind of subquery returns
Criterion Scalar subqueryDerived tableIN or EXISTS subquery
Written in SELECT, WHERE, HAVING, ORDER BYFROM, or a JOINWHERE or HAVING
Returns One value: one row, one columnRows and columns, like a tableTrue or false for each outer row
Needs a name NoYes in MySQL; good practice everywhereNo
Typical use Compare with or show an overall figureAggregate first, then filter, join or aggregate againKeep rows that have, or lack, a match

The third kind, a subquery that tests membership, is the subject of the next lesson. This one covers the first two.

A scalar subquery is one value

The two queries below use the same subquery, once in WHERE and once in SELECT:

Each product against the average price SQL · scalar_values.sql
-- Nine products of a grocery shop, with prices in paise.
CREATE TABLE products (
  id          INTEGER PRIMARY KEY,
  name        TEXT    NOT NULL,
  category    TEXT    NOT NULL,
  price_paise INTEGER NOT NULL
);
INSERT INTO products VALUES
  (1, 'Masala tea 250 g',    'drinks', 18000), (2, 'Filter coffee 500 g', 'drinks', 42000),
  (3, 'Green tea 100 g',     'drinks', 24000), (4, 'Basmati rice 5 kg',   'grains', 65000),
  (5, 'Wheat atta 10 kg',    'grains', 48000), (6, 'Red lentils 1 kg',    'grains', 14000),
  (7, 'Ghee 1 l',            'dairy',  62000), (8, 'Paneer 200 g',        'dairy',   9000),
  (9, 'Curd 400 g',          'dairy',   5000);

-- 1. A scalar subquery in WHERE: the products that cost more than the average product.
SELECT name, price_paise / 100.0 AS rupees
FROM products
WHERE price_paise > (SELECT avg(price_paise) FROM products)
ORDER BY price_paise DESC;

-- 2. The same subquery in SELECT, where it supplies one value for every row.
SELECT name,
       price_paise / 100.0                                              AS rupees,
       round((SELECT avg(price_paise) FROM products) / 100.0, 2)        AS average_rupees,
       round((price_paise - (SELECT avg(price_paise) FROM products)) / 100.0, 2) AS difference
FROM products
ORDER BY difference DESC
LIMIT 4;

Output

┌─────────────────────┬────────┐
│        name         │ rupees │
├─────────────────────┼────────┤
│ Basmati rice 5 kg   │ 650.0  │
│ Ghee 1 l            │ 620.0  │
│ Wheat atta 10 kg    │ 480.0  │
│ Filter coffee 500 g │ 420.0  │
└─────────────────────┴────────┘
┌─────────────────────┬────────┬────────────────┬────────────┐
│        name         │ rupees │ average_rupees │ difference │
├─────────────────────┼────────┼────────────────┼────────────┤
│ Basmati rice 5 kg   │ 650.0  │ 318.89         │ 331.11     │
│ Ghee 1 l            │ 620.0  │ 318.89         │ 301.11     │
│ Wheat atta 10 kg    │ 480.0  │ 318.89         │ 161.11     │
│ Filter coffee 500 g │ 420.0  │ 318.89         │ 101.11     │
└─────────────────────┴────────┴────────────────┴────────────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < scalar_values.sql

The average of the nine prices is 318.89 rupees, and four products cost more. In the second query the subquery supplies that average to every row, so each product can show its distance from it. Scalar subqueries in SELECT lists work like a column whose value is the same on every row. Because this subquery does not refer to the outer row, the engine computes it once and reuses the result; SQLite’s manual says so in those words.

A derived table is a result you can query

A derived table turns a result into something the outer query can treat as a table. That is how you filter on an aggregate without HAVING, and how you aggregate twice:

Revenue per city, then a second step on top SQL · derived_table.sql
-- Fourteen orders from five cities, with amounts in paise.
CREATE TABLE orders (
  id           INTEGER PRIMARY KEY,
  city         TEXT    NOT NULL,
  amount_paise INTEGER NOT NULL
);
INSERT INTO orders VALUES
  (1,  'Pune',    45000), (2,  'Kochi',   12000), (3,  'Pune',    30000), (4,  'Jaipur',   8000),
  (5,  'Pune',    99000), (6,  'Kochi',   15000), (7,  'Indore',  52000), (8,  'Kochi',    7000),
  (9,  'Jaipur',  26000), (10, 'Pune',    18000), (11, 'Indore',  21000), (12, 'Guwahati', 64000),
  (13, 'Jaipur',  33000), (14, 'Kochi',   40000);

-- 1. A derived table: revenue per city, computed by the inner query and read like a table by the outer one.
SELECT city, orders, rupees
FROM (SELECT city, count(*) AS orders, sum(amount_paise) / 100.0 AS rupees
      FROM orders
      GROUP BY city) AS per_city
WHERE orders >= 3
ORDER BY rupees DESC;

-- 2. An aggregate of aggregates needs two steps: the average and the best of the five city totals.
SELECT count(*) AS cities, round(avg(rupees), 2) AS average_city, max(rupees) AS best_city
FROM (SELECT city, sum(amount_paise) / 100.0 AS rupees
      FROM orders
      GROUP BY city) AS per_city;

-- 3. In one step it is an error: aggregates cannot be nested.
SELECT avg(sum(amount_paise)) FROM orders GROUP BY city;

Output (exit status 1)

┌────────┬────────┬────────┐
│  city  │ orders │ rupees │
├────────┼────────┼────────┤
│ Pune   │ 4      │ 1920.0 │
│ Kochi  │ 4      │ 740.0  │
│ Jaipur │ 3      │ 670.0  │
└────────┴────────┴────────┘
┌────────┬──────────────┬───────────┐
│ cities │ average_city │ best_city │
├────────┼──────────────┼───────────┤
│ 5      │ 940.0        │ 1920.0    │
└────────┴──────────────┴───────────┘

Printed as an error (standard error)

Parse error near line 28: misuse of aggregate function sum()

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < derived_table.sql

The inner query of the first statement adds up the orders of each city, and the outer query sees a small table, per_city, with five rows and the columns city, orders and rupees. Its WHERE keeps the three cities with at least three orders. The second statement needs two steps by nature: the average of the city totals, 940 rupees, is an aggregate over aggregates. Written in one step, as the third statement tries, it fails in every engine, because aggregates cannot be nested; SQLite calls it a misuse of sum(), PostgreSQL says that aggregate function calls cannot be nested, and MySQL reports error 1111.

The name after the parentheses, per_city, is the derived table’s alias. Give one every time: MySQL refuses a derived table without it.

When the one-value promise breaks

A scalar subquery promises one row. The data does not always keep that promise, and the engines react differently:

  • When it returns two or more rows, PostgreSQL and MySQL stop with an error, while SQLite quietly uses the first row it finds.
  • When it returns no rows, the value is NULL in every engine, and a comparison with NULL keeps nothing.
  • Joining two tables that both have several rows per customer multiplies the rows, so totals must be aggregated in derived tables first.
  • Names differ too: MySQL needs an alias for every derived table, and SQLite rejects a column list after it.

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 No product has the category 'snacks'. What does this query return?

    Read the code, then choose one answer.

    SELECT (SELECT count(*) FROM products WHERE category = 'snacks') AS snacks;
    Show the answer to question 1

    Answer: One row with 0

    count() returns 0 for an empty set, so the scalar subquery has one row with the value 0. A subquery whose own query returns no rows, or an avg() or sum() over no rows, would give NULL instead.

  2. Question 2 of 5 A scalar subquery returns two rows in PostgreSQL 18. What happens?

    Choose one answer.

    Show the answer to question 2

    Answer: The statement fails with "more than one row returned by a subquery used as an expression"

    PostgreSQL's manual calls it an error to use a query that returns more than one row as a scalar subquery. MySQL fails too, with error 1242; SQLite is the engine that quietly uses the first row.

  3. Question 3 of 5 You need the average of the per-city revenue totals. Why does avg(sum(amount_paise)) ... GROUP BY city fail?

    Choose one answer.

    Show the answer to question 3

    Answer: Aggregates cannot be nested; compute the totals in a derived table and average them in the outer query

    An aggregate cannot take another aggregate as its argument, in PostgreSQL, MySQL or SQLite. A derived table does the first step, one total per city, and the outer query averages those totals.

  4. Question 4 of 5 In which engine does this query fail?

    Read the code, then choose one answer.

    SELECT * FROM (SELECT 1 AS n);
    Show the answer to question 4

    Answer: MySQL 8.4

    MySQL requires a name for every derived table and reports "Every derived table must have its own alias". PostgreSQL allows the name to be left out since version 16, and SQLite always has. Writing AS t keeps the query portable.

  5. Question 5 of 5 A query lists 10,000 products, each next to (SELECT avg(price_paise) FROM products). The subquery does not refer to the outer row. How often does SQLite compute it?

    Choose one answer.

    Show the answer to question 5

    Answer: Once, and the result is reused for every row

    SQLite's manual says that an uncorrelated subquery is evaluated only once and its result reused as necessary. A subquery that refers to the outer row is correlated and is evaluated again for each row, which the lesson on correlated subqueries measures.

Your turn

Exercise · Medium · SQL

List the products priced above their category's average

The table products has the columns id, name, category and price_paise (the price in paise, 100 paise to the rupee).

Write a query that returns the products that cost more than the average price of their own category, with two columns:

  • name: the product's name;
  • above_by_rupees: how much more than its category's average the product costs, in rupees, rounded to two decimal places.

A product that costs exactly its category's average is not listed, and the order of the rows does not matter. Compare each product with its own category, not with the average of the whole shop.

Starter code · above_average.sql

-- Products priced above the average of their own category: name, above_by_rupees.
SELECT name,
       (price_paise - (SELECT avg(price_paise) FROM products)) / 100.0 AS above_by_rupees
FROM products
WHERE price_paise > (SELECT avg(price_paise) FROM products);
The sample tests · tests.yaml
# Sample tests: each one builds a small products table, runs your query on it and compares the rows it returns (in any
# order).
tests:
  - name: compares each product with its own category
    setup: |
      CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, category TEXT NOT NULL, price_paise INTEGER NOT NULL);
      INSERT INTO products VALUES
        (1, 'Masala tea', 'drinks', 20000), (2, 'Filter coffee', 'drinks', 40000),
        (3, 'Basmati rice', 'grains', 90000), (4, 'Wheat atta', 'grains', 50000), (5, 'Red lentils', 'grains', 10000);
    expected:
      columns: [name, above_by_rupees]
      rows:
        - ['Filter coffee', 100]
        - ['Basmati rice', 400]
  - name: leaves out a product that costs exactly the average
    setup: |
      CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, category TEXT NOT NULL, price_paise INTEGER NOT NULL);
      INSERT INTO products VALUES
        (1, 'Ghee', 'dairy', 60000), (2, 'Paneer', 'dairy', 30000), (3, 'Curd', 'dairy', 0),
        (4, 'Butter', 'dairy', 30000), (5, 'Green tea', 'drinks', 25000);
    expected:
      columns: [name, above_by_rupees]
      rows:
        - ['Ghee', 300]
  - name: rounds to the paisa when the average is not a whole number
    setup: |
      CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, category TEXT NOT NULL, price_paise INTEGER NOT NULL);
      INSERT INTO products VALUES
        (1, 'Jaggery', 'sweets', 10000), (2, 'Sugar', 'sweets', 12000), (3, 'Honey', 'sweets', 25000);
    expected:
      columns: [name, above_by_rupees]
      rows:
        - ['Honey', 93.33]
A hint

Compute the averages first, in a derived table with one row per category: (SELECT category, avg(price_paise) AS avg_paise FROM products GROUP BY category) AS c. Join it to products on the category, keep the rows whose price is above c.avg_paise, and turn the difference into rupees with round(... / 100.0, 2).

The sample tests run on this device, in your browser (sql.js): nothing is sent to mysmartcopilot.com. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Two rows or none: a scalar subquery in three engines Pro: opens with a Pro, Premium or Ultimate pass

A scalar subquery that breaks its promise is one of the few places where the three engines give three different results for the same text. This part reproduces both broken cases and compares four ways to keep the promise.

In this part:

  • Two rows: an error in PostgreSQL and MySQL, a silent first row in SQLite
  • No rows: NULL, and the filter that keeps nothing
  • Four ways to keep the promise, compared

2 runnable examples (SQL) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Derived tables that stop double counting, with numbers Pro: opens with a Pro, Premium or Ultimate pass

The most common reason for a derived table in real reports is to stop a join from multiplying rows before they are added up. This part reproduces the multiplication on four customers, measures how wrong each total becomes, and fixes it with one derived table per child table.

In this part:

  • Orders and payments joined: totals two and three times too high
  • Aggregate first, then join: one row per customer on each side
  • Checking the result: totals that must match

2 runnable examples (SQL) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Derived tables that run in PostgreSQL, MySQL and SQLite Pro: opens with a Pro, Premium or Ultimate pass

The three engines agree on what a derived table means but not on how to write one. This part covers names, column lists, row values and inline VALUES tables, and reads SQLite’s plans to show how it runs each kind of subquery.

In this part:

  • Names and column lists: what each engine accepts
  • Row values: two columns compared with one subquery
  • How SQLite runs a subquery, read from its plans

3 runnable examples (SQL) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    What is a scalar subquery, and what does it return when its query finds no row?

    A scalar subquery is a SELECT in parentheses that returns exactly one column and at most one row, so it can stand wherever a single value can: in the SELECT list, in WHERE, HAVING or ORDER BY, or as an argument of a function, as in price > (SELECT avg(price) FROM products). When its query finds no row, the value is NULL in PostgreSQL, MySQL and SQLite, and any comparison with that NULL is not true, so a filter built on it quietly keeps nothing; wrap it in coalesce() when the empty case needs a value. An aggregate such as count(*) is different: over no rows it still returns one row, with 0. When the query finds two or more rows, PostgreSQL and MySQL raise an error, while SQLite uses the first row.

  2. Fresher to mid

    Write a query that returns the second-highest distinct order amount of an orders table as a single value, and NULL when there is no second amount. Then explain what changes when two orders share the highest amount.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    A nightly report ran on SQLite for a year. After the move to PostgreSQL it fails with "more than one row returned by a subquery used as an expression", although the data was copied unchanged. What was the report doing in SQLite, and how do you fix it?

    Model answer, Pro. First step and follow-ups

  4. Fresher to senior

    A customer statement joins customers, orders and payments and groups by customer. For some customers the ordered amount is exactly two or three times too high, and one customer with no payments is missing. Explain the cause and write a correct query.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A scalar subquery returns one value and can stand wherever a value can, in SELECT, WHERE or ORDER BY; a subquery that does not refer to the outer row is computed once.
  • A derived table is a subquery in FROM with a name; it makes two-step questions, such as an average of totals or a filter on an aggregate, into one statement.
  • Two rows from a scalar subquery are an error in PostgreSQL and MySQL and a silent first row in SQLite; no rows give NULL everywhere.
  • Name every derived table, so that the same query runs in MySQL, PostgreSQL and SQLite.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.