Reemplazo de EAV por JSONB en PostgreSQL

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

Introducción

Tomemos un ejemplo clásico, probablemente uno de los usos más antiguos del mundo de las bases de datos relacionales: tenemos una entidad y necesitamos almacenar ciertas propiedades (atributos) de esa entidad. Pero no todos los ejemplos pueden tener el mismo conjunto de propiedades, además de que en el futuro podría ser necesario agregar más propiedades.

La forma más simple de resolver este problema es crear una columna en la tabla de la base de datos para cada valor de propiedad, y simplemente rellenar aquellas que son necesarias para un determinado ejemplo de la entidad. ¡Perfecto! El problema está resuelto... hasta que tu tabla contiene millones de registros y tienes que agregar un nuevo registro.

Consideremos el patrón EAV (Entidad-Atributo-Valor), que se encuentra con bastante frecuencia. Una tabla contiene entidades (registros), otra tabla contiene nombres de propiedades (atributos), y una tercera tabla vincula las entidades con sus atributos y contiene el valor de esos atributos para la entidad actual. Esto te permite tener diferentes conjuntos de propiedades para distintos objetos y también agregar propiedades 'sobre la marcha', sin modificar la estructura de la base de datos.

Sin embargo, no escribiría esta nota si no hubiera desventajas en el enfoque de utilizar EAV. Por ejemplo, para recuperar una o más entidades que tienen 1 atributo, se requieren 2 joins en la consulta: el primero es la unión con la tabla de atributos, y el segundo es la unión con la tabla de valores. Si la entidad tiene 2 atributos, ya se necesitan 4 joins. Además, todos los atributos suelen almacenarse como cadenas, lo que lleva a conversiones de tipo, tanto para el resultado como para la condición WHERE. Si escribes muchas consultas, es bastante derrochador en términos de uso de recursos.

A pesar de estas desventajas evidentes, EAV ha sido utilizado durante mucho tiempo para abordar este tipo de problemas. Estos eran inconvenientes inevitables, y simplemente no había una mejor alternativa.
Pero luego apareció una nueva 'tecnología' en PostgreSQL...

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

El tipo de dato JSONB nos permite reemplazar el engorroso patrón EAV al agregar simplemente una columna JSONB a nuestra tabla de entidades, lo que simplifica significativamente el diseño de la base de datos. Pero muchos argumentan que esto debería ir acompañado de una disminución en el rendimiento… Esta es la razón por la cual apareció este artículo.

Configuración de la base de datos de prueba

Para esta comparación, creé una base de datos en una nueva instalación de PostgreSQL 9.5 en una construcción de 80 dólares Encuesta Ubuntu 14.04. Después de ajustar algunos parámetros en postgresql.conf, ejecuté este el script usando psql. Para representar los datos en formato EAV se crearon las siguientes tablas:

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 la tabla donde se almacenarán los mismos datos, pero con atributos en una columna de tipo JSONB – properties.

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

Se ve mucho más simple, ¿verdad? Luego se añadieron a las tablas de entidades (entity & entity_jsonb) 10 millones de registros, y por lo tanto se poblaron con los mismos datos las tablas donde se utiliza el patrón EAV y el enfoque con la columna JSONB – entity_jsonb.properties. Así, obtuvimos varios tipos de datos entre todo el conjunto de propiedades. Ejemplo de datos:

{
  id:          1
  name:        "Entity1"
  description: "Entidad de prueba n.º 1"
  properties:  {
    color:        "rojo"
    length:       120
    width:        3.1882420
    hasSomething: true
    country:      "Bélgica"
  } 
}

Así que ahora tenemos los mismos datos para dos variantes. ¡Comencemos a comparar las implementaciones en acción!

Simplificación del diseño

Ya se ha mencionado que el diseño de la base de datos se ha simplificado significativamente: una tabla, gracias al uso de la columna JSONB para las propiedades, en lugar de utilizar tres tablas para EAV. Pero, ¿cómo se refleja esto en las consultas? La actualización de una propiedad de la entidad se ve así:

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

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

Como podemos ver, la última consulta no parece ser más sencilla. Para actualizar el valor de una propiedad en un objeto JSONB, debemos usar la función jsonb_set(), y debemos pasar nuestro nuevo valor como un objeto JSONB. Sin embargo, no necesitamos conocer ningún identificador de antemano. Al observar el ejemplo con EAV, necesitamos conocer tanto entity_id como entity_attribute_id para realizar la actualización. Si deseas actualizar una propiedad en la columna JSONB basándote en el nombre del objeto, esto se hace con una simple línea.

Ahora seleccionemos la entidad que acabamos de actualizar, de acuerdo con 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 = 'azul';

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

Creo que podemos estar de acuerdo en que la segunda es más corta (sin un join), y por lo tanto más legible. ¡Aquí JSONB gana! Usamos el operador JSON ->> para obtener el color como un valor de texto del objeto JSONB. También hay una segunda manera de lograr el mismo resultado en el modelo JSONB utilizando el operador @>:

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

Es un poco más complicado: estamos verificando si el objeto JSON en la columna de propiedades contiene el objeto que está a la derecha del operador @>. Menos legible, pero más eficiente (ver más adelante).

Simplifiquemos aún más el uso de JSONB cuando necesitas seleccionar varias propiedades a la vez. Aquí es donde realmente encaja el enfoque JSONB: simplemente seleccionamos las 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 desees consultar. En mi opinión, las consultas anteriores muestran una gran simplificación en el diseño de la base de datos. También puedes ver más ejemplos de cómo escribir consultas para JSONB en este la publicación.
Ahora es momento de hablar sobre el rendimiento.

Rendimiento

Para comparar el rendimiento, utilicé EXPLAIN ANALYZE en las consultas, para calcular el tiempo de ejecución. Cada consulta se ejecutó al menos tres veces, ya que la primera vez el planificador de consultas requiere más tiempo. Primero ejecuté las consultas sin ningún índice. Obviamente, esto benefició a JSONB, ya que las uniones necesarias para EAV no podían utilizar índices (los campos de clave externa no estaban indexados). Después de eso, creé un índice para las 2 columnas de claves externas en la tabla de valores EAV, así como un índice GIN para la columna JSONB.

Las actualizaciones de datos mostraron los siguientes resultados de tiempo (en ms). Tenga en cuenta que la escala es logarítmica:

Reemplazo de EAV por JSONB en PostgreSQL

Vemos que JSONB es mucho (> 50000 veces) más rápido que EAV, si no se utilizan índices, por la razón mencionada anteriormente. Cuando indexamos las 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 en la columna JSONB aquí no tiene ningún efecto, ya que no utilizamos la columna de propiedades en los criterios de evaluación.

Para seleccionar datos basados en el valor de la propiedad, obtenemos los siguientes resultados (escala normal):

Reemplazo de EAV por JSONB en PostgreSQL

Se puede notar que JSONB nuevamente funciona más rápido que EAV sin índices, pero cuando EAV tiene índices, todavía funciona más rápido que JSONB. Pero luego vi que el tiempo para las consultas JSONB era el mismo, esto me llevó a la conclusión de que los índices GIN no estaban activándose. Al parecer, cuando se utiliza un índice GIN para una columna con propiedades llenas, actúa solo cuando se utiliza el operador de inclusión @>. Utilicé esto en una nueva prueba, lo cual tuvo un impacto enorme en el tiempo: ¡solo 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 bastante rápido.

Tamaño de las tablas de la base de datos

Comparamos los tamaños de las tablas entre ambos enfoques. En psql podemos mostrar el tamaño de todas las tablas e índices con el comando dti+

Reemplazo de EAV por JSONB en PostgreSQL

Para el enfoque EAV, el tamaño de las tablas es de aproximadamente 3068 MB, y los índices hasta 3427 MB, lo que suma 6,43 GB. Al utilizar el enfoque con JSONB, se utilizan 1817 MB para la tabla y 318 MB para los índices, lo que equivale a 2,08 GB. ¡Es tres veces menos! Este hecho me sorprendió un poco, ya que almacenamos los nombres de las propiedades en cada objeto JSONB.

Pero los números hablan por sí mismos: en EAV almacenamos 2 claves externas enteras para el valor del atributo, lo que resulta en 8 bytes de datos adicionales. Además, en EAV todos los valores de las propiedades se almacenan como texto, mientras que JSONB utilizará valores numéricos y lógicos internamente siempre que sea posible, lo que da como resultado un menor volumen.

Resultados

En general, creo que almacenar las propiedades de las entidades en formato JSONB puede simplificar significativamente el diseño y mantenimiento de su base de datos. Si realiza muchas consultas, todo lo que se almacena en una tabla junto a la entidad realmente funcionará de manera más eficiente. Y el hecho de que simplifique la interacción entre los datos ya es un beneficio, pero además la base de datos resultante es 3 veces más pequeña en volumen.

Además, según las pruebas realizadas, se puede concluir que las pérdidas de rendimiento son muy insignificantes. En algunos casos, JSONB incluso funciona más rápido que EAV, lo que lo hace aún mejor. Sin embargo, esta prueba de referencia, por supuesto, no cubre todos los aspectos (por ejemplo, entidades con un número muy grande de propiedades, un aumento significativo en el número de propiedades de los datos existentes,…), por lo que si tiene alguna sugerencia sobre cómo mejorarlas, no dude en dejar sus comentarios.

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