Acelere la creación del índice parcial de Postgres

8

Estoy tratando de crear índices parciales para una tabla estática grande (1.2TB) en Postgres 9.4.

Mis datos son completamente estáticos, por lo que puedo insertar todos los datos y luego crear todos los índices.

En esta tabla de 1.2TB, tengo una columna llamada run_idque divide limpiamente los datos. Hemos obtenido un gran rendimiento al crear índices que cubren un rango de run_ids. Aquí hay un ejemplo:

CREATE INDEX perception_run_frame_idx_run_266_thru_270
ON run.perception
(run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;

Estos índices parciales nos dan la velocidad de consulta deseada. Desafortunadamente, la creación de cada índice parcial lleva unos 70 minutos.

Parece que estamos limitados por la CPU ( topse muestra 100% para el proceso).
¿Hay algo que pueda hacer para acelerar la creación de nuestros índices parciales?

Especificaciones del sistema:

  • 18 núcleos Xeon
  • 192 GB de RAM
  • 12 SSD en RAID
  • Las aspiradoras automáticas están apagadas
  • maintenance_work_mem: 64GB (¿Demasiado alto?)

Especificaciones de la mesa:

  • Tamaño: 1,26 TB
  • Número de filas: 10.537 mil millones
  • Tamaño de índice típico: 3.2 GB (hay una variación de ~ .5 GB)

Definición de tabla:

CREATE TABLE run.perception(
id bigint NOT NULL,
run_id bigint NOT NULL,
frame bigint NOT NULL,
by character varying(45) NOT NULL,
by_anyone bigint NOT NULL,
by_me bigint NOT NULL,
by_s_id integer,
owning_p_id bigint NOT NULL,
obj_type_set bigint,
seq integer,
subj_id bigint NOT NULL,
subj_state_frame bigint NOT NULL,
CONSTRAINT perception_pkey PRIMARY KEY (id))

(No lea demasiado los nombres de las columnas, los he ofuscado un poco).

Información de fondo:

  • Tenemos un equipo separado en el sitio que consume estos datos, pero en realidad solo hay uno o dos usuarios. (Todos estos datos se generan a través de una simulación). Los usuarios solo comienzan a analizar los datos una vez que las inserciones están terminadas y los índices están completamente integrados. Nuestra principal preocupación es reducir el tiempo requerido para generar datos utilizables, y en este momento el cuello de botella es el tiempo de creación del índice.
  • La velocidad de consulta ha sido completamente adecuada cuando se usan parciales. De hecho, creo que podríamos aumentar el número de ejecuciones que cubre cada índice y aún así mantener un rendimiento de consulta lo suficientemente bueno.
  • Supongo que tendremos que dividir la tabla. Estamos tratando de agotar todas las demás opciones antes de tomar esa ruta.
burnsy
fuente
Esta información adicional sería instrumental: tipos de datos de columnas involucradas, consulta típica, cardinalidad (recuento de filas), ¿cuántos diferentes run_id? ¿Distribuidos equitativamente? Tamaño del índice resultante en el disco? Los datos son estáticos, ok. ¿Pero eres el único usuario?
Erwin Brandstetter
Actualizado con más información.
Burnsy
1
"Las aspiradoras automáticas están apagadas ": ¿por qué? Esa es una muy mala idea. Esto evita la recopilación de estadísticas y, por lo tanto, generará malos planes de consulta
a_horse_with_no_name
@a_horse_with_no_name Iniciamos manualmente un análisis después de insertar todos los datos
burnsy
Tu situación aún no está clara para mí. ¿Cómo son tus consultas? Si su mesa es completely static, entonces, ¿qué quiere decir con We have a separate team onsite that consumes this data? ¿Acabas de indexar el rango run_id >= 266 AND run_id <= 270o toda la tabla? ¿Cuál es la esperanza de vida de cada índice / cuántas consultas lo usarán? ¿Para cuántos valores diferentes run_id? Suena como ~ 15 millones. filas por run_id, ¿cuál sería alrededor de 800 valores diferentes para run_id? ¿Por qué son obj_type_set, by_s_id, seqno definido NO NULO? ¿Qué porcentaje aproximado de valores NULL para cada uno?
Erwin Brandstetter

Respuestas:

8

Índice BRIN

Disponible desde Postgres 9.5 y probablemente justo lo que estás buscando. Creación de índice mucho más rápida, índice mucho más pequeño. Pero las consultas no suelen ser tan rápidas. El manual:

BRIN son las siglas de Block Range Index. BRIN está diseñado para manejar tablas muy grandes en las que ciertas columnas tienen una correlación natural con su ubicación física dentro de la tabla. Un rango de bloque es un grupo de páginas que están físicamente adyacentes en la tabla; para cada rango de bloque, el índice almacena cierta información de resumen.

Sigue leyendo, hay más.
Depesz realizó una prueba preliminar.

El óptimo para su caso: Si usted puede escribir filas agrupadas en run_id, el índice se vuelve muy pequeña y la creación mucho más barato.

CREATE INDEX foo ON run.perception USING brin (run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;

Incluso podría indexar toda la tabla .

Diseño de la mesa

Independientemente de lo que haga, puede guardar 8 bytes perdidos en el relleno debido a los requisitos de alineación por fila al ordenar columnas como esta:

CREATE TABLE run.perception(
  id               bigint NOT NULL PRIMARY KEY
, run_id           bigint NOT NULL
, frame            bigint NOT NULL
, by_anyone        bigint NOT NULL
, by_me            bigint NOT NULL
, owning_p_id      bigint NOT NULL
, subj_id          bigint NOT NULL
, subj_state_frame bigint NOT NULL
, obj_type_set     bigint
, by_s_id          integer
, seq              integer
, by               varchar(45) NOT NULL -- or just use type text
);

Hace que su tabla sea 79 GB más pequeña si ninguna de las columnas tiene valores NULL. Detalles:

Además, solo tiene tres columnas que pueden ser NULL. El mapa de bits NULL ocupa 8 bytes para 9 - 72 columnas. Si solo una columna entera es NULL, hay un caso de esquina para una paradoja de almacenamiento: sería más barato usar un valor ficticio: 4 bytes desperdiciados pero 8 bytes guardados al no necesitar un mapa de bits NULL para la fila. Más detalles aquí:

Índices parciales

Dependiendo de sus consultas reales, podría ser más eficiente tener estos cinco índices parciales en lugar del anterior:

CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 266;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 267;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 268;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 269;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 270;

Ejecute una transacción para cada uno.

Eliminar run_idde esta manera como columna de índice ahorra 8 bytes por entrada de índice, 32 en lugar de 40 bytes por fila. Cada índice también es más barato de crear, pero crear cinco en lugar de uno solo lleva mucho más tiempo para una tabla que es demasiado grande para permanecer en caché (como comentaron @ Jürgen y @Chris). Entonces eso puede o no ser útil para usted.

Fraccionamiento

Basado en la herencia : la única opción hasta Postgres 9.5.
(La nueva partición declarativa en Postgres 11 o, preferiblemente, 12 es más inteligente).

El manual:

Todas las restricciones en todos los elementos secundarios de la tabla primaria se examinan durante la exclusión de restricciones, por lo que es probable que un gran número de particiones aumente considerablemente el tiempo de planificación de consultas. Por lo tanto, la partición basada en herencia heredada funcionará bien con hasta un centenar de particiones ; No intente utilizar miles de particiones.

El énfasis en negrita es mío. En consecuencia, estimando 1000 valores diferentes para run_id, haría particiones que abarcan alrededor de 10 valores cada una.


maintenance_work_mem

Eché de menos que ya te estás adaptando maintenance_work_memen mi primera lectura. Dejaré una cita y un consejo en mi respuesta como referencia. Por documentación:

maintenance_work_mem (entero)

Especifica la cantidad máxima de memoria para ser utilizada por las operaciones de mantenimiento, tales como VACUUM, CREATE INDEX, y ALTER TABLE ADD FOREIGN KEY. El valor predeterminado es 64 megabytes ( 64MB). Como solo una de estas operaciones puede ejecutarse a la vez mediante una sesión de base de datos, y una instalación normalmente no tiene muchas de ellas ejecutándose simultáneamente, es seguro establecer este valor significativamente más grande que work_mem. Una configuración más grande podría mejorar el rendimiento para pasar la aspiradora y para restaurar los volcados de la base de datos.

Tenga en cuenta que cuando se autovacuumejecuta, hasta autovacuum_max_workersesta memoria puede asignarse, así que tenga cuidado de no establecer el valor predeterminado demasiado alto. Puede ser útil controlar esto por separado setting autovacuum_work_mem.

Solo lo establecería tan alto como sea necesario, lo que depende del tamaño del índice desconocido (para nosotros). Y solo localmente para la sesión de ejecución. Como explica la cita, una configuración general demasiado alta puede privar al servidor de lo contrario, porque el vacío automático también puede reclamar más RAM. Además, no lo establezca mucho más de lo necesario, incluso en la sesión de ejecución, la RAM libre podría utilizarse para almacenar datos en caché.

Podría verse así:

BEGIN;

SET LOCAL maintenance_work_mem = 10GB;  -- depends on resulting index size

CREATE INDEX perception_run_frame_idx_run_266_thru_270 ON run.perception(run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;

COMMIT;

Acerca de SET LOCAL:

Los efectos SET LOCALduran solo hasta el final de la transacción actual, ya sea comprometida o no.

Para medir tamaños de objeto:

El servidor generalmente debe configurarse razonablemente de lo contrario, obviamente.

Erwin Brandstetter
fuente
Apuesto a que su trabajo está obligado a IO ya que la tabla es mucho más grande que la RAM. Leer la tabla con mayor frecuencia empeorará el problema, independientemente de si hay suficiente memoria para ordenar cada índice creado en la memoria o no.
Jürgen Strobel
Estoy con Jurgen en este caso. Creo que debido al tamaño de la tabla, en esencia, debe realizar un análisis secuencial completo de la tabla por índice creado. Además, no estoy seguro de que verás un aumento en el rendimiento al crear los índices parciales separados (estoy 90% seguro de que no verás ningún aumento, pero en esto podría estar apagado). Creo que es mejor La solución para la creación de índices implicaría la creación de un índice sobre el rango completo que desea consultar como un "índice parcial único" para mantener el tiempo de construcción general bajo.
Chris
@Chris: Estoy de acuerdo, 5 índices tomarán más tiempo en crearse que uno solo (incluso si todos juntos son más pequeños, crear cada índice es más barato y las consultas pueden ser más rápidas). Pensando un poco más, este debería ser un caso de uso perfecto para un índice BRIN en Postgres 9.5.
Erwin Brandstetter
3

Tal vez esto solo está sobre diseñado. ¿Realmente has intentado usar un solo índice completo? Los índices parciales que cubren toda la tabla juntos no proporcionan mucha ganancia, si es que hay alguna, para las búsquedas de índice, y de su texto deduzco que tiene índices para todos los run_ids. Puede haber algunas ventajas para indexar exploraciones con índices parciales, pero aún así primero compararía la solución simple de un índice.

Para cada creación de índice, necesita una exploración completa de E / S a través de la tabla. Por lo tanto, la creación de varios índices parciales requiere mucho más IO leyendo la tabla que para un solo índice, aunque la clasificación se derramará en el disco para el índice grande único. Si insiste en índices parciales, puede intentar construir todos (o varios) índices al mismo tiempo en paralelo (si la memoria lo permite).

Para una estimación aproximada de maintenance_work_mem requerida para ordenar todos los run_ids, que son bigints de 8 bytes, en la memoria necesitaría 10.5 * 8 GB + algo de sobrecarga.

Jürgen Strobel
fuente
0

También puede crear los índices en otros espacios de tabla que no sean los predeterminados. Estos espacios de tabla podrían apuntar a discos que no son redundantes (solo recrear los índices si fallan), o están en matrices más rápidas.

También puede considerar particionar la tabla usando los mismos criterios que sus índices parciales. Esto permitiría la misma velocidad que el índice al realizar consultas, sin crear ningún índice en absoluto.

Kirk Roybal
fuente