What is a subquery in SQL?
A SQL subquery is a SELECT statement nested inside another statement and used as a value, set, existence test, or table source. Its location and expected cardinality determine what it means: one value in a scalar expression, one column for IN, any qualifying row for EXISTS, or a complete rowset in FROM.
SELECT product_id, price
FROM products
WHERE price > (
SELECT AVG(price)
FROM products
);The inner query returns one average and the outer query compares every product with it. This shape is concise because the inner result has a guaranteed scalar grain. Before using any subquery, write its contract: which outer values it can see, how many rows and columns it may return, and how zero rows or NULL should affect the answer.
Choose a subquery type from the question you need to answer
Subqueries are not one technique. A scalar subquery supplies one value. A row subquery supplies a tuple where the database supports row comparison. A set subquery feeds IN, ANY, or ALL. An existence subquery answers whether at least one qualifying record exists. A derived table creates an intermediate rowset that can be joined, filtered, or aggregated. A correlated subquery reads values from the current outer row.
| Shape | Contract | Typical question |
|---|---|---|
| Scalar | Zero or one row, one column | Compare with one calculated value |
IN | Any rows, one comparable column | Does this value belong to a set? |
EXISTS | Only row existence matters | Is there a related qualifying row? |
| Derived table | Named columns and rowset grain | What intermediate relation is needed? |
Choose the shape that expresses intent directly. Returning full child rows and later deduplicating them is a poor substitute for EXISTS when the question is purely existential. Conversely, EXISTS cannot provide child attributes that belong in the output. Clarity about the contract prevents accidental grain changes.
Make scalar subqueries truly single-valued
A scalar subquery can appear in a comparison, projection, calculation, or conditional expression. Zero returned rows usually becomes NULL; more than one row normally raises an error. The dangerous pattern is relying on accidental uniqueness or adding an unordered LIMIT 1. That suppresses the error but does not define which related row is correct.
SELECT
c.customer_id,
(
SELECT s.status
FROM customer_status s
WHERE s.customer_id = c.customer_id
ORDER BY s.effective_at DESC, s.status_id DESC
FETCH FIRST 1 ROW ONLY
) AS latest_status
FROM customers c;The secondary key makes ties deterministic, but verify that “latest” is the right business rule and that late-arriving corrections are modeled correctly. If several scalar subqueries independently probe the same child table, calculate the desired child row once with a lateral join, windowed derived table, or named stage instead of repeating the access path.
Use IN and EXISTS with deliberate NULL semantics
IN asks whether a value equals any value returned by a subquery. EXISTS asks whether the subquery returns at least one row, and the selected columns inside it are irrelevant. Optimizers can often transform both into semi-join strategies, so choose by meaning first. The largest correctness difference appears with negation and NULL.
SELECT c.customer_id
FROM customers c
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
AND o.status = 'open'
);NOT IN can become unknown for every outer value when the returned set contains NULL, producing no rows. You can filter nulls deliberately, but NOT EXISTS usually communicates anti-existence more safely. Test a null outer key, a null inner key, an empty inner set, and a mixture of matching and nonmatching rows. Do not assume constraints exist unless the schema actually enforces them.
Use derived tables to establish a controlled intermediate grain
A subquery in FROM creates a derived table. This is valuable when a stage must filter, aggregate, rank, or reshape data before another relationship. Name every output column clearly and document what one row represents. If the derived table aggregates orders to one row per customer, the outer join should rely on that grain rather than assuming it.
SELECT c.customer_id, x.order_count, x.revenue
FROM customers c
JOIN (
SELECT
customer_id,
COUNT(*) AS order_count,
SUM(order_total) AS revenue
FROM orders
GROUP BY customer_id
) x ON x.customer_id = c.customer_id;This prevents order rows from multiplying another customer-level relationship, but it also removes customers without orders because the outer join is inner. Choose join preservation independently from aggregation. Some engines materialize a stage, while others inline it; treat a derived table as a semantic boundary first, not a guaranteed performance barrier.
Limit nesting that hides grain and decision points
Deep nesting is not automatically slow, but it increases review cost. Readers must track aliases, correlated references, aggregation levels, preservation rules, and filters across several scopes. Repeated subqueries can also calculate the same intermediate result more than once. A useful refactoring extracts stages when they have a nameable business meaning, a reusable result, or a validation invariant.
- Annotate grain. Write what one row means after every subquery.
- Mark outer references. Identify correlation and expected probe counts.
- Find repeated logic. Compare predicates, joins, and aggregates structurally.
- Extract named stages. Use a CTE or view when the stage deserves an identity.
- Validate boundaries. Measure rows, distinct keys, nulls, and totals at each stage.
- Inspect the final plan. Confirm whether the engine inlines, materializes, or repeats work.
Diagnose subquery performance with execution evidence
Start by locating work that grows with the outer row count. In an actual plan, inspect loops, rows per loop, total rows, buffer reads, elapsed time, sorts, spills, and remote calls. A cheap inner probe repeated ten times may be fine; the same probe repeated ten million times is not. Parameter-sensitive selectivity and skew can make one execution fast and another pathological.
| Evidence | Possible mechanism | Candidate response |
|---|---|---|
| Very high inner loops | Correlated repeated probes | Index or set-based rewrite |
| Large materialized stage | Late filter or wide projection | Safe pushdown and projection |
| Estimate far below actual | Skew or correlated predicates | Statistics and representative tests |
Do not optimize by syntax folklore. Replacing every subquery with a join can introduce row multiplication; forcing materialization can add I/O; flattening a scalar check can change zero-row behavior. Benchmark the original and rewrite with identical parameters and validate result equivalence before comparing runtime.
Validate subquery contracts before production
- State whether zero, one, or many rows are allowed.
- Test empty results, one result, several results, and NULL values.
- For scalar selection, define a deterministic business rule instead of arbitrary limiting.
- For
NOT IN, prove the returned expression cannot be NULL or use a safer form. - Measure correlated loop counts with realistic outer populations.
- Compare row counts, distinct keys, and metrics before and after any rewrite.
- Review access permissions and remove sensitive literals before using external tools.
- Keep a regression fixture for ties, late data, duplicate keys, and empty groups.
Trace a subquery incident from symptom to mechanism
Suppose a customer dashboard suddenly reports no eligible accounts after a release. The visible predicate uses account_id NOT IN (SELECT blocked_account_id FROM blocks). A new ingestion path has introduced one row whose blocked key is NULL. Because comparison with that set becomes unknown, the outer predicate rejects every account. The immediate repair may filter NULL or use NOT EXISTS, but a complete review also asks why a supposedly identifying field accepted NULL and whether other consumers share the same assumption.
Build the proof in stages. Preserve the original row counts, list null and duplicate keys in the inner result, evaluate the predicate for a tiny set of representative accounts, and compare the repaired query with an independent anti-join control. Then inspect the new actual plan: a semantically safer form can still need an index or updated statistics. Save the failing NULL fixture as a regression test and monitor both blocked-key null rate and eligible-account count. This workflow fixes the mechanism instead of merely restoring today's dashboard.
Inspect the complete nested SQL statement
Paste a sanitized complete statement into the InfiniSynapse SQL Complexity Checker to review nesting, correlation, repeated structures, joins, windows, and aggregation together. Then confirm cardinality and runtime with your database's actual plan.
Open SQL Complexity CheckerRemove credentials, secrets, personal data, and sensitive literals.SQL subquery frequently asked questions
It is a SELECT nested inside another statement and used as a scalar, set, existence test, or rowset.
IN compares a value with a set; EXISTS only tests whether a qualifying row exists. Negated NULL behavior needs special care.
It is expected to return at most one row, so several qualifying rows violate its contract.
No. Optimizers can decorrelate many forms, but repeated probes remain expensive when transformation or access paths are weak.
Refactor when named stages clarify grain, remove repeated work, or create useful validation boundaries.
