Updating a PostgreSQL table with a unique constraint - postgresql

I have a table with a unique constraint
CREATE temp TABLE tmp (
c1 int,
c2 text,
c3 int,
UNIQUE (c2, c3)
);
insert into tmp (c1, c2, c3)
values (1, 'a', 2),
(2, 'a', 1)
Is it possible to run the update below without getting a unique contraint violation and without modifying table tmp?
update tmp
set c3 = c1
where 1=1
ERROR: duplicate key value violates unique constraint "tmp_c2_c3_key". Detail: Key (c2, c3)=(a, 1) already exists.

You can do 2 updates, one that will change your c3 values with other value than then previous ones like
UPDATE tmp
SET c3 = -c1
WHERE 1 = 1;
and then update your c3 value with the good one
UPDATE tmp
SET c3 = c1
WHERE 1 = 1;
It will work if all your values in c1 and c3 are positive

Is it possible to run the update below without getting a unique contraint violation and without modifying table tmp?
No, this is not possible. You would have to make the UNIQUE constraint DEFERRABLE:
...
UNIQUE (c2, c3) DEFERRABLE
...
Then you could set it to deferred like this
SET CONSTRAINTS tmp_c2_c3_key DEFERRED;
and run your update command without a violation error. Note that you must use a transaction for SET CONSTRAINTS to work.

Drop the unique constraint, do your update, reinstate the constraint.

Related

postgresql, deferred constraint not working as I expect

I'm struggling to understand the way DEFERRED constraints work in postgres (v13 if that matters).
I have the following schema and data:
CREATE TEMP TABLE t1 (
id int,
CONSTRAINT unique_id PRIMARY KEY (id)
);
CREATE TEMP TABLE t2 (
id int,
ref int,
CONSTRAINT fk FOREIGN KEY (ref) REFERENCES t1 (id) ON DELETE SET NULL DEFERRABLE INITIALLY IMMEDIATE
);
INSERT INTO t1 VALUES (1);
INSERT INTO t2 (id,ref) VALUES (1,1);
Then I execute the instructions below:
BEGIN;
SET CONSTRAINTS ALL DEFERRED;
DELETE FROM t1;
INSERT INTO t1 VALUES (1);
COMMIT;
Because constraints are deferred, I would expect the ON DELETE SET NULL to trigger at the end of the transaction and preserve the link from t1 to t2. Yet it does not.
SELECT * FROM t2;
id | ref
----+-----
1 |
(1 row)
What am I missing here ?
DEFERRABLE only means that the check if no row violates the constraint is postponed until the transaction is committed. It does not mean that the effect of the DML that you run, is postponed until the end of the transaction.
In your example at the end of the transaction no row in t2 violates the foreign key constraint. So the COMMIT is successful.
Additionally: the DELETE FROM t1 will set all ref columns to NULL in the table t2 that contained the deleted IDs. If you then insert a new row into t1, how should Postgres know which of the (possibly millions) of ref columns that are NULL to reset to the initial value?

how to update and then insert a record with unique key constraint

i have a table with columns a,b,c and c having the unique key constraint.
a |b |c(uk)
----------
aa 1 z
I want to insert a new row with values (bb,1,z), if z already exists then i first want to update the existing row to
a |b |c(uk)
----------
aa 1 null
and then insert (bb,1,z), so that the final rows looks as shown below.
a |b |c(uk)
----------
aa 1 null
bb 1 z
how can i do this a single sql statement?
Unfortunately unique constraints are checked per row in Postgres - unless they are created as "deferrable".
So if you want to do the UPDATE and the INSERT in a single statement, you will have to re-create your unique constraint with the attribute deferrable. Then Postgres will check it at the end of the statement.
create table the_table
(
a text,
b int,
c text
);
alter table the_table
add constraint unique_c
unique(c)
deferrable --<< this is the "magic"
;
Then you can do it with a data modifying CTE
with input (a,b,c) as (
values ('bb', 1, 'z')
), change_old as (
update the_table
set c = null
from input
where input.c = the_table.c
)
insert into the_table
select a,b,c
from input;
I think you should use BEFORE INSERT TRIGGER for this.
In the trigger you would update all the records containing value new.c like:
UPDATE my_table SET
c = NULL
WHERE
c = new.c;
If c is not present - nothing will happen.
You may still run into unique constraint violation error if 2 concurent transactions inserts the same value into c. To avoid that you can use advisory locks providing lock ID from value of c.

How to ignore/skip duplicate (index) on update

I´ve the following table with two columns. The two columns are the combined index key of the table:
ID1 ID2
A X
A Y
A Z
B Z
Now I need to do the following:
UPDATE table SET ID2=X WHERE ID2=Z
As the table has a combined key (ID1 + ID2) this leads to a "duplicate key value violates unique constraint" exception cause there is already an "A-X" combination and the update for row "A-Z" violates that key.
So my question is:
Is there in Postgres an option to skip on duplicate key? Or do you have any other hint for me?
Final state of the table should be:
ID1 ID2
A X
A Y
B X
One solution I can think of, is to define the primary key as deferrable and then do the update and delete in a single statement:
create table t1 (
id1 text,
id2 text,
primary key (id1, id2) deferrable
);
Then you can do the following:
with changed as (
UPDATE t1
SET ID2='X'
WHERE ID2='Z'
returning id1, id2
)
delete from t1
where (id1, id2) in (select id1, id2 from changed)
The DELETE does not see the new row that has been created by the update, but it sees the "old" one, so only the conflicting one(s) are deleted.
Online example
I realize that under normal conditions we prefer a single statement, but at times 2 may be better. This may very well be one of those. In order to implement the solution by #a_horse_with_no_name it will be necessary to drop and recreate the PK. Not a desirable move. So a 2 statement solution which does not require playing around with the PK.
do $$
begin
delete
from tbl t1
where t1.id2 = 'Z'
and exists (
select null
from tbl t2
where t1.id1 = t2.id1
and t2.id2 = 'X'
);
update tbl
set id2 = 'X'
where id2 = 'Z';
end;
$$;
The advantage of the DO block being that if for some reason the second statement fails, the action of the first is rolled back. So the database remains in a consistent state.

Locking during Insert into Partitioned table postgres

Is constraint exclusion possible during inserts?
create table parent(id int)
create table c1 ( constraint c1_constarint CHECK (id = 1)) inherits parent
create table c2 ( constraint c2_constarint CHECK (id = 2)) inherits parent
create table c3 ( constraint c3_constarint CHECK (id = 3)) inherits parent
when doing
insert into c1 values(1) it is acquiring RowExclusive locks on all children(c2, c3)
Is there a way can we do constraint exclusion during insert? Or any hints available to perform this action

Foreign key constraints involving multiple tables

I have the following scenario in a Postgres 9.3 database:
Tables B and C reference Table A.
Table C has an optional field that references table B.
I would like to ensure that for each row of table C that references table B, c.b.a = c.a. That is, if C has a reference to B, both rows should point at the same row in table A.
I could refactor table C so that if c.b is specified, c.a is null but that would make queries joining tables A and C awkward.
I might also be able to make table B's primary key include its reference to table A and then make table C's foreign key to table B include table C's reference to table A but I think this adjustment would be too awkward to justify the benefit.
I think this can be done with a trigger that runs before insert/update on table C and rejects operations that violate the specified constraint.
Is there a better way to enforce data integrity in this situation?
There is a very simple, bullet-proof solution. Works for Postgres 9.3 - when the original question was asked. Works for the current Postgres 13 - when the question in the bounty was added:
Would like information on if this is possible to achieve without database triggers
FOREIGN KEY constraints can span multiple columns. Just include the ID of table A in the FK constraint from table C to table B. This enforces that linked rows in B and C always point to the same row in A. Like:
CREATE TABLE a (
a_id int PRIMARY KEY
);
CREATE TABLE b (
b_id int PRIMARY KEY
, a_id int NOT NULL REFERENCES a
, UNIQUE (a_id, b_id) -- redundant, but required for FK
);
CREATE TABLE c (
c_id int PRIMARY KEY
, a_id int NOT NULL REFERENCES a
, b_id int
, CONSTRAINT fk_simple_and_safe_solution
FOREIGN KEY (a_id, b_id) REFERENCES b(a_id, b_id) -- THIS !
);
Minimal sample data:
INSERT INTO a(a_id) VALUES
(1)
, (2);
INSERT INTO b(b_id, a_id) VALUES
(1, 1)
, (2, 2);
INSERT INTO c(c_id, a_id, b_id) VALUES
(1, 1, NULL) -- allowed
, (2, 2, 2); -- allowed
Disallowed as requested:
INSERT INTO c(c_id, a_id, b_id) VALUES (3,2,1);
ERROR: insert or update on table "c" violates foreign key constraint "fk_simple_and_safe_solution"
DETAIL: Key (a_id, b_id)=(2, 1) is not present in table "b".
db<>fiddle here
The default MATCH SIMPLE behavior of FK constraints works like this (quoting the manual):
MATCH SIMPLE allows any of the foreign key columns to be null; if any of them are null, the row is not required to have a match in the referenced table.
So NULL values in c(b_id) are still allowed (as requested: "optional field"). The FK constraint is "disabled" for this special case.
We need the logically redundant UNIQUE constraint on b(a_id, b_id) to allow the FK reference to it. But by making it out to be on (a_id, b_id) instead of (b_id, a_id), it is also useful in its own right, providing a useful index on b(a_id) to support the other FK constraint, among other things. See:
Is a composite index also good for queries on the first field?
(An additional index on c(a_id) is typically useful accordingly.)
Further reading:
Differences between MATCH FULL, MATCH SIMPLE, and MATCH PARTIAL?
Enforcing constraints “two tables away”
I ended up creating a trigger as follows:
create function "check C.A = C.B.A"()
returns trigger
as $$
begin
if NEW.b is not null then
if NEW.a != (select a from B where id = NEW.b) then
raise exception 'a != b.a';
end if;
end if;
return NEW;
end;
$$
language plpgsql;
create trigger "ensure C.A = C.B.A"
before insert or update on C
for each row
execute procedure "check C.A = C.B.A"();
Would like information on if this is possible to achieve without database triggers
Yes, it is possible. The mechanism is called ASSERTION and it is defined in SQL-92 Standard(though it is not implemented by any major RDBMS).
In short it allows to create multiple-row constraints or multi-table check constraints.
As for PostgreSQL it could be emulated by using view with WITH CHECK OPTION and performing operation on view instead of base table.
WITH CHECK OPTION
This option controls the behavior of automatically updatable views. When this option is specified, INSERT and UPDATE commands on the view will be checked to ensure that new rows satisfy the view-defining condition (that is, the new rows are checked to ensure that they are visible through the view). If they are not, the update will be rejected.
Example:
CREATE TABLE a(id INT PRIMARY KEY, cola VARCHAR(10));
CREATE TABLE b(id INT PRIMARY KEY, colb VARCHAR(10), a_id INT REFERENCES a(id) NOT NULL);
CREATE TABLE c(id INT PRIMARY KEY, colc VARCHAR(10),
a_id INT REFERENCES a(id) NOT NULL,
b_id INT REFERENCES b(id));
Sample inserts:
INSERT INTO a(id, cola) VALUES (1, 'A');
INSERT INTO a(id, cola) VALUES (2, 'A2');
INSERT INTO b(id, colb, a_id) VALUES (12, 'B', 1);
INSERT INTO c(id, colc, a_id) VALUES (15, 'C', 2);
Violating the condition(connecting C with B different a_id on both tables)
UPDATE c SET b_id = 12 WHERE id = 15;;
-- no issues whatsover
Creating view:
CREATE VIEW view_c
AS
SELECT *
FROM c
WHERE NOT EXISTS(SELECT 1
FROM b
WHERE c.b_id = b.id
AND c.a_id != b.a_id) -- here is the clue, we want a_id to be the same
WITH CHECK OPTION ;
Trying update second time(error):
UPDATE view_c SET b_id = 12 WHERE id = 15;
--ERROR: new row violates check option for view "view_c"
--DETAIL: Failing row contains (15, C, 2, 12).
Trying brand new inserts with incorrect data(also errors)
INSERT INTO b(id, colb, a_id) VALUES (20, 'B2', 2);
INSERT INTO view_c(id, colc, a_id, b_id) VALUES (30, 'C2', 1, 20);
--ERROR: new row violates check option for view "view_c"
--DETAIL: Failing row contains (30, C2, 1, 20)
db<>fiddle demo