Hook: Interviewers love this question because it tests whether you know the database, not just the app, must enforce uniqueness under concurrency.
Question: What is a unique index?
Answer: A unique index is an index that also enforces a no-duplicates rule on one column or a combination of columns. It is commonly used for values that must stay one-of-a-kind, like email addresses, usernames, or order numbers. Because the rule lives in the database, it still works when many requests hit the table at the same time.
Interview-Ready Answer: I would say a unique index is a database structure that both speeds up lookups and guarantees that indexed values are not duplicated. I like it for business keys such as email or order_id because it protects data even when two transactions race each other. In most engines it is backed by a B-tree, so the check is usually O(log n).
Detailed Explanation: Think of a unique index as two jobs in one: it is an index, which helps the database find rows quickly, and it is a rule, which says the same key may appear only once. In plain English, it is the database’s built-in way to say, ‘this value must be one of a kind.’
A normal index is mostly about speed. A unique index adds a promise: for the indexed column or column set, the database will reject duplicates. That promise is enforced inside the storage engine, not in application code, so it still holds when two users click the same button at nearly the same time.
INSERT or an UPDATE that changes an indexed value.email or (tenant_id, username).(country_code, phone_number) can be unique even if each column repeats on its own.WHERE deleted_at IS NULL can enforce uniqueness only for active rows.INSERT ... ON CONFLICT rely on a unique index to know what counts as a conflict.| Feature | Unique index | Unique constraint | Primary key |
|---|---|---|---|
| Purpose | Enforce and speed | Declare rule | Identify row |
| Count | Many | Many | One |
| NULLs | Engine-specific | Engine-specific | Not allowed |
| Backed by index | Yes | Usually yes | Usually yes |
The important idea is that a UNIQUE constraint is the logical rule, while a unique index is the physical structure that often enforces it. In PostgreSQL, for example, creating a unique constraint automatically creates a unique index behind the scenes. In many databases, the reverse is also true for reads: the same index can both enforce the rule and help queries that filter by that key.
For exact lookups, a unique index is excellent because the database can jump straight to one key instead of scanning many rows. Time complexity is typically O(log n) for search and insert, and space is O(n) because every row adds an index entry. In practice, B-tree indexes are shallow; even with millions of rows, the tree often has only a few levels, so the work is small. The trade-off is write cost: every insert or update to the indexed columns must also update the index, and page splits can make heavy-write workloads slower. This is why unique indexes are great for read-heavy keys but should not be slapped onto columns that naturally repeat.
NULL as ‘unknown,’ so multiple NULLs may be allowed, but the exact rule varies by database.Alice@example.com and alice@example.com to count as the same value, you usually need a normalized or expression-based index, such as LOWER(email).(a, b) can repeat in a or b alone, but not in the combination.So the mental model is simple: a unique index is both a speed tool and a safety net. It lets the database answer ‘is this key already taken?’ quickly and correctly, even when many sessions are writing at once.
Real-World Story: In a subscription signup service, the team stored customers by email. A developer added a pre-check like ‘does this email already exist?’ before inserting, and it looked fine in testing. Under a promotion, two signups with the same email arrived at the same millisecond, both passed the pre-check, and duplicate accounts were created; password resets and invoices started going to the wrong profile.
The fix was to add a unique index on users(email) and handle the duplicate-key error as a normal business rule, often as an HTTP 409 Conflict. If the app forgets to catch that error, the user sees a 500, logs fill with duplicate-key exceptions, and support starts hearing ‘I created an account, but login says my email is already used.’ That is the real value of the unique index: it stops data corruption at the source instead of hoping every code path remembers the rule.
-- PostgreSQL demo: a unique index blocks duplicate values while still allowing valid rows.
DROP TABLE IF EXISTS users_demo;
CREATE TABLE users_demo (
user_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT,
username TEXT NOT NULL
);
-- This unique index makes email the business key.
-- The database will reject a second row with the same non-NULL email.
CREATE UNIQUE INDEX ux_users_demo_email ON users_demo (email);
INSERT INTO users_demo (email, username) VALUES
('alice@example.com', 'alice'),
('bob@example.com', 'bob'),
(NULL, 'guest_one'),
(NULL, 'guest_two');
-- In PostgreSQL, multiple NULLs are allowed because NULL means 'unknown',
-- and unknown values are not considered equal for uniqueness checks.
-- Failure path: this insert would violate the unique index.
DO $$
BEGIN
INSERT INTO users_demo (email, username)
VALUES ('alice@example.com', 'alice_duplicate');
EXCEPTION
WHEN unique_violation THEN
RAISE NOTICE 'Duplicate email blocked by unique index';
END;
$$;
-- Safe alternative: let the unique index act as the conflict target.
INSERT INTO users_demo (email, username)
VALUES ('bob@example.com', 'bob_duplicate')
ON CONFLICT (email) DO NOTHING;
SELECT user_id, email, username
FROM users_demo
ORDER BY user_id;Follow-up & Tricky Questions:
deleted_at IS NULL. That is a very common soft-delete pattern.LOWER(email).Common Mistakes:
DO NOTHING.Memory Hook: Picture a nightclub bouncer with a guest list: one name, one entry. The unique index is the bouncer; the database refuses the second person with the same name.
Cheat Sheet:
Practice Tasks:
email and add a unique index; try inserting a duplicate and observe the error.(tenant_id, username) and verify that the same username can exist in different tenants.