Compactando uma sequência no PostgreSQL

9

Eu tenho uma id serial PRIMARY KEYcoluna em uma tabela do PostgreSQL. Muitos ids estão ausentes porque eu excluí a linha correspondente.

Agora, quero "compactar" a tabela reiniciando a sequência e reatribuindo os ids de maneira que a idordem original seja preservada. É possível?

Exemplo:

  • Agora:

 id | data  
----+-------
  1 | hello
  2 | world
  4 | foo
  5 | bar
  • Depois de:

 id | data  
----+-------
  1 | hello
  2 | world
  3 | foo
  4 | bar

Tentei o que foi sugerido em uma resposta do StackOverflow , mas não funcionou:

# alter sequence t_id_seq restart;
ALTER SEQUENCE
# update t set id=default;
ERROR:  duplicate key value violates unique constraint t_pkey
DETAIL:  Key (id)=(1) already exists.
rubik
fonte

Respostas:

9

Primeiro, são esperadas lacunas em uma sequência. Pergunte a si mesmo se você realmente precisa removê-los. Sua vida fica mais simples se você apenas viver com ela. Para obter números sem falhas, a alternativa (geralmente melhor) é usar a VIEWcom row_number(). Exemplo nesta resposta relacionada:

Aqui estão algumas receitas para remover as lacunas.

1. Mesa nova e original

Evita complicações com violações únicas e inchaço na mesa e é rápido . Somente para casos simples em que você não está vinculado por referências do FK, visualizações na tabela ou outros objetos dependentes ou pelo acesso simultâneo. Faça isso em uma transação para evitar acidentes:

BEGIN;
LOCK tbl;

CREATE TABLE tbl_new (LIKE tbl INCLUDING ALL);

INSERT INTO tbl_new -- no target list in this case
SELECT row_number() OVER (ORDER BY id), data  -- all columns in default order
FROM   tbl;

ALTER SEQUENCE tbl_id_seq OWNED BY tbl_new.id;  -- make new table own sequence

DROP TABLE tbl;
ALTER TABLE tbl_new RENAME TO tbl;

SELECT setval('tbl_id_seq', max(id)) FROM tbl;  -- reset sequence

COMMIT;

CREATE TABLE tbl_new (LIKE tbl INCLUDING ALL)copia a estrutura incl. restrições e padrões da tabela original. Em seguida, torne a nova coluna da tabela a própria sequência:

E redefina-o para o novo máximo:

Isso traz a vantagem de que a nova tabela está livre de inchaço e agrupada id.

2. UPDATEno lugar

Isso produz muitas linhas mortas e requer (automático) VACUUMposteriormente.

Se a serialcoluna também for PRIMARY KEY(como no seu caso) ou tiver uma UNIQUErestrição, você deverá evitar violações exclusivas no processo. O padrão (mais barato) para as restrições PK / UNIQUE deve ser NOT DEFERRABLE, o que força uma verificação após cada linha. Todos os detalhes nesta pergunta relacionada ao SO:

Você pode definir sua restrição como DEFERRABLE(o que a torna mais cara).
Ou você pode eliminar a restrição e adicioná-la novamente quando terminar:

BEGIN;

LOCK tbl;

ALTER TABLE tbl DROP CONSTRAINT tbl_pkey;  -- remove PK

UPDATE tbl t  -- intermediate unique violations are ignored now
SET    id = t1.new_id
FROM  (SELECT id, row_number() OVER (ORDER BY id) AS new_id FROM tbl) t1
WHERE  t.id = t1.id;

SELECT setval('tbl_id_seq', max(id)) FROM tbl;  -- reset sequence

ALTER TABLE tbl ADD CONSTRAINT tbl_pkey PRIMARY KEY(id); -- add PK back

COMMIT;

Também não é possível enquanto você tiverFOREIGN KEYrestrições referentes à (s) coluna (s) porque ( por documentação ):

As colunas referenciadas devem ser as colunas de uma restrição de chave primária ou única não diferida na tabela referenciada.

Você precisaria (bloquear todas as tabelas envolvidas e) soltar / recriar restrições de FK e atualizar todos os valores de FK manualmente (consulte a opção 3. ). Ou você precisa mover valores fora do caminho com um segundo UPDATEpara evitar conflitos. Por exemplo, supondo que você não tenha números negativos:

BEGIN;
LOCK tbl;

UPDATE tbl SET id = id * -1;  -- avoid conflicts

UPDATE tbl t
SET    id = t1.new_id
FROM  (SELECT id, row_number() OVER (ORDER BY id DESC) AS new_id FROM tbl) t1
WHERE  t.id = t1.id;

SELECT setval('tbl_id_seq', max(id)) FROM tbl;  -- reset sequence

COMMIT;

Desvantagens como mencionado acima.

3. tabela Temp, TRUNCATE,INSERT

Mais uma opção se você tiver bastante RAM. Isso combina algumas das vantagens das duas primeiras maneiras. Quase tão rápido quanto a opção 1. e você obtém uma nova tabela intocada sem inchaço, mas mantém todas as restrições e dependências no lugar, como na opção 2.
No entanto , por documentação:

TRUNCATE não pode ser usado em uma tabela que possui referências de chave estrangeira de outras tabelas, a menos que todas essas tabelas também sejam truncadas no mesmo comando. Verificar a validade nesses casos exigiria varreduras de tabela, e o ponto principal é não fazer uma.

Negrito ênfase minha.

Você pode eliminar as restrições do FK temporariamente e usar CTEs modificadores de dados para atualizar todas as colunas do FK:

SET temp_buffers = 500MB;   -- example value, see 1st link below

BEGIN;

CREATE TEMP TABLE tbl_tmp AS
SELECT row_number() OVER (ORDER BY id) AS new_id, *
FROM   tbl
ORDER  BY id;  -- order here to use index (if one exists)

-- drop FK constraints in other tables referencing this one
-- which takes out an exclusive lock on those tables

TRUNCATE tbl;

INSERT INTO tbl
SELECT new_id, data  -- list all columns in order
FROM tbl_tmp;        -- rely on established order in tbl_tmp
-- ORDER BY id;      -- only to be absolutely sure (not necessary)

--  example for table "fk_tbl" with FK column "fk_id"
UPDATE fk_tbl f
SET    fk_id = t.new_id  -- set to new ID
FROM   tbl_tmp t
WHERE  f.fk_id = t.id;   -- match on old ID

-- add FK constraints in other tables back

COMMIT;

Relacionado, com mais detalhes:

Erwin Brandstetter
fonte
Se tudo FOREIGN KEYSestiver definido como CASCADEvocê não pode simplesmente fazer um loop sobre as chaves primárias antigas e atualizar seus valores no local (do valor antigo para o novo)? Essencialmente, essa é a opção 3 sem TRUNCATE tbl, substituindo INSERTpor UPDATE, e sem a necessidade de atualizar chaves estrangeiras manualmente.
Gili
@ Gili: Você poderia , mas esse tipo de loop é extremamente caro. Como você não pode atualizar a tabela inteira de uma vez devido a violações de chave exclusivas no índice, você precisa de um UPDATEcomando separado para cada linha. Veja a explicação em ② ou tente e veja por si mesmo.
precisa saber é o seguinte
Não acho que o desempenho seja um problema no meu caso. Do meu ponto de vista, existem dois tipos de algoritmos: os que "param o mundo" e os que correm silenciosamente em segundo plano, sem ter que derrubar o servidor. Supondo que a compactação ocorra apenas uma vez na lua azul (por exemplo, ao se aproximar do limite superior de um tipo de dados), não há realmente um limite superior na quantidade de tempo que deve levar. Enquanto compactarmos os registros mais rapidamente do que os novos, vamos ficar bem.
Gili