SQL • Subqueries and CTEs

Scalar Subqueries — Use scalar subqueries in SELECT (intro)

Embed single-value queries inside SELECT lists to compute derived columns on the fly.

SQL illustration with a database, structured table, and query code symbols
Inside this lesson 10 sections 5 questions 7 figures

SECTION 1

Historical Context & Motivation

The relational model, proposed by E.F. Codd in 1970, introduced the idea that data could be queried declaratively rather than navigated procedurally. Early implementations of SQL—then called SEQUEL—provided basic retrieval capabilities, but the language quickly needed composability: the ability to nest one query inside another. Subqueries emerged as the primary mechanism for this composition, allowing a query to reference the result of another query as though it were a value, a table, or a condition. Among the various forms of subqueries, the scalar subquery—a subquery guaranteed to return exactly one row and one column—became especially useful for embedding computed values directly into the SELECT list. This capability addressed a fundamental question: how can you augment each row of a result set with a value that depends on a separate aggregation or lookup, without resorting to temporary tables or application-level code?

1970 • Codd's Relational Model

E.F. Codd publishes "A Relational Model of Data for Large Shared Data Banks," establishing the theoretical foundation for relational algebra and the declarative query paradigm.

1986 • SQL-86 Standard (ANSI)

The first ANSI SQL standard formalizes basic SELECT, INSERT, UPDATE, and DELETE operations. Subqueries in WHERE clauses are supported, but usage in the SELECT list is limited.

1992 • SQL-92 Introduces Scalar Subqueries

SQL-92 formally defines scalar subqueries and permits them in virtually any expression context, including the SELECT list. This enables derived columns computed from independent queries.

1999 • SQL:1999 and Common Table Expressions

SQL:1999 adds CTEs via the WITH clause, providing an alternative pattern for complex subquery logic. Scalar subqueries remain a concise tool for single-value derivations.

2010s • Modern Optimizer Advances

Query planners in PostgreSQL, MySQL, SQL Server, and others increasingly optimize correlated scalar subqueries via lateral joins and hash-based de-correlation, making them practical even at scale.

The central question this lesson addresses is straightforward yet powerful: how do you place a complete, self-contained query inside the SELECT clause so that every row in your result set receives a dynamically computed scalar value? Understanding this technique unlocks an elegant pattern for column-level enrichment without the overhead of explicit JOINs or multi-step procedural logic.

SECTION 2

Core Principles & Definitions

A scalar subquery is defined by a single constraint: it must return exactly one row and one column. When placed inside the SELECT list, the database engine evaluates the subquery for each row produced by the outer query (conceptually, at least—the optimizer may rewrite the execution plan). If the subquery returns zero rows, the result is NULL; if it returns more than one row, the engine raises a runtime error. This guarantee of a single value is what makes the subquery "scalar"—analogous to a scalar quantity in mathematics, which has magnitude but no additional dimensionality.

1

Scalar Guarantee

The inner query must produce at most one row with one column. Aggregate functions (COUNT, SUM, AVG, MAX, MIN) naturally satisfy this constraint when applied without GROUP BY.
2

Correlation (Optional)

A scalar subquery may be correlated—referencing columns from the outer query—or uncorrelated. Correlated subqueries produce row-specific results; uncorrelated ones return a constant for every row.
3

Placement in SELECT

Scalar subqueries appear as column expressions in the SELECT list, typically aliased with AS. They behave like any other expression and can participate in arithmetic, CASE, and COALESCE.
4

NULL Handling

When the inner query returns no rows (empty set), the result is NULL—not zero. Use COALESCE to provide a fallback value when this behavior is undesirable.
5

Performance Considerations

Naïvely, a correlated scalar subquery executes once per outer row (N+1 pattern). Modern optimizers often rewrite these into joins, but awareness of the cost model remains essential.
✦

KEY TAKEAWAY

Think of a scalar subquery in SELECT as a function call that computes a single value for each row. Just as a pure function in software engineering maps an input to one output with no side effects, a scalar subquery maps the current outer row's context to exactly one derived value. If you have ever called len() or Math.sqrt() inline in an expression, you already understand the paradigm—replace the function with a SQL query, and you have a scalar subquery.

SECTION 3

Visual Explanation — Anatomy of a Scalar Subquery in SELECT

Anatomy of a Scalar Subquery in SELECTOUTER QUERYSELECTe.name, e.dept_id, (SELECT AVG(salary) FROM employees e2 WHERE e2.dept_id = e.dept_id)) AS avg_dept_salaryOUTER QUERY← SCALAR SUBQUERYFROMemployees e;Execution Flow (per outer row)① Outer row producede.dept_id = 3② Subquery evaluatesAVG(salary) WHERE dept=3③ Scalar returned72500.00Result Setnamedept_idavg_dept_salaryAlice372500.00Bob165000.00Carol372500.00
The diagram shows three layers. At the top, the SQL statement highlights the scalar subquery (dashed cyan border) embedded within the outer SELECT. The middle row traces the per-row execution: the outer query produces a row, the correlated subquery evaluates for that row's dept_id, and a single scalar value is returned. The bottom table displays the final result set with the derived avg_dept_salary column.

Notice how the correlation variable e.dept_id in the subquery's WHERE clause creates a dependency on the outer row. For each employee, the engine conceptually re-evaluates the inner AVG over only those employees sharing the same department. The result is that Alice and Carol—both in department 3—receive the same avg_dept_salary of 72500.00, while Bob, in department 1, receives a different average. This is precisely the kind of row-specific enrichment that makes scalar subqueries in SELECT so versatile: each row can carry a derived value that would otherwise require a JOIN to a grouped aggregate or a window function.

SECTION 4

How Scalar Subqueries Execute — The Mechanics

General Syntax Pattern

UNCORRELATED SCALAR SUBQUERY

SELECTcol1,col2,(SELECTagg(x)FROMtableB)ASaliasFROMtableA;SELECT col₁, col₂, (SELECT agg(x) FROM tableB) AS alias FROM tableA;

The inner query references no columns from the outer query. It is evaluated once and the same scalar value is injected into every result row.

CORRELATED SCALAR SUBQUERY

SELECTcol1,(SELECTagg(x)FROMtableBbWHEREb.key=a.key)ASaliasFROMtableAa;SELECT col₁, (SELECT agg(x) FROM tableB b WHERE b.key = a.key) AS alias FROM tableA a;

The inner query references a.key from the outer query. Conceptually evaluated once per outer row. The optimizer may convert this to a hash join internally.

Logical Evaluation Order

Although SQL is declarative and the physical execution plan varies by engine, the logical evaluation order remains consistent. The FROM clause establishes the source rows. The WHERE clause filters them. Only after filtering does the SELECT list evaluate its expressions—including any scalar subqueries. This means a scalar subquery in SELECT cannot filter the outer rows; it merely decorates them. If you want filtering behavior based on a subquery, the subquery belongs in the WHERE clause. Understanding this distinction prevents a common design error where developers attempt row filtering via a SELECT-level subquery and wonder why all rows still appear.

LOGICAL CLAUSE ORDER

FROM→WHERE→GROUPBY→HAVING→SELECT(scalarsubquerieshere)→ORDERBY→LIMITFROM → WHERE → GROUP BY → HAVING → SELECT (scalar subqueries here) → ORDER BY → LIMIT

Scalar subqueries in the SELECT list execute in the SELECT phase, after filtering but before ordering.

Runtime Error Conditions

The scalar contract is strict. If your subquery ever returns more than one row for any given evaluation, the database will raise an error such as PostgreSQL's ERROR: more than one row returned by a subquery used as an expression or SQL Server's Msg 512: Subquery returned more than 1 value. Aggregate functions without GROUP BY are the most reliable way to guarantee a single-row result. Alternatively, adding LIMIT 1 can enforce scalar behavior, though this may mask data issues and should be used judiciously.

SECTION 5

Common Scalar Subquery Patterns in SELECT

Scalar subqueries in SELECT appear in several recurring patterns. The following diagram categorizes the most common use cases you will encounter in practice, from simple constant injection to per-row correlated lookups.

Scalar Subquery Patterns in SELECTPATTERN 1: Global AggregateSELECT name, (SELECT COUNT(*) FROM orders) AS total_ordersFROM customers;PATTERN 2: Correlated AggregateSELECT name, (SELECT COUNT(*) FROM orders o WHERE o.cust_id = c.id)FROM customers c;PATTERN 3: Percentage of TotalSELECT dept, SUM(salary) AS dept_sal, SUM(salary) * 100.0 / (SELECT SUM(salary) FROM emp) AS pct_totalFROM emp GROUP BY dept;PATTERN 4: Lookup / LabelSELECT order_id, amount, (SELECT name FROM customers c WHERE c.id = o.cust_id) AS customer_nameFROM orders o;When to Use Each PatternGlobal Aggregate — Same value every row. Uncorrelated, fast (single execution).Correlated Aggregate — Per-row computation. Most common. Watch for N+1 cost.Percentage of Total — Combines per-group agg with global agg. Elegant ratio calculation.Lookup/Label — Replaces a JOIN when only one column is needed. Concise but limited.
Four canonical scalar subquery patterns. Pattern 1 (global aggregate) injects a constant. Pattern 2 (correlated aggregate) computes per-row values. Pattern 3 computes ratios by combining a grouped aggregation with a global subquery. Pattern 4 fetches a label from a related table, functioning as a lightweight alternative to a JOIN.

Patterns 1 and 3 are uncorrelated: the inner query does not reference any column from the outer query, so it can be evaluated once and its result cached. Patterns 2 and 4 are correlated: the inner query references a column from the outer query (e.g., o.cust_id or c.id), so the engine must logically re-evaluate the inner query for each outer row. The distinction matters for performance reasoning: uncorrelated subqueries are O(1) with respect to the outer row count, while correlated ones are conceptually O(N × M) where N is the outer row count and M is the inner scan cost per evaluation—though modern optimizers aggressively de-correlate these into hash joins.

SECTION 6

Worked Example — Employee Salary vs. Department Average

Suppose we have an employees table with columns id, name, dept_id, and salary. We want to produce a report that shows each employee's name, salary, the average salary in their department, and how much they deviate from that average—all in a single query.

Scalar Subqueries for Salary Analysis

  1. 1

    Step 1 — Identify the outer query

    The outer query scans the employees table and projects each employee's name and salary. We alias the outer table as e so the subquery can reference its columns.
    SELECT e.name, e.salary FROM employees e;
  2. 2

    Step 2 — Write the correlated scalar subquery for department average

    We embed a SELECT that computes AVG(salary) from the same table, filtered to only rows that share the current employee's dept_id. The AVG aggregate guarantees a single row is returned, satisfying the scalar constraint.
    (SELECT AVG(e2.salary) FROM employees e2 WHERE e2.dept_id = e.dept_id) AS dept_avg
  3. 3

    Step 3 — Compute the deviation using arithmetic on the subquery

    Since a scalar subquery is just an expression, we can subtract it from e.salary. However, to avoid evaluating the subquery twice, we use ROUND for readability. In the deviation column, we simply repeat the subquery (the optimizer may cache it) or, alternatively, wrap the whole query as a CTE.
    e.salary - (SELECT AVG(e2.salary) FROM employees e2 WHERE e2.dept_id = e.dept_id) AS deviation
  4. 4

    Step 4 — Assemble the complete query

    Combining all elements, the final query is:
    SELECT e.name, e.salary, ROUND((SELECT AVG(e2.salary) FROM employees e2 WHERE e2.dept_id = e.dept_id), 2) AS dept_avg, ROUND(e.salary - (SELECT AVG(e2.salary) FROM employees e2 WHERE e2.dept_id = e.dept_id), 2) AS deviation FROM employees e ORDER BY deviation DESC;
  5. 5

    Step 5 — Interpret the result

    If Alice earns 85000 in a department where the average is 72500, her deviation is +12500. If Bob earns 60000 in a department averaging 65000, his deviation is −5000. The ORDER BY sorts employees from most above their department average to most below.
    A positive deviation indicates the employee earns above their department average; negative indicates below.
💡

Optimization Note

The query above contains the same correlated subquery twice (once for dept_avg, once for deviation). In practice, you could wrap the inner result in a CTE or derived table, or use a window function AVG(salary) OVER (PARTITION BY dept_id) to avoid repeated evaluation. We use the scalar subquery form here to illustrate the concept clearly.

SECTION 7

Strengths, Limitations & Alternatives

AspectScalar Subquery in SELECTJOIN + GROUP BYWindow Function
ReadabilityHigh for single derived columns; self-documentingCan be verbose with multiple grouped sourcesCompact; OVER clause is concise
Performance (small tables)Comparable; optimizer may convert to joinEfficient, single pass with hash joinEfficient, single pass
Performance (large tables)Risk of N+1 if optimizer cannot de-correlateGenerally good with proper indexingGenerally good; may require sort
Multiple derived columnsRequires separate subquery per columnOne JOIN can supply many columnsMultiple window functions share one OVER
PortabilitySQL-92+ — universally supportedSQL-92+ — universally supportedSQL:2003+ — older MySQL versions lack support

Comparison of scalar subqueries, JOINs, and window functions for deriving computed columns

✦

KEY TAKEAWAY

Scalar subqueries in SELECT are like calling a microservice for each row: you get a clean, self-contained value, but the round-trip cost matters at scale. If you only need one or two derived values and the tables are modest in size, scalar subqueries yield the most readable SQL. As the number of derived columns grows or the table size becomes large, a JOIN or window function is often the better architectural choice—analogous to batching API calls instead of making them one at a time.

SECTION 8

Connection to Window Functions and CTEs

Scalar subqueries in SELECT represent the earliest composability mechanism in SQL. As the language evolved, two more powerful abstractions emerged that often serve the same purpose: window functions (introduced in SQL:2003) and Common Table Expressions (CTEs) (SQL:1999). Understanding the relationship between these three tools is essential for writing idiomatic, maintainable SQL.

FeatureScalar Subquery in SELECTWindow Function (OVER)CTE (WITH clause)
IntroducedSQL-92SQL:2003SQL:1999
Return shapeSingle value (1×1)One value per row (same row count)Named result set (any shape)
ReusabilityMust be duplicated if used in multiple columnsSame OVER clause can be sharedDefined once, referenced many times
Ideal use caseOne-off derived column, lookup from another tablePartition-aware aggregation, ranking, running totalsComplex multi-step logic, recursive queries

Evolution of SQL composition mechanisms

As you progress beyond this introduction, you will find that many queries initially written with scalar subqueries can be refactored into window functions for better performance and clarity—particularly when you need multiple aggregates over the same partition. Similarly, CTEs allow you to define reusable named result sets that eliminate subquery duplication. Think of scalar subqueries as the foundational building block: once you understand how a single-value subquery fits into the SELECT list, the transition to window functions (AVG(salary) OVER (PARTITION BY dept_id)) and CTEs (WITH dept_avg AS (SELECT ...)) becomes a natural generalization of the same idea: composing queries from queries.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain why a scalar subquery in the SELECT list must return exactly one row and one column. What happens if it returns zero rows? What happens if it returns more than one row?
Show answer
The SELECT list expects each expression to produce a single value for every output row. A scalar subquery occupies an expression position, so it must resolve to exactly one value—one row, one column. If the subquery returns zero rows, the result is NULL, because the SQL standard defines an empty scalar subquery as NULL rather than raising an error. If the subquery returns more than one row, the database raises a runtime error (e.g., PostgreSQL's "more than one row returned by a subquery used as an expression"), because the engine cannot deterministically pick one value from multiple candidates.

Question 2: BASIC CALCULATION

Given tables products(id, name, price) and categories(id, category_name) (with products.category_id referencing categories.id), write a query that returns each product's name, price, and the overall average price of all products (as a constant column named global_avg).
Show answer
This requires an uncorrelated scalar subquery because the average is the same for every row: SELECT p.name, p.price, (SELECT ROUND(AVG(price), 2) FROM products) AS global_avg FROM products p; The inner query has no reference to the outer query's columns, so the optimizer evaluates it once and reuses the result. Every row in the output will display the same global_avg value. This is Pattern 1 (Global Aggregate) from our classification.

Question 3: INTERMEDIATE

Using the same schema, write a query that returns each product's name, price, and the average price of products within the same category (aliased category_avg). Also include a column price_diff that shows how much the product's price differs from its category average.
Show answer
This requires a correlated scalar subquery: SELECT p.name, p.price, (SELECT ROUND(AVG(p2.price), 2) FROM products p2 WHERE p2.category_id = p.category_id) AS category_avg, ROUND(p.price - (SELECT AVG(p2.price) FROM products p2 WHERE p2.category_id = p.category_id), 2) AS price_diff FROM products p ORDER BY price_diff DESC; The subquery in both category_avg and price_diff references p.category_id, making it correlated. The engine evaluates the inner AVG for each product's specific category. Note the subquery is duplicated for price_diff—this is a known trade-off of scalar subqueries that window functions or CTEs could eliminate.

Question 4: APPLIED

You are building a dashboard for an e-commerce platform. Table orders(id, customer_id, total, order_date) and customers(id, name, signup_date). Write a query that returns each customer's name, signup_date, the number of orders they have placed, and the date of their most recent order. Use scalar subqueries in the SELECT list.
Show answer
SELECT c.name, c.signup_date, (SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.id) AS order_count, (SELECT MAX(o.order_date) FROM orders o WHERE o.customer_id = c.id) AS last_order_date FROM customers c ORDER BY order_count DESC; Both subqueries are correlated on c.id. COUNT() and MAX() are aggregate functions that inherently return a single row, satisfying the scalar constraint. For customers with zero orders, COUNT() returns 0 (not NULL, because COUNT of an empty set is 0) while MAX returns NULL (the max of an empty set is undefined). This distinction between COUNT and other aggregates on empty sets is a subtle but important SQL behavior.

Question 5: CRITICAL THINKING

A colleague writes the following query and complains it is slow on a table with 500,000 rows: SELECT e.name, e.salary, (SELECT d.dept_name FROM departments d WHERE d.id = e.dept_id) AS dept_name, (SELECT AVG(e2.salary) FROM employees e2 WHERE e2.dept_id = e.dept_id) AS dept_avg, (SELECT MAX(e3.salary) FROM employees e3 WHERE e3.dept_id = e.dept_id) AS dept_max FROM employees e; Analyze the performance implications and propose a refactored version that achieves the same result more efficiently.
Show answer
This query contains three correlated scalar subqueries, each conceptually executing once per outer row. On 500,000 rows, this means up to 1.5 million subquery evaluations (though the optimizer may de-correlate some). The dept_name lookup and the two aggregates over employees share the same correlation key (dept_id). A more efficient approach uses a CTE or JOIN: WITH dept_stats AS (SELECT dept_id, AVG(salary) AS dept_avg, MAX(salary) AS dept_max FROM employees GROUP BY dept_id) SELECT e.name, e.salary, d.dept_name, ds.dept_avg, ds.dept_max FROM employees e JOIN departments d ON d.id = e.dept_id JOIN dept_stats ds ON ds.dept_id = e.dept_id; This version scans employees once for the CTE aggregation, then performs two hash joins. It reduces the complexity from O(N × K) per subquery to O(N) for the CTE plus O(N) for the joins. Alternatively, window functions could replace the CTE: AVG(salary) OVER (PARTITION BY dept_id) and MAX(salary) OVER (PARTITION BY dept_id) compute both aggregates in a single pass.

SUMMARY

Scalar Subqueries in SELECT — Summary

Summary

A scalar subquery is a SELECT statement embedded as an expression that returns exactly one row and one column. When placed in the SELECT list, it enriches each output row with a dynamically computed value. Uncorrelated scalar subqueries return a constant (evaluated once), while correlated scalar subqueries reference outer query columns and conceptually execute per row. The four canonical patterns are global aggregate, correlated aggregate, percentage of total, and lookup/label.

Key caveats include NULL on empty result sets (use COALESCE to handle), runtime errors when more than one row is returned, and potential N+1 performance costs for correlated subqueries on large tables. As your SQL fluency grows, you will often refactor scalar subqueries into window functions or CTEs for better performance and reusability—but scalar subqueries remain the conceptual foundation for understanding how SQL composes queries from queries.

Varsity Tutors • SQL • Scalar Subqueries — Use scalar subqueries in SELECT (intro)