skip to content

Why can GROUP BY on a primary key let you select non-grouped columns of that table?

level: middleimportance: should knowfreq 40%

answer

  1. The key fixes every other column's value
  2. One row of that table per group
  3. SQL:1999 relaxed the SQL-92 wording
  4. Functional dependence on a candidate key
  5. Not all engines implement it

basics

~20 s

Grouping by a primary key gives each group at most one row of that table, so every other column of it has exactly one value per group. SQL:1999 permits selecting such functionally dependent columns without listing them.

solid answer

~40 s

The select-list rule exists because an ungrouped column may hold many values inside one group. Grouping by a **primary key** removes that ambiguity: the key identifies at most one row of its table, so every other column of that same table is *functionally determined* by the grouping key and has exactly one value per group. SQL:1999 added this functional-dependency relaxation, so `SELECT u.id, u.email, COUNT(o.id) FROM users u LEFT JOIN orders o ON o.user_id = u.id GROUP BY u.id` is legal. Support is uneven: PostgreSQL recognizes the primary-key case, while other engines still demand every column be listed. Treat it as convenience, not portability — spelling out `GROUP BY u.id, u.email` runs everywhere and is always correct.

code

sql · 6 lines
sql
-- Legal where the primary-key relaxation is implemented:
-- u.email and u.created_at are functionally dependent on u.id
SELECT u.id, u.email, u.created_at, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id;

go deeper

for a junior

Know the safe default first: list every non-aggregated select-list column in GROUP BY. Recognize that grouping by a primary key alone sometimes works, and be able to say why.

for a middle

Define functional dependence and derive the relaxation from it — a primary key means one row of that table per group, so its other columns have exactly one value each.

for a senior

Show the portability judgment: state which engines you rely on, or write the explicit GROUP BY list so the query survives an engine change without a silent rewrite.

for a principal

Own it as a style rule across teams — decide whether queries may depend on engine-specific relaxations at all, and prefer aggregate-then-join shapes that make the question moot.

## The rule, and the hole the standard punched in it The baseline rule for a grouped query is that each select-list item must be a grouping expression or an aggregate, because an ungrouped column can hold many values inside one group. SQL:1999 noticed that this is sometimes provably not the case, and added a relaxation: a column may be selected bare if it is **functionally dependent** on the grouping columns. ## What functional dependence means here Column `B` is functionally dependent on column `A` when knowing `A` fixes `B` — one value of `A` never coexists with two different values of `B`. A **primary key** is the canonical source of such a dependency: `users.id` uniquely identifies a row of `users`, so `users.email`, `users.created_at` and every other column of that table are determined by it. If you group by `u.id`, each group corresponds to a single `users` row, so `u.email` has exactly one value in the group — the ambiguity the rule guards against cannot arise, and the standard lets you select it. ## The query this actually helps The common shape is a parent table joined to a child table and counted: ```sql SELECT u.id, u.email, u.created_at, COUNT(o.id) AS order_count FROM users u LEFT JOIN orders o ON o.user_id = u.id GROUP BY u.id; ``` Without the relaxation you must write `GROUP BY u.id, u.email, u.created_at` — and add another column to the `GROUP BY` every time you add one to the select list. With it, grouping by the key alone suffices, and adding `u.display_name` to the select list requires no other edit. On a wide parent table the difference in readability is real. Note the join does not weaken the argument. The join may multiply rows (one per order), but every row of the result that shares `u.id` carries the *same* `u.email`, because they all originate from the same `users` row. The dependency is a property of the base table and survives the join. ## Where it stops The relaxation is far narrower in practice than the abstract rule suggests. - **PostgreSQL recognizes only the primary-key case.** Its documentation is explicit that functional dependence is recognized when the grouping columns include the primary key of the table containing the ungrouped column. Grouping by a `UNIQUE NOT NULL` column that happens to determine the others is not recognized, even though it determines them just as well. - **Many engines implement none of it** and still require every select-list column to be listed in `GROUP BY`. A query that runs on one engine can fail outright on another. - **It does not cross tables the way people hope.** Grouping by `o.user_id` (a foreign key) does not license selecting `u.email` in every implementation; the dependency the engine reasons about is keyed to the table that owns the ungrouped column. - **It does not apply to expressions.** Grouping by `u.id` does not let you select `COUNT(*) / SOME_UNGROUPED_COLUMN`; only column references of the dependent table are covered. ## Portability advice Two defensible policies: 1. **Use it deliberately** on a codebase pinned to an engine that implements it, and note in the query's comment that it depends on the primary-key relaxation. 2. **Spell out every column** in `GROUP BY`. Verbose, but valid everywhere and immune to a future engine change. Since the columns are functionally dependent, listing them cannot change the result — you get the same groups either way. A third pattern avoids the question entirely: aggregate first, then join the parent for its columns. ```sql SELECT u.id, u.email, COALESCE(c.order_count, 0) AS order_count FROM users u LEFT JOIN (SELECT user_id, COUNT(*) AS order_count FROM orders GROUP BY user_id) c ON c.user_id = u.id; ``` Here nothing is grouped and projected in the same block, so the select-list rule never binds on `u.email` at all. ## Interview framing The answer that lands says three things: what functional dependence is, why a primary key supplies it (one row per group, hence one value per column), and that support is engine-dependent so the explicit `GROUP BY` list is the portable choice. Candidates who only say "Postgres lets you do it" have the fact but not the reason.

  • Does grouping by a UNIQUE NOT NULL column earn the same relaxation?
    Logically it determines the other columns just as well, but implementations are stricter than the theory. PostgreSQL documents that it recognizes functional dependence only when the grouping columns include the table's primary key, so a unique column does not qualify there. Relying on it is unportable; list the columns explicitly instead.
  • If you list all the dependent columns in GROUP BY anyway, can the result change?
    No. Because those columns are determined by the key, adding them creates no new distinct combinations — the group boundaries are identical. The only differences are verbosity and portability, which is why the explicit list is a safe default.
  • How do you get the same report without depending on the relaxation at all?
    Aggregate in a derived table keyed by the join column, then join the parent table for its descriptive columns. Nothing is grouped and projected in the same query block, so the select-list rule never applies to the parent's columns, and the query runs on any engine.

saying these in an interview costs you the question

  • Says any unique-looking column licenses bare selection
  • Assumes every engine implements the SQL:1999 relaxation
  • Thinks the join multiplying rows breaks the dependency
  • Claims listing all columns in GROUP BY changes the result
  • Believes grouping by a foreign key determines the parent's columns

context