Tengo una mesa como esta:
CREATE TABLE products (
id serial PRIMARY KEY,
category_ids integer[],
published boolean NOT NULL,
score integer NOT NULL,
title varchar NOT NULL);
Un producto puede pertenecer a múltiples categorías. category_ids
La columna contiene una lista de identificadores de todas las categorías de productos.
La consulta típica se ve así (siempre buscando una sola categoría):
SELECT * FROM products WHERE published
AND category_ids @> ARRAY[23465]
ORDER BY score DESC, title
LIMIT 20 OFFSET 8000;
Para acelerarlo utilizo el siguiente índice:
CREATE INDEX idx_test1 ON products
USING GIN (category_ids gin__int_ops) WHERE published;
Esto ayuda mucho a menos que haya demasiados productos en una categoría. Filtra rápidamente los productos que pertenecen a esa categoría, pero luego hay una operación de clasificación que debe hacerse de la manera difícil (sin índice).
A he instalado una btree_gin
extensión que me permite crear un índice GIN de varias columnas como este:
CREATE INDEX idx_test2 ON products USING GIN (
category_ids gin__int_ops, score, title) WHERE published;
Pero Postgres no quiere usar eso para ordenar . Incluso cuando elimino el DESC
especificador en la consulta.
Cualquier enfoque alternativo para optimizar la tarea es muy bienvenido.
Información Adicional:
- PostgreSQL 9.4, con extensión intarray
- El número total de productos actualmente es de 260k, pero se espera que crezca significativamente (hasta 10M, esta es una plataforma de comercio electrónico de múltiples inquilinos)
- productos por categoría 1..10000 (puede crecer hasta 100k), el promedio es inferior a 100 pero las categorías con gran cantidad de productos tienden a atraer muchas más solicitudes
El siguiente plan de consulta se obtuvo del sistema de prueba más pequeño (4680 productos en la categoría seleccionada, 200k productos en total en la tabla):
Limit (cost=948.99..948.99 rows=1 width=72) (actual time=82.330..82.341 rows=20 loops=1)
-> Sort (cost=948.37..948.99 rows=245 width=72) (actual time=80.231..81.337 rows=4020 loops=1)
Sort Key: score, title
Sort Method: quicksort Memory: 928kB
-> Bitmap Heap Scan on products (cost=13.90..938.65 rows=245 width=72) (actual time=1.919..16.044 rows=4680 loops=1)
Recheck Cond: ((category_ids @> '{292844}'::integer[]) AND published)
Heap Blocks: exact=3441
-> Bitmap Index Scan on idx_test2 (cost=0.00..13.84 rows=245 width=0) (actual time=1.185..1.185 rows=4680 loops=1)
Index Cond: (category_ids @> '{292844}'::integer[])
Planning time: 0.202 ms
Execution time: 82.404 ms
Nota # 1 : 82 ms podría no parecer tan aterrador, pero eso se debe a que el buffer de clasificación se ajusta a la memoria. Una vez que selecciono todas las columnas de la tabla de productos ( SELECT * FROM ...
y en la vida real hay unas 60 columnas), se Sort Method: external merge Disk: 5696kB
duplica el tiempo de ejecución. Y eso es solo para 4680 productos.
Punto de acción n. ° 1 (viene de la Nota n. ° 1): para reducir la huella de memoria de la operación de clasificación y, por lo tanto, acelerarla un poco, sería aconsejable buscar, ordenar y limitar los identificadores de producto primero, luego buscar registros completos:
SELECT * FROM products WHERE id IN (
SELECT id FROM products WHERE published AND category_ids @> ARRAY[23465]
ORDER BY score DESC, title LIMIT 20 OFFSET 8000
) ORDER BY score DESC, title;
Esto nos lleva de regreso a Sort Method: quicksort Memory: 903kB
~ 80 ms para 4680 productos. Todavía puede ser lento cuando el número de productos crece a 100k.
fuente
score
puede ser NULL, pero aún así ordenar porscore DESC
, noscore DESC NULLS LAST
. Uno u otro parece no estar bien ...score
de hecho NO ES NULO: he corregido la definición de la tabla.Respuestas:
He experimentado mucho y aquí están mis hallazgos.
GIN y clasificación
El índice GIN actualmente (a partir de la versión 9.4) no puede ayudar a ordenar .
work_mem
Gracias Chris por señalar este parámetro de configuración . Su valor predeterminado es 4 MB, y en caso de que su conjunto de registros sea más grande, aumentarlo
work_mem
al valor adecuado (se puede encontrar enEXPLAIN ANALYSE
) puede acelerar significativamente las operaciones de clasificación.Reinicie el servidor para que el cambio surta efecto, luego verifique dos veces:
Consulta original
He llenado mi base de datos con 650k productos con algunas categorías que contienen hasta 40k productos. He simplificado un poco la consulta eliminando la
published
cláusula:Como podemos ver
work_mem
, no fue suficiente, por lo que tuvimosSort Method: external merge Disk: 29656kB
(el número aquí es aproximado, necesita un poco más de 32 MB para la clasificación rápida en memoria).Reduce la huella de memoria
No seleccione registros completos para ordenar, use identificadores, aplique clasificación, desplazamiento y límite, luego cargue solo 10 registros que necesitamos:
Nota
Sort Method: quicksort Memory: 7396kB
. El resultado es mucho mejor.ÚNASE e índice adicional del árbol B
Como Chris aconsejó, he creado un índice adicional:
Primero intenté unirme así:
El plan de consulta difiere ligeramente, pero el resultado es el mismo:
Jugando con varias compensaciones y recuentos de productos, no pude hacer que PostgreSQL usara un índice B-tree adicional.
Así que fui de manera clásica y creé la tabla de unión :
Aún sin usar el índice B-tree, el conjunto de resultados no se ajustaba
work_mem
, por lo tanto, los malos resultados.Pero en algunas circunstancias, tener una gran cantidad de productos y un pequeño desplazamiento PostgreSQL ahora decide usar el índice B-tree:
De hecho, esto es bastante lógico ya que el índice del árbol B aquí no produce resultados directos, solo se usa como guía para el escaneo secuencial.
Comparemos con la consulta GIN:
El resultado de GIN es mucho mejor. Verifiqué con varias combinaciones de número de productos y compensación, bajo ninguna circunstancia el enfoque de la tabla de unión fue mejor .
El poder del índice real
Para que PostgreSQL utilice completamente el índice para la clasificación, todos los
WHERE
parámetros de consulta , así como losORDER BY
parámetros, deben residir en un solo índice de árbol B. Para hacer esto, he copiado los campos de clasificación del producto a la tabla de unión:Y este es el peor escenario con gran cantidad de productos en la categoría elegida y gran compensación. Cuando offset = 300 el tiempo de ejecución es de solo 0.5 ms.
Lamentablemente, mantener una mesa de conexiones de este tipo requiere un esfuerzo adicional. Podría lograrse a través de vistas materializadas indexadas, pero eso solo es útil cuando sus datos se actualizan raramente, ya que actualizar dicha vista materializada es una operación bastante pesada.
Por lo tanto, me quedo con el índice GIN hasta ahora, con una
work_mem
consulta de huella de memoria aumentada y reducida.fuente
work_mem
puesta en postgresql.conf. Recargar es suficiente. Y permítanme advertirme de no establecerwork_mem
una configuración global demasiado alta en un entorno multiusuario (tampoco demasiado baja). Si tiene algunas consultas que necesitan máswork_mem
, configúrelo más alto para la sesión solo conSET
, o solo con la transacciónSET LOCAL
. Ver: dba.stackexchange.com/a/48633/3684Aquí hay algunos consejos rápidos que pueden ayudarlo a mejorar su rendimiento. Comenzaré con el consejo más fácil, que es casi sin esfuerzo de su parte, y pasaré al consejo más difícil después del primero.
1)
work_mem
Entonces, veo de inmediato que un tipo reportado en su plan de explicación
Sort Method: external merge Disk: 5696kB
consume menos de 6 MB, pero se está derramando en el disco. Necesita aumentar suwork_mem
configuración en supostgresql.conf
archivo para que sea lo suficientemente grande como para que la clasificación pueda caber en la memoria.EDITAR: Además, en una inspección adicional, veo que después de usar el índice para verificar
catgory_ids
cuál se ajusta a sus criterios, el escaneo del índice de mapa de bits se ve obligado a "perder" y debe volver a verificar la condición al leer las filas desde las páginas relevantes del montón . Consulte esta publicación en postgresql.org para obtener una explicación mejor de lo que he dado. : P El punto principal es que tuwork_mem
es demasiado bajo. Si no ha ajustado la configuración predeterminada en su servidor, no funcionará bien.Esencialmente, esta solución no le llevará tiempo. Un cambio a
postgresql.conf
, y te vas! Consulte esta página de ajuste del rendimiento para obtener más consejos.2. Cambio de esquema
Entonces, ha tomado la decisión en su diseño de esquema de desnormalizar
category_ids
en una matriz de enteros, lo que luego lo obliga a usar un índice GIN o GIST para obtener un acceso rápido. En mi experiencia, su elección de un índice GIN será más rápido para las lecturas que un GIST, por lo que en ese caso tomó la decisión correcta. Sin embargo, GIN es un índice sin clasificar; pensar que es más como una clave-valor, donde los predicados de igualdad son fáciles de comprobar, pero las operaciones tales comoWHERE >
,WHERE <
oORDER BY
no se ven facilitadas por el índice.Un enfoque decente sería normalizar su diseño utilizando una tabla de puente / tabla de unión , utilizada para especificar relaciones de muchos a muchos en las bases de datos.
En este caso, tiene muchas categorías y un conjunto de enteros correspondientes
category_id
, y tiene muchos productos y sus correspondientesproduct_id
. En lugar de una columna en su tabla de productos que es una matriz entera decategory_id
s, elimine esta columna de matriz de su esquema y cree una tabla comoLuego, puede generar índices de árbol B en las dos columnas de la tabla puente,
Solo mi humilde opinión, pero estos cambios pueden marcar una gran diferencia para usted. Pruebe ese
work_mem
cambio lo primero, como mínimo.¡La mejor de las suertes!
EDITAR:
Cree un índice adicional para ayudar a ordenar
Por lo tanto, si con el tiempo su línea de productos se expande, ciertas consultas pueden arrojar muchos resultados (¿miles, decenas de miles?) Pero que pueden ser solo un pequeño subconjunto de su línea total de productos. En estos casos, la clasificación puede incluso ser bastante costosa si se realiza en la memoria, pero se puede utilizar un índice diseñado adecuadamente para ayudar a la clasificación.
Consulte la documentación oficial de PostgreSQL que describe los índices y ORDER BY .
Si crea un índice que coincida con sus
ORDER BY
requisitosentonces Postgres optimizará y decidirá si usar el índice o realizar una ordenación explícita será más rentable. Tenga en cuenta que no hay garantía de que Postgres use el índice; buscará optimizar el rendimiento y elegir entre usar el índice o ordenarlo explícitamente. Si crea este índice, vigílelo para ver si se está utilizando lo suficiente como para justificar su creación, y suéltelo si la mayoría de sus tipos se están haciendo explícitamente.
Aún así, en este punto, su mejora de "mayor rendimiento" probablemente utilizará más
work_mem
, pero hay casos en los que el índice podría admitir la clasificación.fuente
work_mem
configuración fue pensada como una solución a su problema de 'ordenar en disco', así como a su problema de verificación de la condición. A medida que crece la cantidad de productos, es posible que deba tener un índice adicional para ordenar. Por favor vea mis ediciones arriba para aclaraciones.