You are setting the schema convention for a large database: should uniqueness always be declared as a named table constraint, or are bare unique indexes acceptable? How would you decide, and what follows from the choice?
answer
- default: named constraint for logical-model rules
- exceptions are capability-driven (partial, expression, opclass, include)
- index-cleanup automation can drop a bare unique index
- constraint owns its index → rebuild needs adopt path
- exception ⇒ no FK target ⇒ surrogate key
basics
~20 sDefault to named constraints: they are the objects foreign keys, upsert targets and schema tooling can cite, and they are hard to delete by accident. Allow bare unique indexes only where constraint syntax cannot express the rule — conditional or computed uniqueness — and document each exception.
solid answer
~50 sMy default is: **every uniqueness rule that is part of the logical model is a named constraint**. Reasons that hold up in review: - Constraints are the citable objects — foreign keys and conflict targets resolve against them, migration diff tools and ORM reverse-engineering read them, and a dump reproduces the model. - They are safe from index-cleanup automation, which happily drops "unused" indexes and would otherwise remove enforcement. - The name is a contract: error-mapping code turns `account_email_key` into a decent user-facing message. **Exceptions are capability-driven, not taste-driven**: partial/filtered uniqueness, expression-based uniqueness, non-default collation or opclass, included payload columns. Each such index gets the same naming convention and a comment saying which rule it enforces. The fallout to plan for: a partial or expression index cannot be a foreign-key target, so those tables need surrogate keys; and the constraint owns its index, so an online index rebuild needs the "adopt an existing index" path rather than a drop-and-recreate that would leave an unenforced window.
go deeper
Know the default — declare uniqueness as a constraint — and that indexes are the fallback when the rule is conditional or computed.
Justify the default with referenceability and tooling, and list the specific capabilities that force an index.
Add the operational consequences: cleanup automation, rebuild mechanics, error-name mapping, and the surrogate-key implication of each exception.
Present it as a checkable convention with an explicit exception rule and consequence chain, and acknowledge the counter-argument for large tables where index rebuilds are routine.
## Framing the decision The two forms enforce identical rules at identical cost — the engine backs a unique constraint with a unique index. So this is not a performance decision. It is a decision about **which objects the rest of the system is allowed to depend on**, and about failure modes over a schema's lifetime. ## The case for constraints as the default **Referenceability.** Foreign keys resolve against declared keys; strict engines accept nothing else, and even the lenient ones exclude partial, expression, invalid and deferrable indexes. If a table's business identifier is expressed only as a bare index, a future foreign key is blocked until someone reworks the schema. **Tooling.** Migration frameworks, schema-diff tools, ORM reverse-engineering and documentation generators model constraints as part of the logical schema and indexes as physical tuning. A rule expressed as an index tends to be dropped from generated models and from the mental model of anyone reading them. **Accident resistance.** Index-maintenance automation — "drop indexes with zero scans in 90 days" — is common and correct in spirit. A bare unique index that is never used for reads is exactly what such a script targets, and dropping it silently removes a data-integrity rule. A constraint cannot be removed that way. **Naming and error handling.** Duplicate-key errors carry the object name. A convention like `<table>_<columns>_key` lets a single mapping layer convert violations into specific API errors instead of a generic 500. This works for indexes too, but only if the same naming discipline is applied. **Readability of intent.** `UNIQUE (tenant_id, code)` in the table definition states a fact about the domain. `CREATE UNIQUE INDEX ...` three files later reads as tuning, and reviewers treat it accordingly. ## When a bare index is the right call Constraint syntax accepts a column list and nothing else. Reach for an index when the rule needs more: - **Conditional uniqueness** — unique among live rows, among rows in a given state, among non-archived tenants. - **Computed uniqueness** — unique on a normalized or derived value. - **Collation/opclass or ordering specifics**, or **included payload columns** for index-only reads that a constraint's index cannot express. The honest phrasing of the convention is therefore *capability-driven exception*, not "use whichever you like". A reviewer should be able to ask "which constraint capability is missing here?" and get a one-line answer. Sometimes the better move is to change the model instead of taking the exception: moving archived rows to a history table restores a plain constraint; materialising a normalized value as a generated column turns computed uniqueness back into an ordinary constraint. Both trade a little storage or migration work for a schema that stays inside the simple rules. ## Lifecycle consequences to plan for **Rebuilds.** A constraint owns its index. You cannot drop that index alone, so replacing a bloated one means either an engine feature that lets an existing unique index be adopted as the constraint's index, or a drop-and-recreate of the constraint — which is heavier and, done carelessly, leaves a window in which the rule is not enforced. Bare indexes can be built alongside and swapped freely. If you run large tables with regular index maintenance, this is a genuine argument in the other direction, and worth writing into the runbook rather than discovering during an incident. **Enforcement gaps.** Any scheme where the rule is temporarily absent must be treated as a correctness event, not a maintenance detail: duplicates inserted during the gap will block the rebuild at the end and require data surgery. **Surrogate keys.** If exceptions exist, they imply the affected tables carry a surrogate primary key so children have something stable to reference. Make that part of the convention rather than a case-by-case discovery. **One rule, one object.** Forbid declaring both a constraint and an equivalent index on the same columns. It is duplicated write cost for no gain and it appears surprisingly often after two migrations from different authors. ## Making the convention stick A convention only works if it is checkable. Practical enforcement: a naming standard for both forms; a periodic catalog query that flags redundant index/constraint pairs and unique indexes with no accompanying comment; a review checklist item asking which capability justified each bare unique index; and a rule that index-cleanup automation ignores unique indexes entirely. The convention should also state the consequence chain explicitly — exception implies no FK target implies surrogate key — because that is the part teams rediscover painfully six months later when someone needs to reference the table.
- What argues in favour of bare unique indexes on very large tables?Index maintenance. A constraint owns its index, so you cannot build a replacement and swap it without either an engine feature that adopts an existing index into the constraint, or dropping and recreating the constraint — which is heavier and can leave the rule unenforced for a window. A bare index can be rebuilt alongside and swapped, which matters when rebuilds are routine.
- How would you audit an existing schema against this convention?Query the catalog for unique indexes that are not owned by a constraint and classify each: is it partial, expression-based, or otherwise beyond constraint syntax, or is it a plain unique index that should be converted? Separately, flag any index whose column list duplicates an existing constraint, since that is doubled write cost. Both checks are cheap enough to run in CI against a migrated schema.
saying these in an interview costs you the question
- Treating the choice as a performance question when both forms cost the same
- Declaring both a unique constraint and a matching unique index 'to be safe'
- Adopting bare unique indexes broadly and then being blocked when a foreign key is needed
- Letting generic 'drop unused indexes' automation consider unique indexes
- Assuming a constraint's index can be rebuilt online the same way a bare index can