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.

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.

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.

Visual Explanation — Anatomy of a Scalar Subquery in SELECT

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.

How Scalar Subqueries Execute — The Mechanics

General Syntax Pattern

UNCORRELATED SCALAR SUBQUERY
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
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 → 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.

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.

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.

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
Step 1 — Identify the outer queryThe 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
Step 2 — Write the correlated scalar subquery for department averageWe 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
Step 3 — Compute the deviation using arithmetic on the subquerySince 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
Step 4 — Assemble the complete queryCombining 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
Step 5 — Interpret the resultIf 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.

Strengths, Limitations & Alternatives

Comparison of scalar subqueries, JOINs, and window functions for deriving computed columns
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
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.

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.

Evolution of SQL composition mechanisms
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

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.

Practice Problems

PROBLEM 1CONCEPTUAL
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?
PROBLEM 2BASIC 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).
PROBLEM 3INTERMEDIATE
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.
PROBLEM 4APPLIED
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.
PROBLEM 5CRITICAL 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.

Scalar Subqueries in SELECT — 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)