Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swapping records' values for a column with a UNIQUE constraint in PostgreSQL

Goal:

Swapping values in some table's records for a particular column that has a UNIQUE constraint.


Example:

Considering the following table:

CREATE TABLE records (id numeric, name text);
ALTER TABLE records ADD CONSTRAINT uniq UNIQUE (id);
INSERT INTO records VALUES
  (1, 'First record'),
  (3, 'Second record'),
  (2, 'Third record'),
  (4, 'Fourth record');

We get, from querying for SELECT id, name FROM records ORDER BY id;:

| id |          name |
|----|---------------|
|  1 |  First record |
|  2 |  Third record |
|  3 | Second record |
|  4 | Fourth record |

I need the ids for the Second record and Third record swapped. That is, I want the following output:

| id |          name |
|----|---------------|
|  1 |  First record |
|  2 | Second record | <-- this record previously had the id 3
|  3 |  Third record | <-- this record previously had the id 2
|  4 | Fourth record |

Obviously:

  • I can't just swap the names for these records, my actual database is a tad bigger than this:
    • this table has more information stored and
    • its id column is actually a PRIMARY KEY that is referenced by other tables.
  • I don't wanna go through the hassle of utilizing temporary ids that aren't assigned to any record yet to execute this swapping.
  • The 'swapping' in question does not only involve two records, but an arbitrary long set of them.
  • The records also don't technically get 'swapped' but simply reassigned, with the following properties:
    • each id will always be unique after each bulk reassignment operation and
    • most of the ids from a previous assignment will overlap with those of the new assignment map.

What I've tried:

I have tried swapping my data in a single query by generating a temporary reassignment table and using it in an UPDATE ... SET ... FROM ... WHERE query.

Here's my temporary reassignment table, which will swap the ids 2 and 3:

(VALUES
  (3, 2),
  (2, 3)
) AS swap(id, new_id)

And here's how I use it:

UPDATE records AS record SET
  id = swap.new_id
FROM (VALUES
  (3, 2),
  (2, 3)
) AS swap(id, new_id)
WHERE record.id = swap.id;

SELECT id, name FROM records ORDER BY id;

As you can see in this SQLFiddle link, it works pretty well... until you add the UNIQUE constraint on the id column.


How could I get this working under all the conditions listed above?

like image 434
ccjmne Avatar asked May 27 '16 21:05

ccjmne


People also ask

How do I change the unique column in PostgreSQL?

The syntax for creating a unique constraint using an ALTER TABLE statement in PostgreSQL is: ALTER TABLE table_name ADD CONSTRAINT constraint_name UNIQUE (column1, column2, ... column_n);

What does the unique constraint do in PostgreSQL?

PostgreSQL provides you with the UNIQUE constraint that maintains the uniqueness of the data correctly. When a UNIQUE constraint is in place, every time you insert a new row, it checks if the value is already in the table. It rejects the change and issues an error if the value already exists.

How do I switch columns in PostgreSQL?

Postgres currently defines column order based on the attnum column of the pg_attribute table. The only way to change column order is either by recreating the table, or by adding columns and rotating data until you reach the desired layout.


1 Answers

The issue is with the CONSTRAINT's definition.


PosgreSQL's CONSTRAINTS can be DEFERRABLE. From their documentation, we gather the following:

IMMEDIATE constraints are checked at the end of each statement. DEFERRED constraints are not checked until transaction commit. Each constraint has its own IMMEDIATE or DEFERRED mode.

In the case we're facing, the ids will still be UNIQUEs AFTER the transaction, while indeed that CONSTRAINT cannot be satisfied DURING the reassignment of ids.

Simply making the relevant CONSTRAINT DEFERRABLE will solve the problem at hand.

CREATE TABLE records (id numeric, name text);
ALTER TABLE records ADD CONSTRAINT uniq UNIQUE (id) DEFERRABLE INITIALLY IMMEDIATE;
INSERT INTO records VALUES --                           ^
  (1, 'First record'),     --                          HERE
  (3, 'Second record'),
  (2, 'Third record'),
  (4, 'Fourth record');
like image 131
ccjmne Avatar answered Oct 30 '22 08:10

ccjmne