¿Cómo acelerar ORDENAR POR ordenar cuando se usa el índice GIN en PostgreSQL?


12

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_idsLa 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_ginextensió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 DESCespecificador 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: 5696kBduplica 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.


En esta página: hlinnaka.iki.fi/2014/03/28/… hay comentarios de que btree_gin no se puede usar para ordenar.
Mladen Uzelac

OK, reformulé el título para permitir más opciones.
Yaroslav Stavnichiy

¿Siempre estás buscando una sola categoría? Y proporcione información más básica: versión de Postgres, cardinalidades, filas por categoría (min / avg / max). considere las instrucciones en la información de la etiqueta para postgresql-performance . Y: scorepuede ser NULL, pero aún así ordenar por score DESC, no score DESC NULLS LAST. Uno u otro parece no estar bien ...
Erwin Brandstetter

He agregado información adicional según lo solicitado. Siempre estoy buscando una sola categoría. Y scorede hecho NO ES NULO: he corregido la definición de la tabla.
Yaroslav Stavnichiy

Respuestas:


9

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 .

De los tipos de índice actualmente admitidos por PostgreSQL, solo el árbol B puede producir resultados ordenados; los otros tipos de índice devuelven filas coincidentes en un orden no especificado, dependiente de la implementación.

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_memal valor adecuado (se puede encontrar en EXPLAIN ANALYSE) puede acelerar significativamente las operaciones de clasificación.

ALTER SYSTEM SET work_mem TO '32MB';

Reinicie el servidor para que el cambio surta efecto, luego verifique dos veces:

SHOW work_mem;

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 publishedcláusula:

SELECT * FROM products WHERE category_ids @> ARRAY [248688]
ORDER BY score DESC, title LIMIT 10 OFFSET 30000;

Limit  (cost=2435.62..2435.62 rows=1 width=1390) (actual time=1141.254..1141.256 rows=10 loops=1)
  ->  Sort  (cost=2434.00..2435.62 rows=646 width=1390) (actual time=1115.706..1140.513 rows=30010 loops=1)
        Sort Key: score, title
        Sort Method: external merge  Disk: 29656kB
        ->  Bitmap Heap Scan on products  (cost=17.01..2403.85 rows=646 width=1390) (actual time=11.831..25.646 rows=41666 loops=1)
              Recheck Cond: (category_ids @> '{248688}'::integer[])
              Heap Blocks: exact=6471
              ->  Bitmap Index Scan on idx_products_category_ids_gin  (cost=0.00..16.85 rows=646 width=0) (actual time=10.140..10.140 rows=41666 loops=1)
                    Index Cond: (category_ids @> '{248688}'::integer[])
Planning time: 0.288 ms
Execution time: 1146.322 ms

Como podemos ver work_mem, no fue suficiente, por lo que tuvimos Sort 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:

SELECT * FROM products WHERE id in (
  SELECT id FROM products WHERE category_ids @> ARRAY[248688]
  ORDER BY score DESC, title LIMIT 10 OFFSET 30000
) ORDER BY score DESC, title;

Sort  (cost=2444.10..2444.11 rows=1 width=1390) (actual time=707.861..707.862 rows=10 loops=1)
  Sort Key: products.score, products.title
  Sort Method: quicksort  Memory: 35kB
  ->  Nested Loop  (cost=2436.05..2444.09 rows=1 width=1390) (actual time=707.764..707.803 rows=10 loops=1)
        ->  HashAggregate  (cost=2435.63..2435.64 rows=1 width=4) (actual time=707.744..707.746 rows=10 loops=1)
              Group Key: products_1.id
              ->  Limit  (cost=2435.62..2435.62 rows=1 width=72) (actual time=707.732..707.734 rows=10 loops=1)
                    ->  Sort  (cost=2434.00..2435.62 rows=646 width=72) (actual time=704.163..706.955 rows=30010 loops=1)
                          Sort Key: products_1.score, products_1.title
                          Sort Method: quicksort  Memory: 7396kB
                          ->  Bitmap Heap Scan on products products_1  (cost=17.01..2403.85 rows=646 width=72) (actual time=11.587..35.076 rows=41666 loops=1)
                                Recheck Cond: (category_ids @> '{248688}'::integer[])
                                Heap Blocks: exact=6471
                                ->  Bitmap Index Scan on idx_products_category_ids_gin  (cost=0.00..16.85 rows=646 width=0) (actual time=9.883..9.883 rows=41666 loops=1)
                                      Index Cond: (category_ids @> '{248688}'::integer[])
        ->  Index Scan using products_pkey on products  (cost=0.42..8.45 rows=1 width=1390) (actual time=0.004..0.004 rows=1 loops=10)
              Index Cond: (id = products_1.id)
Planning time: 0.682 ms
Execution time: 707.973 ms

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:

CREATE INDEX idx_test7 ON products (score DESC, title);

Primero intenté unirme así:

SELECT * FROM products NATURAL JOIN
  (SELECT id FROM products WHERE category_ids @> ARRAY[248688]
  ORDER BY score DESC, title LIMIT 10 OFFSET 30000) c
ORDER BY score DESC, title;

El plan de consulta difiere ligeramente, pero el resultado es el mismo:

Sort  (cost=2444.10..2444.11 rows=1 width=1390) (actual time=700.747..700.747 rows=10 loops=1)
  Sort Key: products.score, products.title
  Sort Method: quicksort  Memory: 35kB
  ->  Nested Loop  (cost=2436.05..2444.09 rows=1 width=1390) (actual time=700.651..700.690 rows=10 loops=1)
        ->  HashAggregate  (cost=2435.63..2435.64 rows=1 width=4) (actual time=700.630..700.630 rows=10 loops=1)
              Group Key: products_1.id
              ->  Limit  (cost=2435.62..2435.62 rows=1 width=72) (actual time=700.619..700.619 rows=10 loops=1)
                    ->  Sort  (cost=2434.00..2435.62 rows=646 width=72) (actual time=697.304..699.868 rows=30010 loops=1)
                          Sort Key: products_1.score, products_1.title
                          Sort Method: quicksort  Memory: 7396kB
                          ->  Bitmap Heap Scan on products products_1  (cost=17.01..2403.85 rows=646 width=72) (actual time=10.796..32.258 rows=41666 loops=1)
                                Recheck Cond: (category_ids @> '{248688}'::integer[])
                                Heap Blocks: exact=6471
                                ->  Bitmap Index Scan on idx_products_category_ids_gin  (cost=0.00..16.85 rows=646 width=0) (actual time=9.234..9.234 rows=41666 loops=1)
                                      Index Cond: (category_ids @> '{248688}'::integer[])
        ->  Index Scan using products_pkey on products  (cost=0.42..8.45 rows=1 width=1390) (actual time=0.004..0.004 rows=1 loops=10)
              Index Cond: (id = products_1.id)
Planning time: 1.015 ms
Execution time: 700.918 ms

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 :

CREATE TABLE prodcats AS SELECT id AS product_id, unnest(category_ids) AS category_id FROM products;
CREATE INDEX idx_prodcats_cat_prod_id ON prodcats (category_id, product_id);

SELECT p.* FROM products p JOIN prodcats c ON (p.id=c.product_id)
WHERE c.category_id=248688
ORDER BY p.score DESC, p.title LIMIT 10 OFFSET 30000;

Limit  (cost=122480.06..122480.09 rows=10 width=1390) (actual time=1290.360..1290.362 rows=10 loops=1)
  ->  Sort  (cost=122405.06..122509.00 rows=41574 width=1390) (actual time=1264.250..1289.575 rows=30010 loops=1)
        Sort Key: p.score, p.title
        Sort Method: external merge  Disk: 29656kB
        ->  Merge Join  (cost=50.46..94061.13 rows=41574 width=1390) (actual time=117.746..182.048 rows=41666 loops=1)
              Merge Cond: (p.id = c.product_id)
              ->  Index Scan using products_pkey on products p  (cost=0.42..90738.43 rows=646067 width=1390) (actual time=0.034..116.313 rows=210283 loops=1)
              ->  Index Only Scan using idx_prodcats_cat_prod_id on prodcats c  (cost=0.43..1187.98 rows=41574 width=4) (actual time=0.022..7.137 rows=41666 loops=1)
                    Index Cond: (category_id = 248688)
                    Heap Fetches: 0
Planning time: 0.873 ms
Execution time: 1294.826 ms

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:

SELECT p.* FROM products p JOIN prodcats c ON (p.id=c.product_id)
WHERE c.category_id=248688
ORDER BY p.score DESC, p.title LIMIT 10 OFFSET 300;

Limit  (cost=3986.65..4119.51 rows=10 width=1390) (actual time=264.176..264.574 rows=10 loops=1)
  ->  Nested Loop  (cost=0.98..552334.77 rows=41574 width=1390) (actual time=250.378..264.558 rows=310 loops=1)
        ->  Index Scan using idx_test7 on products p  (cost=0.55..194665.62 rows=646067 width=1390) (actual time=0.030..83.026 rows=108037 loops=1)
        ->  Index Only Scan using idx_prodcats_cat_prod_id on prodcats c  (cost=0.43..0.54 rows=1 width=4) (actual time=0.001..0.001 rows=0 loops=108037)
              Index Cond: ((category_id = 248688) AND (product_id = p.id))
              Heap Fetches: 0
Planning time: 0.585 ms
Execution time: 264.664 ms

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:

SELECT * FROM products WHERE id in (
  SELECT id FROM products WHERE category_ids @> ARRAY[248688]
  ORDER BY score DESC, title LIMIT 10 OFFSET 300
) ORDER BY score DESC, title;

Sort  (cost=2519.53..2519.55 rows=10 width=1390) (actual time=143.809..143.809 rows=10 loops=1)
  Sort Key: products.score, products.title
  Sort Method: quicksort  Memory: 35kB
  ->  Nested Loop  (cost=2435.14..2519.36 rows=10 width=1390) (actual time=143.693..143.736 rows=10 loops=1)
        ->  HashAggregate  (cost=2434.71..2434.81 rows=10 width=4) (actual time=143.678..143.680 rows=10 loops=1)
              Group Key: products_1.id
              ->  Limit  (cost=2434.56..2434.59 rows=10 width=72) (actual time=143.668..143.670 rows=10 loops=1)
                    ->  Sort  (cost=2433.81..2435.43 rows=646 width=72) (actual time=143.642..143.653 rows=310 loops=1)
                          Sort Key: products_1.score, products_1.title
                          Sort Method: top-N heapsort  Memory: 68kB
                          ->  Bitmap Heap Scan on products products_1  (cost=17.01..2403.85 rows=646 width=72) (actual time=11.625..31.868 rows=41666 loops=1)
                                Recheck Cond: (category_ids @> '{248688}'::integer[])
                                Heap Blocks: exact=6471
                                ->  Bitmap Index Scan on idx_products_category_ids_gin  (cost=0.00..16.85 rows=646 width=0) (actual time=9.916..9.916 rows=41666 loops=1)
                                      Index Cond: (category_ids @> '{248688}'::integer[])
        ->  Index Scan using products_pkey on products  (cost=0.42..8.45 rows=1 width=1390) (actual time=0.004..0.004 rows=1 loops=10)
              Index Cond: (id = products_1.id)
Planning time: 0.630 ms
Execution time: 143.921 ms

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 WHEREparámetros de consulta , así como los ORDER BYpará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:

CREATE TABLE prodcats AS SELECT id AS product_id, unnest(category_ids) AS category_id, score, title FROM products;
CREATE INDEX idx_prodcats_1 ON prodcats (category_id, score DESC, title, product_id);

SELECT * FROM products WHERE id in (SELECT product_id FROM prodcats WHERE category_id=248688 ORDER BY score DESC, title LIMIT 10 OFFSET 30000) ORDER BY score DESC, title;

Sort  (cost=2149.65..2149.67 rows=10 width=1390) (actual time=7.011..7.011 rows=10 loops=1)
  Sort Key: products.score, products.title
  Sort Method: quicksort  Memory: 35kB
  ->  Nested Loop  (cost=2065.26..2149.48 rows=10 width=1390) (actual time=6.916..6.950 rows=10 loops=1)
        ->  HashAggregate  (cost=2064.83..2064.93 rows=10 width=4) (actual time=6.902..6.904 rows=10 loops=1)
              Group Key: prodcats.product_id
              ->  Limit  (cost=2064.02..2064.71 rows=10 width=74) (actual time=6.893..6.895 rows=10 loops=1)
                    ->  Index Only Scan using idx_prodcats_1 on prodcats  (cost=0.56..2860.10 rows=41574 width=74) (actual time=0.010..6.173 rows=30010 loops=1)
                          Index Cond: (category_id = 248688)
                          Heap Fetches: 0
        ->  Index Scan using products_pkey on products  (cost=0.42..8.45 rows=1 width=1390) (actual time=0.003..0.003 rows=1 loops=10)
              Index Cond: (id = prodcats.product_id)
Planning time: 0.318 ms
Execution time: 7.066 ms

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_memconsulta de huella de memoria aumentada y reducida.


Usted no tiene que reiniciar para un cambio del general work_mempuesta en postgresql.conf. Recargar es suficiente. Y permítanme advertirme de no establecer work_memuna configuración global demasiado alta en un entorno multiusuario (tampoco demasiado baja). Si tiene algunas consultas que necesitan más work_mem, configúrelo más alto para la sesión solo con SET, o solo con la transacción SET LOCAL. Ver: dba.stackexchange.com/a/48633/3684
Erwin Brandstetter

Que gran respuesta. Me ayudó mucho, específicamente con el disco -> operación de clasificación en memoria, cambio rápido para una gran victoria, ¡gracias!
Ricardo Villamil

4

Aquí 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: 5696kBconsume menos de 6 MB, pero se está derramando en el disco. Necesita aumentar su work_memconfiguración en su postgresql.confarchivo 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_idscuá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 tu work_memes 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_idsen 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 como WHERE >, WHERE <o ORDER BYno 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 correspondientes product_id. En lugar de una columna en su tabla de productos que es una matriz entera de category_ids, elimine esta columna de matriz de su esquema y cree una tabla como

CREATE TABLE join_products_categories (product_id int, category_id int);

Luego, puede generar índices de árbol B en las dos columnas de la tabla puente,

CREATE INDEX idx_products_in_join_table ON join_products_categories (product_id);
CREATE INDEX idx_products_in_join_table ON join_products_categories (category_id);

Solo mi humilde opinión, pero estos cambios pueden marcar una gran diferencia para usted. Pruebe ese work_memcambio 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 BYrequisitos

CREATE INDEX idx_product_sort ON products (score DESC, title);

entonces 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.


También estaba pensando en usar la mesa de conexiones para evitar GIN. Pero no especificó cómo va a ayudar eso con la clasificación. Creo que no ayudará. Intenté unir la tabla de productos con un conjunto de identificadores de productos recopilados a través de la consulta GIN, que creo que es bastante similar a la combinación que está ofreciendo, y esa operación no pudo usar el índice b-tree en la puntuación y el título. Tal vez construí un índice incorrecto. ¿Podría por favor explicar eso?
Yaroslav Stavnichiy

Disculpas, tal vez no expliqué con suficiente claridad. La alteración de su work_memconfiguració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.
Chris
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.