Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Atomic multi-row update with a unique constraint

I have a table of labels that are displayed in a ranked order. To ensure that no two rows can have the same rank, their values are unique:

create table label (
  id_label serial not null,
  rank integer not null,
  title text not null,
  constraint pri primary key (id_label),
  constraint unq unique (rank)
)

Doesn't matter if it's PostgreSQL or MySQL, they exhibit the same behaviour. A query might look like select title from label order by rank. Assume the table contains:

id_label rank title
1        10   Cow
2        20   Apple
3        45   Horse
4        60   Beer

Now suppose I want to reorder two labels, e.g. have Apple ranked before Cow. The easiest way is to swap their rank values:

update label
set rank = case when rank = 20 then 10 else 20 end
where id_label in (1,2)

Nope. Nor:

update label
set rank = case when rank = 20 then rank - 10 else rank + 10 end
where id_label in (1,2)

Nor even:

update label
set rank = 30 - rank
where id_label in (1,2)

Each time, the unique constraint fires on the first row update and aborts the operation. If I could defer the check until the end of the statement I would be fine. This happens on both PostgreSQL and MySQL.

An ACID-safe workaround is to:

  1. begin transaction
  2. select ranks of first, second record, and highest (max) rank in table (which offhand will probably require a union)
  3. update first record to rank = max + 1
  4. update second record to rank of first
  5. update first record to rank of second
  6. commit

That's just unspeakably ugly. Worse is to drop the constraint, update, and then recreate the constraint. Granting such privileges to an operational role is asking for trouble. So my question is this: is there a simple technique I have overlooked that solves this problem, or am I SOL?

like image 512
dland Avatar asked Mar 23 '11 09:03

dland


1 Answers

With PostgreSQL this can only be solved in a "nice" way using Version 9.0 because you can define unique constraints to be deferrable there.

With PostgreSQL 9.0 you'd simply do:

create table label (
  id_label serial not null,
  rank integer not null,
  title text not null,
  constraint pri primary key (id_label)
);
alter table label add constraint unique_rank unique (rank) 
      deferrable initially immediate;

Then the update is as simple as this:

begin;
set constraints unique_rank DEFERRED;
update rank
   set rank = case when rank = 20 then 10 else 20 end
   where id_label in (1,2);
commit;

Edit:
If you don't want to bother setting the constraint to deferred inside your transaction, you can simply define the constraint as initially deferred.

like image 128
a_horse_with_no_name Avatar answered Sep 22 '22 11:09

a_horse_with_no_name