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 NULL
restricció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?
postgresql
storage
ddl
Collin Peters
fuente
fuente
ALTER TABLE .. ADD COLUMN ...
o esa parte también debe ser respondida?Respuestas:
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 lauuid
columna usandoCREATE TABLE AS
. ¿Por qué?El siguiente código utiliza una función del
uuid-oss
módulo adicional .Bloquee la tabla contra cambios concurrentes en el
SHARE
modo (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_mem
má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.
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
/DELETE
en la misma tabla después de que su transacción haya tomado elSHARE
bloqueo, 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á:
¿Dónde
123456
está 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.
Actualización en el lugar (posiblemente ejecutando la actualización en segmentos pequeños a la vez) antes de agregar la
NOT NULL
restricción. Agregar una nueva columna con valores NULL y sinNOT NULL
restricciones es barato.Desde Postgres 9.2 también puede crear una
CHECK
restricción conNOT VALID
: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
VACUUM
manualmente si no hay suficiente tiempo intermedio para que se active el vacío automático). Finalmente, agregue laNOT NULL
restricción y elimine laNOT VALID CHECK
restricción:Respuesta relacionada discutiendo
NOT VALID
con más detalle:Prepare el nuevo estado en una tabla temporal ,
TRUNCATE
el original y rellene desde la tabla temporal. Todo en una transacción . Todavía debeSHARE
bloquear antes de preparar la nueva tabla para evitar perder escrituras concurrentes.Detalles en estas respuestas relacionadas sobre SO:
fuente
LOCK
hasta y excluyendo elDROP
. Solo podía pronunciar conjeturas salvajes e inútiles. En cuanto a 2., considere la adición a mi respuesta.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.
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:
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.
fuente