Recetas para consultas SQL problemáticas

Hace unos meses anunciamos explain.tensor.ru — un servicio público para el análisis y visualización de planes de consultas para PostgreSQL.

En el tiempo transcurrido, ya lo has utilizado más de 6000 veces, pero una de las funciones convenientes podría haber pasado desapercibida: se trata de sugerencias estructurales, que lucen aproximadamente así:

Recetas para consultas SQL problemáticas

Escucha estas sugerencias y tus consultas "serán suaves como la seda". 🙂

Y en serio, muchas situaciones que hacen que una consulta sea lenta y "hambrienta" de recursos, son típicas y pueden ser reconocidas por la estructura y los datos del plan.

En este caso, cada desarrollador individual no tendrá que buscar una opción de optimización por sí solo, basándose únicamente en su experiencia; podemos sugerirle qué está sucediendo, cuál podría ser la causa y cómo se puede abordar la solución. Lo que hicimos.

Recetas para consultas SQL problemáticas

Veamos estos casos con más detalle: cómo se determinan y a qué recomendaciones conducen.

Para una mejor comprensión del tema, primero se puede escuchar el bloque correspondiente de mi presentación en PGConf.Rusia 2020, y luego pasar al análisis detallado de cada ejemplo:

Reproducir video

#1: индексная «недосортировка»

Cuando surge

Mostrar la última factura del cliente «ООО Колокольчик».

Cómo identificar

-> Limit
   -> Sort
      -> Index [Only] Scan [Backward] | Bitmap Heap Scan

Recomendaciones

Índice utilizado ampliar con campos de ordenación.

Ejemplo:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk  -- 100K "hechos"
, (random() * 1000)::integer fk_cli; -- 1K diferentes claves externas

CREATE INDEX ON tbl(fk_cli); -- índice para clave foránea

SELECT
  *
FROM
  tbl
WHERE
  fk_cli = 1 -- selección por relación específica
ORDER BY
  pk DESC -- queremos solo un "último" registro
LIMIT 1;

Recetas para consultas SQL problemáticas
[ver en explain.tensor.ru]

Se puede notar de inmediato que se leyeron más de 100 registros por índice, los cuales luego fueron ordenados, y después se dejó solo uno.

Corregimos:

DROP INDEX tbl_fk_cli_idx;
CREATE INDEX ON tbl(fk_cli, pk DESC); -- se agregó la clave de ordenación

Recetas para consultas SQL problemáticas
[ver en explain.tensor.ru]

Incluso en esta selección primitiva — 8.5 veces más rápida y con 33 veces menos lecturas. El efecto será aún más evidente cuantas más "hechos" tengas por cada valor fk.

Cabe señalar que tal índice funcionará como "prefijo" tan bien como el anterior y en otras consultas con fk, donde no había y no hay ordenamientos por pk No se puede y está (se puede leer más sobre esto en mi artículo sobre la búsqueda de índices ineficientes). Además, asegurará un soporte adecuado para la clave foránea explícita en este campo. por este campo.

#2: пересечение индексов (BitmapAnd)

Cuando surge

Mostrar todos los contratos del cliente «ООО Колокольчик», celebrados en nombre de «НАО Лютик».

Cómo identificar

-> BitmapAnd
   -> Bitmap Index Scan
   -> Bitmap Index Scan

Recomendaciones

Crear índice compuesto por campos de ambos orígenes o ampliar uno de los existentes con campos del segundo.

Ejemplo:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk      -- 100K "hechos"
, (random() *  100)::integer fk_org  -- 100 claves externas diferentes
, (random() * 1000)::integer fk_cli; -- 1K claves externas diferentes

CREATE INDEX ON tbl(fk_org); -- índice para la clave externa
CREATE INDEX ON tbl(fk_cli); -- índice para la clave externa

SELECT
  *
FROM
  tbl
WHERE
  (fk_org, fk_cli) = (1, 999); -- filtrado por un par específico

Recetas para consultas SQL problemáticas
[ver en explain.tensor.ru]

Corregimos:

DROP INDEX tbl_fk_org_idx;
CREATE INDEX ON tbl(fk_org, fk_cli);

Recetas para consultas SQL problemáticas
[ver en explain.tensor.ru]

Aquí la mejora es menor, ya que el Bitmap Heap Scan es bastante eficiente por sí mismo. Sin embargo, es 7 veces más rápido y tiene 2.5 veces menos lecturas.

#3: объединение индексов (BitmapOr)

Cuando surge

Mostrar las primeras 20 solicitudes «propias» o no asignadas para procesamiento, priorizando las propias.

Cómo identificar

-> BitmapOr
   -> Bitmap Index Scan
   -> Bitmap Index Scan

Recomendaciones

Usar UNION [ALL] para combinar subconsultas de cada uno de los bloques de condiciones OR.

Ejemplo:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk  -- 100K "hechos"
, CASE
    WHEN random() < 1::real/16 THEN NULL -- con 1:16 de probabilidad la entrada es "nula"
    ELSE (random() * 100)::integer -- 100 claves externas diferentes
  END fk_own;

CREATE INDEX ON tbl(fk_own, pk); -- índice con un orden "aparentemente adecuado"

SELECT
  *
FROM
  tbl
WHERE
  fk_own = 1 OR -- propias
  fk_own IS NULL -- ... o "nulas"
ORDER BY
  pk
, (fk_own = 1) DESC -- primero "propias"
LIMIT 20;

Recetas para consultas SQL problemáticas
[ver en explain.tensor.ru]

Corregimos:

(
  SELECT
    *
  FROM
    tbl
  WHERE
    fk_own = 1 -- primero "propias" 20
  ORDER BY
    pk
  LIMIT 20
)
UNION ALL
(
  SELECT
    *
  FROM
    tbl
  WHERE
    fk_own IS NULL -- luego "nulas" 20
  ORDER BY
    pk
  LIMIT 20
)
LIMIT 20; -- pero en total - 20, no más es necesario

Recetas para consultas SQL problemáticas
[ver en explain.tensor.ru]

Aprovechamos que los 20 registros necesarios se obtuvieron de inmediato en el primer bloque, por lo que el segundo, con un Bitmap Heap Scan más "costoso", ni siquiera se ejecutó; en resultado es 22 veces más rápido, con 44 veces menos lecturas!

Una descripción más detallada de este método de optimización con ejemplos concretos se puede leer en los artículos Antipatrón de PostgreSQL: JOIN y OR perjudiciales y PostgreSQL Antipatterns: la historia de la mejora iterativa de búsqueda por título, o «Optimización de ida y vuelta».

Una versión generalizada de la selección ordenada por múltiples claves (y no solo por un par const/NULL) se analiza en el artículo SQL HowTo: escribiendo un ciclo while directamente en la consulta, o «Elemental tres vías».

#4: читаем много лишнего

Cuando surge

Generalmente, surge al querer «agregar otro filtro» a una consulta existente.

«¿No tienes algo así, pero con botones de nácar?» película «La mano de diamante»

Por ejemplo, al modificar la tarea anterior, mostrar las primeras 20 solicitudes 'críticas' más antiguas para su procesamiento, independientemente de su destino.

Cómo identificar

-> Seq Scan | Bitmap Heap Scan | Index [Only] Scan [Backward]
   && 5 × rows < RRbF -- filtrado >80% leído
   && loops × RRbF > 100 -- y además más de 100 registros en total

Recomendaciones

Crear [más] especializado índice con condición WHERE o incluir en el índice campos adicionales.

Si la condición de filtrado es 'estática' para sus tareas, es decir, no implica expansión de la lista de valores en el futuro, es mejor usar un índice WHERE. Esta categoría incluye bien diferentes estados booleanos/enum.

Si la condición de filtrado puede aceptar diferentes valores, es mejor expandir el índice con estos campos, como en la situación con BitmapAnd arriba.

Ejemplo:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk -- 100K 'hechos'
, CASE
    WHEN random() < 1::real/16 THEN NULL
    ELSE (random() * 100)::integer -- 100 diferentes claves externas
  END fk_own
, (random() < 1::real/50) critical; -- 1:50, que la solicitud es 'crítica'

CREATE INDEX ON tbl(pk);
CREATE INDEX ON tbl(fk_own, pk);

SELECT
  *
FROM
  tbl
WHERE
  critical
ORDER BY
  pk
LIMIT 20;

Recetas para consultas SQL problemáticas
[ver en explain.tensor.ru]

Corregimos:

CREATE INDEX ON tbl(pk)
  WHERE critical; -- añadimos 'condición de filtrado' 'estática'

Recetas para consultas SQL problemáticas
[ver en explain.tensor.ru]

Como podemos ver, la filtración del plan ha desaparecido por completo, y la consulta se ha vuelto 5 veces más rápida.

#5: разреженная таблица

Cuando surge

Diversos intentos de crear colas de procesamiento de tareas propias, cuando un gran número de actualizaciones/eliminaciones de registros en la tabla conduce a una situación de gran cantidad de registros 'muertos'.

Cómo identificar

-> Seq Scan | Bitmap Heap Scan | Index [Only] Scan [Backward]
   && loops × (rows + RRbF) < (shared hit + shared read) × 8
      -- se ha leído más de 1KB por cada registro
   && shared hit + shared read > 64

Recomendaciones

Realizar regularmente de forma manual VACUUM [FULL] o lograr una ejecución adecuadamente frecuente autovacuum ajustando sus parámetros, incluyendo para una tabla específica.

En la mayoría de los casos, estos problemas resultan de una mala composición de consultas en llamadas de lógica empresarial como las que se han revisado en PostgreSQL Antipatterns: luchando contra hordas de 'muertos'.

Pero hay que entender que incluso VACUUM FULL puede no ayudar siempre. Para tales casos, vale la pena familiarizarse con el algoritmo del artículo DBA: cuando VACUUM falla — limpiamos la tabla manualmente.

#6: чтение с «середины» индекса

Cuando surge

Parece que se ha leído un poco, y todo por índice, y no se ha filtrado a nadie extra, pero aun así se han leído considerablemente más páginas de las que se hubiera querido.

Cómo identificar

-> Índice [Solo] Escanear [Hacia atrás]
   && bucles × (filas + RRbF)  64

Recomendaciones

Revisar detenidamente la estructura del índice utilizado y los campos clave especificados en la consulta — probablemente, parte del índice no está definida. Es probable que tenga que crear un índice similar, pero sin campos de prefijo o aprender a iterar sus valores.

Ejemplo:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk      -- 100K "hechos"
, (random() *  100)::integer fk_org  -- 100 claves externas diferentes
, (random() * 1000)::integer fk_cli; -- 1K claves externas diferentes

CREATE INDEX ON tbl(fk_org, fk_cli); -- todo casi como en #2
-- solo que el índice separado por fk_cli ya lo hemos considerado innecesario y lo hemos eliminado

SELECT
  *
FROM
  tbl
WHERE
  fk_cli = 999 -- y fk_org no está especificada, aunque está en el índice antes
LIMIT 20;

Recetas para consultas SQL problemáticas
[ver en explain.tensor.ru]

Parece que todo está bien, incluso por el índice, pero es un poco sospechoso — para cada uno de los 20 registros leídos, se tuvieron que leer 4 páginas de datos, 32KB por registro — ¿no es demasiado? Y el nombre del índice tbl_fk_org_fk_cli_idx provoca reflexión.

Corregimos:

CREATE INDEX ON tbl(fk_cli);

Recetas para consultas SQL problemáticas
[ver en explain.tensor.ru]

De repente — 10 veces más rápido, y 4 veces menos lectura!

Otros ejemplos de situaciones de uso ineficiente de índices se pueden ver en el artículo DBA: encontrando índices innecesarios.

#7: CTE × CTE

Cuando surge

En la consulta se utilizaron CTE "gordos" de diferentes tablas, y luego decidieron hacer entre ellas JOIN.

El caso es relevante para versiones anteriores a v12 o consultas con WITH MATERIALIZED.

Cómo identificar

-> CTE Escanear
   && bucles > 10
   && bucles × (filas + RRbF) > 10000
      -- producto cartesiano de CTE demasiado grande

Recomendaciones

Analizar cuidadosamente la consulta — ¿son realmente necesarios los CTE aquí? aplicar "dictionarización" en hstore/json? Если все-таки да, то según el modelo descrito en PostgreSQL Antipatterns: golpeamos el diccionario contra un JOIN pesado El procesamiento único (ordenación o unificación) de un gran número de registros no cabe en la memoria asignada para ello..

#8: swap на диск (temp written)

Cuando surge

-> * && temp written > 0

Cómo identificar

Si la cantidad de memoria utilizada por la operación no excede significativamente el valor establecido del parámetro

Recomendaciones

work_mem , vale la pena ajustarlo. Se puede hacer directamente en la configuración para todos, o a través deSET [LOCAL] para una consulta/transacción específica. SHOW work_mem; -- "16MB"SELECT random() FROM generate_series(1, 1000000) ORDER BY 1;

Ejemplo:

SET work_mem = '128MB'; -- antes de ejecutar la consulta

Recetas para consultas SQL problemáticas
[ver en explain.tensor.ru]

Corregimos:

Por razones comprensibles, si solo se utiliza la memoria y no el disco, entonces la consulta se ejecutará mucho más rápido. Además, parte de la carga se elimina del HDD.

Recetas para consultas SQL problemáticas
[ver en explain.tensor.ru]

Por razones evidentes, si se utiliza solo memoria y no disco, la consulta se ejecutará mucho más rápido. Además, parte de la carga se elimina del HDD.

Pero hay que entender que no siempre se puede asignar una cantidad enorme de memoria; simplemente no habrá suficiente para todos.

#9: неактуальная статистика

Cuando surge

Se inyectó mucho en la base de datos de inmediato, pero no se logró procesar. ANALIZAR.

Cómo identificar

-> Escaneo Secuencial | Escaneo de Bitmap | Escaneo de Índice [Solo] [Invertido]
   && ratio >> 10

Recomendaciones

Realizarlo ANALIZAR.

Más detalles sobre esta situación se describen en Antipatrones de PostgreSQL: la estadística es la clave.

#10: «что-то пошло не так»

Cuando surge

Ocurrió una espera por bloqueo impuesta por una consulta competidora, o no había suficientes recursos de hardware CPU/hipervisor.

Cómo identificar

-> *
   && (acierto compartido / 8K) + (lectura compartida / 1K) < tiempo / 1000
      -- acierto de RAM = 64MB/s, lectura de HDD = 8MB/s
   && tiempo > 100ms -- leímos poco, pero demasiado lento

Recomendaciones

Utilice un sistema externo para monitorizar servidores en busca de bloqueos o consumo anómalo de recursos. Ya hemos hablado de nuestra manera de organizar este proceso para cientos de servidores. aquí y aquí.

Recetas para consultas SQL problemáticas
Recetas para consultas SQL problemáticas

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster