301

As I can understand documentation the following definitions are equivalent:

create table foo (
    id serial primary key,
    code integer,
    label text,
    constraint foo_uq unique (code, label));

create table foo (
    id serial primary key,
    code integer,
    label text);
create unique index foo_idx on foo using btree (code, label);    

However, a note in the manual for Postgres 9.4 says:

The preferred way to add a unique constraint to a table is ALTER TABLE ... ADD CONSTRAINT. The use of indexes to enforce unique constraints could be considered an implementation detail that should not be accessed directly.

(Edit: this note was removed from the manual with Postgres 9.5.)

Is it only a matter of good style? What are practical consequences of choice one of these variants (e.g. in performance)?

11
  • 39
    The (only) practical difference is that you can create a foreign key to a unique constraint but not to a unique index. Commented May 8, 2014 at 13:13
  • 40
    An advantage the other way around (as came up in another question recently) is that you can have a partial unique index, such as "Unique ( foo ) Where bar Is Null". AFAIK, there's no way to do that with a constraint. Commented May 8, 2014 at 13:21
  • 4
    @a_horse_with_no_name I'm not sure when this happened, but this no longer appears to be true. This SQL fiddle allows foreign key references to a unique index: sqlfiddle.com/#!17/20ee9; EDIT: adding a 'filter' to the unique index causes this to stop working (as expected) Commented Jan 26, 2018 at 16:01
  • 4
    from postgres documentation: PostgreSQL automatically creates a unique index when a unique constraint or primary key is defined for a table. postgresql.org/docs/9.4/static/indexes-unique.html Commented May 26, 2018 at 2:36
  • 4
    one of the differences is deferrable behavior - constraints support it, indexes don't postgresql.org/docs/current/sql-set-constraints.html Commented Jan 24, 2021 at 16:46

13 Answers 13

243

I had some doubts about this basic but important issue, so I decided to learn by example.

Let's create test table master with two columns, con_id with unique constraint and ind_id indexed by unique index.

create table master (
    con_id integer unique,
    ind_id integer
);
create unique index master_unique_idx on master (ind_id);

    Table "public.master"
 Column |  Type   | Modifiers
--------+---------+-----------
 con_id | integer |
 ind_id | integer |
Indexes:
    "master_con_id_key" UNIQUE CONSTRAINT, btree (con_id)
    "master_unique_idx" UNIQUE, btree (ind_id)

In table description (\d in psql) you can tell unique constraint from unique index.

Uniqueness

Let's check uniqueness, just in case.

test=# insert into master values (0, 0);
INSERT 0 1
test=# insert into master values (0, 1);
ERROR:  duplicate key value violates unique constraint "master_con_id_key"
DETAIL:  Key (con_id)=(0) already exists.
test=# insert into master values (1, 0);
ERROR:  duplicate key value violates unique constraint "master_unique_idx"
DETAIL:  Key (ind_id)=(0) already exists.
test=#

It works as expected!

Foreign keys

Now we'll define detail table with two foreign keys referencing to our two columns in master.

create table detail (
    con_id integer,
    ind_id integer,
    constraint detail_fk1 foreign key (con_id) references master(con_id),
    constraint detail_fk2 foreign key (ind_id) references master(ind_id)
);

    Table "public.detail"
 Column |  Type   | Modifiers
--------+---------+-----------
 con_id | integer |
 ind_id | integer |
Foreign-key constraints:
    "detail_fk1" FOREIGN KEY (con_id) REFERENCES master(con_id)
    "detail_fk2" FOREIGN KEY (ind_id) REFERENCES master(ind_id)

Well, no errors. Let's make sure it works.

test=# insert into detail values (0, 0);
INSERT 0 1
test=# insert into detail values (1, 0);
ERROR:  insert or update on table "detail" violates foreign key constraint "detail_fk1"
DETAIL:  Key (con_id)=(1) is not present in table "master".
test=# insert into detail values (0, 1);
ERROR:  insert or update on table "detail" violates foreign key constraint "detail_fk2"
DETAIL:  Key (ind_id)=(1) is not present in table "master".
test=#

Both columns can be referenced in foreign keys.

Constraint using index

You can add table constraint using existing unique index.

alter table master add constraint master_ind_id_key unique using index master_unique_idx;

    Table "public.master"
 Column |  Type   | Modifiers
--------+---------+-----------
 con_id | integer |
 ind_id | integer |
Indexes:
    "master_con_id_key" UNIQUE CONSTRAINT, btree (con_id)
    "master_ind_id_key" UNIQUE CONSTRAINT, btree (ind_id)
Referenced by:
    TABLE "detail" CONSTRAINT "detail_fk1" FOREIGN KEY (con_id) REFERENCES master(con_id)
    TABLE "detail" CONSTRAINT "detail_fk2" FOREIGN KEY (ind_id) REFERENCES master(ind_id)

Now there is no difference between column constraints description.

Partial indexes

In table constraint declaration you cannot create partial indexes. It comes directly from the definition of create table .... In unique index declaration you can set WHERE clause to create partial index. You can also create index on expression (not only on column) and define some other parameters (collation, sort order, NULLs placement).

You cannot add table constraint using partial index.

alter table master add column part_id integer;
create unique index master_partial_idx on master (part_id) where part_id is not null;

alter table master add constraint master_part_id_key unique using index master_partial_idx;
ERROR:  "master_partial_idx" is a partial index
LINE 1: alter table master add constraint master_part_id_key unique ...
                               ^
DETAIL:  Cannot create a primary key or unique constraint using such an index.
Sign up to request clarification or add additional context in comments.

3 Comments

is it actual info? especially about partial indexes
@anatol - yes, it is.
57

One more advantage of using UNIQUE INDEX vs. UNIQUE CONSTRAINT is that you can easily DROP/CREATE an index CONCURRENTLY, whereas with a constraint you can't.

2 Comments

AFAIK it's not possible to drop concurrently a unique index. postgresql.org/docs/9.3/static/sql-dropindex.html "There are several caveats to be aware of when using this option. Only one index name can be specified, and the CASCADE option is not supported. (Thus, an index that supports a UNIQUE or PRIMARY KEY constraint cannot be dropped this way.)"
In Postgres 12.0 I receive no error when attempting to drop a unique index concurrently; the table definition after running the command shows that the index was indeed dropped. DROP INDEX CONCURRENTLY IF EXISTS idx_name
38

Uniqueness is a constraint. It happens to be implemented via the creation of a unique index since an index is quickly able to search all existing values in order to determine if a given value already exists.

Conceptually the index is an implementation detail and uniqueness should be associated only with constraints.

The full text

So speed performance should be same

2 Comments

From that quote I read the speed performance is faster in index and NOT the same. I thought that is the whole reason for index.
@Zaffer: Probably you confused by quickly, it is not equal quicker. Quote says that index is used to check that value already exists. And this task is done quickly. Also it says that when you use unique constraint under hood it uses index. Because of that performance is same.
30

Since various people have provided advantages of unique indexes over unique constraints, here's a drawback: a unique constraint can be deferred (only checked at the end of the transaction), a unique index can not be.

7 Comments

How can this be, given that all unique constraints have a unique index?
Because indexes do not have an API for deferring, only constraints do, so while the deferral machinery exists under the cover to support unique constraints, there's no way to declare an index as deferrable, or to defer it.
That is correct, assuming the constraint is deferrable and deferred. Also note that not all constraints are deferrable: NOT NULL and CHECK constraints are always immediate.
You need to create the constraint as DEFERRABLE in the first place (the default is NOT DEFERRABLE), and either create it as DEFERRABLE INITIALLY DEFERRED (in which case it's always deferred by default), or SET CONSTRAINTS <constraint_name> DEFERRED in the specific transactions where you want to defer the constraint's checking. Which is best depends on your specific use case e.g. whether you always want the constraint to be deferred, or only have a few places where that's a necessity.
Minor remark, for clarity: there are 3 possible points of constraint validation, not 2. Not deferrable, gets validated mid-statement. Deferrable but not currently deferred is checked at the end of the statement. Deferred is checked at the end of transaction. There might be a difference between the first two.
|
21

A very minor thing that can be done with constraints only and not with indexes is using the ON CONFLICT ON CONSTRAINT clause (see also this question).

This doesn't work:

CREATE TABLE T (a INT PRIMARY KEY, b INT, c INT);
CREATE UNIQUE INDEX u ON t(b);

INSERT INTO T (a, b, c)
VALUES (1, 2, 3)
ON CONFLICT ON CONSTRAINT u
DO UPDATE SET c = 4
RETURNING *;

It produces:

[42704]: ERROR: constraint "u" for table "t" does not exist

Turn the index into a constraint:

DROP INDEX u;
ALTER TABLE t ADD CONSTRAINT u UNIQUE (b);

And the INSERT statement now works.

5 Comments

But on unique index you can still do by listing fields in braces: ON CONFLICT (b) DO UPDATE SET
@BIOHAZARD: Yes. But you may get this error: "there is no unique or exclusion constraint matching the ON CONFLICT specification", even if there is such a CREATE UNIQUE INDEX existing.
@BIOHAZARD you can also run into problems while using functions in your index that you want to be the ON CONFLICT one. You can't use ... ON CONFLICT (a, COALESCE(b, -1)) DO UPDATE SET ... i.e.
@JimCarnicelli did you find the problem? I have this err message when I tried to use ON CONFLICT with functional index like ... ON CONFLICT (a, COALESCE(b, -1)) DO UPDATE SET .... Are there any ohter cases when PG can't find existsing unique index?
@ytatichno: Per Lukas Eder's answer, the uber solution is to add a constraint to the table explicitly. Then reference it using something like "ON CONFLICT ON CONSTRAINT <constraint name> DO NOTHING". Sometimes it's not necessary. But it should work every time as a fallback.
20

Another thing I've encountered is that you can use sql expressions in unique indexes but not in constraints.

So, this does not work:

CREATE TABLE users (
    name text,
    UNIQUE (lower(name))
);

but following works.

CREATE TABLE users (
    name text
);
CREATE UNIQUE INDEX uq_name on users (lower(name));

4 Comments

I would use the citext extension.
@ceving it depends on the use case. sometimes you want to preserve casing while ensuring case-insensitive uniqueness
I ran into this as well. I was worried that not being able to add the constraint would be a problem but it seems to be working fine. I tried to do: ALTER TABLE films ADD CONSTRAINT unique_file_title UNIQUE USING INDEX lower_title_idx; but got error Cannot create a primary key or unique constraint using such an index. Index contains expressions. I tried inserting case-insensitive data and it seems to work even without the constraint.
I needed this specific functionality too, and I could achieve it using function indices as described, while constraints didn't help.
14

There is a difference in locking.
Adding an index does not block read access to the table.
Adding a constraint does put a table lock (so all selects are blocked) since it is added via ALTER TABLE.

2 Comments

this is a good point I think
This sounds like a massive benefit for using unique index.
9

As of PostgreSQL 18, both still end up enforcing uniqueness exactly the same way but aren't equivalent in terms of how they command the underlying index. There's also a third way to do unique, in the form of exclusion constraints (since 9.0, but below matrix refers to state as of 18):

control over index constraint index as constraint exclusion constraint
collation, direction, null order YES no no YES
custom equality no no no YES
expressional YES no no YES
cross-column unique no no no YES
include payload column YES YES YES YES
index storage parameters YES YES YES YES
tablespace YES YES YES YES
partial (WHERE) YES no no YES
multi-column YES YES YES YES
deferred validation no YES YES YES
nulls not distinct YES YES YES YES* (emulated by coalesce() or requires not distinct from operator)
creating concurrently YES no YES no (and no equivalent index syntax to enable adding as constraint)
index types,
operator classes
BTree BTree BTree BTree, GiST, SPGiST, hash

In general, the price for the flexibility of an exclusion constraint is performance:

If all of the specified operators test for equality, this is equivalent to a UNIQUE constraint, although an ordinary unique constraint will be faster. However, exclusion constraints can specify constraints that are more general than simple equality.

Demo at db<>fiddle


Constraint:

create table t (
    id bigserial primary key,
    val1 text,
    val2 text,
    unique /*concurrently*/
        nulls not distinct 
       (--(val1||val2) --expression
         val1
       /*collate unicode 
         text_ops 
         desc nulls last
         with operator(pg_catalog.=)*/) 
        include(val2)
        with(fillfactor=100)
        using index tablespace pg_default
        --where (id%2=1)
        deferrable initially deferred
);

Index:

create table t2 (
    id bigserial primary key,
    val1 text,
    val2 text
);
create unique index concurrently 
  on t2 ((val1||val2)--expression
         collate unicode 
         text_ops 
         desc nulls last
         /*with operator(pg_catalog.=)*/) 
  include(val2)
  nulls not distinct 
  with(fillfactor=100)
  tablespace pg_default
  where id%2=1
  /*deferrable initially deferred*/;

Index added as constraint:

create unique index concurrently uidx
  on t3 (--(val1||val2)--expression
         val1 
         /*collate unicode 
         text_ops 
         desc nulls last
         with operator(pg_catalog.=)*/) 
  include(val2)
  nulls not distinct 
  with(fillfactor=100)
  tablespace pg_default
  /*where id%2=1*/;
alter table t3 
  add constraint ucns 
  unique using index uidx 
  deferrable initially deferred;

Exclusion:

create table t4 (
    id bigserial primary key,
    val1 text check(val1<>'reserved'),
    val2 text,
    exclude /*concurrently*/
            (coalesce(val1,'reserved') --expression
             collate unicode 
             text_ops 
             desc nulls last 
             with operator(pg_catalog.=))
        include(val2)
        with(fillfactor=100)
        using index tablespace pg_default
        where (id%2=1)
        deferrable initially deferred
);

Comments

3

In addition to the other answers, there's a topic of whether unique constraints are also used to speed-up queries as indexes are.

Apparently constraints are actually used for Index Scans as indicated by EXPLAIN:

ALTER TABLE mytable
    ADD CONSTRAINT mytable_uc UNIQUE (other_id, name);

explain select * from mytable
    where name = 'name' and other_id = 154

Result:

Index Scan using mytable_uc on mytable  (cost=0.28..2.29 rows=1 width=101)
  Index Cond: ((other_id = 154) AND ((name)::text = 'name'::text))

Comments

0

I read this in the doc:

ADD table_constraint [ NOT VALID ]

This form adds a new constraint to a table using the same syntax as CREATE TABLE, plus the option NOT VALID, which is currently only allowed for foreign key constraints. If the constraint is marked NOT VALID, the potentially-lengthy initial check to verify that all rows in the table satisfy the constraint is skipped. The constraint will still be enforced against subsequent inserts or updates (that is, they'll fail unless there is a matching row in the referenced table). But the database will not assume that the constraint holds for all rows in the table, until it is validated by using the VALIDATE CONSTRAINT option.

So I think it is what you call "partial uniqueness" by adding a constraint.

And, about how to ensure the uniqueness:

Adding a unique constraint will automatically create a unique B-tree index on the column or group of columns listed in the constraint. A uniqueness restriction covering only some rows cannot be written as a unique constraint, but it is possible to enforce such a restriction by creating a unique partial index.

Note: The preferred way to add a unique constraint to a table is ALTER TABLE … ADD CONSTRAINT. The use of indexes to enforce unique constraints could be considered an implementation detail that should not be accessed directly. One should, however, be aware that there’s no need to manually create indexes on unique columns; doing so would just duplicate the automatically-created index.

So we should add constraint, which creates an index, to ensure uniqueness.

How I see this problem?

A "constraint" aims to gramatically ensure that this column should be unique, it establishes a law, a rule; while "index" is semantical, about "how to implement, how to achieve the uniqueness, what does unique means when it comes to implementation". So, the way Postgresql implements it, is very logical: first, you declare that a column should be unique, then, Postgresql adds the implementation of adding an unique index for you.

1 Comment

"So I think it is what you call "partial uniqueness" by adding a constraint." indexes can apply to only a well-defined subset of the records through the where clause, so you can define that records are unique IFF they satisfy some criteria. This simply disables the constrains for an undefined set of records which predate the constraint being created. It's completely different, and the latter is significantly less useful, though it's convenient for progressive migrations I guess.
0

As I found in ibm descriptions:

In DML statements, enabled unique constraints on a logged table are checked at the end of a statement, but unique indexes are checked on a row-by-row basis, thereby preventing any insert or update of a row that might potentially violate the uniqueness of the specified column (or for a multiple-column column constraint or index, the column list).

For example, if you stored the values 1, 2, and 3 in rows of a logged table that has an INT column, an UPDATE operation on that table that specifies SET c = c + 1 would fail with an error if there were a unique index on the column c, but the statement would succeed if the column had a unique constraint. Blockquote

Comments

0

There seems to be an important difference between the Postgresql and DB2 implementation of a unique index across multiple fields, which include fields that can be null

For example on a table like:

create table if not exists bus_target_log
    (k_targetlog_id                    varchar(36)     primary key
    ,c_message_id                      varchar(50)     not null
    ,c_lep_name                        varchar(50)
    ,c_unique                          varchar(36)
    ,d_delivered_timestamp             timestamp       not null default statement_timestamp()
    ,c_target                          varchar(30)     not null)

I add a unique index:

create unique index if not exists bus_target_log_ix1
    on bus_target_log
    (c_unique                          asc
    ,c_message_id                      asc
    ,c_lep_name                        asc
    );

If I know insert the following on DB2, I would get an error on the second insert.

insert into bus_target_log(k_targetlog_id, c_message_id, c_lep_name, c_target) values ('4444','111', 'targetname1', 'target'); -- works
insert into bus_target_log(k_targetlog_id, c_message_id, c_lep_name, c_target) values ('5555','111', 'targetname1', 'target'); -- fails

However on Postgresql both inserts work!

On postgresql I can run the following:

insert into bus_target_log(k_targetlog_id, c_message_id, c_lep_name, c_target, c_unique) values ('1','111', 'targetname1', 'target','1');
-- works
insert into bus_target_log(k_targetlog_id, c_message_id, c_lep_name, c_target, c_unique) values ('2','111', 'targetname1', 'target','1');
-- fails
insert into bus_target_log(k_targetlog_id, c_message_id, c_lep_name, c_target) values ('3','111', 'targetname1', 'target');
-- works
insert into bus_target_log(k_targetlog_id, c_message_id, c_lep_name, c_target) values ('4','111', 'targetname1', 'target');
-- works

This can be fixed in Postgresql by ensuring all fields in the unique index are not null, for example

    create table if not exists bus_target_log
(k_targetlog_id                    varchar(36)     primary key
,c_message_id                      varchar(50)     not null
,c_lep_name                        varchar(50)     not null default '0'
,c_unique                          varchar(36)     not null default '0'
,d_delivered_timestamp             timestamp       not null default statement_timestamp()
,c_target                          varchar(30)     not null)

1 Comment

PostgreSQL has NULLS DISTINCT and NULLS NOT DISTINCT clauses, the first one being the default.
-2
SELECT a.phone_number,count(*) FROM public.users a
Group BY phone_number Having count(*)>1;

SELECT a.phone_number,count(*) FROM public.retailers a
Group BY phone_number Having count(*)>1;

select a.phone_number from users a inner join users b
on a.id <> b.id and a.phone_number = b.phone_number order by a.id;


select a.phone_number from retailers a inner join retailers b
on a.id <> b.id and a.phone_number = b.phone_number order by a.id
DELETE FROM
    users a
        USING users b
WHERE
    a.id > b.id
    AND a.phone_number = b.phone_number;
    
DELETE FROM
    retailers a
        USING retailers b
WHERE
    a.id > b.id
    AND a.phone_number = b.phone_number;
    
CREATE UNIQUE INDEX CONCURRENTLY users_phone_number 
ON users (phone_number);

To Verify:

insert into users(name,phone_number,created_at,updated_at) select name,phone_number,created_at,updated_at from users

2 Comments

This question is really old, please pay attention to newer ones. I think you wanted to paste this to other post, because it is OT.
If you think that this answer is related to the given question, please add some explanation to it such that others can learn from it

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.