Agregar 'serial' a la columna existente en Postgres

91

Tengo una pequeña tabla (~ 30 filas) en mi base de datos de Postgres 9.0 con un campo de ID de número entero (la clave principal) que actualmente contiene enteros secuenciales únicos que comienzan en 1, pero que no se creó con la palabra clave 'serial'.

¿Cómo puedo alterar esta tabla para que de ahora en adelante las inserciones en esta tabla hagan que este campo se comporte como si se hubiera creado con 'serial' como tipo?

nicolaskruchten
fuente
5
Para su información, el SERIALpseudo-tipo ahora es heredado , reemplazado por la nueva GENERATED … AS IDENTITYcaracterística definida en SQL: 2003 , en Postgres 10 y posteriores. Ver explicación .
Basil Bourque
Para la versión moderna de Postgres (> = 10), vea esta pregunta: stackoverflow.com/questions/2944499
a_horse_with_no_name

Respuestas:

132

Observe los siguientes comandos (especialmente el bloque 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
fuente
Dado que está mencionando claves primarias en su OP, es posible que también desee hacerlo ALTER TABLE foo ADD PRIMARY KEY (a).
Skippy le Grand Gourou
SERIAL es azúcar sintáctico y no se almacena en los metadatos de la base de datos, por lo que el código anterior sería 100% equivalente.
DKroot
Si existe la posibilidad de que la tabla de destino haya sido creada por un usuario diferente, deberá hacerlo ALTER TABLE foo OWNER TO current_user;primero.
DKroot
2
¿No deberías MAX(a)+1ponerte en setval? SELECT MAX(a)+1 FROM foo; SELECT setval('foo_a_seq', 6);
SunnyPro
48

También puede usar START WITHpara comenzar una secuencia desde un punto en particular, aunque setval logra lo mismo, como en la respuesta de Euler, por ejemplo,

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
fuente
28

TL; DR

Aquí hay una versión en la que no necesita un ser humano para leer un valor y escribirlo ellos mismos.

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'); 

Otra opción sería emplear el Functioncompartido reutilizable al final de esta respuesta.


Una solución no interactiva

Solo agregando a las otras dos respuestas, para aquellos de nosotros que necesitamos que estos mensajes de correo electrónico sean Sequencecreados por un script no interactivo , mientras parcheamos una base de datos en vivo, por ejemplo.

Es decir, cuando no desea SELECTel valor manualmente y lo escribe usted mismo en una CREATEdeclaración posterior .

En resumen, se puede no hacer:

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

... ya que la START [WITH]cláusula en CREATE SEQUENCEespera un valor , no una subconsulta.

Nota: Como regla general, que se aplica a todos los no-CRUD ( es decir : que no sea nada INSERT, SELECT, UPDATE, DELETEdeclaraciones en) pgSQL yo sepa.

¡Sin embargo, lo setval()hace! Por lo tanto, lo siguiente está absolutamente bien:

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

Si no hay datos y usted no (quiere) saberlo, use coalesce()para establecer el valor predeterminado:

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

Sin embargo, tener el valor de secuencia actual establecido en 0es torpe, si no ilegal.
Usar la forma de tres parámetros de setvalsería más apropiado:

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

Establecer el tercer parámetro opcional de setvalto falseevitará que el siguiente nextvalavance en la secuencia antes de devolver un valor, y así:

el siguiente nextvaldevolverá exactamente el valor especificado, y el avance de la secuencia comienza con lo siguiente nextval.

- de esta entrada en la documentación

En una nota no relacionada, también puede especificar la columna a la que pertenece Sequencedirectamente CREATE, no tiene que modificarla más tarde:

CREATE SEQUENCE foo_a_seq OWNED BY foo.a;

En resumen:

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 un Function

Alternativamente, si planea hacer esto para varias columnas, puede optar por usar un archivo 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;

Úselo así:

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
fuente
Excelente respuesta, pero tenga en cuenta coalesce(max(a), 0))que no funcionará la mayor parte del tiempo, ya que los identificadores generalmente comienzan desde 1. La forma más correcta seríacoalesce(max(a), 1))
Amiko
1
¡Gracias @Amiko por el comentario! En setvalrealidad, la función solo establece el "último valor utilizado" actual para la secuencia. ¡El siguiente valor disponible (el primero que se utilizará realmente) será uno más! El uso setval(..., coalesce(max(a), 1))en una columna vacía lo configuraría para "comenzar" con 2(el siguiente valor disponible), como se ilustra en la documentación .
ccjmne
1
@Amiko Sin embargo, tienes razón al decir que hay un problema en mi código: currvalnunca debería ser así 0, incluso si no se reflejaría en el conjunto de datos real. Usando la forma de tres parámetros de setvalsería más apropiado: setval(..., coalesce(max(a), 0) + 1, false). ¡Respuesta actualizada en consecuencia!
ccjmne
1
De acuerdo, me lo perdí por completo. Gracias por la respuesta me salvó el tiempo.
Amiko