A table stores several rows per user but at most one of them may be flagged active at a time. How would you enforce that with a unique index carrying a WHERE predicate, and what are the limits of that approach?
answer
- unique + predicate = uniqueness within the subset
- engine-enforced → no check-then-write race
- one rule = one index
- deactivate before activate in the same transaction
- upper bound only, never 'exactly one'
basics
~20 sCreate a unique index on user_id restricted by a predicate matching only active rows. Uniqueness is then enforced within that subset only, so many inactive rows per user are fine. Limits: it enforces one condition on one subset, and swapping the active row can conflict mid-transaction.
solid answer
~60 sDefine a unique index on the user identifier with a predicate that selects only the active rows. The index contains one entry per active row, so a second active row for the same user violates uniqueness and the write is rejected by the database itself — no application-level check, no race window, correct under concurrency because the uniqueness check happens in the same place as the write. What it does not give you: - **Scope.** It constrains exactly the subset in the predicate. "At most one active *and* at most one primary" needs two indexes. - **Swap ordering.** Promoting a different row within one transaction can transiently produce two active rows; whether that fails depends on when the index check happens, so the usual pattern is to deactivate the old row first in the same transaction. - **Contention.** All active rows for one user hit the same tiny index region, so heavy flipping concentrates writes on few pages. It is also a real index: the planner can use it for queries that look up active rows for a user.
code
sql · 6 linesCREATE UNIQUE INDEX uq_memberships_one_active_per_user
ON memberships (user_id)
WHERE status = 'active';
-- second active row for the same user is rejected
-- unlimited inactive rows per user remain legalgo deeper
Know that a unique index with a predicate enforces uniqueness only among the matching rows, and give the one-active-row example.
Explain why it is race-free compared with an application check, and handle the swap-ordering pitfall inside a transaction.
Add contention on the hot subset, the one-rule-per-index scope, engine support differences, and the fact that it doubles as a useful access path.
Weigh it against modelling alternatives such as a current-row pointer on the parent, considering invariant strength, write hot spots, and how many such conditional rules the schema can carry before the index portfolio becomes the cost.
## The problem "At most one X per Y" is a partial uniqueness rule: unique within a subset of rows, unconstrained outside it. A plain unique constraint cannot express it, because it applies to every row. Enforcing it in application code means a read-then-write, which two concurrent transactions can both pass, producing exactly the duplicate you forbade. ## The solution A **unique partial index** — a unique index whose definition carries a predicate — contains one entry per qualifying row and enforces uniqueness only among those entries. Rows outside the predicate are absent from the index, so they cannot collide with anything. This puts the invariant where it belongs: in the storage engine, checked as part of the write, under the same concurrency control as the data. Two concurrent inserts of an active row for the same user cannot both succeed; the second blocks on the index entry and then fails. ## Why the concurrency story works A unique index enforces its rule by refusing to hold two entries with the same key. When a transaction inserts an entry, a concurrent transaction inserting the same key waits until the first commits or rolls back, then either fails or proceeds. This is engine machinery, not application logic, so there is no window between check and write. Any application-level guard — select, then decide, then insert — has that window by construction unless it takes an explicit lock, which is more code and more contention for a weaker guarantee. ## Choosing the predicate shape Two common encodings of "active": - A boolean or status column: the predicate selects the active value. - A nullable timestamp such as an end-of-validity column: the predicate selects rows where it has no value, meaning the row is current. Either works. The important part is that the predicate be a simple, deterministic condition on stored columns — the engine must be able to decide membership from the row alone. ## Limits worth naming **One rule per index.** The index enforces exactly the subset it names. Additional rules — one primary address, one default payment method — each need their own index. That is fine, but each is another index to maintain. **Transactional swap.** Making a different row active usually means deactivating one and activating another. If the new activation is written before the old deactivation, the constraint sees two active rows at that instant. Whether that fails depends on the engine's checking model — some check immediately per statement, some can defer to commit if the constraint is declared deferrable, and index-backed rules are not always deferrable. The portable habit is to order the statements so the old row is cleared first within one transaction. **Hot-spot contention.** The index is small — one entry per user — which is exactly what makes it fast, but a workload that flips the active row constantly concentrates inserts and deletes into few pages, producing lock contention and page-level churn. **Not a foreign key or a check.** It says nothing about whether *at least* one row is active. "Exactly one" is a different, harder invariant that a partial unique index cannot express — it enforces the upper bound only. **Engine support varies.** Not every relational engine supports predicates on indexes, and the syntax differs where they do. Some engines instead require a computed column trick or a filtered unique constraint. The concept transfers; the spelling does not. ## Bonus: it is still an index Because it is a real index over active rows keyed by user, the planner can use it for lookups of a user's active row — a very common query. You get the constraint and the access path from one structure. Consider whether extra key columns would make it more useful for reads, remembering that adding columns to a unique index changes what is unique, so anything added must not weaken the rule. ## Alternatives and why they are worse - **Application check.** Racy, as described. - **Advisory or row lock on the user before writing.** Correct but hand-rolled, and it serialises unrelated work. - **A separate table holding the current row per user, keyed by user.** This is a legitimate design — uniqueness becomes an ordinary primary key — but it splits the data across two tables and adds joins. - **A trigger.** Correct only if it locks properly; it is more code doing what the index does natively. ## How to answer State the index, state why it is concurrency-safe (the check lives with the write), then volunteer the limits: one subset per index, the swap-ordering pitfall, contention on a hot subset, and that it bounds above but does not require at least one.
- Why is this better than checking in application code before inserting?An application check reads, decides, then writes, and two concurrent transactions can both read a state with no active row and both insert one. The unique partial index performs the check inside the same write that creates the row, under the engine's concurrency control, so the second writer is blocked and then rejected. It is also unconditional — every code path, including migrations and manual fixes, is covered.
- Can this enforce that exactly one row is active per user, not just at most one?No. The index can only prevent a second entry with the same key; it cannot require an entry to exist. Enforcing a lower bound needs a different mechanism, such as making the current row a column on the parent table with a not-null foreign key, or a deferred check evaluated at commit. In practice most systems accept the upper bound and treat 'no active row' as a valid state.
saying these in an interview costs you the question
- Claiming a plain unique constraint on the pair of columns achieves the same thing
- Assuming the index also guarantees at least one active row exists
- Activating the new row before deactivating the old one and being surprised by a violation
- Thinking an application-side check plus a retry is equivalent under concurrency
- Assuming every relational engine supports index predicates with the same syntax