Cómo acelerar el rendimiento de inserción en PostgreSQL

215

Estoy probando el rendimiento de inserción de Postgres. Tengo una tabla con una columna con número como tipo de datos. También hay un índice en él. Llené la base de datos usando esta consulta:

insert into aNumber (id) values (564),(43536),(34560) ...

Inserté 4 millones de filas muy rápidamente 10,000 a la vez con la consulta anterior. Después de que la base de datos alcanzara los 6 millones de filas, el rendimiento disminuyó drásticamente a 1 millón de filas cada 15 minutos. ¿Hay algún truco para aumentar el rendimiento de inserción? Necesito un rendimiento de inserción óptimo en este proyecto.

Usando Windows 7 Pro en una máquina con 5 GB de RAM.

Lucas101
fuente
55
Vale la pena mencionar su versión de Pg en las preguntas también. En este caso, no hace mucha diferencia, pero lo hace para muchas preguntas.
Craig Ringer el
1
suelte los índices en la tabla y los activadores, si los hay, y ejecute el script de inserción. Una vez que haya completado la carga masiva, puede volver a crear los índices.
Sandeep

Respuestas:

481

Consulte llenar una base de datos en el manual de PostgreSQL, el excelente artículo de Depesz sobre el tema y esta pregunta SO .

(Tenga en cuenta que esta respuesta es sobre la carga masiva de datos en una base de datos existente o para crear una nueva. Si está interesado en el rendimiento de restauración de base de datos pg_restoreo la psqlejecución de la pg_dumpsalida, gran parte de esto no se aplica desde entonces pg_dumpy pg_restoreya hace cosas como crear dispara e indexa después de que termina un esquema + restauración de datos) .

Hay mucho por hacer. La solución ideal sería importar en una UNLOGGEDtabla sin índices, luego cambiarlo a registrado y agregar los índices. Desafortunadamente, en PostgreSQL 9.4 no hay soporte para cambiar tablas de UNLOGGEDa registradas. 9.5 agrega ALTER TABLE ... SET LOGGEDpara permitirle hacer esto.

Si puede desconectar su base de datos para la importación masiva, use pg_bulkload.

De otra manera:

  • Deshabilitar cualquier desencadenante en la mesa

  • Descarte los índices antes de comenzar la importación, vuelva a crearlos después. (Se necesita mucho menos tiempo para crear un índice en una pasada que agregar los mismos datos progresivamente, y el índice resultante es mucho más compacto).

  • Si realiza la importación dentro de una sola transacción, es seguro eliminar las restricciones de clave externa, realizar la importación y volver a crear las restricciones antes de confirmar. No haga esto si la importación se divide en varias transacciones, ya que podría introducir datos no válidos.

  • Si es posible, use en COPYlugar de INSERTs

  • Si no puede usar, COPYconsidere usar INSERTs de valores múltiples si es práctico. Parece que ya estás haciendo esto. Sin embargo, no intente enumerar demasiados valores en un solo VALUES; esos valores tienen que caber en la memoria un par de veces, así que manténgalos en unos pocos cientos por declaración.

  • Lote sus inserciones en transacciones explícitas, haciendo cientos de miles o millones de inserciones por transacción. No existe un límite práctico AFAIK, pero el procesamiento por lotes le permitirá recuperarse de un error al marcar el inicio de cada lote en sus datos de entrada. De nuevo, parece que ya estás haciendo esto.

  • Uso synchronous_commit=offy un enorme commit_delaypara reducir los costos de fsync (). Sin embargo, esto no ayudará mucho si ha agrupado su trabajo en grandes transacciones.

  • INSERTo COPYen paralelo desde varias conexiones. La cantidad depende del subsistema de disco de su hardware; como regla general, desea una conexión por disco duro físico si usa almacenamiento conectado directamente.

  • Establezca un checkpoint_segmentsvalor alto y habilítelo log_checkpoints. Mire los registros de PostgreSQL y asegúrese de que no se queje de que los puntos de control ocurran con demasiada frecuencia.

  • Si y solo si no le importa perder todo su clúster de PostgreSQL (su base de datos y cualquier otra en el mismo clúster) por una corrupción catastrófica si el sistema se bloquea durante la importación, puede detener Pg, configurar fsync=off, iniciar Pg, hacer su importación, luego (vitalmente) detiene Pg y establece de fsync=onnuevo. Ver configuración de WAL . No haga esto si ya hay datos que le interesan en alguna base de datos en su instalación de PostgreSQL. Si configura fsync=offtambién puede configurar full_page_writes=off; nuevamente, solo recuerde volver a encenderlo después de la importación para evitar la corrupción de la base de datos y la pérdida de datos. Ver configuraciones no duraderas en el manual de Pg.

También debe considerar ajustar su sistema:

  • Utilice SSD de buena calidad para almacenar tanto como sea posible. Los buenos SSD con cachés de escritura confiables y protegidos contra la energía hacen que las tasas de confirmación sean increíblemente más rápidas. Son menos beneficiosos cuando sigue los consejos anteriores, que reducen los enjuagues de disco / número de fsync()s, pero aún pueden ser de gran ayuda. No use unidades de estado sólido baratas sin la protección adecuada contra fallas de energía a menos que no le importe conservar sus datos.

  • Si está utilizando RAID 5 o RAID 6 para el almacenamiento conectado directo, deténgase ahora. Haga una copia de seguridad de sus datos, reestructura su matriz RAID a RAID 10 e intente nuevamente. RAID 5/6 son inútiles para el rendimiento de escritura masiva, aunque un buen controlador RAID con una gran caché puede ayudar.

  • Si tiene la opción de usar un controlador RAID de hardware con una gran memoria caché de escritura respaldada por batería, esto realmente puede mejorar el rendimiento de escritura para cargas de trabajo con muchas confirmaciones. No ayuda tanto si está utilizando la confirmación asíncrona con commit_delay o si está haciendo menos grandes transacciones durante la carga masiva.

  • Si es posible, almacene WAL ( pg_xlog) en un disco / matriz de discos por separado. No tiene mucho sentido usar un sistema de archivos separado en el mismo disco. Las personas a menudo eligen usar un par RAID1 para WAL. Nuevamente, esto tiene más efecto en los sistemas con altas tasas de compromiso, y tiene poco efecto si está utilizando una tabla no registrada como objetivo de carga de datos.

También puede estar interesado en Optimizar PostgreSQL para realizar pruebas rápidas .

Craig Ringer
fuente
1
¿Estaría de acuerdo en que la penalización de escritura de RAID 5/6 se mitiga un poco si se utilizan SSD de buena calidad? Obviamente, todavía hay una penalización, pero creo que la diferencia es mucho menos dolorosa que con los discos duros.
1
No he probado eso. Yo diría que probablemente sea menos malo: los desagradables efectos de amplificación de escritura y (para pequeñas escrituras) todavía necesitan un ciclo de lectura-modificación-escritura, pero la penalización severa por la búsqueda excesiva no debería ser un problema.
Craig Ringer
¿Podemos deshabilitar los índices en lugar de soltarlos, por ejemplo, estableciendo indisvalid( postgresql.org/docs/8.3/static/catalog-pg-index.html ) en falso, luego cargamos datos y luego ponemos en línea los índices REINDEX?
Vladislav Rastrusny
1
@ CraigRinger He probado RAID-5 vs RAID-10 con SSD en un Perc H730. RAID-5 es en realidad más rápido. También vale la pena señalar que insertar / transacciones en combinación con grandes bytea parece ser más rápido que la copia. En general, un buen consejo.
Atlas
2
¿Alguien está viendo mejoras importantes en la velocidad con UNLOGGED? Una prueba rápida muestra algo así como una mejora del 10-20%.
serg
15

El uso COPY table TO ... WITH BINARYque está de acuerdo con la documentación es " algo más rápido que los formatos de texto y CSV ". Solo haga esto si tiene millones de filas para insertar y si se siente cómodo con los datos binarios.

Aquí hay una receta de ejemplo en Python, usando psycopg2 con entrada binaria .

Mike T
fuente
1
El modo binario puede ser un gran ahorro de tiempo en algunas entradas, como las marcas de tiempo, donde analizarlas no es trivial. Para muchos tipos de datos, no ofrece muchos beneficios o incluso puede ser un poco más lento debido al mayor ancho de banda (por ejemplo, enteros pequeños). Buen punto planteándolo.
Craig Ringer el
11

Además de la excelente publicación de Craig Ringer y la publicación de blog de Depesz, si desea acelerar sus inserciones a través de la interfaz ODBC ( psqlodbc ) mediante el uso de inserciones de declaraciones preparadas dentro de una transacción, hay algunas cosas adicionales que debe hacer para hacerlo trabaja rapido:

  1. Establezca el nivel de reversión de errores en "Transacción" especificando Protocol=-1en la cadena de conexión. De forma predeterminada, psqlodbc usa el nivel "Sentencia", que crea un SAVEPOINT para cada instrucción en lugar de una transacción completa, lo que hace que las inserciones sean más lentas.
  2. Utilice declaraciones preparadas del lado del servidor especificando UseServerSidePrepare=1en la cadena de conexión. Sin esta opción, el cliente envía la declaración de inserción completa junto con cada fila que se inserta.
  3. Deshabilite la confirmación automática en cada instrucción usando SQLSetConnectAttr(conn, SQL_ATTR_AUTOCOMMIT, reinterpret_cast<SQLPOINTER>(SQL_AUTOCOMMIT_OFF), 0);
  4. Una vez que se hayan insertado todas las filas, confirme la transacción usando SQLEndTran(SQL_HANDLE_DBC, conn, SQL_COMMIT);. No hay necesidad de abrir explícitamente una transacción.

Desafortunadamente, psqlodbc "implementa" SQLBulkOperationsal emitir una serie de instrucciones de inserción no preparadas, de modo que para lograr la inserción más rápida, uno necesita codificar los pasos anteriores manualmente.

Maxim Egorushkin
fuente
El tamaño del búfer de socket grande, A8=30000000en la cadena de conexión, también debe usarse para acelerar las inserciones.
Andrus
9

Pasé alrededor de 6 horas en el mismo tema hoy. Las inserciones van a una velocidad 'normal' (menos de 3 segundos por 100K) hasta filas de 5MI (de un total de 30MI) y luego el rendimiento se reduce drásticamente (hasta 1 minuto por 100K).

No enumeraré todas las cosas que no funcionaron y cortaré directamente a la carne.

Me cayó una clave primaria en la tabla de destino (que era un GUID) y mi 30MI o filas felizmente fluía a su destino a una velocidad constante de menos de 3 segundos por 100K.

Dennis
fuente
6

Si sucede que debe insertar columnas con UUID (que no es exactamente su caso) y agregar a la respuesta @Dennis (aún no puedo comentar), tenga en cuenta que usar gen_random_uuid () (requiere PG 9.4 y el módulo pgcrypto) es (a lot) más rápido que uuid_generate_v4 ()

=# explain analyze select uuid_generate_v4(),* from generate_series(1,10000);
                                                        QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.00..12.50 rows=1000 width=4) (actual time=11.674..10304.959 rows=10000 loops=1)
 Planning time: 0.157 ms
 Execution time: 13353.098 ms
(3 filas)

vs


=# explain analyze select gen_random_uuid(),* from generate_series(1,10000);
                                                        QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.00..12.50 rows=1000 width=4) (actual time=252.274..418.137 rows=10000 loops=1)
 Planning time: 0.064 ms
 Execution time: 503.818 ms
(3 filas)

Además, es la forma oficial sugerida de hacerlo

Nota

Si solo necesita UUID generados aleatoriamente (versión 4), considere usar la función gen_random_uuid () del módulo pgcrypto en su lugar.

Esto redujo el tiempo de inserción de ~ 2 horas a ~ 10 minutos para 3.7M de filas.

Francisco Reynoso
fuente
1

Para un rendimiento óptimo de inserción, deshabilite el índice si esa es una opción para usted. Aparte de eso, un mejor hardware (disco, memoria) también es útil.

Ícaro
fuente
-1

También encontré este problema de rendimiento de inserción. Mi solución es generar algunas rutinas para terminar el trabajo de inserción. Mientras tanto, SetMaxOpenConnsse le debe dar un número adecuado; de lo contrario, se alertarían demasiados errores de conexión abierta.

db, _ := sql.open() 
db.SetMaxOpenConns(SOME CONFIG INTEGER NUMBER) 
var wg sync.WaitGroup
for _, query := range queries {
    wg.Add(1)
    go func(msg string) {
        defer wg.Done()
        _, err := db.Exec(msg)
        if err != nil {
            fmt.Println(err)
        }
    }(query)
}
wg.Wait()

La velocidad de carga es mucho más rápida para mi proyecto. Este fragmento de código acaba de dar una idea de cómo funciona. Los lectores deberían poder modificarlo fácilmente.

Patricio
fuente
Bueno, puedes decir eso. Pero reduce el tiempo de ejecución de unas pocas horas a varios minutos para millones de filas para mi caso. :)
Patrick