Adicionando 'serial' à coluna existente no Postgres

91

Eu tenho uma pequena tabela (~ 30 linhas) em meu banco de dados Postgres 9.0 com um campo de ID de inteiro (a chave primária) que atualmente contém inteiros sequenciais únicos começando em 1, mas que não foi criado usando a palavra-chave 'serial'.

Como posso alterar esta tabela de modo que a partir de agora inserções nesta tabela façam com que este campo se comporte como se tivesse sido criado com 'serial' como um tipo?

nicolaskruchten
fonte
5
FYI, o SERIALpseudo-tipo agora é legado , suplantado pelo novo GENERATED … AS IDENTITYrecurso definido no SQL: 2003 , no Postgres 10 e posterior. Veja a explicação .
Basil Bourque
Para a versão moderna do Postgres (> = 10), consulte esta pergunta: stackoverflow.com/questions/2944499
a_horse_with_no_name

Respostas:

132

Observe os seguintes comandos (especialmente o bloco comentado).

DROP TABLE foo;
DROP TABLE bar;

CREATE TABLE foo (a int, b text);
CREATE TABLE bar (a serial, b text);

INSERT INTO foo (a, b) SELECT i, 'foo ' || i::text FROM generate_series(1, 5) i;
INSERT INTO bar (b) SELECT 'bar ' || i::text FROM generate_series(1, 5) i;

-- blocks of commands to turn foo into bar
CREATE SEQUENCE foo_a_seq;
ALTER TABLE foo ALTER COLUMN a SET DEFAULT nextval('foo_a_seq');
ALTER TABLE foo ALTER COLUMN a SET NOT NULL;
ALTER SEQUENCE foo_a_seq OWNED BY foo.a;    -- 8.2 or later

SELECT MAX(a) FROM foo;
SELECT setval('foo_a_seq', 5);  -- replace 5 by SELECT MAX result

INSERT INTO foo (b) VALUES('teste');
INSERT INTO bar (b) VALUES('teste');

SELECT * FROM foo;
SELECT * FROM bar;
Euler Taveira
fonte
Já que você está mencionando chaves primárias em seu OP, você também pode querer ALTER TABLE foo ADD PRIMARY KEY (a).
Skippy le Grand Gourou de
SERIAL é um açúcar sintático e não é armazenado nos metadados do banco de dados, então o código acima seria 100% equivalente.
DKroot
Se houver uma chance de que a tabela de destino foi criada por um usuário diferente, você precisará fazer ALTER TABLE foo OWNER TO current_user;primeiro.
DKroot
2
Você não deveria estar definindo MAX(a)+1em setval? SELECT MAX(a)+1 FROM foo; SELECT setval('foo_a_seq', 6);
SunnyPro
48

Você também pode usar START WITHpara iniciar uma sequência a partir de um ponto específico, embora setval realize a mesma coisa, como na resposta de Euler, por exemplo,

SELECT MAX(a) + 1 FROM foo;
CREATE SEQUENCE foo_a_seq START WITH 12345; -- replace 12345 with max above
ALTER TABLE foo ALTER COLUMN a SET DEFAULT nextval('foo_a_seq');
John Powell
fonte
28

TL; DR

Aqui está uma versão em que você não precisa de um ser humano para ler um valor e digitá-lo por conta própria.

CREATE SEQUENCE foo_a_seq OWNED BY foo.a;
SELECT setval('foo_a_seq', coalesce(max(a), 0) + 1, false) FROM foo;
ALTER TABLE foo ALTER COLUMN a SET DEFAULT nextval('foo_a_seq'); 

Outra opção seria empregar o reaproveitável Functioncompartilhado ao final desta resposta.


Uma solução não interativa

Apenas adicionando as outras duas respostas, para aqueles de nós que precisam ter esses Sequences criados por um script não interativo , enquanto corrige um banco de dados live-ish, por exemplo.

Isto é, quando você não quer SELECTo valor manualmente e o digita em uma CREATEinstrução subsequente .

Resumindo, você não pode fazer:

CREATE SEQUENCE foo_a_seq
    START WITH ( SELECT max(a) + 1 FROM foo );

... já que a START [WITH]cláusula em CREATE SEQUENCEespera um valor , não uma subconsulta.

Nota: Como regra geral, que se aplica a todos os não-CRUD ( ou seja : nada além de INSERT, SELECT, UPDATE, DELETE) declarações em pgSQL AFAIK.

No entanto, setval()sim! Portanto, o seguinte está absolutamente correto:

SELECT setval('foo_a_seq', max(a)) FROM foo;

Se não houver dados e você não (quiser) saber sobre eles, use coalesce()para definir o valor padrão:

SELECT setval('foo_a_seq', coalesce(max(a), 0)) FROM foo;
--                         ^      ^         ^
--                       defaults to:       0

No entanto, ter o valor da sequência atual definido como 0é desajeitado, se não ilegal.
Usar a forma de três parâmetros de setvalseria mais apropriado:

--                                             vvv
SELECT setval('foo_a_seq', coalesce(max(a), 0) + 1, false) FROM foo;
--                                                  ^   ^
--                                                is_called

Definir o terceiro parâmetro opcional de setvalpara falseevitará que o próximo nextvalavance na sequência antes de retornar um valor e, portanto:

o próximo nextvalretornará exatamente o valor especificado e o avanço da sequência começará com o seguinte nextval.

- desta entrada na documentação

Em uma nota não relacionada, você também pode especificar a coluna proprietária de Sequencediretamente com CREATE, você não precisa alterá-la mais tarde:

CREATE SEQUENCE foo_a_seq OWNED BY foo.a;

Em suma:

CREATE SEQUENCE foo_a_seq OWNED BY foo.a;
SELECT setval('foo_a_seq', coalesce(max(a), 0) + 1, false) FROM foo;
ALTER TABLE foo ALTER COLUMN a SET DEFAULT nextval('foo_a_seq'); 

Usando um Function

Como alternativa, se você planeja fazer isso para várias colunas, pode optar por usar um real Function.

CREATE OR REPLACE FUNCTION make_into_serial(table_name TEXT, column_name TEXT) RETURNS INTEGER AS $$
DECLARE
    start_with INTEGER;
    sequence_name TEXT;
BEGIN
    sequence_name := table_name || '_' || column_name || '_seq';
    EXECUTE 'SELECT coalesce(max(' || column_name || '), 0) + 1 FROM ' || table_name
            INTO start_with;
    EXECUTE 'CREATE SEQUENCE ' || sequence_name ||
            ' START WITH ' || start_with ||
            ' OWNED BY ' || table_name || '.' || column_name;
    EXECUTE 'ALTER TABLE ' || table_name || ' ALTER COLUMN ' || column_name ||
            ' SET DEFAULT nextVal(''' || sequence_name || ''')';
    RETURN start_with;
END;
$$ LANGUAGE plpgsql VOLATILE;

Use-o assim:

INSERT INTO foo (data) VALUES ('asdf');
-- ERROR: null value in column "a" violates not-null constraint

SELECT make_into_serial('foo', 'a');
INSERT INTO foo (data) VALUES ('asdf');
-- OK: 1 row(s) affected
ccjmne
fonte
Ótima resposta, mas tenha em mente coalesce(max(a), 0))que não funcionará na maioria das vezes, já que os IDs geralmente começam em 1. A maneira mais correta seriacoalesce(max(a), 1))
Amiko
1
Obrigado @Amiko pelo comentário! A setvalfunção, na verdade, define apenas o "último valor usado" atual para a sequência. O próximo valor disponível (o primeiro a ser realmente usado) será mais um! Usar setval(..., coalesce(max(a), 1))em uma coluna vazia configuraria para "começar" com 2(o próximo valor disponível), conforme ilustrado na documentação .
ccjmne
1
@Amiko Você está certo ao dizer que há um problema no meu código: isso currvalnunca deveria haver 0, mesmo que não fosse refletido no conjunto de dados real. Usando o formulário três parâmetros de setvalseria mais apropriado: setval(..., coalesce(max(a), 0) + 1, false). Resposta atualizada em conformidade!
ccjmne
1
Concordo, eu perdi totalmente isso. Obrigado pela resposta economizou meu tempo.
Amiko