Reemplazo de EAV con JSONB en PostgreSQL

TL; DR: JSONB puede simplificar enormemente el desarrollo de esquemas de bases de datos sin sacrificar el rendimiento de las consultas.

introducción

Demos un ejemplo clásico de probablemente uno de los casos de uso más antiguos en el mundo de una base de datos relacional (base de datos): tenemos una entidad y necesitamos guardar ciertas propiedades (atributos) de esta entidad. Pero es posible que no todas las instancias tengan el mismo conjunto de propiedades y es posible que se agreguen más propiedades en el futuro.

La forma más sencilla de resolver este problema es crear una columna en la tabla de la base de datos para cada valor de propiedad y simplemente completar las que sean necesarias para una instancia de entidad específica. ¡Excelente! Problema resuelto... hasta que tu tabla contenga millones de registros y necesites agregar un nuevo registro.

Considere el patrón EAV (Entidad-Atributo-Valor), ocurre con bastante frecuencia. Una tabla contiene entidades (registros), otra tabla contiene nombres de propiedades (atributos) y una tercera tabla asocia entidades con sus atributos y contiene el valor de esos atributos para la entidad actual. Esto le brinda la posibilidad de tener diferentes conjuntos de propiedades para diferentes objetos y también agregar propiedades sobre la marcha sin cambiar la estructura de la base de datos.

Sin embargo, no estaría escribiendo esta publicación si el enfoque EVA no tuviera algunas desventajas. Entonces, por ejemplo, para obtener una o más entidades que tengan 1 atributo cada una, se requieren 2 uniones en la consulta: la primera es una unión con la tabla de atributos, la segunda es una unión con la tabla de valores. Si una entidad tiene 2 atributos, ¡entonces se necesitan 4 uniones! Además, todos los atributos normalmente se almacenan como cadenas, lo que da como resultado una conversión de tipos tanto para el resultado como para la cláusula WHERE. Si escribe muchas consultas, esto es un desperdicio en términos de uso de recursos.

A pesar de estas obvias deficiencias, EAV se ha utilizado durante mucho tiempo para resolver este tipo de problemas. Se trataba de deficiencias inevitables y simplemente no había otra alternativa mejor.
Pero entonces apareció una nueva “tecnología” en PostgreSQL...

A partir de PostgreSQL 9.4, se agregó el tipo de datos JSONB para almacenar datos binarios JSON. Aunque almacenar JSON en este formato normalmente requiere un poco más de espacio y tiempo que JSON en texto plano, realizar operaciones en él es mucho más rápido. JSONB también admite la indexación, lo que hace que las consultas sean aún más rápidas.

El tipo de datos JSONB nos permite reemplazar el engorroso patrón EAV agregando solo una columna JSONB a nuestra tabla de entidades, lo que simplifica enormemente el diseño de la base de datos. Pero muchos argumentan que esto debería ir acompañado de una disminución de la productividad... Por eso escribí este artículo.

Configurar una base de datos de prueba

Para esta comparación, creé la base de datos en una instalación nueva de PostgreSQL 9.5 en la versión de $80. Digital Ocean Ubuntu 14.04 Después de configurar algunos parámetros en postgresql.conf ejecuté este secuencia de comandos usando psql. Se crearon las siguientes tablas para presentar los datos en formato EAV:

CREATE TABLE entity ( 
  id           SERIAL PRIMARY KEY, 
  name         TEXT, 
  description  TEXT
);
CREATE TABLE entity_attribute (
  id          SERIAL PRIMARY KEY, 
  name        TEXT
);
CREATE TABLE entity_attribute_value (
  id                  SERIAL PRIMARY KEY, 
  entity_id           INT    REFERENCES entity(id), 
  entity_attribute_id INT    REFERENCES entity_attribute(id), 
  value               TEXT
);

A continuación se muestra una tabla donde se almacenarán los mismos datos, pero con atributos en una columna tipo JSONB – propiedades.

CREATE TABLE entity_jsonb (
  id          SERIAL PRIMARY KEY, 
  name        TEXT, 
  description TEXT,
  properties  JSONB
);

Parece mucho más sencillo, ¿no? Luego se agregó a las tablas de entidades (entidad & entidad_jsonb) 10 millones de registros y, en consecuencia, la tabla se llenó con los mismos datos utilizando el patrón EAV y el enfoque con una columna JSONB - entidad_jsonb.properties. Por lo tanto, recibimos varios tipos de datos diferentes entre todo el conjunto de propiedades. Datos de ejemplo:

{
  id:          1
  name:        "Entity1"
  description: "Test entity no. 1"
  properties:  {
    color:        "red"
    lenght:       120
    width:        3.1882420
    hassomething: true
    country:      "Belgium"
  } 
}

Entonces ahora tenemos los mismos datos para ambas opciones. ¡Comencemos a comparar implementaciones en el trabajo!

Simplifica tu diseño

Anteriormente se indicó que el diseño de la base de datos se simplificó enormemente: una tabla, utilizando una columna JSONB para las propiedades, en lugar de utilizar tres tablas para EAV. Pero ¿cómo se refleja esto en las solicitudes? La actualización de una propiedad de entidad se ve así:

-- EAV
UPDATE entity_attribute_value 
SET value = 'blue' 
WHERE entity_attribute_id = 1 
  AND entity_id = 120;

-- JSONB
UPDATE entity_jsonb 
SET properties = jsonb_set(properties, '{"color"}', '"blue"') 
WHERE id = 120;

Como puede ver, la última solicitud no parece más sencilla. Para actualizar el valor de una propiedad en un objeto JSONB tenemos que usar la función jsonb_set()y deberíamos pasar nuestro nuevo valor como un objeto JSONB. Sin embargo, no necesitamos conocer ningún identificador de antemano. Mirando el ejemplo de EAV, necesitamos conocer tanto el id_entidad como el id_atributo_entidad para poder realizar la actualización. Si desea actualizar una propiedad en una columna JSONB según el nombre del objeto, todo se hace en una simple línea.

Ahora seleccionemos la entidad que acabamos de actualizar según su nuevo color:

-- EAV
SELECT e.name 
FROM entity e 
  INNER JOIN entity_attribute_value eav ON e.id = eav.entity_id
  INNER JOIN entity_attribute ea ON eav.entity_attribute_id = ea.id
WHERE ea.name = 'color' AND eav.value = 'blue';

-- JSONB
SELECT name 
FROM entity_jsonb 
WHERE properties ->> 'color' = 'blue';

Creo que podemos estar de acuerdo en que el segundo es más corto (¡sin unirse!) y, por lo tanto, más legible. ¡JSONB gana aquí! Usamos el operador JSON ->> para obtener el color como valor de texto de un objeto JSONB. También existe una segunda forma de lograr el mismo resultado en el modelo JSONB usando el operador @>:

-- JSONB 
SELECT name 
FROM entity_jsonb 
WHERE properties @> '{"color": "blue"}';

Esto es un poco más complicado: verificamos si el objeto JSON en su columna de propiedades contiene un objeto que está a la derecha del operador @>. Menos legible, más productivo (ver más abajo).

Hagamos que el uso de JSONB sea aún más fácil cuando necesite seleccionar varias propiedades a la vez. Aquí es donde realmente entra en juego el enfoque JSONB: simplemente seleccionamos propiedades como columnas adicionales en nuestro conjunto de resultados sin necesidad de uniones:

-- JSONB 
SELECT name
  , properties ->> 'color'
  , properties ->> 'country'
FROM entity_jsonb 
WHERE id = 120;

Con EAV necesitarás 2 uniones para cada propiedad que quieras consultar. En mi opinión, las consultas anteriores muestran una gran simplificación en el diseño de bases de datos. Vea más ejemplos de cómo escribir consultas JSONB, también en Este correo.
Ahora es el momento de hablar de rendimiento.

Rendimiento

Para comparar el rendimiento utilicé EXPLICAR ANALIZAR en consultas, para calcular el tiempo de ejecución. Cada consulta se ejecutó al menos tres veces porque el planificador de consultas tarda más la primera vez. Primero ejecuté las consultas sin ningún índice. Obviamente, esto era una ventaja de JSONB, ya que las uniones requeridas para EAV no podían usar índices (los campos de clave externa no estaban indexados). Después de esto, creé un índice en las 2 columnas de clave externa de la tabla de valores EAV, así como un índice. GIN para una columna JSONB.

La actualización de datos mostró los siguientes resultados en términos de tiempo (en ms). Tenga en cuenta que la escala es logarítmica:

Reemplazo de EAV con JSONB en PostgreSQL

Vemos que JSONB es mucho (> 50000-x) más rápido que EAV si no utiliza índices, por el motivo indicado anteriormente. Cuando indexamos columnas con claves primarias, la diferencia casi desaparece, pero JSONB sigue siendo 1,3 veces más rápido que EAV. Tenga en cuenta que el índice de la columna JSONB no tiene ningún efecto aquí ya que no utilizamos la columna de propiedades en los criterios de evaluación.

Para seleccionar datos según el valor de la propiedad, obtenemos los siguientes resultados (escala normal):

Reemplazo de EAV con JSONB en PostgreSQL

Puede notar que JSONB nuevamente funciona más rápido que EAV sin índices, pero cuando EAV con índices, aún funciona más rápido que JSONB. Pero luego vi que los tiempos para las consultas JSONB eran los mismos, esto me llevó al hecho de que los índices GIN no funcionan. Aparentemente, cuando usa un índice GIN en una columna con propiedades completas, solo tiene efecto cuando usa el operador de inclusión @>. Utilicé esto en una nueva prueba y tuvo un gran impacto en el tiempo: ¡sólo 0,153 ms! Esto es 15000 veces más rápido que EAV y 25000 veces más rápido que el operador ->>.

¡Creo que fue lo suficientemente rápido!

Tamaño de la tabla de base de datos

Comparemos los tamaños de las tablas para ambos enfoques. En psql podemos mostrar el tamaño de todas las tablas e índices usando el comando dti+

Reemplazo de EAV con JSONB en PostgreSQL

Para el enfoque EAV, los tamaños de tabla son de alrededor de 3068 MB y los índices de hasta 3427 MB para un total de 6,43 GB. El enfoque JSONB utiliza 1817 MB para la tabla y 318 MB para los índices, lo que equivale a 2,08 GB. ¡Resulta 3 veces menos! Este hecho me sorprendió un poco porque almacenamos nombres de propiedades en cada objeto JSONB.

Pero aún así, los números hablan por sí solos: en EAV almacenamos 2 claves externas enteras por valor de atributo, lo que da como resultado 8 bytes de datos adicionales. Además, EAV almacena todos los valores de propiedad como texto, mientras que JSONB utilizará valores numéricos y booleanos internamente siempre que sea posible, lo que dará como resultado una huella más pequeña.

resultados

En general, creo que guardar las propiedades de la entidad en formato JSONB puede facilitar mucho el diseño y el mantenimiento de su base de datos. Si está ejecutando muchas consultas, mantener todo en la misma tabla que la entidad funcionará de manera más eficiente. Y el hecho de que esto simplifique la interacción entre datos ya es una ventaja, pero la base de datos resultante tiene un volumen 3 veces menor.

Además, según las pruebas realizadas, podemos concluir que las pérdidas de rendimiento son muy insignificantes. En algunos casos, JSONB es incluso más rápido que EAV, lo que lo hace aún mejor. Sin embargo, este punto de referencia, por supuesto, no cubre todos los aspectos (por ejemplo, entidades con una gran cantidad de propiedades, un aumento significativo en la cantidad de propiedades de los datos existentes,...), así que si tiene alguna sugerencia sobre cómo mejorarlas , ¡no dudes en dejarlo en los comentarios!

Fuente: habr.com

Compre alojamiento confiable para sitios con protección DDoS, servidores VPS VDS 🔥 Compra alojamiento web fiable con protección DDoS, servidores VPS VDS | ProHoster