Continuamos una serie de artículos dedicados al estudio de formas poco conocidas para mejorar el rendimiento de consultas PostgreSQL "aparentemente simples":
No pienses que no me gusta unirme tanto ... :)
Pero a menudo sin ella, la solicitud es significativamente más productiva que con él. Por lo tanto, hoy intentaremos
deshacernos por completo
de JOIN, que requiere muchos recursos , con la ayuda de un diccionario.

Comenzando con PostgreSQL 12, algunas de las situaciones descritas a continuación pueden reproducirse de manera un poco diferente debido a la no materialización de CTE por defecto . Este comportamiento puede revertirse utilizando la tecla MATERIALIZED
.
Muchos "hechos" en un vocabulario limitado
Tomemos una aplicación muy real: debe enumerar los
mensajes entrantes o las tareas activas con los remitentes:
25.01 | .. | . 22.01 | .. | : JOIN. 20.01 | .. | . 18.01 | .. | : JOIN . 16.01 | .. | .
En el mundo abstracto, los autores de las tareas deben distribuirse de manera uniforme entre todos los empleados de nuestra organización, pero en realidad las
tareas provienen, por regla general, de un número bastante limitado de personas : "de los superiores" en la jerarquía o "de los aliados" de los departamentos vecinos (analistas, diseñadores comercialización ...).
Supongamos que en nuestra organización de 1000 personas, solo 20 autores (generalmente incluso menos) establecen tareas para cada artista específico y
utilizan este conocimiento del tema para acelerar la solicitud "tradicional".
Mostramos las últimas 100 tareas para un artista específico:
SELECT task.* , person.name FROM task LEFT JOIN person ON person.id = task.author_id WHERE owner_id = 777 ORDER BY task_date DESC LIMIT 100;
[mira explicar.tensor.ru]Resulta que
1/3 de todo el tiempo y 3/4 lecturas de las páginas de datos se realizaron solo para buscar al autor 100 veces, para cada tarea mostrada. Pero sabemos que entre este centenar
solo hay
20 diferentes : ¿es posible utilizar este conocimiento?
diccionario hstore
Usamos
el tipo hstore para generar un "diccionario" clave-valor:
CREATE EXTENSION hstore
Es suficiente para nosotros poner la identificación del autor y su nombre en el diccionario, para que luego podamos extraer usando esta clave:
[mira explicar.tensor.ru]¡Tomó
2 veces menos tiempo obtener información sobre personas
y 7 veces menos datos leídos ! Además del "engaño", estos resultados nos ayudaron a lograr la
extracción masiva de registros de la tabla en una sola pasada usando
= ANY(ARRAY(...))
.
Entradas de tabla: serialización y deserialización
Pero, ¿qué sucede si necesitamos guardar en el diccionario no un campo de texto, sino un registro completo? En este caso, la capacidad de PostgreSQL
para trabajar escribiendo una tabla como un solo valor nos ayudará a:
... , dict AS ( SELECT hstore( array_agg(id)::text[] , array_agg(p)::text[]
Veamos lo que sucedió aquí:
- Tomamos p como un alias para el registro completo de la tabla de personas y reunimos una matriz de ellos.
- Este conjunto de entradas se reformuló en un conjunto de cadenas de texto (persona [] :: texto []) para colocarlo en el diccionario hstore como un conjunto de valores.
- Al recibir el registro vinculado, lo sacamos del diccionario por clave como una cadena de texto.
- Necesitamos convertir el texto en el valor del tipo de la tabla de personas (para cada tabla, el tipo del mismo nombre se crea automáticamente).
- "Desplegó" un registro escrito en columnas usando
(...).*
.
diccionario json
Pero tal truco, como hemos aplicado anteriormente, no funcionará si no hay un tipo de tabla correspondiente para hacer un "desabroche". Aparecerá exactamente la misma situación, y si como fuente de datos para la serialización tratamos de usar la
fila CTE, y no la tabla "real" .
En este caso, las
funciones para trabajar con json nos ayudarán:
... , p AS (
Cabe señalar que al describir la estructura de destino, no podemos enumerar todos los campos de la cadena fuente, sino solo aquellos que realmente necesitamos. Si tenemos una tabla "nativa", entonces es mejor usar la función
json_populate_record
.
Todavía tenemos acceso al diccionario una vez, pero los
costos de la serialización json- [de] son bastante altos , por lo que es razonable utilizar este método solo en algunos casos cuando el CTE Scan "honesto" se muestra peor.
Prueba de rendimiento
Entonces, tenemos dos formas de serializar datos en un diccionario:
hstore / json_object . Además, las matrices de claves y valores también se pueden generar de dos maneras, con conversión interna o externa a texto:
array_agg (i :: text) / array_agg (i) :: text [] .
Verifiquemos la efectividad de los diferentes tipos de serialización utilizando un ejemplo puramente sintético:
serializamos un número diferente de claves :
WITH dict AS ( SELECT hstore( array_agg(i::text) , array_agg(i::text) ) FROM generate_series(1, ...) i ) TABLE dict;
Script de evaluación: serialización WITH T AS ( SELECT * , ( SELECT regexp_replace(ea[array_length(ea, 1)], '^Execution Time: (\d+\.\d+) ms$', '\1')::real et FROM ( SELECT array_agg(el) ea FROM dblink('port= ' || current_setting('port') || ' dbname=' || current_database(), $$ explain analyze WITH dict AS ( SELECT hstore( array_agg(i::text) , array_agg(i::text) ) FROM generate_series(1, $$ || (1 << v) || $$) i ) TABLE dict $$) T(el text) ) T ) et FROM generate_series(0, 19) v , LATERAL generate_series(1, 7) i ORDER BY 1, 2 ) SELECT v , avg(et)::numeric(32,3) FROM T GROUP BY 1 ORDER BY 1;

En PostgreSQL 11, hasta un tamaño de diccionario de 2 ^ 12 teclas, la
serialización en json lleva menos tiempo . La combinación de json_object y la conversión de tipo "interno" de
array_agg(i::text)
es la más eficiente.
Ahora intentemos leer el valor de cada clave 8 veces, porque si no accede al diccionario, ¿por qué es necesario?
Script de evaluación: lectura de un diccionario WITH T AS ( SELECT * , ( SELECT regexp_replace(ea[array_length(ea, 1)], '^Execution Time: (\d+\.\d+) ms$', '\1')::real et FROM ( SELECT array_agg(el) ea FROM dblink('port= ' || current_setting('port') || ' dbname=' || current_database(), $$ explain analyze WITH dict AS ( SELECT json_object( array_agg(i::text) , array_agg(i::text) ) FROM generate_series(1, $$ || (1 << v) || $$) i ) SELECT (TABLE dict) -> (i % ($$ || (1 << v) || $$) + 1)::text FROM generate_series(1, $$ || (1 << (v + 3)) || $$) i $$) T(el text) ) T ) et FROM generate_series(0, 19) v , LATERAL generate_series(1, 7) i ORDER BY 1, 2 ) SELECT v , avg(et)::numeric(32,3) FROM T GROUP BY 1 ORDER BY 1;

Y ... ya en aproximadamente
2 ^ 6 teclas, la lectura del diccionario json comienza a perder a la lectura de hstore varias veces, para jsonb sucede lo mismo en 2 ^ 9.
Conclusiones finales
- si necesita hacer una UNIÓN con registros repetidos repetidamente , es mejor usar la "coincidencia de tabla"
- si se espera que su diccionario sea pequeño y leerá un poco , puede usar json [b]
- en todos los demás casos, hstore + array_agg (i :: text) será más eficiente