Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

For ordinary uniqueness across one or more columns, use a PostgreSQL UNIQUE constraint: it records the rule in the table schema, and PostgreSQL creates the unique index that enforces it. Choose a standalone unique index when you need index-specific behavior, such as uniqueness for only some rows or uniqueness based on an expression.

To add a constraint to a live table without a prolonged write-blocking index build, create an eligible unique index with CREATE UNIQUE INDEX CONCURRENTLY, then attach it with ALTER TABLE ... ADD CONSTRAINT ... UNIQUE USING INDEX. This is not lock-free: the build takes longer and waits on transactions, while the attach step still acquires a table lock.

How a unique constraint differs from a unique index

Both mechanisms enforce uniqueness. A UNIQUE constraint is a named rule in the table schema; PostgreSQL implements it using an associated unique index. A standalone unique index is an index object that directly enforces the rule without representing it as a table constraint. PostgreSQL’s PostgreSQL 18 documentation on unique indexes says there is no need to create another index on columns already covered by a unique constraint, because that duplicates the automatically created index.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Question UNIQUE constraint Standalone unique index
What does it represent? A named table-level data rule, enforced by an associated unique index. An index object that enforces uniqueness.
When is it the natural choice? For ordinary uniqueness on one or more columns when the rule should be visible as a constraint in the schema. When uniqueness needs a partial predicate, an expression key, or another index-specific feature.
Can it use a partial predicate or expression key? No. Those index forms cannot be attached as a constraint with UNIQUE USING INDEX. Yes, where the desired uniqueness rule requires them.
Can a foreign key reference it? Yes. A foreign key can reference columns of a non-partial unique index; it cannot reference a partial unique index.
How is the index handled? PostgreSQL creates the supporting index automatically. You create and manage the index directly.

PostgreSQL currently supports unique indexes only with the B-tree access method. For ordinary column-based uniqueness, the constraint is usually the clearest declaration of intent; use a standalone index when the rule itself depends on index features.

Choose based on the uniqueness rule

Use a constraint for ordinary uniqueness

Use UNIQUE (email), for example, when every row must have a distinct email value and the rule should be represented as a table constraint. For a composite rule such as UNIQUE (tenant_id, external_id), PostgreSQL rejects rows only when the values in all indexed columns match an existing row. A match in just one column is not enough.

Use a standalone index for partial or expression-based uniqueness

A standalone index fits rules that apply only to qualifying rows or that compare a computed value rather than a plain column. For example, an index on an expression can enforce uniqueness over that expression; a partial unique index can enforce it only for rows matching its predicate. These index forms cannot be attached as a constraint using UNIQUE USING INDEX.

Decide how NULL should behave

By default, PostgreSQL treats NULL values as distinct in a unique index, so multiple rows can contain NULL in a column covered by the index. If NULLs should count as equal for uniqueness, PostgreSQL supports NULLS NOT DISTINCT. Make this choice explicit when defining the rule; it can change whether existing data is acceptable.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Check foreign-key needs

A foreign key may reference a primary key, a unique constraint, or the columns of a non-partial unique index. PostgreSQL does not automatically create an index on the referencing columns, so assess that separately if queries or referential actions need one.

Add a unique constraint to a live table with minimal write blocking

The low-blocking route is a two-operation migration: first build a unique index concurrently, then attach it as a constraint. Substitute your actual table and object names, and validate the intended uniqueness rule against current data before starting.

  1. Confirm the rule. Decide which columns define uniqueness, whether NULLs may repeat, and whether the rule applies to every row. For a composite key, confirm that the combined values—not each column individually—must be unique.
  2. Find and resolve duplicates. Check existing rows against the intended rule and resolve conflicts before the build. The unique build can fail on duplicate entries. Writes that occur during the concurrent build can also encounter uniqueness errors before the index is marked ready.
  3. Build the index outside a transaction block.
    CREATE UNIQUE INDEX CONCURRENTLY users_email_key_idx
        ON users (email);

    CREATE INDEX CONCURRENTLY cannot run inside a transaction block. Configure migration tooling to run this statement outside its usual transaction wrapper. PostgreSQL’s CREATE INDEX documentation explains that the concurrent option permits inserts, updates, and deletes during the build, unlike a standard build, which blocks writes until it finishes.

  4. Attach the index as a constraint. After the index build succeeds, run:
ALTER TABLE users
    ADD CONSTRAINT users_email_key
    UNIQUE USING INDEX users_email_key_idx;

The index must be a B-tree with the default sort ordering, and it cannot contain expression columns or a partial predicate. The ALTER TABLE documentation describes using an existing index as a way to add a constraint without blocking table updates for a long time. The attach operation still acquires a table lock. Once attached, the constraint takes ownership of the index; dropping the constraint also drops that index.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  1. Verify the result. Confirm that the constraint exists and that the index is valid and in the expected state before considering the migration complete.

What “without locking the table” does—and does not—mean

Concurrent index creation avoids a prolonged lock that prevents concurrent writes; it does not mean that the migration has no operational impact. PostgreSQL performs two table scans and waits for relevant transactions. Compared with a standard index build, this takes more work and usually longer, and the added CPU and I/O can slow other activity. The attach step also needs a table lock, even though PostgreSQL documents this route as avoiding a long block on updates.

  • Only one concurrent index build can run on a given table at a time.
  • Schema modification of the table is not allowed while the concurrent build is running.
  • Concurrent index creation must be run outside a transaction block.
  • A standard index build blocks writes until it finishes, although reads are not blocked by that build.

Recover if the concurrent build fails

A failed concurrent build can leave an invalid index. An invalid index is ignored for query planning but can still add overhead to updates. If the second scan fails, a unique index may continue enforcing uniqueness despite being invalid, so inspect its state instead of blindly rerunning the same statement.

PostgreSQL documents dropping the failed index and retrying, or rebuilding it with REINDEX INDEX CONCURRENTLY. Choose a recovery path only after checking the index state and whether it is still enforcing uniqueness.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Cases that need a different migration plan

Partitioned tables

The documented UNIQUE USING INDEX operation is not supported on partitioned tables, and concurrent creation of partitioned indexes is not directly supported. PostgreSQL’s CREATE INDEX documentation describes building indexes on individual partitions, then creating the partitioned index separately to reduce the write-locking period. Treat this as a separate, version-specific migration plan rather than applying the two-step procedure above unchanged.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Adding a primary key from an index

When attaching an index as a primary key, PostgreSQL attempts to set the indexed columns to NOT NULL if they are not already marked that way. That can require a table scan, so this path has a different operational consideration from adding a unique constraint.

Trying to use NOT VALID

NOT VALID is not available for a UNIQUE constraint. PostgreSQL currently permits it only for foreign-key, CHECK, and not-null constraints.

Version scope

The behavior and links here refer to PostgreSQL 18 documentation, accessed October 7, 2026. PostgreSQL documentation also lists older supported major versions; check the documentation for the exact server version you operate before scheduling a production migration.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.