¿La mejor manera de llenar una nueva columna en una tabla grande?


33

Tenemos una tabla de 2.2 GB en Postgres con 7,801,611 filas. Estamos agregando una columna uuid / guid y me pregunto cuál es la mejor manera de llenar esa columna (ya que queremos agregarle una NOT NULLrestricción).

Si entiendo Postgres correctamente, una actualización es técnicamente una eliminación e inserción, por lo que básicamente se trata de reconstruir toda la tabla de 2.2 gb. También tenemos un esclavo corriendo, así que no queremos que se quede atrás.

¿Hay alguna manera mejor que escribir un script que lo llene lentamente con el tiempo?


2
¿Ya ha ejecutado ALTER TABLE .. ADD COLUMN ...o esa parte también debe ser respondida?
ypercubeᵀᴹ

Todavía no he ejecutado ninguna modificación de la tabla, solo en la etapa de planificación. He hecho esto antes agregando la columna, rellenándola y luego agregando la restricción o el índice. Sin embargo, esta tabla es significativamente más grande y estoy preocupado por la carga, el bloqueo, la replicación, etc.
Collin Peters

Respuestas:


45

Depende mucho de los detalles de sus requisitos.

Si tiene suficiente espacio libre (al menos el 110% de pg_size_pretty((pg_total_relation_size(tbl))) en el disco y puede permitirse un bloqueo compartido durante algún tiempo y un bloqueo exclusivo por un tiempo muy corto , cree una nueva tabla que incluya la uuidcolumna usando CREATE TABLE AS. ¿Por qué?

El siguiente código utiliza una función del uuid-ossmódulo adicional .

  • Bloquee la tabla contra cambios concurrentes en el SHAREmodo (aún permitiendo lecturas concurrentes). Los intentos de escribir en la tabla esperarán y eventualmente fracasarán. Vea abajo.

  • Copie toda la tabla mientras llena la nueva columna sobre la marcha, posiblemente ordenando filas favorablemente mientras está en ella.
    Si va a reordenar filas, asegúrese de establecer lo work_memmás alto posible (solo para su sesión, no a nivel mundial).

  • Luego agregue restricciones, claves foráneas, índices, disparadores, etc. a la nueva tabla. Al actualizar grandes porciones de una tabla, es mucho más rápido crear índices desde cero que agregar filas de forma iterativa.

  • Cuando la nueva tabla esté lista, descarte la antigua y cambie el nombre de la nueva para que sea un reemplazo directo. Solo este último paso adquiere un bloqueo exclusivo en la tabla anterior para el resto de la transacción, que debería ser muy breve ahora.
    También requiere que elimine cualquier objeto según el tipo de tabla (vistas, funciones que utilizan el tipo de tabla en la firma, ...) y que luego los vuelva a crear.

  • Hazlo todo en una transacción para evitar estados incompletos.

BEGIN;
LOCK TABLE tbl IN SHARE MODE;

SET LOCAL work_mem = '???? MB';  -- just for this transaction

CREATE TABLE tbl_new AS 
SELECT uuid_generate_v1() AS tbl_uuid, <list of all columns in order>
FROM   tbl
ORDER  BY ??;  -- optionally order rows favorably while being at it.

ALTER TABLE tbl_new
   ALTER COLUMN tbl_uuid SET NOT NULL
 , ALTER COLUMN tbl_uuid SET DEFAULT uuid_generate_v1()
 , ADD CONSTRAINT tbl_uuid_uni UNIQUE(tbl_uuid);

-- more constraints, indices, triggers?

DROP TABLE tbl;
ALTER TABLE tbl_new RENAME tbl;

-- recreate views etc. if any
COMMIT;

Esto debería ser más rápido. Cualquier otro método de actualización en el lugar tiene que reescribir toda la tabla también, solo de una manera más costosa. Solo iría por esa ruta si no tiene suficiente espacio libre en el disco o no puede permitirse bloquear toda la tabla o generar errores para intentos de escritura concurrentes.

¿Qué pasa con las escrituras concurrentes?

Otra transacción (en otras sesiones) que intente INSERT/ UPDATE/ DELETEen la misma tabla después de que su transacción haya tomado el SHAREbloqueo, esperará hasta que se libere el bloqueo o se active un tiempo de espera, lo que ocurra primero. Ellos fallar de cualquier manera, ya que la mesa que estaban tratando de escribir ha sido borrado de debajo de ellos.

La nueva tabla tiene un nuevo OID de tabla, pero las transacciones concurrentes ya han resuelto el nombre de la tabla al OID de la tabla anterior . Cuando finalmente se libera el bloqueo, intentan bloquear la mesa ellos mismos antes de escribir y descubren que se ha ido. Postgres responderá:

ERROR: could not open relation with OID 123456

¿Dónde 123456está el OID de la tabla anterior? Debe detectar esa excepción y volver a intentar consultas en el código de su aplicación para evitarla.

Si no puede permitirse que eso suceda, debe conservar su tabla original.

Dos alternativas para mantener la tabla existente.

  1. Actualización en el lugar (posiblemente ejecutando la actualización en segmentos pequeños a la vez) antes de agregar la NOT NULLrestricción. Agregar una nueva columna con valores NULL y sin NOT NULLrestricciones es barato.
    Desde Postgres 9.2 también puede crear una CHECKrestricción conNOT VALID :

    La restricción aún se aplicará contra inserciones o actualizaciones posteriores

    Eso le permite actualizar filas peu à peu , en múltiples transacciones separadas . Esto evita mantener bloqueos de fila durante demasiado tiempo y también permite reutilizar filas muertas. (Tendrá que ejecutarse VACUUMmanualmente si no hay suficiente tiempo intermedio para que se active el vacío automático). Finalmente, agregue la NOT NULLrestricción y elimine la NOT VALID CHECKrestricción:

    ALTER TABLE tbl ADD CONSTRAINT tbl_no_null CHECK (tbl_uuid IS NOT NULL) NOT VALID;
    
    -- update rows in multiple batches in separate transactions
    -- possibly run VACUUM between transactions
    
    ALTER TABLE tbl ALTER COLUMN tbl_uuid SET NOT NULL;
    ALTER TABLE tbl ALTER DROP CONSTRAINT tbl_no_null;

    Respuesta relacionada discutiendo NOT VALIDcon más detalle:

  2. Prepare el nuevo estado en una tabla temporal , TRUNCATEel original y rellene desde la tabla temporal. Todo en una transacción . Todavía debe SHAREbloquear antes de preparar la nueva tabla para evitar perder escrituras concurrentes.

    Detalles en estas respuestas relacionadas sobre SO:


Fantástica respuesta! Exactamente la información que estaba buscando. Dos preguntas 1. ¿Tiene alguna idea sobre una manera fácil de probar cuánto tiempo tomaría una acción como esta? 2. Si se tarda unos 5 minutos, ¿qué sucede con las acciones que intentan actualizar una fila en esa tabla durante esos 5 minutos?
Collin Peters el

@CollinPeters: 1. La mayor parte del tiempo se dedicaría a copiar la tabla grande, y posiblemente a volver a crear índices y restricciones (eso depende). Dejar caer y renombrar es barato. Para probar, puede ejecutar su script SQL preparado sin el LOCKhasta y excluyendo el DROP. Solo podía pronunciar conjeturas salvajes e inútiles. En cuanto a 2., considere la adición a mi respuesta.
Erwin Brandstetter

@ErwinBrandstetter Continuar con las vistas de recreación, así que si tengo una docena de vistas que todavía usan la tabla anterior (oid) después del cambio de nombre de la tabla. ¿Hay alguna forma de realizar una sustitución profunda en lugar de volver a ejecutar toda la actualización / creación de la vista?
CodeFarmer

@CodeFarmer: si solo cambia el nombre de una tabla, las vistas siguen funcionando con la tabla renombrada. Para que las vistas usen la nueva tabla, debe volver a crearlas en función de la nueva tabla. (También para permitir que se elimine la tabla anterior). No hay forma (práctica) de evitarla.
Erwin Brandstetter

14

No tengo una "mejor" respuesta, pero tengo una "menos mala" respuesta que podría permitirle hacer las cosas razonablemente rápido.

Mi tabla tenía filas de 2MM y el rendimiento de la actualización estaba disminuyendo cuando intenté agregar una columna de marca de tiempo secundaria que estaba predeterminada en la primera.

ALTER TABLE mytable ADD new_timestamp TIMESTAMP ;
UPDATE mytable SET new_timestamp = old_timestamp ;
ALTER TABLE mytable ALTER new_timestamp SET NOT NULL ;

Después de que colgó durante 40 minutos, probé esto en un pequeño lote para tener una idea de cuánto tiempo podría llevar esto: el pronóstico era de alrededor de 8 horas.

La respuesta aceptada es definitivamente mejor, pero esta tabla se usa mucho en mi base de datos. Hay unas pocas docenas de mesas que FKEY en él; Quería evitar cambiar LLAVES EXTRANJERAS en tantas tablas. Y luego hay vistas.

Un poco de búsqueda de documentos, estudios de casos y StackOverflow, y tuve el "¡A-Ha!" momento. El drenaje no estaba en la ACTUALIZACIÓN central, sino en todas las operaciones de ÍNDICE. Mi tabla tenía 12 índices: algunos para restricciones únicas, algunos para acelerar el planificador de consultas y algunos para búsqueda de texto completo.

Cada fila que se ACTUALIZÓ no solo funcionaba en DELETE / INSERT, sino también la sobrecarga de alterar cada índice y verificar las restricciones.

Mi solución fue eliminar cada índice y restricción, actualizar la tabla y luego agregar todos los índices / restricciones nuevamente.

Tomó alrededor de 3 minutos escribir una transacción SQL que hiciera lo siguiente:

  • EMPEZAR;
  • índices caídos / constaints
  • tabla de actualización
  • volver a agregar índices / restricciones
  • COMETER;

El script tardó 7 minutos en ejecutarse.

La respuesta aceptada es definitivamente mejor y más adecuada ... y prácticamente elimina la necesidad de tiempo de inactividad. Sin embargo, en mi caso, habría llevado mucho más trabajo de "Desarrollador" para usar esa solución y teníamos una ventana de 30 minutos de tiempo de inactividad programado en el que podría lograrse. Nuestra solución lo abordó en 10.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.