Here a usable example for an arbitrary number of columns (might require casting the columns to e.g. bit varying before
makingthe arrays):
Just replace c1,c2 with your (casted) number of columns on index creation.
create function fn(cs anyelement)
returns int
as
'select count(1) from unnest(cs) t(c) where c is not null;'
language sql
immutable;
create unique index idx on t ((
case
when fn(ARRAY[c1,c2])=0 then null
else ARRAY[c1,c2]
end
));
Result:
db=# insert into t(c1,c2) values (null,null);
INSERT 0 1
db=# insert into t(c1,c2) values (null,null);
INSERT 0 1
db=# insert into t(c1,c2) values (null,null);
INSERT 0 1
db=# insert into t(c1,c2) values (1,null);
INSERT 0 1
db=# insert into t(c1,c2) values (1,null);
ERROR: duplicate key value violates unique constraint "idx"
DETAIL: Key ((
CASE
WHEN fn(ARRAY[c1, c2]) = 0 THEN NULL::integer[]
ELSE ARRAY[c1, c2]
END))=({1,NULL}) already exists.
db=# insert into t(c1,c2) values (null,1);
INSERT 0 1
db=# insert into t(c1,c2) values (null,1);
ERROR: duplicate key value violates unique constraint "idx"
DETAIL: Key ((
CASE
WHEN fn(ARRAY[c1, c2]) = 0 THEN NULL::integer[]
ELSE ARRAY[c1, c2]
END))=({NULL,1}) already exists.
db=# insert into t(c1,c2) values (null,2);
INSERT 0 1
db=# insert into t(c1,c2) values (null,2);
ERROR: duplicate key value violates unique constraint "idx"
DETAIL: Key ((
CASE
WHEN fn(ARRAY[c1, c2]) = 0 THEN NULL::integer[]
ELSE ARRAY[c1, c2]
END))=({NULL,2}) already exists.