Campo de incremento com restrição não nula e única no PostgreSQL 8.3
-
25-09-2019 - |
Pergunta
Eu tenho uma tabela "itens" com uma coluna "posição". A posição tem uma restrição única e não nula. Para inserir uma nova linha na posição x, primeiro tente incrementar as posições dos itens subsequentes:
UPDATE items SET position = position + 1 WHERE position >= x;
Isso resulta em uma violação de restrição única:
ERROR: duplicate key value violates unique constraint
O problema parece ser a ordem em que o PostgreSQL executa as atualizações. Restrições exclusivas no PostgreSQL <9,0 não são adiadas e, infelizmente, o uso de 9.0 atualmente não é uma opção. Além disso, a declaração de atualização não suporta um pedido por cláusula e o seguinte também não funciona (ainda duplica a violação -chave):
UPDATE items SET position = position + 1 WHERE id IN (
SELECT id FROM items WHERE position >= x ORDER BY position DESC)
Alguém conhece uma solução que não envolve iteração de todos os itens no código?
Solução
Outra tabela, com vários índices exclusivos:
create table utest(id integer, position integer not null, unique(id, position));
test=# \d utest
Table "public.utest"
Column | Type | Modifiers
----------+---------+-----------
id | integer |
position | integer | not null
Indexes:
"utest_id_key" UNIQUE, btree (id, "position")
Alguns dados:
insert into utest(id, position) select generate_series(1,3), 1;
insert into utest(id, position) select generate_series(1,3), 2;
insert into utest(id, position) select generate_series(1,3), 3;
test=# select * from utest order by id, position;
id | position
----+----------
1 | 1
1 | 2
1 | 3
2 | 1
2 | 2
2 | 3
3 | 1
3 | 2
3 | 3
(9 rows)
Criei um procedimento que atualize os valores de posição na ordem adequada:
create or replace function update_positions(i integer, p integer)
returns void as $$
declare
temprec record;
begin
for temprec in
select *
from utest u
where id = i and position >= p
order by position desc
loop
raise notice 'Id = [%], Moving % to %',
i,
temprec.position,
temprec.position+1;
update utest
set position = position+1
where position=temprec.position and id = i;
end loop;
end;
$$ language plpgsql;
Alguns testes:
test=# select * from update_positions(1, 2);
NOTICE: Id = [1], Moving 3 to 4
NOTICE: Id = [1], Moving 2 to 3
update_positions
------------------
(1 row)
test=# select * from utest order by id, position;
id | position
----+----------
1 | 1
1 | 3
1 | 4
2 | 1
2 | 2
2 | 3
3 | 1
3 | 2
3 | 3
(9 rows)
Espero que ajude.
Outras dicas
Como o PostgreSQL suporta um conjunto completo de DDL transacional, você pode facilmente fazer algo assim:
create table utest(id integer unique not null);
insert into utest(id) select generate_series(1,4);
A tabela parece agora:
test=# \d utest
Table "public.utest"
Column | Type | Modifiers
--------+---------+-----------
id | integer | not null
Indexes:
"utest_id_key" UNIQUE, btree (id)
test=# select * from utest;
id
----
1
2
3
4
(4 rows)
E agora toda a mágica:
begin;
alter table utest drop constraint utest_id_key;
update utest set id = id + 1;
alter table utest add constraint utest_id_key unique(id);
commit;
Depois disso, temos:
test=# \d utest
Table "public.utest"
Column | Type | Modifiers
--------+---------+-----------
id | integer | not null
Indexes:
"utest_id_key" UNIQUE, btree (id)
test=# select * from utest;
id
----
2
3
4
5
(4 rows)
Esta solução tem uma desvantagem: precisa travar a tabela inteira, mas talvez isso não seja um problema aqui.
A solução 'correcter' pode ser fazer a restrição DEFERRABLE
ALTER TABLE channels ADD CONSTRAINT
channels_position_unique unique("position")
DEFERRABLE INITIALLY IMMEDIATE
e, em seguida, defina essa restrição para adiar ao incrementar e defini -la de volta ao imediato assim que terminar.
SET CONSTRAINTS channels_position_unique DEFERRED;
UPDATE channels SET position = position+1
WHERE position BETWEEN 1 AND 10;
SET CONSTRAINTS channels_position_unique IMMEDIATE;
Variante sem alterar a tabela e a restrição de gota:
UPDATE items t1
SET position = t2.position + 1
FROM (SELECT position
FROM items
ORDER BY position DESC) t2
WHERE t2.position >= x AND t1.position = t2.position
Exemplo online: http://rextester.com/fau54991