sql >> Base de Datos >  >> RDS >> PostgreSQL

Indexación de bases de datos en PostgreSQL

La indexación de bases de datos es el uso de estructuras de datos especiales que tienen como objetivo mejorar el rendimiento al lograr el acceso directo a las páginas de datos. El índice de una base de datos funciona como la sección de índice de un libro impreso:al mirar en la sección de índice, es mucho más rápido identificar las páginas que contienen el término que nos interesa. Podemos ubicar fácilmente las páginas y acceder a ellas directamente. . Esto es en lugar de escanear las páginas del libro secuencialmente, hasta que encontremos el término que estamos buscando.

Los índices son una herramienta esencial en manos de un DBA. El uso de índices puede proporcionar grandes ganancias de rendimiento para una variedad de dominios de datos. PostgreSQL es conocido por su gran extensibilidad y la rica colección de complementos principales y de terceros, y la indexación no es una excepción a esta regla. Los índices de PostgreSQL cubren un amplio espectro de casos, desde los índices b-tree más simples en tipos escalares hasta los índices geoespaciales GiST, fts o json o índices GIN de matriz.

Sin embargo, los índices, por maravillosos que parezcan (¡y en realidad lo son!), no son gratuitos. Hay una cierta penalización que va con las escrituras en una tabla indexada. Por lo tanto, el DBA, antes de examinar sus opciones para crear un índice específico, primero debe asegurarse de que dicho índice tenga sentido, lo que significa que las ganancias de su creación superarán la pérdida de rendimiento en las escrituras.

Terminología del índice básico de PostgreSQL

Antes de describir los tipos de índices en PostgreSQL y su uso, echemos un vistazo a la terminología que cualquier DBA encontrará tarde o temprano al leer los documentos.

  • Método de acceso al índice (también llamado como método de acceso ):el tipo de índice (B-tree, GiST, GIN, etc.)
  • Tipo: el tipo de datos de la columna indexada
  • Operador: una función entre dos tipos de datos
  • Familia de operadores: operador de tipos de datos cruzados, agrupando operadores de tipos con comportamiento similar
  • Clase de operador (también mencionado como estrategia de índice ):define los operadores que utilizará el índice para una columna

En el catálogo del sistema de PostgreSQL, los métodos de acceso se almacenan en pg_am, las clases de operadores en pg_opclass, las familias de operadores en pg_opfamily. Las dependencias de lo anterior se muestran en el siguiente diagrama:

Tipos de índices en PostgreSQL

PostgreSQL proporciona los siguientes tipos de índice:

  • Árbol B: el índice predeterminado, aplicable a los tipos que se pueden ordenar
  • Hash: solo maneja la igualdad
  • GiST: adecuado para tipos de datos no escalares (por ejemplo, formas geométricas, pies, matrices)
  • SP-GiST: Espacio particionado GIST, una evolución de GiST para el manejo de estructuras no equilibradas (quadtrees, k-d árboles, radix árboles)
  • GINEBRA: adecuado para tipos complejos (por ejemplo, jsonb, fts, arrays)
  • BRIN: un tipo de índice relativamente nuevo que admite datos que se pueden ordenar almacenando valores mínimos/máximos en cada bloque

Bajo, intentaremos ensuciarnos las manos con algunos ejemplos del mundo real. Todos los ejemplos dados están hechos con PostgreSQL 10.0 (con clientes de 10 y 9 psql) en FreeBSD 11.1.

Índices de árbol B

Supongamos que tenemos la siguiente tabla:

create table part (
id serial primary key, 
partno varchar(20) NOT NULL UNIQUE, 
partname varchar(80) NOT NULL, 
partdescr text,
machine_id int NOT NULL
);
testdb=# \d part
                                  Table "public.part"
   Column       |         Type          |                     Modifiers                     
------------+-----------------------+---------------------------------------------------
 id         | integer                 | not null default nextval('part_id_seq'::regclass)
 partno     | character varying(20)| not null
 partname       | character varying(80)| not null
 partdescr      | text                    |
 machine_id     | integer                 | not null
Indexes:
    "part_pkey" PRIMARY KEY, btree (id)
    "part_partno_key" UNIQUE CONSTRAINT, btree (partno)

Cuando definimos esta tabla bastante común, PostgreSQL crea dos índices de árbol B únicos detrás de escena:part_pkey y part_partno_key. Entonces, cada restricción única en PostgreSQL se implementa con un ÍNDICE único. Completemos nuestra tabla con un millón de filas de datos:

testdb=# with populate_qry as (select gs from generate_series(1,1000000) as gs )
insert into part (partno, partname,machine_id) SELECT 'PNo:'||gs, 'Part '||gs,0 from populate_qry;
INSERT 0 1000000

Ahora intentemos hacer algunas consultas en nuestra tabla. Primero le decimos al cliente psql que informe los tiempos de consulta escribiendo \timing:

testdb=# select * from part where id=100000;
   id   |   partno   |  partname   | partdescr | machine_id
--------+------------+-------------+-----------+------------
 100000 | PNo:100000 | Part 100000 |           |          0
(1 row)

Time: 0,284 ms
testdb=# select * from part where partno='PNo:100000';
   id   |   partno   |  partname   | partdescr | machine_id
--------+------------+-------------+-----------+------------
 100000 | PNo:100000 | Part 100000 |           |          0
(1 row)

Time: 0,319 ms

Observamos que solo se necesitan fracciones de milisegundo para obtener nuestros resultados. Esperábamos esto ya que para ambas columnas utilizadas en las consultas anteriores, ya hemos definido los índices apropiados. Ahora intentemos consultar en la columna partname, para la cual no existe índice.

testdb=# select * from part where partname='Part 100000';
   id   |   partno   |  partname   | partdescr | machine_id
--------+------------+-------------+-----------+------------
 100000 | PNo:100000 | Part 100000 |           |          0
(1 row)

Time: 89,173 ms

Aquí vemos claramente que para la columna no indexada, el rendimiento cae significativamente. Ahora vamos a crear un índice en esa columna y repetir la consulta:

testdb=# create index part_partname_idx ON part(partname);
CREATE INDEX
Time: 15734,829 ms (00:15,735)
testdb=# select * from part where partname='Part 100000';
   id   |   partno   |  partname   | partdescr | machine_id
--------+------------+-------------+-----------+------------
 100000 | PNo:100000 | Part 100000 |           |          0
(1 row)

Time: 0,525 ms

Nuestro nuevo índice part_partname_idx también es un índice B-tree (el valor predeterminado). En primer lugar, observamos que la creación del índice en la tabla del millón de filas tomó una cantidad de tiempo significativa, alrededor de 16 segundos. Luego observamos que nuestra velocidad de consulta aumentó de 89 ms a 0,525 ms. Los índices de árbol B, además de verificar la igualdad, también pueden ayudar con consultas que involucran a otros operadores en tipos ordenados, como <,<=,>=,>. Probemos con <=y>=

testdb=# select count(*) from part where partname>='Part 9999900';
 count
-------
     9
(1 row)

Time: 0,359 ms
testdb=# select count(*) from part where partname<='Part 9999900';
 count  
--------
 999991
(1 row)

Time: 355,618 ms

La primera consulta es mucho más rápida que la segunda, al usar las palabras clave EXPLAIN (o EXPLAIN ANALYZE) podemos ver si se usa o no el índice real:

testdb=# explain select count(*) from part where partname>='Part 9999900';
                                       QUERY PLAN                                        
-----------------------------------------------------------------------------------------
 Aggregate  (cost=8.45..8.46 rows=1 width=8)
   ->  Index Only Scan using part_partname_idx on part  (cost=0.42..8.44 rows=1 width=0)
         Index Cond: (partname >= 'Part 9999900'::text)
(3 rows)

Time: 0,671 ms
testdb=# explain select count(*) from part where partname<='Part 9999900';
                                       QUERY PLAN                                       
----------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=14603.22..14603.23 rows=1 width=8)
   ->  Gather  (cost=14603.00..14603.21 rows=2 width=8)
         Workers Planned: 2
         ->  Partial Aggregate  (cost=13603.00..13603.01 rows=1 width=8)
               ->  Parallel Seq Scan on part  (cost=0.00..12561.33 rows=416667 width=0)
                     Filter: ((partname)::text <= 'Part 9999900'::text)
(6 rows)

Time: 0,461 ms

En el primer caso, el planificador de consultas elige utilizar el índice part_partname_idx. También observamos que esto dará como resultado un escaneo de solo índice, lo que significa que no hay acceso a las tablas de datos. En el segundo caso, el planificador determina que no tiene sentido usar el índice ya que los resultados devueltos son una gran parte de la tabla, en cuyo caso se cree que un escaneo secuencial es más rápido.

Índices hash

Se desaconsejó el uso de índices hash hasta e incluyendo PgSQL 9.6 debido a razones relacionadas con la falta de escritura WAL. A partir de PgSQL 10.0, esos problemas se solucionaron, pero los índices hash aún tenían poco sentido de usar. Hay esfuerzos en PgSQL 11 para hacer que los índices hash sean un método de índice de primera clase junto con sus hermanos mayores (B-tree, GiST, GIN). Entonces, con esto en mente, probemos un índice hash en acción.

Enriqueceremos nuestra tabla de piezas con una nueva columna tipo de pieza y la completaremos con valores de distribución equitativa, y luego ejecutaremos una consulta que pruebe el tipo de pieza igual a 'Dirección':

testdb=# alter table part add parttype varchar(100) CHECK (parttype in ('Engine','Suspension','Driveline','Brakes','Steering','General')) NOT NULL DEFAULT 'General';
ALTER TABLE
Time: 42690,557 ms (00:42,691)
testdb=# with catqry as  (select id,(random()*6)::int % 6 as cat from part)
update part SET parttype = CASE WHEN cat=1 THEN 'Engine' WHEN cat=2 THEN 'Suspension' WHEN cat=3 THEN 'Driveline' WHEN cat=4 THEN 'Brakes' WHEN cat=5 THEN 'Steering' ELSE 'General' END FROM catqry WHERE part.id=catqry.id;
UPDATE 1000000
Time: 46345,386 ms (00:46,345)
testdb=# select count(*) from part where id % 500 = 0 AND parttype = 'Steering';
 count
-------
   322
(1 row)

Time: 93,361 ms

Ahora creamos un índice Hash para esta nueva columna y volvemos a intentar la consulta anterior:

testdb=# create index part_parttype_idx ON part USING hash(parttype);
CREATE INDEX
Time: 95525,395 ms (01:35,525)
testdb=# analyze ;
ANALYZE
Time: 1986,642 ms (00:01,987)
testdb=# select count(*) from part where id % 500 = 0 AND parttype = 'Steering';
 count
-------
   322
(1 row)

Time: 63,634 ms

Notamos la mejora después de usar el índice hash. Ahora compararemos el rendimiento de un índice hash en números enteros con el índice de árbol b equivalente.

testdb=# update part set machine_id = id;
UPDATE 1000000
Time: 392548,917 ms (06:32,549)
testdb=# select * from part where id=500000;
   id   |   partno   |  partname   | partdescr | machine_id |  parttype  
--------+------------+-------------+-----------+------------+------------
 500000 | PNo:500000 | Part 500000 |           |     500000 | Suspension
(1 row)

Time: 0,316 ms
testdb=# select * from part where machine_id=500000;
   id   |   partno   |  partname   | partdescr | machine_id |  parttype  
--------+------------+-------------+-----------+------------+------------
 500000 | PNo:500000 | Part 500000 |           |     500000 | Suspension
(1 row)

Time: 97,037 ms
testdb=# create index part_machine_id_idx ON part USING hash(machine_id);
CREATE INDEX
Time: 4756,249 ms (00:04,756)
testdb=#
testdb=# select * from part where machine_id=500000;
   id   |   partno   |  partname   | partdescr | machine_id |  parttype  
--------+------------+-------------+-----------+------------+------------
 500000 | PNo:500000 | Part 500000 |           |     500000 | Suspension
(1 row)

Time: 0,297 ms

Como vemos, con el uso de índices hash, la velocidad de las consultas que verifican la igualdad es muy cercana a la velocidad de los índices B-tree. Se dice que los índices hash son marginalmente más rápidos para la igualdad que los árboles B, de hecho, tuvimos que probar cada consulta dos o tres veces hasta que el índice hash dio un resultado mejor que el equivalente del árbol B.

Descargue el documento técnico hoy Administración y automatización de PostgreSQL con ClusterControlObtenga información sobre lo que necesita saber para implementar, monitorear, administrar y escalar PostgreSQLDescargar el documento técnico

Índices GiST

GiST (Árbol de búsqueda generalizado) es más que un solo tipo de índice, sino más bien una infraestructura para construir muchas estrategias de indexación. La distribución predeterminada de PostgreSQL brinda soporte para tipos de datos geométricos, tsquery y tsvector. En contrib hay implementaciones de muchas otras clases de operadores. Al leer los documentos y el directorio de contribuciones, el lector observará que existe una superposición bastante grande entre los casos de uso de GiST y GIN:arreglos int, búsqueda de texto completo para nombrar los casos principales. En esos casos, GIN es más rápido, y la documentación oficial lo establece explícitamente. Sin embargo, GiST proporciona una amplia compatibilidad con tipos de datos geométricos. Además, al momento de escribir este artículo, GiST (y SP-GiST) es el único método significativo que se puede usar con restricciones de exclusión. Veremos un ejemplo sobre esto. Supongamos (manteniéndose en el campo de la ingeniería mecánica) que tenemos un requisito para definir variaciones de tipo de máquinas para un tipo de máquina en particular, que son válidas para un cierto período de tiempo; y que para una variación en particular, no puede existir otra variación para el mismo tipo de máquina cuyo período de tiempo se superponga (entre en conflicto) con el período de variación en particular.

create table machine_type (
	id SERIAL PRIMARY KEY, 
	mtname varchar(50) not null, 
	mtvar varchar(20) not null, 
	start_date date not null, 
	end_date date, 
	CONSTRAINT machine_type_uk UNIQUE (mtname,mtvar)
);

Arriba le decimos a PostgreSQL que para cada nombre de tipo de máquina (mtname) solo puede haber una variación (mtvar). Start_date indica la fecha de inicio del período en el que esta variación del tipo de máquina es válida y end_date indica la fecha de finalización de este período. Null end_date significa que la variación del tipo de máquina es actualmente válida. Ahora queremos expresar el requisito de no superposición con una restricción. La forma de hacerlo es con una restricción de exclusión:

testdb=# alter table machine_type ADD CONSTRAINT machine_type_per EXCLUDE USING GIST (mtname WITH =,daterange(start_date,end_date) WITH &&);

La sintaxis EXCLUDE de PostgreSQL nos permite especificar muchas columnas de diferentes tipos y con un operador diferente para cada una. &&es el operador superpuesto para intervalos de fechas y =es el operador de igualdad común para varchar. Pero mientras presionamos enter, PostgreSQL se queja con un mensaje:

ERROR:  data type character varying has no default operator class for access method "gist"
HINT:  You must specify an operator class for the index or define a default operator class for the data type.

Lo que falta aquí es el soporte de opclass de GiST para varchar. Siempre que hayamos creado e instalado correctamente la extensión btree_gist, podemos continuar con la creación de la extensión:

testdb=# create extension btree_gist ;
CREATE EXTENSION

Y luego volver a intentar crear la restricción y probarla:

testdb=# alter table machine_type ADD CONSTRAINT machine_type_per EXCLUDE USING GIST (mtname WITH =,daterange(start_date,end_date) WITH &&);
ALTER TABLE
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SH','2008-01-01','2013-01-01');
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SG','2002-01-01','2009-01-01');
ERROR:  conflicting key value violates exclusion constraint "machine_type_per"
DETAIL:  Key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2002-01-01,2009-01-01)) conflicts with existing key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2008-01-01,2013-01-01)).
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SG','2002-01-01','2008-01-01');
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SJ','2013-01-01',null);
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SJ2','2018-01-01',null);
ERROR:  conflicting key value violates exclusion constraint "machine_type_per"
DETAIL:  Key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2018-01-01,)) conflicts with existing key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2013-01-01,)).

Índices SP-GiST

SP-GiST, que significa GiST con particiones espaciales, como GiST, es una infraestructura que permite el desarrollo de muchas estrategias diferentes en el dominio de estructuras de datos basadas en disco no balanceadas. La distribución predeterminada de PgSQL ofrece soporte para puntos bidimensionales, (cualquier tipo) rangos, texto y tipos inet. Al igual que GiST, SP-GiST se puede utilizar en restricciones de exclusión, de manera similar al ejemplo que se muestra en el capítulo anterior.

Índices GIN

GIN (Índice invertido generalizado) como GiST y SP-GiST pueden proporcionar muchas estrategias de indexación. GIN es adecuado cuando queremos indexar columnas de tipos compuestos. La distribución de PostgreSQL predeterminada brinda soporte para cualquier tipo de matriz, jsonb y búsqueda de texto completo (tsvector). En contrib hay implementaciones de muchas otras clases de operadores. Jsonb, una característica muy elogiada de PostgreSQL (y un desarrollo relativamente reciente (9.4+)) se basa en GIN para la compatibilidad con índices. Otro uso común de GIN es la indexación para la búsqueda de texto completo. La búsqueda de texto completo en PgSQL merece un artículo por sí solo, por lo que cubriremos aquí solo la parte de indexación. Primero, preparemos nuestra tabla, dando valores no nulos a la columna partdescr y actualizando una sola fila con un valor significativo:

testdb=# update part set partdescr ='';
UPDATE 1000000
Time: 383407,114 ms (06:23,407)
testdb=# update part set partdescr = 'thermostat for the cooling system' where id=500000;
UPDATE 1
Time: 2,405 ms

Luego realizamos una búsqueda de texto en la columna recién actualizada:

testdb=# select * from part where partdescr @@ 'thermostat';
   id   |   partno   |  partname   |             partdescr             | machine_id |  parttype  
--------+------------+-------------+-----------------------------------+------------+------------
 500000 | PNo:500000 | Part 500000 | thermostat for the cooling system |     500000 | Suspension
(1 row)

Time: 2015,690 ms (00:02,016)

Esto es bastante lento, casi 2 segundos para traer nuestro resultado. Ahora intentemos crear un índice GIN en el tipo tsvector y repitamos la consulta, usando una sintaxis amigable con el índice:

testdb=# CREATE INDEX part_partdescr_idx ON part USING gin(to_tsvector('english',partdescr));
CREATE INDEX
Time: 1431,550 ms (00:01,432)
testdb=# select * from part where to_tsvector('english',partdescr) @@ to_tsquery('thermostat');
   id   |   partno   |  partname   |             partdescr             | machine_id |  parttype  
--------+------------+-------------+-----------------------------------+------------+------------
 500000 | PNo:500000 | Part 500000 | thermostat for the cooling system |     500000 | Suspension
(1 row)

Time: 0,952 ms

Y obtenemos una aceleración de 2000 veces. También podemos notar el tiempo relativamente corto que tomó crear el índice. Puede experimentar con el uso de GiST en lugar de GIN en el ejemplo anterior y medir el rendimiento de lecturas, escrituras y creación de índices para ambos métodos de acceso.

Índices BRIN

BRIN (Block Range Index) es la incorporación más reciente al conjunto de tipos de índice de PostgreSQL, ya que se introdujo en PostgreSQL 9.5, y tiene solo unos pocos años como característica central estándar. BRIN funciona en tablas muy grandes al almacenar información de resumen para un conjunto de páginas llamado "Rango de bloque". Los índices BRIN tienen pérdidas (como GiST) y esto requiere una lógica adicional en el ejecutor de consultas de PostgreSQL y también la necesidad de un mantenimiento adicional. Veamos BRIN en acción:

testdb=# select count(*) from part where machine_id BETWEEN 5000 AND 10000;
 count
-------
  5001
(1 row)

Time: 100,376 ms
testdb=# create index part_machine_id_idx_brin ON part USING BRIN(machine_id);
CREATE INDEX
Time: 569,318 ms
testdb=# select count(*) from part where machine_id BETWEEN 5000 AND 10000;
 count
-------
  5001
(1 row)

Time: 5,461 ms

Aquí vemos en promedio una aceleración de ~ 18 veces por el uso del índice BRIN. Sin embargo, el verdadero hogar de BRIN está en el dominio de los grandes datos, por lo que esperamos probar esta tecnología relativamente nueva en escenarios del mundo real en el futuro.