UNIQUE Constraint in PostgreSQL

Learn how PostgreSQL UNIQUE constraints prevent duplicate values in a column or combination of columns, and how to add them to existing tables using concurrent index builds.

4 min read · PostgreSQL 10+ · Back to overview

Quick Answer

A PostgreSQL UNIQUE constraint ensures all values in a column or combination of columns are distinct across rows. PostgreSQL automatically creates a unique B-tree index to enforce it. Unlike primary keys, UNIQUE columns allow NULL values — and multiple NULLs are permitted because NULL is not considered equal to NULL.

Spin up a Postgres database in 20 seconds with Vela.

Try Vela Sandbox

The UNIQUE constraint prevents duplicate values from being stored in a column or group of columns. Every time a row is inserted or updated, PostgreSQL checks the existing values and rejects the operation if a duplicate is found. PostgreSQL automatically creates a unique B-tree index to enforce this efficiently, so no separate index is needed on the same column.

Syntax

Inline column constraint:

CREATE TABLE table_name (
  col data_type UNIQUE
);

Named table-level constraint (recommended for multi-column or named constraints):

CREATE TABLE table_name (
  col1 data_type,
  col2 data_type,
  CONSTRAINT constraint_name UNIQUE (col1, col2)
);

Practical Example

Create a products table where SKUs and barcodes must be globally unique:

CREATE TABLE categories (
  category_id   SERIAL PRIMARY KEY,
  category_name VARCHAR(100) NOT NULL
);

CREATE TABLE products (
  product_id    SERIAL PRIMARY KEY,
  sku           VARCHAR(30) NOT NULL,
  barcode       VARCHAR(13),
  product_name  VARCHAR(200) NOT NULL,
  category_id   INT REFERENCES categories (category_id),
  CONSTRAINT uq_products_sku     UNIQUE (sku),
  CONSTRAINT uq_products_barcode UNIQUE (barcode)
);

Attempt to insert two rows with the same SKU:

INSERT INTO products (sku, product_name) VALUES ('SKU-ALPHA', 'USB-C Hub');
INSERT INTO products (sku, product_name) VALUES ('SKU-ALPHA', 'USB-A Hub');

-- ERROR:  duplicate key value violates unique constraint "uq_products_sku"
-- DETAIL:  Key (sku)=(SKU-ALPHA) already exists.

NULL values are permitted — multiple products can have a NULL barcode:

INSERT INTO products (sku, product_name) VALUES ('SKU-001', 'Mechanical Keyboard');
INSERT INTO products (sku, product_name) VALUES ('SKU-002', 'Trackpad');
-- Both rows have barcode = NULL — this succeeds because NULL ≠ NULL

Verify the constraint and its backing index:

\d products
-- Indexes:
--   "products_pkey" PRIMARY KEY, btree (product_id)
--   "uq_products_sku" UNIQUE CONSTRAINT, btree (sku)
--   "uq_products_barcode" UNIQUE CONSTRAINT, btree (barcode)

Composite UNIQUE Constraints and Low-Lock Approach for Existing Tables

Composite uniqueness — the combination of values must be distinct, not each individual value:

CREATE TABLE order_line_items (
  order_id   INT NOT NULL,
  product_id INT NOT NULL,
  quantity   INT NOT NULL DEFAULT 1,
  CONSTRAINT uq_order_product UNIQUE (order_id, product_id)
);

This allows the same product to appear in different orders, but prevents duplicate line items for the same product within one order.

For tables with heavy concurrent traffic, adding uniqueness without locking requires two steps:

-- Step 1: build the index without blocking writes (takes a ShareUpdateExclusiveLock)
CREATE UNIQUE INDEX CONCURRENTLY uq_products_sku_idx
  ON products (sku);

-- Step 2: promote the index to a named constraint (brief ACCESS EXCLUSIVE lock)
ALTER TABLE products
  ADD CONSTRAINT uq_products_sku
  UNIQUE USING INDEX uq_products_sku_idx;

If the CONCURRENTLY build fails, it leaves an INVALID index behind. Drop and retry:

SELECT indexname, indisvalid
FROM pg_indexes
JOIN pg_index ON pg_indexes.indexname = (SELECT relname FROM pg_class WHERE oid = indexrelid)
WHERE tablename = 'products';

DROP INDEX CONCURRENTLY uq_products_sku_idx;
-- then retry CREATE UNIQUE INDEX CONCURRENTLY

Testing with Vela

Before adding a UNIQUE constraint to a production table, you need to confirm no duplicate values already exist in that column. Vela’s database branching lets you run the uniqueness audit and the CREATE UNIQUE INDEX CONCURRENTLY build on a copy-on-write clone of production data — letting you measure index build time, spot pre-existing duplicates, and test the USING INDEX promotion step — all without risk to the live database.

Production Tips

  • UNIQUE constraints allow multiple NULL values. If you need at most one NULL, use a partial unique index: CREATE UNIQUE INDEX ON products (sku) WHERE sku IS NOT NULL;
  • Use the CONCURRENTLY index approach when adding uniqueness to large, active tables to avoid prolonged locks that block writes.
  • Name constraints explicitly (e.g., uq_products_sku) so migration rollbacks and error messages are self-descriptive.
  • A UNIQUE constraint’s backing index serves lookup queries automatically — there is no need to create a separate regular index on the same column.
  • Before adding a UNIQUE constraint, identify existing duplicates: SELECT col, COUNT(*) FROM t GROUP BY col HAVING COUNT(*) > 1; — clean these up first.
  • To check whether a CONCURRENTLY index build is still in progress, query pg_stat_progress_create_index.

Continue in Database Constraints: NOT NULL Constraint.

Related in this section: Primary Key · Foreign Key · CHECK Constraint

Frequently Asked Questions

Does a PostgreSQL UNIQUE constraint allow NULL values?
Yes. UNIQUE constraints allow NULL values, and multiple rows can have NULL in a unique column because NULL is not considered equal to NULL. If you need to enforce at most one NULL, create a partial unique index with a WHERE col IS NOT NULL condition.
Does adding a UNIQUE constraint lock the table?
Yes. ALTER TABLE ... ADD CONSTRAINT UNIQUE acquires an ACCESS EXCLUSIVE lock and validates all existing rows. On large or busy tables, create the unique index first using CREATE UNIQUE INDEX CONCURRENTLY (which uses a weaker, non-blocking lock) and then attach it as a constraint to minimize downtime.
What is the difference between a UNIQUE constraint and a UNIQUE index in PostgreSQL?
They are functionally equivalent — PostgreSQL implements UNIQUE constraints by creating a unique index internally. The difference is declarative: a UNIQUE constraint appears in constraint listings and error messages, while a standalone UNIQUE index does not. You can promote a UNIQUE index to a constraint using ALTER TABLE ... ADD CONSTRAINT ... UNIQUE USING INDEX.
Can I add a UNIQUE constraint on multiple columns?
Yes. Define it as a table constraint listing all columns: UNIQUE (col1, col2). The combination of values must be unique across rows, but individual columns can repeat independently.
How do I remove a UNIQUE constraint in PostgreSQL?
Use ALTER TABLE table_name DROP CONSTRAINT constraint_name. Find the constraint name with \d table_name in psql or by querying pg_constraint WHERE contype = 'u' AND conrelid = 'your_table'::regclass.