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

Sobre la utilidad de los índices de expresión

Cuando imparto capacitaciones de PostgreSQL, tanto en temas básicos como avanzados, a menudo descubro que los asistentes tienen muy poca idea de cuán poderosos pueden ser los índices de expresión (si es que los conocen). Así que déjame darte una breve descripción.

Entonces, digamos que tenemos una tabla, con un rango de marcas de tiempo (sí, tenemos la función generate_series que puede generar fechas):

CREATE TABLE t AS
SELECT d, repeat(md5(d::text), 10) AS padding
  FROM generate_series(timestamp '1900-01-01',
                       timestamp '2100-01-01',
                       interval '1 day') s(d);
VACUUM ANALYZE t;

La mesa también incluye una columna de relleno, para que sea un poco más grande. Ahora, hagamos una consulta de rango simple, seleccionando solo un mes de los ~200 años incluidos en la tabla. Si explica la consulta, verá algo como esto:

EXPLAIN SELECT * FROM t WHERE d BETWEEN '2001-01-01' AND '2001-02-01';

                               QUERY PLAN
------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..4416.75 rows=32 width=332)
   Filter: ((d >= '2001-01-01 00:00:00'::timestamp without time zone)
        AND (d <= '2001-02-01 00:00:00'::timestamp without time zone))
(2 rows)

y en mi computadora portátil, esto se ejecuta en ~ 20 ms. No está mal, considerando que esto tiene que recorrer toda la tabla con ~75k filas.

Pero vamos a crear un índice en la columna de marca de tiempo (todos los índices aquí son del tipo predeterminado, es decir, btree, a menos que se mencione explícitamente):

CREATE INDEX idx_t_d ON t (d);

Y ahora intentemos ejecutar la consulta de nuevo:

                               QUERY PLAN
------------------------------------------------------------------------
 Index Scan using idx_t_d on t  (cost=0.29..9.97 rows=34 width=332)
   Index Cond: ((d >= '2001-01-01 00:00:00'::timestamp without time zone)
            AND (d <= '2001-02-01 00:00:00'::timestamp without time zone))
(2 rows)

y esto se ejecuta en 0,5 ms, por lo que aproximadamente 40 veces más rápido. Pero eso fue, por supuesto, índices simples, creados directamente en la columna, no en el índice de expresión. Así que supongamos que necesitamos seleccionar datos de cada primer día de cada mes, haciendo una consulta como esta

SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;

que, sin embargo, no puede usar el índice, ya que necesita evaluar una expresión en la columna mientras que el índice se crea en la columna misma, como se muestra en EXPLAIN ANALYZE:

                               QUERY PLAN
------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..4416.75 rows=365 width=332)
                (actual time=0.045..40.601 rows=2401 loops=1)
   Filter: (date_part('day'::text, d) = '1'::double precision)
   Rows Removed by Filter: 70649
 Planning time: 0.209 ms
 Execution time: 43.018 ms
(5 rows)

Entonces, no solo tiene que hacer un escaneo secuencial, también tiene que hacer la evaluación, aumentando la duración de la consulta a 43 ms.

La base de datos no puede utilizar el índice por varios motivos. Los índices (al menos los índices btree) dependen de la consulta de datos ordenados, proporcionados por la estructura de árbol, y mientras que la consulta de rango puede beneficiarse de eso, la segunda consulta (con la llamada `extraer`) no puede.

Nota:Otro problema es que el conjunto de operadores compatibles con los índices (es decir, que se pueden evaluar directamente en los índices) es muy limitado. Y la función "extraer" no es compatible, por lo que la consulta no puede solucionar el problema de pedido mediante un escaneo de índice de mapa de bits.

En teoría, la base de datos podría tratar de transformar la condición en condiciones de rango, pero eso es extremadamente difícil y específico de la expresión. En este caso, tendríamos que generar un número infinito de tales rangos "por día", porque el planificador realmente no conoce las marcas de tiempo mínimas y máximas en la tabla. Entonces la base de datos ni siquiera lo intenta.

Pero mientras que la base de datos no sabe cómo transformar las condiciones, los desarrolladores a menudo sí lo saben. Por ejemplo, con condiciones como

(column + 1) >= 1000

no es difícil reescribirlo así

column >= (1000 - 1)

que funciona bien con los índices.

Pero, ¿y si tal transformación no es posible, como por ejemplo para la consulta de ejemplo?

SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;

En este caso, el desarrollador tendría que enfrentar el mismo problema con un mínimo/máximo desconocido para la columna d, e incluso entonces generaría muchos rangos.

Bueno, esta publicación de blog trata sobre índices de expresión, y hasta ahora solo hemos usado índices regulares, creados directamente en la columna. Entonces, creemos el primer índice de expresión:

CREATE INDEX idx_t_expr ON t ((extract(day FROM d)));
ANALYZE t;

que luego nos da este plan explicativo

                               QUERY PLAN
------------------------------------------------------------------------
 Bitmap Heap Scan on t  (cost=47.35..3305.25 rows=2459 width=332)
                        (actual time=2.400..12.539 rows=2401 loops=1)
   Recheck Cond: (date_part('day'::text, d) = '1'::double precision)
   Heap Blocks: exact=2401
   ->  Bitmap Index Scan on idx_t_expr  (cost=0.00..46.73 rows=2459 width=0)
                                (actual time=1.243..1.243 rows=2401 loops=1)
         Index Cond: (date_part('day'::text, d) = '1'::double precision)
 Planning time: 0.374 ms
 Execution time: 17.136 ms
(7 rows)

Entonces, si bien esto no nos brinda la misma aceleración de 40x que el índice en el primer ejemplo, eso es algo esperado ya que esta consulta devuelve muchas más tuplas (2401 frente a 32). Además, están repartidos por toda la tabla y no tan localizados como en el primer ejemplo. Por lo tanto, es una buena aceleración 2x ​​y, en muchos casos del mundo real, verá mejoras mucho mayores.

Pero la capacidad de usar índices para condiciones con expresiones complejas no es la información más interesante aquí, esa es la razón por la cual las personas crean índices de expresión. Pero ese no es el único beneficio.

Si observa los dos planes de explicación presentados anteriormente (sin y con el índice de expresión), puede notar esto:

                               QUERY PLAN
------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..4416.75 rows=365 width=332)
                (actual time=0.045..40.601 rows=2401 loops=1)
 ...
                               QUERY PLAN
------------------------------------------------------------------------
 Bitmap Heap Scan on t  (cost=47.35..3305.25 rows=2459 width=332)
                        (actual time=2.400..12.539 rows=2401 loops=1)
 ...

Correcto:la creación del índice de expresión mejoró significativamente las estimaciones. Sin el índice, solo tenemos estadísticas (MCV + histograma) para las columnas de la tabla sin procesar, por lo que la base de datos no sabe cómo estimar la expresión

EXTRACT(day FROM d) = 1

Entonces, en su lugar, aplica una estimación predeterminada para las condiciones de igualdad, que es el 0,5 % de todas las filas; como la tabla tiene 73050 filas, terminamos con una estimación de solo 365 filas. Es común ver errores de estimación mucho peores en aplicaciones del mundo real.

Sin embargo, con el índice, la base de datos también recopiló estadísticas sobre las columnas del índice y, en este caso, la columna contiene los resultados de la expresión. Y durante la planificación, el optimizador se da cuenta de esto y produce una estimación mucho mejor.

Este es un gran beneficio y puede ayudar a corregir algunos casos de planes de consulta deficientes causados ​​​​por estimaciones inexactas. Sin embargo, la mayoría de las personas desconocen esta práctica herramienta.

Y la utilidad de esta herramienta solo aumentó con la introducción del tipo de datos JSONB en 9.4, porque es la única forma de recopilar estadísticas sobre el contenido de los documentos JSONB.

Al indexar documentos JSONB, existen dos estrategias básicas de indexación. Puede crear un índice GIN/GiST en todo el documento, p. así

CREATE INDEX ON t USING GIN (jsonb_column);

lo que le permite consultar rutas arbitrarias en la columna JSONB, usar el operador de contención para hacer coincidir los subdocumentos, etc. Eso es genial, pero aún tiene solo las estadísticas básicas por columna, que
no son muy útiles como los documentos se tratan como valores escalares (y nadie coincide con documentos completos o usa un rango de documentos).

Índices de expresión, por ejemplo creados así:

CREATE INDEX ON t ((jsonb_column->'id'));

solo será útil para la expresión en particular, es decir, este índice recién creado será útil para

SELECT * FROM t WHERE jsonb_column ->> 'id' = 123;

pero no para consultas que acceden a otras claves JSON, como 'valor', por ejemplo

SELECT * FROM t WHERE jsonb_column ->> 'value' = 'xxxx';

Esto no quiere decir que los índices GIN/GiST en todo el documento sean inútiles, pero tiene que elegir. O crea un índice de expresión enfocado, útil cuando se consulta una clave en particular y con el beneficio adicional de las estadísticas sobre la expresión. O crea un índice GIN/GiST en todo el documento, capaz de manejar consultas en claves arbitrarias, pero sin las estadísticas.

Sin embargo, puede tener un pastel y comérselo también, en este caso, porque puede crear ambos índices al mismo tiempo, y la base de datos elegirá cuál de ellos usar para consultas individuales. Y tendrás estadísticas precisas, gracias a los índices de expresión.

Lamentablemente, no puedes comerte todo el pastel, porque los índices de expresión y los índices GIN/GiST usan condiciones diferentes

-- expression (btree)
SELECT * FROM t WHERE jsonb_column ->> 'id' = 123;

-- GIN/GiST
SELECT * FROM t WHERE jsonb_column @> '{"id" : 123}';

por lo que el planificador no puede usarlos al mismo tiempo:índices de expresión para estimación y GIN/GiST para ejecución.