Hook: Interviewers love this one because it checks whether you know the 'one value only' rule — the tiny detail that saves you from big production bugs.
Question: What is a scalar subquery in SQL?
Answer: A scalar subquery is a subquery that returns exactly one column and at most one row, so SQL can use it like a single value inside a bigger query. If it returns no rows, the result is NULL. If it returns more than one row, most databases raise an error because a single value was expected.
Interview-Ready Answer: I’d say a scalar subquery is a subquery that behaves like one value. I can use it in places where SQL expects a single expression, like the SELECT list or a WHERE comparison. The important rule is that it must return one column and zero or one row; zero rows becomes NULL, and more than one row usually throws an error. In practice, I use it for small lookups or aggregate values, but if I need to combine many rows, I usually switch to a JOIN.
A scalar subquery is a subquery used in an expression position, meaning SQL expects one value there. 'Scalar' here means single-valued: one column, one row at most. A subquery that returns rows like a tiny table is a different thing entirely.
NULL.Many optimizers can decorrelate a subquery, which means they rewrite it into a join or aggregate plan so it does not literally rerun the inner query for every row. But you should still think in the simple mental model first: 'ask for one value, then paste it into the outer query'.
| Tool | Shape | Best use | Main risk |
|---|---|---|---|
| Scalar subquery | One value | Single lookup or aggregate | Too many rows cause error |
| JOIN | Many rows | Combine tables | Can duplicate outer rows |
| CTE | Named result set | Readability and reuse | Not a single-value tool by itself |
A quick rule: if you need one answer, scalar subquery is natural. If you need to attach data from another table to many rows, a join is often clearer and faster. If you need to break a complex query into named pieces, a CTE helps readability, but it does not replace the single-value rule.
An uncorrelated scalar subquery is often cheap because the engine can evaluate it once. A correlated scalar subquery can be much more expensive, because the naive plan is one inner lookup per outer row. For example, 10,000 outer rows with an indexed inner key may mean about 10,000 index probes, which is usually fine; 100,000 outer rows against an unindexed 1,000,000-row table can become painfully slow.
As a rough mental model, think O(1) for an uncorrelated subquery evaluated once, and O(N * lookup) for a correlated one. If the inner side has no useful index, that can drift toward O(N * M), which is why a query that looked elegant can suddenly take seconds or minutes. Interviewers like to hear that you would add an index on the correlated key, or rewrite to a join or grouped CTE if the plan is bad.
NULL, not an error.= NULL is never true; use IS NULL or COALESCE if you expect no match.One more practical tip: an aggregate like MAX or AVG is often used inside a scalar subquery because aggregates always return one row. That can be useful, but it can also hide bad data if you really expected uniqueness.
Real-World Story: Imagine a checkout service for an e-commerce site. Each order needs the customer's current discount tier, and the developer writes a scalar subquery to fetch that one discount rate while building the invoice. It works for months until a data bug creates two active discount rows for the same customer segment.
Now the scalar subquery returns more than one row, and the checkout query starts failing. Users see 500 errors at payment time, logs fill with the database message about a subquery returning multiple rows, and support tickets spike because carts cannot complete. The lesson is that scalar subqueries are only safe when the data model truly guarantees one answer, or when you deliberately aggregate or enforce uniqueness first.
-- Scalar subquery demo: one value, one row at most.
-- This script is runnable as-is in PostgreSQL and other SQL engines with similar syntax.
DROP TABLE IF EXISTS employees;
DROP TABLE IF EXISTS departments;
CREATE TABLE departments (
department_id INTEGER PRIMARY KEY,
department_name VARCHAR(50) NOT NULL
);
CREATE TABLE employees (
employee_id INTEGER PRIMARY KEY,
employee_name VARCHAR(50) NOT NULL,
department_id INTEGER,
salary DECIMAL(10,2) NOT NULL
);
INSERT INTO departments (department_id, department_name) VALUES
(10, 'Engineering'),
(20, 'Sales'),
(30, 'Finance');
INSERT INTO employees (employee_id, employee_name, department_id, salary) VALUES
(1, 'Ava', 10, 120000.00),
(2, 'Ben', 10, 110000.00),
(3, 'Cara', 20, 90000.00),
(4, 'Dan', NULL, 80000.00);
-- 1) Correlated scalar subquery in the SELECT list.
-- The inner query uses the current employee row to fetch exactly one department name.
SELECT
e.employee_id,
e.employee_name,
(SELECT d.department_name
FROM departments d
WHERE d.department_id = e.department_id) AS department_name
FROM employees e
ORDER BY e.employee_id;
-- 2) Uncorrelated scalar subquery in the WHERE clause.
-- The average salary is computed once, then each row is compared to that single value.
SELECT
e.employee_name,
e.salary
FROM employees e
WHERE e.salary > (SELECT AVG(e2.salary) FROM employees e2)
ORDER BY e.salary DESC;
-- 3) Edge case: no matching row returns NULL, not an error.
SELECT
(SELECT d.department_name
FROM departments d
WHERE d.department_id = 999) AS missing_department;
-- 4) Safe handling for the no-row case.
SELECT
COALESCE(
(SELECT d.department_name
FROM departments d
WHERE d.department_id = 999),
'Unknown'
) AS department_name_or_default;
-- 5) Failure-path note: if a scalar subquery returns more than one row,
-- most databases raise an error. This query is intentionally commented out
-- so the script still runs, but it shows the bug shape clearly.
-- SELECT (SELECT d.department_name
-- FROM departments d
-- WHERE d.department_id IN (10, 20)) AS bad_example;
-- 6) A common pattern: use an aggregate inside a scalar subquery to force one row.
-- This is safe for cardinality, but make sure it matches your business rule.
SELECT
e.department_id,
(SELECT MAX(d.department_name)
FROM departments d
WHERE d.department_id = e.department_id) AS any_department_name
FROM employees e
ORDER BY e.department_id NULLS LAST;Follow-up & Tricky Questions:
NULL. That is why people often wrap it in COALESCE when they want a default value.SELECT list only? No. You can also use it in WHERE, HAVING, ORDER BY, and many expression contexts wherever SQL expects one value.= (SELECT ...) the same as IN (SELECT ...)? No. = expects a single scalar value, while IN checks membership in a set of values.LIMIT 1? You can in some databases, but it is usually a smell because it hides bad data and may return an arbitrary row unless you also define a deterministic order.Common Mistakes:
NULL; the error is when there is more than one row.JOIN is often clearer.NULL with =. Correction: use IS NULL or COALESCE, because = NULL is not true.Memory Hook: Think of a scalar subquery like asking one clerk for one number. If the clerk hands you zero slips, you get NULL; if they hand you two slips, the counter stops the line and throws an error.
Cheat Sheet:
NULL.JOIN or a grouped CTE when you need set-based work.Practice Tasks:
NULL, then wrap it with COALESCE.