skip to content

What are the ways to implement a one-to-one relationship between two tables, and when is splitting the attributes across two tables worth it instead of keeping one wider table?

level: middleimportance: should knowfreq 50%

answer

  1. 1:1 = 1:N plus uniqueness
  2. Shared PK: child PK is the FK — uniqueness for free
  3. Unique FK: child keeps own identity, drop UNIQUE to relax to 1:N
  4. Split for optional / bulky / cold / sensitive columns
  5. Mandatory both ways needs deferrable constraints or app logic

basics

~20 s

Either share the primary key — the child's primary key is also a foreign key to the parent — or put a unique foreign key on one side. Split only for optional, bulky, rarely read, or separately secured attributes.

solid answer

~60 s

There are three implementations, and the first is often the right one. 1. **One table.** If both sides are mandatory and always read together, one-to-one usually means the attributes belong in the same row. Splitting buys a join for nothing. 2. **Shared primary key.** The child table's primary key *is* the foreign key to the parent: `user_profile.user_id PRIMARY KEY REFERENCES user_account(id)`. Uniqueness comes free from the primary key, joins are key-to-key, and there is no extra index. This is my default when I do split. 3. **Unique foreign key.** A separate surrogate key plus `UNIQUE (parent_id)`. Equivalent in strength; useful when the child already has its own identity referenced elsewhere. Splitting earns its place when the child rows are **optional** (only some users have a profile), **bulky or cold** (large text or blobs that would bloat the hot row), **read or written on a very different schedule**, or **more sensitive** so access is granted separately. The hard part is mandatory-on-both-sides: the schema cannot force both rows to exist without deferrable constraints or a trigger, so that rule usually ends up in the application transaction.

code

sql · 11 lines
sql
-- shared primary key: profile is a dependent extension of the account
CREATE TABLE user_profile (
    user_id  BIGINT PRIMARY KEY REFERENCES user_account(id) ON DELETE CASCADE,
    bio      TEXT,
    avatar   BYTEA
);

-- unique foreign key: the parking space exists on its own
ALTER TABLE employee
    ADD COLUMN parking_space_id BIGINT NULL REFERENCES parking_space(id),
    ADD CONSTRAINT employee_parking_space_uq UNIQUE (parking_space_id);

go deeper

for a junior

Know that 1:1 is a foreign key plus uniqueness, and be able to describe the shared-primary-key form.

for a middle

Compare the three options, explain what the shared primary key buys (free uniqueness, no extra index, cheap join), and give concrete reasons to split.

for a senior

Bring in physical consequences — narrow hot rows, out-of-line storage of large values, write amplification and MVCC row versions — and be explicit that mandatory-both-sides is not enforceable without deferred checks.

for a principal

Treat splitting as a boundary decision about lifecycle, access frequency, ownership and data-protection scope, and state the operational cost of the join and the two-insert transaction that the split imposes on every caller.

## What one-to-one means A one-to-one (1:1) relationship means a row on each side relates to at most one row on the other. It is really 1:N with an added uniqueness restriction, which is why every implementation is a foreign key plus a way of making that foreign key unique. ## Option 1 — do not split at all If every parent has exactly one child and the columns are read together, 1:1 is a sign the attributes describe the same entity and belong in one row. Splitting introduces a join on every read, a second insert on every create, and a second place for the transaction to fail. Interviewers expect you to say this first; jumping straight to two tables without asking why is a modelling smell. ## Option 2 — shared primary key The child table uses the parent's key as its own: ``` user_profile(user_id PK, REFERENCES user_account(id), bio, avatar_url) ``` Properties: - **Uniqueness is structural.** The primary key already forbids two profiles for one user; no extra constraint or index is needed. - **Joins are cheap.** Key-to-key equality, both sides indexed by their primary key, and clustered storage engines physically co-locate by that key. - **No surrogate sprawl.** There is only one identity in the system for this entity; nothing has to decide whether to reference `user_account.id` or `user_profile.id`. - **Direction is explicit.** The child's existence depends on the parent, which is usually the truth of the model. This is the standard choice for attribute extension, and also for subtype tables where each subtype row extends a supertype row. ## Option 3 — unique foreign key The child has its own surrogate primary key plus a unique constraint on the foreign key: ``` employee(id PK, ..., parking_space_id BIGINT UNIQUE REFERENCES parking_space(id)) ``` Enforcement is equally strong: the unique index makes the relationship at most one-to-one. Reasons to prefer it: - The child is an independent entity that predates or outlives the pairing (a parking space exists whether or not it is assigned). - Other tables already reference the child by its own id. - The relationship might later become 1:N, in which case you drop the unique constraint rather than restructuring keys. Which side holds the key matters: put it on the side that is **optional**, so the column can be nullable and you avoid rows that exist only to hold a NULL. If only some employees have a parking space, the key goes on `employee` (or on a separate assignment table if both sides are optional and sparse). ## Deciding to split Good reasons: - **Optionality.** Only a minority of parents have the extra attributes. Splitting avoids a wide row full of NULLs and keeps the main table dense. - **Size and temperature.** Large text, JSON documents or binary payloads inflate the hot table, hurt sequential scans and, in engines that store oversized values out of line, still cost extra I/O. Moving them to a side table keeps the frequently scanned row narrow. - **Access pattern divergence.** One part is read on every request and the other once a month; one part is updated constantly and the other is immutable. Splitting reduces write amplification, index churn and row-version pressure under MVCC. - **Security.** Sensitive attributes (national id, salary) in a separate table can be granted separately, and the base table can be exposed without them. - **Ownership.** Different teams or modules own different attribute sets, and you want the schema boundary to match. Bad reasons: "the table has too many columns", "it looks cleaner", or an ORM's inheritance defaults chosen without thought. ## Costs of splitting - Every read of the full entity is a join; every create is two inserts in one transaction. - Absence is now ambiguous: a missing child row and a child row full of NULLs mean different things and code must handle both. - Enforcing **mandatory on both sides** is genuinely hard. Two foreign keys pointing at each other create a chicken-and-egg problem for inserts, solvable only with deferrable constraints checked at commit (where supported), or a trigger, or by accepting that the rule lives in the application's transaction boundary. Be honest about this in an interview: most schemas enforce parent-requires-child in code, not in DDL. - Reporting queries need to remember the join, and an accidental inner join silently drops parents that have no child. ## Which to pick Default order: one table → shared primary key → unique foreign key. Choose the shared primary key when the child is a dependent extension of the parent; choose the unique foreign key when the child is an entity in its own right or the pairing may loosen to 1:N later. Whichever you choose, name the enforcement explicitly — an unenforced 1:1 that relies on application discipline will eventually hold two child rows, and the query that returns duplicated parents will be blamed on the join.

  • How would you enforce that a user must always have exactly one profile row, in the schema itself?
    Strict enforcement needs constraints that are checked at commit rather than per statement — a deferrable foreign key from user_account to user_profile combined with the profile's own foreign key back, so both rows can be inserted in one transaction and validated at the end. Engines that lack deferrable constraints need a trigger or a check on a view. Most teams instead create both rows inside one application transaction and accept that the schema only enforces the child-to-parent direction.
  • Which side should hold the unique foreign key when only one side is optional?
    The optional side holds it, so the column can be NULL for rows without a counterpart and no placeholder rows are needed. If employees may lack a parking space, employee.parking_space_id is nullable and unique. If both sides are optional and the pairing is sparse, a small separate assignment table with a unique constraint on each column is cleaner than two nullable columns.

Shared primary key is a passport page stapled into the same passport — it has no number of its own. A unique foreign key is a locker assignment: the locker exists independently, and the rule is just that no two people get the same one.

saying these in an interview costs you the question

  • Splitting a 1:1 by default without an optionality, size, access-pattern or security reason
  • Believing a plain foreign key alone enforces one-to-one without a unique constraint or shared primary key
  • Giving the child its own surrogate id and then referencing it inconsistently alongside the parent id
  • Claiming a pair of mutual foreign keys enforces mandatory participation on both sides — it just makes inserts impossible without deferral
  • Putting the nullable foreign key on the mandatory side, producing rows whose only content is a NULL

context