Compactando una secuencia en PostgreSQL

9

Tengo una id serial PRIMARY KEYcolumna en una tabla PostgreSQL. idFaltan muchos s porque he eliminado la fila correspondiente.

Ahora quiero "compactar" la tabla reiniciando la secuencia y reasignando la ids de tal manera que idse conserve el orden original . ¿Es posible?

Ejemplo:

  • Ahora:

 id | data  
----+-------
  1 | hello
  2 | world
  4 | foo
  5 | bar
  • Después:

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

Intenté lo que se sugirió en una respuesta de StackOverflow , pero no funcionó:

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

Respuestas:

9

En primer lugar, se deben esperar huecos en una secuencia. Pregúntese si realmente necesita eliminarlos. Tu vida se vuelve más simple si solo vives con ella. Para obtener números sin espacios, la alternativa (a menudo mejor) es usar un VIEWcon row_number(). Ejemplo en esta respuesta relacionada:

Aquí hay algunas recetas para eliminar huecos.

1. Nueva mesa prístina

Evita complicaciones con infracciones únicas e hinchazón de la mesa y es rápido . Solo para casos simples en los que no está vinculado por referencias FK, vistas en la tabla u otros objetos dependientes, o por acceso concurrente. Hazlo en una transacción para evitar accidentes:

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 la estructura incl. restricciones y valores predeterminados de la tabla original. Luego, haga que la nueva columna de la tabla posea la secuencia:

Y restablecerlo al nuevo máximo:

Esto conlleva la ventaja de que la nueva tabla está libre de hinchazón y agrupada id.

2. UPDATEen su lugar

Esto produce muchas filas muertas y requiere (auto) VACUUMmás tarde.

Si la serialcolumna también es PRIMARY KEY(como en su caso) o tiene una UNIQUErestricción, debe evitar infracciones únicas en el proceso. El valor predeterminado (más barato) para las restricciones PK / UNIQUE es ser NOT DEFERRABLE, lo que obliga a una comprobación después de cada fila individual. Todos los detalles bajo esta pregunta relacionada sobre SO:

Puede definir su restricción como DEFERRABLE(lo que lo hace más costoso).
O puede soltar la restricción y volver a agregarla cuando haya terminado:

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;

Tampoco es posible mientras tengaFOREIGN KEYrestriccionesquehagan referencia a la (s) columna (s) porque ( según la documentación ):

Las columnas a las que se hace referencia deben ser las columnas de una restricción de clave primaria o única no diferible en la tabla de referencia.

Debería (bloquear todas las tablas involucradas y) soltar / recrear restricciones FK y actualizar todos los valores FK manualmente (ver opción 3. ). O tiene que mover valores fuera del camino con un segundo UPDATEpara evitar conflictos. Por ejemplo, suponiendo que no tiene 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;

Inconvenientes como se mencionó anteriormente.

3. Mesa Temp TRUNCATE,INSERT

Una opción más si tienes mucha RAM. Esto combina algunas de las ventajas de las dos primeras formas. Casi tan rápido como la opción 1. y obtienes una tabla nueva e impecable sin hinchazón, pero mantienes todas las restricciones y dependencias como en la opción 2.
Sin embargo , según la documentación:

TRUNCATE no se puede usar en una tabla que tenga referencias de clave externa de otras tablas, a menos que todas esas tablas también se trunquen en el mismo comando. Verificar la validez en tales casos requeriría escaneos de tabla, y el punto no es hacer uno.

El énfasis audaz es mío.

Puede eliminar las restricciones FK temporalmente y usar CTE modificadores de datos para actualizar todas las columnas 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, con más detalles:

Erwin Brandstetter
fuente
Si todo FOREIGN KEYSestá configurado para CASCADE, ¿no podría simplemente recorrer las claves primarias antiguas y actualizar sus valores en el lugar (del valor antiguo al nuevo)? Esencialmente, esta es la opción 3 sin TRUNCATE tbl, reemplazando INSERTpor una UPDATE, y sin necesidad de actualizar las claves externas manualmente.
Gili
@Gili: Podría , pero ese tipo de bucle es extremadamente costoso. Como no puede actualizar toda la tabla a la vez debido a infracciones de clave únicas en el índice, necesita un UPDATEcomando separado para cada fila. Vea la explicación en ② o intente y vea por usted mismo.
Erwin Brandstetter
No creo que el rendimiento sea un problema en mi caso. A mi modo de ver, hay dos tipos de algoritmos: los que "detienen el mundo" y los que se ejecutan silenciosamente en segundo plano sin tener que apagar el servidor. Suponiendo que la compactación solo ocurre una vez en una luna azul (por ejemplo, cuando se acerca al límite superior de un tipo de datos), en realidad no hay un límite superior en la cantidad de tiempo que debería tomar. Mientras estemos compactando registros más rápido que los nuevos que se agregan, deberíamos estar bien.
Gili