TL DR: JSONB puede simplificar enormemente el desarrollo del esquema de la base de datos sin sacrificar el rendimiento de la consulta.
Introduccion
Pongamos un ejemplo clásico, probablemente, de uno de los casos de uso más antiguos de bases de datos relacionales (base de datos): tenemos una entidad y es necesario preservar ciertas propiedades (atributos) de esta entidad. Pero no todas las instancias pueden tener el mismo conjunto de propiedades, además, en el futuro, la posible adición de más propiedades.
La forma más fácil de resolver este problema es crear una columna en la tabla de la base de datos para cada valor de propiedad, y simplemente completar los que son necesarios para una instancia de entidad específica. Genial El problema se resuelve ... hasta que su tabla contenga millones de registros y no necesite agregar un nuevo registro.
Considere el patrón EAV (
Entity-Attribute-Value ), es bastante común. Una tabla contiene entidades (registros), otra tabla contiene los nombres de propiedades (atributos), y la tercera tabla asocia entidades con sus atributos y contiene el valor de estos atributos para la entidad actual. Esto le brinda la oportunidad de tener diferentes conjuntos de propiedades para diferentes objetos, así como agregar propiedades sobre la marcha, sin cambiar la estructura de la base de datos.
Sin embargo, no escribiría esta nota si no hubiera fallas en el enfoque al usar EVA. Entonces, por ejemplo, para obtener una o más entidades que tienen 1 atributo cada una, se requieren 2 join'a (uniones) en la consulta: la primera es una unión con la tabla de atributos, la segunda es la unión con la tabla de valores. Si una entidad tiene 2 atributos, ¡ya se necesitan 4 combinaciones! Además, todos los atributos generalmente se almacenan como cadenas, lo que conduce a la conversión de tipos tanto para el resultado como para la cláusula WHERE. Si escribe muchas solicitudes, esto es un desperdicio en términos de uso de recursos.
A pesar de estos defectos obvios, EAV se ha utilizado durante mucho tiempo para resolver este tipo de problemas. Estos eran defectos inevitables, y simplemente no había otra alternativa mejor.
Pero luego apareció una nueva "tecnología" en PostgreSQL ...
A partir de PostgreSQL 9.4, se ha agregado un tipo de datos JSONB para almacenar datos JSON binarios. Aunque almacenar JSON en este formato generalmente requiere un poco más de espacio y tiempo que el texto sin formato JSON, las operaciones con él son mucho más rápidas. JSONB también es compatible con la indexación, lo que hace que consultarlos sea aún más rápido.
El tipo de datos JSONB nos permite reemplazar el voluminoso 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 en la productividad ... Es por eso que apareció en este artículo.
Probar la configuración de la base de datos
Para esta comparación, creé una base de datos en una nueva instalación de PostgreSQL 9.5 en la compilación de $ 80 de
DigitalOcean Ubuntu 14.04. Después de configurar algunos parámetros en postgresql.conf, ejecuté
este script usando psql. Las siguientes tablas se crearon para representar los datos como 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 la columna de tipo JSONB -
propiedades .
CREATE TABLE entity_jsonb ( id SERIAL PRIMARY KEY, name TEXT, description TEXT, properties JSONB );
Parece mucho más fácil, ¿verdad? Luego, se agregaron 10 millones de registros a las tablas de entidades (
entity &
entity_jsonb ) y, en consecuencia, se completaron los mismos datos de la tabla utilizando el patrón EAV y el enfoque con la columna
JSONB -
entity_jsonb.properties . Por lo tanto, recibimos varios tipos de datos diferentes entre todo el conjunto de propiedades. Datos de muestra:
{ 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 dos opciones. ¡Comencemos a comparar implementaciones en el trabajo!
Simplificación de diseño
Ya se ha dicho que el diseño de la base de datos se simplificó enormemente: una tabla, usando la columna JSONB para propiedades, en lugar de usar tres tablas para EAV. Pero, ¿cómo se refleja esto en las solicitudes? La actualización de una propiedad de una entidad es la siguiente:
Como puede ver, la última solicitud no parece más fácil. 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 por adelantado. Mirando el ejemplo EAV, necesitamos conocer tanto entity_id como entity_attribute_id para poder actualizar. Si desea actualizar una propiedad en una columna JSONB basada en el nombre del objeto, todo esto se hace en una fila simple.
Ahora, elija la entidad que acabamos de actualizar, de acuerdo con la condición de su nuevo color:
Creo que podemos estar de acuerdo en que el segundo es más corto (¡sin unirse!) Y, por lo tanto, más legible. ¡Aquí está la victoria de JSONB! Usamos el operador JSON - >> para obtener el color como un valor de texto de un objeto JSONB. También hay una segunda forma de lograr el mismo resultado en el modelo JSONB utilizando el operador @>:
Esto es un poco más complicado: verificamos si el objeto JSON en la columna de propiedades contiene el objeto a la derecha del operador @>. Menos legible, más productivo (ver más abajo).
Simplifique aún más el uso de JSONB cuando necesite seleccionar varias propiedades a la vez. Aquí es donde realmente entra el enfoque JSONB: simplemente seleccionamos propiedades como columnas adicionales en nuestro conjunto de resultados sin la necesidad de unir:
Con EAV, necesitará 2 uniones para cada propiedad que desee solicitar. En mi opinión, las consultas anteriores muestran una gran simplificación en el diseño de la base de datos. Vea más ejemplos de cómo escribir solicitudes JSONB, también en
esta publicación.
Ahora es el 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 solicitud se ejecutó al menos tres veces porque la primera vez que el planificador de consultas tarda más. Al principio, ejecuté consultas sin ningún índice. Obviamente, esto sirvió como una ventaja de JSONB, ya que la unión requerida para EAV no podía usar índices (los campos de clave externa no estaban indexados). Después de eso, creé un índice para 2 columnas de claves foráneas en la tabla de valores EAV, así como un índice
GIN para la columna JSONB.
Las actualizaciones de datos mostraron los siguientes resultados en el tiempo (en ms). Tenga en cuenta que la escala es logarítmica:

Vemos que JSONB es mucho (> 50,000-x) más rápido que EAV si no usa índices, por la razón indicada 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 no tiene ningún efecto aquí, ya que no usamos la columna de propiedades en los criterios de evaluación.
Para seleccionar datos basados en un valor de propiedad, obtenemos los siguientes resultados (escala normal):

Puede notar que JSONB es más rápido que EAV sin índices, pero cuando EAV está con índices, aún funciona más rápido que JSONB. Pero luego vi que el tiempo para las solicitudes JSONB era el mismo, esto me llevó al hecho de que el índice GIN no funcionaba. Aparentemente, cuando usa el índice GIN para una columna con propiedades rellenas, solo actúa cuando usa el operador de inclusión @>. Utilicé esto en una nueva prueba, que tuvo un gran impacto en el tiempo: ¡solo 0.153 ms! Esto es 15,000 veces más rápido que EAV, y 25,000 veces más rápido que el operador - >>.
¡Creo que fue lo suficientemente rápido!
Tamaño de la tabla DB
Comparemos los tamaños de tabla para ambos enfoques. En psql, podemos mostrar el tamaño de todas las tablas e índices usando el
comando \ dti +
Para el enfoque EAV, los tamaños de las tablas son de aproximadamente 3068 MB, y los índices son de hasta 3427 MB, lo que en total da 6.43 GB. Usando el enfoque JSONB, se usan 1817 MB para la tabla y 318 MB para los índices, que es 2.08 GB. ¡Resulta 3 veces menos! Este hecho me sorprendió un poco porque almacenamos nombres de propiedades en cada objeto JSONB.
Pero de todos modos, los números hablan por sí mismos: en EAV almacenamos 2 claves externas enteras para el valor del atributo, como resultado de lo cual obtenemos 8 bytes de datos adicionales. Además, en EAV, todos los valores de propiedad se almacenan como texto, mientras que JSONB usará valores numéricos y lógicos dentro, donde sea posible, lo que dará como resultado menos volumen.
Resumen
En general, creo que almacenar las propiedades de la entidad en formato JSONB puede simplificar enormemente el diseño y el mantenimiento de su base de datos. Si ejecuta muchas consultas, todo lo que esté almacenado en la misma tabla con la entidad realmente funcionará de manera más eficiente. Y el hecho de que esto simplifique la interacción entre los datos ya es una ventaja, pero la base de datos resultante es 3 veces más pequeña en volumen.
Además, según la prueba, podemos concluir que la pérdida de rendimiento es muy leve. En algunos casos, JSONB incluso funciona 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, ...), por lo tanto, si tiene alguna sugerencia sobre cómo mejorarlos, por favor ¡No dudes en dejar un comentario!