Trabajo de índices en PostgreSQL


73

Tengo un par de preguntas sobre el funcionamiento de los índices en PostgreSQL. Tengo una Friendstabla con el siguiente índice:

   Friends ( user_id1 ,user_id2) 

user_id1y user_id2son claves foráneas para la usermesa

  1. ¿Son estos equivalentes? Si no es así, ¿por qué?

    Index(user_id1,user_id2) and Index(user_id2,user_id1)
  2. Si creo la clave primaria (user_id1, user_id2), ¿crea automáticamente índices para ella y

    Si los índices en la primera pregunta no son equivalentes, ¿qué índice se crea en el comando de teclado primario anterior?

Respuestas:


77

Estos son los resultados de consultar una tabla en la segunda columna de un índice de varias columnas .
Los efectos son fáciles de reproducir para cualquiera. Solo inténtalo en casa.

Probé con PostgreSQL 9.0.5 en Debian usando una tabla de tamaño medio de una base de datos de la vida real con 23322 filas. Implementa la relación n: m entre las tablas adr(dirección) y att(atributo), pero eso no es relevante aquí. Esquema simplificado:

CREATE TABLE adratt (
  adratt_id serial PRIMARY KEY
, adr_id    integer NOT NULL
, att_id    integer NOT NULL
, log_up    timestamp(0) NOT NULL DEFAULT (now())::timestamp(0)
, CONSTRAINT adratt_uni UNIQUE (adr_id, att_id)
);

La UNIQUErestricción implementa efectivamente un índice único. Repetí la prueba con un índice simple para estar seguro y obtuve resultados idénticos a los esperados.

CREATE INDEX adratt_idx ON adratt(adr_id, att_id)

La tabla está agrupada en el adratt_uniíndice y antes de la prueba que ejecuté:

CLUSTER adratt;
ANALYZE adratt;

Los escaneos secuenciales para consultas (adr_id, att_id)son tan rápidos como sea posible. El índice de varias columnas se seguirá utilizando para una condición de consulta solo en la segunda columna de índice.

Ejecuté las consultas un par de veces para llenar el caché y elegí la mejor de diez ejecuciones para obtener resultados comparables.

1. Consulta usando ambas columnas

SELECT *
FROM   adratt
WHERE  att_id = 90
AND    adr_id = 10;

 adratt_id | adr_id | att_id |       log_up
-----------+--------+--------+---------------------
       123 |     10 |     90 | 2008-07-29 09:35:54
(1 row)

Salida de EXPLAIN ANALYZE:

Index Scan using adratt_uni on adratt  (cost=0.00..3.48 rows=1 width=20) (actual time=0.022..0.025 rows=1 loops=1)
  Index Cond: ((adr_id = 10) AND (att_id = 90))
Total runtime: 0.067 ms

2. Consulta usando la primera columna

SELECT * FROM adratt WHERE adr_id = 10

 adratt_id | adr_id | att_id |       log_up
-----------+--------+--------+---------------------
       126 |     10 |     10 | 2008-07-29 09:35:54
       125 |     10 |     13 | 2008-07-29 09:35:54
      4711 |     10 |     21 | 2008-07-29 09:35:54
     29322 |     10 |     22 | 2011-06-06 15:50:38
     29321 |     10 |     30 | 2011-06-06 15:47:17
       124 |     10 |     62 | 2008-07-29 09:35:54
     21913 |     10 |     78 | 2008-07-29 09:35:54
       123 |     10 |     90 | 2008-07-29 09:35:54
     28352 |     10 |    106 | 2010-11-22 12:37:50
(9 rows)

Salida de EXPLAIN ANALYZE:

Index Scan using adratt_uni on adratt  (cost=0.00..8.23 rows=9 width=20) (actual time=0.007..0.023 rows=9 loops=1)
  Index Cond: (adr_id = 10)
Total runtime: 0.058 ms

3. Consulta usando la segunda columna

SELECT * FROM adratt WHERE att_id = 90

 adratt_id | adr_id | att_id |       log_up
-----------+--------+--------+---------------------
       123 |     10 |     90 | 2008-07-29 09:35:54
       180 |     39 |     90 | 2008-08-29 15:46:07
...
(83 rows)

Salida de EXPLAIN ANALYZE:

Index Scan using adratt_uni on adratt  (cost=0.00..818.51 rows=83 width=20) (actual time=0.014..0.694 rows=83 loops=1)
  Index Cond: (att_id = 90)
Total runtime: 0.849 ms

4. Deshabilitar indexscan y bitmapscan

SET enable_indexscan = off;
SELECT * FROM adratt WHERE att_id = 90

Salida de EXPLAIN ANALYZE:

Bitmap Heap Scan on adratt  (cost=779.94..854.74 rows=83 width=20) (actual time=0.558..0.743 rows=83 loops=1)
  Recheck Cond: (att_id = 90)
  ->  Bitmap Index Scan on adratt_uni  (cost=0.00..779.86 rows=83 width=0) (actual time=0.544..0.544 rows=83 loops=1)
        Index Cond: (att_id = 90)
Total runtime: 0.894 ms
SET enable_bitmapscan = off
SELECT * FROM adratt WHERE att_id = 90

Salida de EXPLAIN ANALYZE:

Seq Scan on adratt  (cost=0.00..1323.10 rows=83 width=20) (actual time=0.009..2.429 rows=83 loops=1)
  Filter: (att_id = 90)
Total runtime: 2.680 ms

Conclusión

Como se esperaba, el índice de varias columnas se usa para una consulta solo en la segunda columna.
Como se esperaba, es menos efectivo, pero la consulta sigue siendo 3 veces más rápida que sin el índice.
Después de deshabilitar las exploraciones de índice, el planificador de consultas elige una exploración de montón de mapa de bits, que funciona casi tan rápido. Solo después de deshabilitar eso, también, recurre a un escaneo secuencial.


la agrupación hará una diferencia si el número de coincidencias en el índice es lo suficientemente alto (vea aquí para la prueba - tenga en cuenta las corridas dobles para almacenar en caché los datos)
Jack Douglas

1
@JackDouglas: He pensado más en esto. La agrupación puede ayudar en general, porque efectivamente también es a vacuum fully a reindex. Aparte de eso, ayudará a indexar los escaneos en la primera o ambas columnas iniciales mucho , pero perjudicará las consultas en la segunda columna. En una tabla recién agrupada, las filas con el mismo valor en la segunda columna se extienden, por lo que deberá leerse un máximo de bloques.
Erwin Brandstetter

28

re 1) Sí y no.

Para una consulta que usa ambas columnas, por ejemplo where (user_id1, user_id2) = (1,2), no importa qué índice se cree.

Para una consulta que tiene una condición en solo una de las columnas, por ejemplo where user_id1 = 1, es importante porque generalmente el optimizador solo puede usar las columnas "iniciales" para una comparación. Por where user_id1 = 1lo tanto, podría usar el índice (user_id1, user_id2) pero no podría usar un índice (user_id2, user_id1) para todos los casos.

Después de jugar con esto (después de que Erwin nos mostró amablemente una configuración donde funciona), parece que esto depende en gran medida de la distribución de datos de la segunda columna, aunque todavía no he descubierto qué situación permite que el optimizador use columnas finales para una condición WHERE.

Oracle 11 que también puede (a veces) usar columnas que no están al comienzo de la definición del índice.

re 2) Sí, creará un índice

Cita del manual

Agregar una clave primaria creará automáticamente un índice btree único en la columna o grupo de columnas utilizado en la clave primaria.

re 2a) Primary Key (user_id1,user_id2)creará un índice en (user_id1, user_id2) (que puede descubrir usted mismo muy fácilmente simplemente creando una clave primaria de este tipo)

Le recomiendo que lea el capítulo sobre índices en el manual , básicamente responde a todas las preguntas anteriores.

Además, ¿qué índice crear? by depesz hace un buen trabajo al explicar el orden en las columnas de índice y otros temas relacionados con el índice.


11

Anuncio 1)
Existen limitaciones en PostgreSQL como @a_horse_with_no_name describe . Hasta la versión 8.0, los índices de varias columnas solo se podían usar para consultas en las columnas iniciales. Esto se ha mejorado en la versión 8.1. El manual actual para Postgres 10 (actualizado) explica:

Se puede usar un índice de árbol B de varias columnas con condiciones de consulta que involucren cualquier subconjunto de las columnas del índice, pero el índice es más eficiente cuando hay restricciones en las columnas iniciales (más a la izquierda). La regla exacta es que las restricciones de igualdad en las columnas iniciales, más cualquier restricción de desigualdad en la primera columna que no tenga una restricción de igualdad, se utilizarán para limitar la parte del índice que se escanea. Las restricciones en las columnas a la derecha de estas columnas se verifican en el índice, por lo que guardan las visitas a la tabla propiamente dicha, pero no reducen la parte del índice que debe escanearse. Por ejemplo, dado un índice (a, b, c)y una condición de consulta WHERE a = 5 AND b >= 42 AND c < 77, el índice tendría que escanearse desde la primera entrada con a= 5 yb= 42 hasta la última entrada con a= 5. Se omitirían las entradas de índice con c> = 77, pero aún tendrían que escanearse. En principio, este índice podría usarse para consultas que tienen restricciones by / o csin restricciones a, pero todo el índice tendría que escanearse, por lo que en la mayoría de los casos el planificador preferiría una exploración de tabla secuencial en lugar de usar el índice.

El énfasis es mío. Puedo confirmar eso por experiencia.
También vea el caso de prueba agregado mi respuesta posterior aquí .


11

Esto es en respuesta a la respuesta de Jack , un comentario no serviría.

No había índices de cobertura en PostgreSQL antes de la versión 9.2. Debido al modelo MVCC, se debe visitar cada tupla en el conjunto de resultados para verificar la visibilidad. Puede que estés pensando en Oracle.

Los desarrolladores de PostgreSQL hablan sobre "escaneos de solo índice" . De hecho, la función se ha lanzado con Postgres 9.2. Lee el mensaje de confirmación .
Depesz escribió una publicación de blog muy informativa .

Los índices de cobertura verdaderos (actualización) se presentan con la INCLUDEcláusula con Postgres 11. Relacionado:

Esto también es un poco extraño:

se basa en el hecho de que una 'exploración completa' de un índice a menudo es más rápida que una 'exploración completa' de la tabla indexada debido a las columnas adicionales en la tabla que no aparecen en el índice.

Como se informó en los comentarios sobre mi otra respuesta, también he realizado pruebas con una tabla de dos enteros y nada más. El índice contiene las mismas columnas que la tabla. El tamaño de un índice btree es aproximadamente 2/3 del de la tabla. No es suficiente para explicar una aceleración del factor 3. Ejecuté más pruebas, basadas en su configuración, simplificadas a dos columnas y con 100000 filas. En mi instalación de PostgreSQL 9.0, los resultados fueron consistentes.

Si la tabla tiene columnas adicionales, la aceleración con índice se vuelve más sustancial, pero ese no es el único factor aquí .

Para resumir los puntos principales:

  • Los índices de varias columnas se pueden usar con consultas en columnas no iniciales, pero la aceleración es solo alrededor del factor 3 para criterios selectivos (pequeño porcentaje de filas en el resultado). Más alto para tuplas más grandes, más bajo para porciones más grandes de la tabla en el conjunto de resultados.

  • Cree un índice adicional en esas columnas si el rendimiento es importante.

  • Si todas las columnas involucradas están incluidas en un índice (índice de cobertura) y todas las filas involucradas (por bloque) son visibles para todas las transacciones, puede obtener un "escaneo de solo índice" en la página 9.2 o posterior.


7
  1. ¿Son estos equivalentes? Si no es así, ¿por qué?

    Index (user_id1, user_id2) e Index (user_id2, user_id1)

Estos no son equivalentes y, en general, el índice (bar, baz) no será eficiente para consultas del formulario select * from foo where baz=?

Erwin ha demostrado que tales índices pueden acelerar una consulta, pero este efecto es limitado y no es del mismo orden que generalmente se espera que un índice mejore una búsqueda: se basa en el hecho de que a menudo se realiza una 'exploración completa' de un índice más rápido que una 'exploración completa' de la tabla indexada debido a las columnas adicionales en la tabla que no aparecen en el índice.

Resumen: los índices pueden ayudar a las consultas incluso en columnas no iniciales, pero de una de las dos formas secundarias y relativamente menores y no de la forma dramática que normalmente espera que un índice ayude debido a su estructura btree

nb las dos formas en que el índice puede ayudar son si un análisis completo del índice es significativamente más barato que un análisis completo de la tabla y: 1. las búsquedas de la tabla son baratas (porque hay pocas o están agrupadas), o 2. el índice está cubriendo, por lo que no hay búsquedas de tablas en absoluto , vea los comentarios de Erwins aquí

banco de pruebas:

create table foo(bar integer not null, baz integer not null, qux text not null);

insert into foo(bar, baz, qux)
select random()*100, random()*100, 'some random text '||g from generate_series(1,10000) g;

consulta 1 (sin índice, golpeando 74 buffers ):

explain (buffers, analyze, verbose) select max(qux) from foo where baz=0;
                                                  QUERY PLAN
--------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=181.41..181.42 rows=1 width=32) (actual time=3.301..3.302 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=74
   ->  Seq Scan on stack.foo  (cost=0.00..181.30 rows=43 width=32) (actual time=0.043..3.228 rows=52 loops=1)
         Output: bar, baz, qux
         Filter: (foo.baz = 0)
         Buffers: shared hit=74
 Total runtime: 3.335 ms

consulta 2 (con índice - el optimizador ignora el índice - golpeando 74 buffers nuevamente):

create index bar_baz on foo(bar, baz);

explain (buffers, analyze, verbose) select max(qux) from foo where baz=0;
                                                  QUERY PLAN
--------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=199.12..199.13 rows=1 width=32) (actual time=3.277..3.277 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=74
   ->  Seq Scan on stack.foo  (cost=0.00..199.00 rows=50 width=32) (actual time=0.043..3.210 rows=52 loops=1)
         Output: bar, baz, qux
         Filter: (foo.baz = 0)
         Buffers: shared hit=74
 Total runtime: 3.311 ms

consulta 2 (con índice, y engañamos al optimizador para que lo use):

explain (buffers, analyze, verbose) select max(qux) from foo where bar>-1000 and baz=0;
                                                       QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=115.56..115.57 rows=1 width=32) (actual time=1.495..1.495 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=36 read=30
   ->  Bitmap Heap Scan on stack.foo  (cost=73.59..115.52 rows=17 width=32) (actual time=1.370..1.428 rows=52 loops=1)
         Output: bar, baz, qux
         Recheck Cond: ((foo.bar > (-1000)) AND (foo.baz = 0))
         Buffers: shared hit=36 read=30
         ->  Bitmap Index Scan on bar_baz  (cost=0.00..73.58 rows=17 width=0) (actual time=1.356..1.356 rows=52 loops=1)
               Index Cond: ((foo.bar > (-1000)) AND (foo.baz = 0))
               Buffers: shared read=30
 Total runtime: 1.535 ms

Por lo tanto, el acceso a través del índice es el doble de rápido en este caso, llegando a 30 buffers , ¡lo que en términos de indexación es 'ligeramente más rápido'! de los datos en la tabla

Por el contrario, las consultas en la columna inicial hacen uso de la estructura btree del índice, en este caso golpeando 2 buffers :

explain (buffers, analyze, verbose) select max(qux) from foo where bar=0;
                                                       QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=75.70..75.71 rows=1 width=32) (actual time=0.172..0.173 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=38
   ->  Bitmap Heap Scan on stack.foo  (cost=4.64..75.57 rows=50 width=32) (actual time=0.036..0.097 rows=59 loops=1)
         Output: bar, baz, qux
         Recheck Cond: (foo.bar = 0)
         Buffers: shared hit=38
         ->  Bitmap Index Scan on bar_baz  (cost=0.00..4.63 rows=50 width=0) (actual time=0.024..0.024 rows=59 loops=1)
               Index Cond: (foo.bar = 0)
               Buffers: shared hit=2
 Total runtime: 0.209 ms
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.