Tier 2 · Core SQL

Derived Tables (Subqueries in FROM) in SQL

By Owen Middleton · Updated September 2026 · Examples run on PostgreSQL 17

What are Derived Tables in SQL?

A derived table is a subquery written in the FROM clause and treated as a temporary table for the rest of the query.

You want to find customers who placed more than 3 orders. You know how to count orders per customer with GROUP BY. But you can't filter on COUNT(*) directly in a WHERE clause — WHERE runs before aggregation, before any counts exist. HAVING filters at the aggregation layer, but it's not designed for everything you might want to do with grouped results. A derived table solves this by letting you run the aggregation in an inner query and then write a plain WHERE filter on the result in the outer query.

How do you write a subquery in the FROM clause?

The structure: write the aggregation in parentheses, give it an alias, and treat it like a table:

SELECT customer_id, order_count
FROM (
  SELECT customer_id, COUNT(*) AS order_count
  FROM orders
  GROUP BY customer_id
) AS customer_orders
WHERE order_count > 3

The inner query runs first. It produces one row per customer with their order count. The outer query treats that result as a table called customer_orders and filters it with WHERE. That name comes from the alias after the closing parenthesis.

How do you filter on an aggregate without HAVING?

This two-layer structure is the core use case: the inner query shapes or summarizes the data, and the outer query applies further logic to that shaped result. Aggregating first and filtering second is the most common pattern:

SELECT category_id, total_value
FROM (
  SELECT category_id, SUM(price) AS total_value
  FROM products
  GROUP BY category_id
) AS category_totals
WHERE total_value > 2000

Categories with total product value above $2,000. The outer query can only reference columns that the inner query explicitly selected. Here that's category_id and total_value — not the underlying price column.

Derived tables compose with other query features. You can aggregate the derived table's output in the outer query:

SELECT COUNT(*) AS qualifying_customers
FROM (
  SELECT customer_id, COUNT(*) AS order_count
  FROM orders
  GROUP BY customer_id
) AS customer_orders
WHERE order_count >= 3

The outer query counts how many customers qualify. The inner query determines which ones do. Each layer handles one step of the logic.

You can also combine derived tables with scalar subqueries to make comparisons against computed thresholds:

SELECT status, avg_order_value
FROM (
  SELECT status, AVG(total_amount) AS avg_order_value
  FROM orders
  GROUP BY status
) AS status_averages
WHERE avg_order_value > (SELECT AVG(total_amount) FROM orders)

Order statuses whose average order value exceeds the overall average. The inner query computes the per-status average; the scalar subquery computes the overall average; the outer WHERE compares the two.

Does a derived table need an alias in PostgreSQL?

The one thing that trips people up: leaving off the alias.

PostgreSQL 16 and later will run a derived table without one — delete AS customer_orders from the first query and it returns exactly the same rows. Write the alias anyway. The SQL standard requires one, PostgreSQL did too until version 16, and without a name you cannot write customer_orders.order_count, which you need as soon as you join the derived table to another table that shares a column name. Pick something descriptive that makes the query readable — AS customer_orders, AS status_averages — rather than a generic AS t.

Practice Derived Tables in SQL

Practice · easy ecommerce · Brightlane

Brightlane's CRM team is building a high-value customer list and needs to identify buyers with substantial order history.

Write a query to return the customer ID and total order count for every customer who has placed more than three orders.

Assumptions:

  • The orders table contains every order Brightlane has processed.
  • The threshold (> 3) applies to the per-customer count.
  • Each customer's order count is computed first, then the per-customer counts are narrowed to those above the threshold.

Output:

  • One row per qualifying customer, with columns customer_id and order_count.
Schema · ecommerce5 tables? = nullable
categories
idinteger
nametext
parent_id?integer
products
idinteger
nametext
category_id?integer
pricenumeric
stock_qtyinteger
attributes?jsonb
order_items
idinteger
order_id?integer
product_id?integer
quantityinteger
unit_pricenumeric
customers
idinteger
nametext
emailtext
city?text
countrytext
created_attimestamptz
is_activeboolean
orders
idinteger
customer_id?integer
ordered_attimestamptz
statustext
total_amountnumeric

Run previews · Check grades

Write a query, then run it to see results here.

Worked solution

The full breakdown walks through the shape, each clause, why this approach beats the alternatives, and the trap to avoid.

See the full worked solution

9 Derived Tables practice problems

Start learning to practice all 9 Derived Tables problems, with instant grading and mastery tracking.

Common questions about Derived Tables

Can a derived table see a column from the outer query?

No. A subquery in FROM is evaluated on its own, so referencing an outer table raises an invalid reference to a FROM-clause entry. Writing LATERAL before it lifts exactly that restriction, which is the whole reason LATERAL exists.

Can you nest a derived table inside another?

Yes, as deep as the logic needs. Each layer is a complete query with its own alias. Past two levels a chain of named CTEs usually reads better, because each step gets a name instead of another set of brackets.

Why does a derived table need its own alias?

So the outer query has a name to refer to. Without one you cannot qualify a column, which matters as soon as the derived table is joined to something that shares a column name. Give it a descriptive name rather than a single letter.

How you actually get good at SQL

Reading explains SQL. Writing it, over and over with instant feedback, is what makes you fluent.

That's the whole SQLMaxx loop: 600+ real problems, instant AI feedback, mastery you can actually see, and spaced review that won't let you forget.

A stack of SQL practice problem cards, the top card showing an employees table.
615 problems · 66 concepts

Real problems. Not toy examples.

615 hand-built problems spanning all 66 concepts, from basic SELECTs to window functions, built on real schemas and real business questions, the kind you'll actually get asked on the job. Enough reps to make SQL automatic.

A retro computer showing a SQL query marked correct with a green checkmark.
Instant AI feedback

Write a query. Know if it's right in one second.

No copying an answer and hoping it clicked. The AI grader checks your real query against real data, catches exactly what's wrong, and explains the fix in plain English, like a senior analyst reading over your shoulder on every problem.

A circular mastery progress dial filling from blue to green, the SQLMaxx diamond at its center.
Mastery tracking

Stop guessing whether you actually know it.

SQLMaxx tracks every concept and shows you what you've mastered and what's still shaky. Your skills fill in one concept at a time, so 'I think I get joins' becomes something you can prove.

A SQL query editor circled by a blue return arrow with a clock, scheduled to come back for review.
Spaced review

Learn it once. Keep it for good.

Most of what you learn this week fades by next week. So when a concept comes due for review, SQLMaxx hands you a fresh problem to solve from a blank editor, not a flashcard to re-read. A research-backed spaced-repetition algorithm (FSRS) times each return for right before you'd forget, so your SQL is still there months later, when the interview or the job actually needs it.