sql >> Base de Datos >  >> RDS >> PostgreSQL

Uso de JSONB en PostgreSQL:cómo almacenar e indexar datos JSON de manera efectiva en PostgreSQL

JSON significa notación de objetos de JavaScript. Es un formato estándar abierto que organiza los datos en pares clave/valor y matrices detallados en RFC 7159. JSON es el formato más común utilizado por los servicios web para intercambiar datos, almacenar documentos, datos no estructurados, etc. En esta publicación, vamos a para mostrarle consejos y técnicas sobre cómo almacenar e indexar de manera efectiva datos JSON en PostgreSQL.

También puede consultar nuestro seminario web Trabajar con datos JSON en PostgreSQL frente a MongoDB en asociación con PostgresConf para obtener más información sobre el tema, y ​​consultar nuestra página de SlideShare para descargar las diapositivas.

¿Por qué almacenar JSON en PostgreSQL?

¿Por qué una base de datos relacional debería siquiera preocuparse por los datos no estructurados? Resulta que hay algunos escenarios en los que es útil.

  1. Flexibilidad del esquema

    Una de las principales razones para almacenar datos con el formato JSON es la flexibilidad del esquema. Almacenar sus datos en JSON es útil cuando su esquema es fluido y cambia con frecuencia. Si almacena cada una de las claves como columnas, dará como resultado operaciones DML frecuentes; esto puede ser difícil cuando su conjunto de datos es grande, por ejemplo, seguimiento de eventos, análisis, etiquetas, etc. Nota:si una clave en particular está siempre presente en su documento, podría tener sentido almacenarlo como una columna de primera clase. Discutimos más sobre este enfoque en la sección "Patrones y antipatrones JSON" a continuación.

  2. Objetos anidados

    Si su conjunto de datos tiene objetos anidados (de uno o varios niveles), en algunos casos, es más fácil manejarlos en JSON en lugar de desnormalizar los datos en columnas o varias tablas.

  3. Sincronización con fuentes de datos externas

    A menudo, un sistema externo proporciona datos como JSON, por lo que podría ser un almacenamiento temporal antes de que los datos se ingieran en otras partes del sistema. Por ejemplo, transacciones de Stripe.

Cronología de compatibilidad con JSON en PostgreSQL

La compatibilidad con JSON en PostgreSQL se introdujo en 9.2 y ha mejorado constantemente en cada versión en el futuro.

  • Oleada 1:PostgreSQL 9.2  (2012) agregó soporte para el tipo de datos JSON

    La base de datos de JSON en 9.2 era bastante limitada (y probablemente sobrevalorada en ese momento), básicamente una cadena glorificada con algo de validación de JSON incluida. Es útil para validar el JSON entrante y almacenarlo en la base de datos. Más detalles se proporcionan a continuación.

  • Oleada 2:PostgreSQL 9.4 (2014) agregó soporte para el tipo de datos JSONB

    JSONB significa "JSON Binario" o "JSON mejor", según a quién le pregunte. Es un formato binario descompuesto para almacenar JSON. JSONB admite la indexación de datos JSON y es muy eficaz para analizar y consultar los datos JSON. En la mayoría de los casos, cuando trabaja con JSON en PostgreSQL, debe usar JSONB.

  • Oleada 3:PostgreSQL 12 (2019) agregó soporte para el estándar SQL/JSON y consultas JSONPATH

    JSONPath trae un potente motor de consultas JSON a PostgreSQL.

¿Cuándo debería usar JSON frente a JSONB?

En la mayoría de los casos, JSONB es lo que debería usar. Sin embargo, hay algunos casos específicos en los que JSON funciona mejor:

  • JSON conserva el formato original (también conocido como espacio en blanco) y el orden de las claves.
  • JSON conserva claves duplicadas.
  • JSON es más rápido de ingerir que JSONB; sin embargo, si realiza más procesamiento, JSONB será más rápido.

Por ejemplo, si solo ingiere registros JSON y no los consulta de ninguna manera, entonces JSON podría ser una mejor opción para usted. A los efectos de este blog, cuando nos referimos a la compatibilidad con JSON en PostgreSQL, nos referiremos a JSONB en el futuro.

Uso de JSONB en PostgreSQL:cómo almacenar e indexar datos JSON de manera efectiva en PostgreSQLHaga clic para twittear

Patrones y Antipatrones JSONB

Si PostgreSQL tiene un gran soporte para JSONB, ¿por qué necesitamos más columnas? ¿Por qué no simplemente crear una tabla con un blob JSONB y deshacerse de todas las columnas como el siguiente esquema:

CREATE TABLE test(id int, data JSONB, PRIMARY KEY (id));

Al final del día, las columnas siguen siendo la técnica más eficiente para trabajar con sus datos. El almacenamiento JSONB tiene algunos inconvenientes frente a las columnas tradicionales:

  • PostreSQL no almacena estadísticas de columna para columnas JSONB

    PostgreSQL mantiene estadísticas sobre las distribuciones de valores en cada columna de la tabla:valores más comunes (MCV), entradas NULL, histograma de distribución. En función de estos datos, el planificador de consultas de PostgreSQL toma decisiones inteligentes sobre el plan que se utilizará para la consulta. En este punto, PostgreSQL no almacena ninguna estadística para columnas o claves JSONB. Esto a veces puede dar lugar a malas decisiones, como el uso de uniones de bucle anidado frente a uniones hash, etc. Se proporciona un ejemplo más detallado de esto en esta publicación de blog:Cuándo evitar JSONB en un esquema de PostgreSQL.

  • El almacenamiento JSONB da como resultado una huella de almacenamiento más grande

    El almacenamiento JSONB no deduplica los nombres de las claves en el JSON. Esto puede generar un espacio de almacenamiento considerablemente mayor en comparación con MongoDB BSON en WiredTiger o el almacenamiento en columna tradicional. Realicé una prueba simple con el modelo JSONB a continuación que almacena alrededor de 10 millones de filas de datos, y aquí están los resultados:en cierto modo, esto es similar al modelo de almacenamiento MongoDB MMAPV1 donde las claves en JSONB se almacenaron tal cual sin ninguna compresión. Una solución a largo plazo es mover los nombres de las claves a un diccionario de nivel de tabla y consultar este diccionario en lugar de almacenar los nombres de las claves repetidamente. Hasta entonces, la solución podría ser usar nombres más compactos (estilo Unix) en lugar de nombres más descriptivos. Por ejemplo, si está almacenando millones de instancias de una clave en particular, sería mejor en términos de almacenamiento nombrarla "pb" en lugar de "publisherName".

La forma más eficiente de aprovechar JSONB en PostgreSQL es combinar columnas y JSONB. Si una clave aparece con mucha frecuencia en sus blobs JSONB, probablemente sea mejor almacenarla como una columna. Use JSONB como "catch all" para manejar las partes variables de su esquema mientras aprovecha las columnas tradicionales para campos que son más estables.

Estructuras de datos JSONB

Tanto JSONB como MongoDB BSON son esencialmente estructuras de árbol que utilizan nodos de varios niveles para almacenar los datos JSONB analizados. MongoDB BSON tiene una estructura muy similar.

Fuente de las imágenes

JSONB Y TOSTADAS

Otra consideración importante para el almacenamiento es cómo JSONB interactúa con TOAST (la técnica de almacenamiento de atributos de gran tamaño). Por lo general, cuando el tamaño de su columna supera el TOAST_TUPLE_THRESHOLD (valor predeterminado de 2 kb), PostgreSQL intentará comprimir los datos y ajustarlos en 2 kb. Si eso no funciona, los datos se mueven a un almacenamiento fuera de línea. Esto es lo que llaman "tostar" los datos. Cuando se recuperan los datos, debe ocurrir el proceso inverso "desTOASTear". También puede controlar la estrategia de almacenamiento TOAST:

  • Extendido – Permite almacenamiento y compresión fuera de línea (usando pglz). Esta es la opción predeterminada.
  • Externo – Permite el almacenamiento fuera de línea, pero no la compresión.

Si experimenta retrasos debido a la compresión o descompresión TOAST, una opción es establecer de forma proactiva el almacenamiento de columnas en 'EXTENDIDO'. Para obtener todos los detalles, consulte este documento de PostgreSQL.

Operadores y funciones JSONB

PostgreSQL proporciona una variedad de operadores para trabajar en JSONB. De los documentos:

Operador Descripción
-> Obtenga el elemento de matriz JSON (indexado desde cero, los números enteros negativos cuentan desde el final)
-> Obtener campo de objeto JSON por clave
->> Obtener elemento de matriz JSON como texto
->> Obtener campo de objeto JSON como texto
#> Obtenga el objeto JSON en la ruta especificada
#>> Obtenga el objeto JSON en la ruta especificada como texto
@> ¿El valor JSON izquierdo contiene las entradas de ruta/valor JSON derecho en el nivel superior?
<@ ¿Las entradas de valor/ruta JSON de la izquierda están contenidas en el nivel superior dentro del valor JSON de la derecha?
? ¿La cadena existe como una clave de nivel superior dentro del valor JSON?
?| Haga cualquiera de estas cadenas de matriz existen como claves de nivel superior?
?& Haz todas estas cadenas de matriz existen como claves de nivel superior?
|| Concatenar dos valores jsonb en un nuevo valor jsonb
- Eliminar par clave/valor o cadena elemento del operando izquierdo. Los pares clave/valor se comparan en función de su valor clave.
- Eliminar varios pares clave/valor o cadena elementos del operando izquierdo. Los pares clave/valor se comparan en función de su valor clave.
- Elimine el elemento de la matriz con el índice especificado (los enteros negativos cuentan desde el final). Lanza un error si el contenedor de nivel superior no es una matriz.
#- Elimine el campo o elemento con la ruta especificada (para matrices JSON, los enteros negativos cuentan desde el final)
@? ¿La ruta JSON devuelve algún elemento para el valor JSON especificado?
@@ Devuelve el resultado de la verificación de predicado de ruta JSON para el valor JSON especificado. Solo se tiene en cuenta el primer elemento del resultado. Si el resultado no es booleano, se devuelve nulo.

PostgreSQL también proporciona una variedad de funciones de creación y funciones de procesamiento para trabajar con los datos JSONB.

Índices JSONB

JSONB proporciona una amplia gama de opciones para indexar sus datos JSON. En un nivel alto, vamos a profundizar en 3 tipos diferentes de índices:GIN, BTREE y HASH. No todos los tipos de índice admiten todas las clases de operadores, por lo que se necesita planificación para diseñar sus índices según el tipo de operadores y consultas que planea usar.

Índices GIN

GIN significa "índices invertidos generalizados". De los documentos:

“GIN está diseñado para manejar casos en los que los elementos que se indexarán son valores compuestos y las consultas que manejará el índice deben buscar elementos valores que aparecen dentro de los elementos compuestos. Por ejemplo, los elementos podrían ser documentos y las consultas podrían ser búsquedas de documentos que contengan palabras específicas”.

GIN admite dos clases de operadores:

  • jsonb_ops (predeterminado) – ?, ?|, ?&, @>, @@, @? [Indexar cada clave y valor en el elemento JSONB]
  • jsonb_pathops – @>, @@, @? [Indice solo los valores en el elemento JSONB]
CREATE INDEX datagin ON books USING gin (data);

Operadores de existencia (?, ?|, ?&)

Estos operadores se pueden usar para verificar la existencia de claves de nivel superior en JSONB. Vamos a crear un índice GIN en la columna de datos JSONB. Por ejemplo, busque todos los libros que están disponibles en braille. El JSON se parece a esto:

"{"tags": {"nk594127": {"ik71786": "iv678771"}}, "braille": false, "keywords": ["abc", "kef", "keh"], "hardcover": true, "publisher": "EfgdxUdvB0", "criticrating": 1}
demo=# select * from books where data ? 'braille';
id | author | isbn | rating | data

---------+-----------------+------------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------
------------------
1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags": {"nk455671": {"ik937456": "iv506075"}}, "braille": true, "keywords": ["abc", "kef", "keh"], "hardcover": false, "publisher": "zSfZIAjGGs", "
criticrating": 4}
.....

demo=# explain analyze select * from books where data ? 'braille';
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on books (cost=12.75..1005.25 rows=1000 width=158) (actual time=0.033..0.039 rows=15 loops=1)
Recheck Cond: (data ? 'braille'::text)
Heap Blocks: exact=2
-> Bitmap Index Scan on datagin (cost=0.00..12.50 rows=1000 width=0) (actual time=0.022..0.022 rows=15 loops=1)
Index Cond: (data ? 'braille'::text)
Planning Time: 0.102 ms
Execution Time: 0.067 ms
(7 rows)

Como puede ver en el resultado de la explicación, el índice GIN que creamos se usa para la búsqueda. ¿Qué pasaría si quisiéramos encontrar libros que estuvieran en braille o en tapa dura?

demo=# explain analyze select * from books where data ?| array['braille','hardcover'];
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on books (cost=16.75..1009.25 rows=1000 width=158) (actual time=0.029..0.035 rows=15 loops=1)
Recheck Cond: (data ?| '{braille,hardcover}'::text[])
Heap Blocks: exact=2
-> Bitmap Index Scan on datagin (cost=0.00..16.50 rows=1000 width=0) (actual time=0.023..0.023 rows=15 loops=1)
Index Cond: (data ?| '{braille,hardcover}'::text[])
Planning Time: 0.138 ms
Execution Time: 0.057 ms
(7 rows)

El índice GIN admite los operadores de "existencia" solo en claves de "nivel superior". Si la clave no está en el nivel superior, no se utilizará el índice. Dará como resultado un escaneo secuencial:

demo=# select * from books where data->'tags' ? 'nk455671';
id | author | isbn | rating | data

---------+-----------------+------------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------
------------------
1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags": {"nk455671": {"ik937456": "iv506075"}}, "braille": true, "keywords": ["abc", "kef", "keh"], "hardcover": false, "publisher": "zSfZIAjGGs", "
criticrating": 4}
685122 | GWfuvKfQ1PCe1IL | jnyhYYcF66 | 3 | {"tags": {"nk455671": {"ik615925": "iv253423"}}, "publisher": "b2NwVg7VY3", "criticrating": 0}
(2 rows)

demo=# explain analyze select * from books where data->'tags' ? 'nk455671';
QUERY PLAN
----------------------------------------------------------------------------------------------------------
Seq Scan on books (cost=0.00..38807.29 rows=1000 width=158) (actual time=0.018..270.641 rows=2 loops=1)
Filter: ((data -> 'tags'::text) ? 'nk455671'::text)
Rows Removed by Filter: 1000017
Planning Time: 0.078 ms
Execution Time: 270.728 ms
(5 rows)

La forma de verificar la existencia de documentos anidados es usar "índices de expresión". Vamos a crear un índice en datos->etiquetas:

CREATE INDEX datatagsgin ON books USING gin (data->'tags');
demo=# select * from books where data->'tags' ? 'nk455671';
id | author | isbn | rating | data

---------+-----------------+------------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------
------------------
1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags": {"nk455671": {"ik937456": "iv506075"}}, "braille": true, "keywords": ["abc", "kef", "keh"], "hardcover": false, "publisher": "zSfZIAjGGs", "
criticrating": 4}
685122 | GWfuvKfQ1PCe1IL | jnyhYYcF66 | 3 | {"tags": {"nk455671": {"ik615925": "iv253423"}}, "publisher": "b2NwVg7VY3", "criticrating": 0}
(2 rows)

demo=# explain analyze select * from books where data->'tags' ? 'nk455671';
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on books (cost=12.75..1007.75 rows=1000 width=158) (actual time=0.031..0.035 rows=2 loops=1)
Recheck Cond: ((data ->'tags'::text) ? 'nk455671'::text)
Heap Blocks: exact=2
-> Bitmap Index Scan on datatagsgin (cost=0.00..12.50 rows=1000 width=0) (actual time=0.021..0.021 rows=2 loops=1)
Index Cond: ((data ->'tags'::text) ? 'nk455671'::text)
Planning Time: 0.098 ms
Execution Time: 0.061 ms
(7 rows)

Nota:una alternativa aquí es usar el operador @>:

select * from books where data @> '{"tags":{"nk455671":{}}}'::jsonb;

Sin embargo, esto solo funciona si el valor es un objeto. Por lo tanto, si no está seguro de si el valor es un objeto o un valor primitivo, podría generar resultados incorrectos.

Operadores de ruta @>, <@

El operador "ruta" se puede usar para consultas de varios niveles de sus datos JSONB. Usémoslo similar al ? operador arriba:

select * from books where data @> '{"braille":true}'::jsonb;
demo=# explain analyze select * from books where data @> '{"braille":true}'::jsonb;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on books (cost=16.75..1009.25 rows=1000 width=158) (actual time=0.040..0.048 rows=6 loops=1)
Recheck Cond: (data @> '{"braille": true}'::jsonb)
Rows Removed by Index Recheck: 9
Heap Blocks: exact=2
-> Bitmap Index Scan on datagin (cost=0.00..16.50 rows=1000 width=0) (actual time=0.030..0.030 rows=15 loops=1)
Index Cond: (data @> '{"braille": true}'::jsonb)
Planning Time: 0.100 ms
Execution Time: 0.076 ms
(8 rows)

Los operadores de ruta admiten la consulta de objetos anidados u objetos de nivel superior:

demo=# select * from books where data @> '{"publisher":"XlekfkLOtL"}'::jsonb;
id | author | isbn | rating | data
-----+-----------------+------------+--------+-------------------------------------------------------------------------------------
346 | uD3QOvHfJdxq2ez | KiAaIRu8QE | 1 | {"tags": {"nk88": {"ik37": "iv161"}}, "publisher": "XlekfkLOtL", "criticrating": 3}
(1 row)

demo=# explain analyze select * from books where data @> '{"publisher":"XlekfkLOtL"}'::jsonb;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on books (cost=16.75..1009.25 rows=1000 width=158) (actual time=0.491..0.492 rows=1 loops=1)
Recheck Cond: (data @> '{"publisher": "XlekfkLOtL"}'::jsonb)
Heap Blocks: exact=1
-> Bitmap Index Scan on datagin (cost=0.00..16.50 rows=1000 width=0) (actual time=0.092..0.092 rows=1 loops=1)
Index Cond: (data @> '{"publisher": "XlekfkLOtL"}'::jsonb)
Planning Time: 0.090 ms
Execution Time: 0.523 ms

Las consultas también pueden ser de varios niveles:

demo=# select * from books where data @> '{"tags":{"nk455671":{"ik937456":"iv506075"}}}'::jsonb;
id | author | isbn | rating | data

---------+-----------------+------------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------
------------------
1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags": {"nk455671": {"ik937456": "iv506075"}}, "braille": true, "keywords": ["abc", "kef", "keh"], "hardcover": false, "publisher": "zSfZIAjGGs", "
criticrating": 4}
(1 row)

Clase de operador "pathops" del índice GIN

GIN también admite una opción de "pathops" para reducir el tamaño del índice GIN. Cuando utiliza la opción pathops, el único operador compatible es el “@>”, por lo que debe tener cuidado con sus consultas. De los documentos:

“La diferencia técnica entre un índice GIN jsonb_ops y jsonb_path_ops es que el primero crea elementos de índice independientes para cada clave y valor en los datos, mientras que el segundo crea elementos de índice solo para cada valor en los datos”

Puede crear un índice de pathops GIN de la siguiente manera:

CREATE INDEX dataginpathops ON books USING gin (data jsonb_path_ops);

En mi pequeño conjunto de datos de 1 millón de libros, puede ver que el índice GIN de pathops es más pequeño; debe probar con su conjunto de datos para comprender los ahorros:

public | dataginpathops | index | sgpostgres | books | 67 MB |
public | datatagsgin | index | sgpostgres | books | 84 MB |

Volvamos a ejecutar nuestra consulta anterior con el índice pathops:

demo=# select * from books where data @> '{"tags":{"nk455671":{"ik937456":"iv506075"}}}'::jsonb;
id | author | isbn | rating | data

---------+-----------------+------------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------
------------------
1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags": {"nk455671": {"ik937456": "iv506075"}}, "braille": true, "keywords": ["abc", "kef", "keh"], "hardcover": false, "publisher": "zSfZIAjGGs", "
criticrating": 4}
(1 row)

demo=# explain select * from books where data @> '{"tags":{"nk455671":{"ik937456":"iv506075"}}}'::jsonb;
QUERY PLAN
-----------------------------------------------------------------------------------------
Bitmap Heap Scan on books (cost=12.75..1005.25 rows=1000 width=158)
Recheck Cond: (data @> '{"tags": {"nk455671": {"ik937456": "iv506075"}}}'::jsonb)
-> Bitmap Index Scan on dataginpathops (cost=0.00..12.50 rows=1000 width=0)
Index Cond: (data @> '{"tags": {"nk455671": {"ik937456": "iv506075"}}}'::jsonb)
(4 rows)

Sin embargo, como se mencionó anteriormente, la opción "pathops" no es compatible con todos los escenarios que admite la clase de operador predeterminada. Con un índice GIN "pathops", todas estas consultas no pueden aprovechar el índice GIN. Para resumir, tiene un índice más pequeño pero admite un caso de uso más limitado.

select * from books where data ? 'tags'; => Sequential scan
select * from books where data @> '{"tags" :{}}'; => Sequential scan
select * from books where data @> '{"tags" :{"k7888":{}}}' => Sequential scan

Índices B-Tree

Los índices B-tree son el tipo de índice más común en las bases de datos relacionales. Sin embargo, si indexa una columna JSONB completa con un índice de árbol B, los únicos operadores útiles son "=", <, <=,>,>=. Esencialmente, esto solo se puede usar para comparaciones de objetos completos, lo que tiene un caso de uso muy limitado.

Un escenario más común es usar "índices de expresión" de árbol B. Para obtener una introducción, consulte aquí:Índices de expresiones. Los índices de expresión de árbol B pueden admitir los operadores de comparación comunes '=', '<', '>', '>=', '<='. Como recordará, los índices GIN no admiten estos operadores. Consideremos el caso en el que queremos recuperar todos los libros con datos->críticar> 4. Por lo tanto, crearía una consulta como esta:

demo=# select * from books where data->'criticrating' > 4;
ERROR: operator does not exist: jsonb >= integer
LINE 1: select * from books where data->'criticrating'  >= 4;
^
HINT: No operator matches the given name and argument types. You might need to add explicit type casts.

Bueno, eso no funciona ya que el operador '->' devuelve un tipo JSONB. Así que necesitamos usar algo como esto:

demo=# select * from books where (data->'criticrating')::int4 > 4;

Si está usando una versión anterior a PostgreSQL 11, se vuelve más feo. Primero debe consultar como texto y luego convertirlo en un número entero:

demo=# select * from books where (data->'criticrating')::int4 > 4;

Para los índices de expresión, el índice debe coincidir exactamente con la expresión de consulta. Entonces, nuestro índice se vería así:

demo=# CREATE INDEX criticrating ON books USING BTREE (((data->'criticrating')::int4));
CREATE INDEX

demo=# explain analyze select * from books where (data->'criticrating')::int4 = 3;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
Index Scan using criticrating on books (cost=0.42..4626.93 rows=5000 width=158) (actual time=0.069..70.221 rows=199883 loops=1)
Index Cond: (((data -> 'criticrating'::text))::integer = 3)
Planning Time: 0.103 ms
Execution Time: 79.019 ms
(4 rows)

demo=# explain analyze select * from books where (data->'criticrating')::int4 = 3;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
Index Scan using criticrating on books (cost=0.42..4626.93 rows=5000 width=158) (actual time=0.069..70.221 rows=199883 loops=1)
Index Cond: (((data -> 'criticrating'::text))::integer = 3)
Planning Time: 0.103 ms
Execution Time: 79.019 ms
(4 rows)
1
From above we can see that the BTREE index is being used as expected.

Índices hash

Si solo está interesado en el operador "=", entonces los índices hash se vuelven interesantes. Por ejemplo, considere el caso cuando buscamos una etiqueta particular en un libro. El elemento a indexar puede ser un elemento de nivel superior o anidado profundamente.

Ej. etiquetas->editor =XlekfkLOtL

CREATE INDEX publisherhash ON books USING HASH ((data->'publisher'));

Los índices hash también tienden a ser más pequeños que los índices B-tree o GIN. Por supuesto, esto depende en última instancia de su conjunto de datos.

demo=# select * from books where data->'publisher' = 'XlekfkLOtL'
demo-# ;
id | author | isbn | rating | data
-----+-----------------+------------+--------+-------------------------------------------------------------------------------------
346 | uD3QOvHfJdxq2ez | KiAaIRu8QE | 1 | {"tags": {"nk88": {"ik37": "iv161"}}, "publisher": "XlekfkLOtL", "criticrating": 3}
(1 row)

demo=# explain analyze select * from books where data->'publisher' = 'XlekfkLOtL';
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
Index Scan using publisherhash on books (cost=0.00..2.02 rows=1 width=158) (actual time=0.016..0.017 rows=1 loops=1)
Index Cond: ((data -> 'publisher'::text) = 'XlekfkLOtL'::text)
Planning Time: 0.080 ms
Execution Time: 0.035 ms
(4 rows)

Mención especial:Índices de trigramas GIN

PostgreSQL admite la coincidencia de cadenas mediante índices de trigramas. Los índices de trigramas funcionan dividiendo el texto en trigramas. Trigrams are basically words broken up into sequences of 3 letters. More information can be found in the documentation. GIN indexes support the “gin_trgm_ops” class that can be used to index the data in JSONB. You can choose to use expression indexes to build the trigram index on a particular column.

CREATE EXTENSION pg_trgm;
CREATE INDEX publisher ON books USING GIN ((data->'publisher') gin_trgm_ops);

demo=# select * from books where data->'publisher' LIKE '%I0UB%';
 id |     author      |    isbn    | rating |                                      data
----+-----------------+------------+--------+---------------------------------------------------------------------------------
  4 | KiEk3xjqvTpmZeS | EYqXO9Nwmm |      0 | {"tags": {"nk3": {"ik1": "iv1"}}, "publisher": "MI0UBqZJDt", "criticrating": 1}
(1 row)

As you can see in the query above, we can search for any arbitrary string occurring at any potion. Unlike the B-tree indexes, we are not restricted to left anchored expressions.

demo=# explain analyze select * from books where data->'publisher' LIKE '%I0UB%';
                                                     QUERY PLAN
--------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on books  (cost=9.78..111.28 rows=100 width=158) (actual time=0.033..0.033 rows=1 loops=1)
   Recheck Cond: ((data -> 'publisher'::text) ~~ '%I0UB%'::text)
   Heap Blocks: exact=1
   ->  Bitmap Index Scan on publisher  (cost=0.00..9.75 rows=100 width=0) (actual time=0.025..0.025 rows=1 loops=1)
         Index Cond: ((data -> 'publisher'::text) ~~ '%I0UB%'::text)
 Planning Time: 0.213 ms
 Execution Time: 0.058 ms
(7 rows)

Special Mention:GIN Array Indexes

JSONB has great built-in support for indexing arrays. Let's consider an example of indexing an array of strings using a GIN index in the case when our JSONB data contains a "keyword" element and we would like to find rows with particular keywords:

{"tags": {"nk780341": {"ik397357": "iv632731"}}, "keywords": ["abc", "kef", "keh"], "publisher": "fqaJuAdjP5", "criticrating": 2}

CREATE INDEX keywords ON books USING GIN ((data->'keywords') jsonb_path_ops);

demo=# select * from books where data->'keywords' @> '["abc", "keh"]'::jsonb;
   id    |     author      |    isbn    | rating |                                                               data
---------+-----------------+------------+--------+-----------------------------------------------------------------------------------------------------------------------------------
 1000003 | zEG406sLKQ2IU8O | viPdlu3DZm |      4 | {"tags": {"nk263020": {"ik203820": "iv817928"}}, "keywords": ["abc", "kef", "keh"], "publisher": "7NClevxuTM", "criticrating": 2}
 1000004 | GCe9NypHYKDH4rD | so6TQDYzZ3 |      4 | {"tags": {"nk780341": {"ik397357": "iv632731"}}, "keywords": ["abc", "kef", "keh"], "publisher": "fqaJuAdjP5", "criticrating": 2}
(2 rows)

demo=# explain analyze select * from books where data->'keywords' @> '["abc", "keh"]'::jsonb;
                                                     QUERY PLAN
---------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on books  (cost=54.75..1049.75 rows=1000 width=158) (actual time=0.026..0.028 rows=2 loops=1)
   Recheck Cond: ((data -> 'keywords'::text) @> '["abc", "keh"]'::jsonb)
   Heap Blocks: exact=1
   ->  Bitmap Index Scan on keywords  (cost=0.00..54.50 rows=1000 width=0) (actual time=0.014..0.014 rows=2 loops=1)
         Index Cond: ((data -> 'keywords'::text) @&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt; '["abc", "keh"]'::jsonb)
 Planning Time: 0.131 ms
 Execution Time: 0.063 ms
(7 rows)

The order of the items in the array on the right does not matter. For example, the following query would return the same result as the previous:

demo=# explain analyze select * from books where data->'keywords' @> '["keh","abc"]'::jsonb;

All elements in the right side array of the containment operator need to be present - basically like an "AND" operator. If you want "OR" behavior, you can construct it in the WHERE clause:

demo=# explain analyze select * from books where (data->'keywords' @> '["abc"]'::jsonb OR data->'keywords' @> '["keh"]'::jsonb);

More details on the behavior of the containment operators with arrays can be found in the documentation.

SQL/JSON &JSONPath

SQL standard added support for JSON  in SQL - SQL/JSON Standard-2016. With the PostgreSQL 12/13 releases, PostgreSQL has one of the best implementations of the SQL/JSON standard. For more details refer to the PostgreSQL 12 announcement.

One of the core features of SQL/JSON is support for the JSONPath language to query JSONB data. JSONPath allows you to specify an expression (using a syntax similar to the property access notation in Javascript) to query your JSONB data. This makes it simple and intuitive, but is also very powerful to query your JSONB data. Think of  JSONPath as the logical equivalent of XPath for XML.

.key Returns an object member with the specified key.
[*] Wildcard array element accessor that returns all array elements.
.* Wildcard member accessor that returns the values of all members located at the top level of the current object.
.** Recursive wildcard member accessor that processes all levels of the JSON hierarchy of the current object and returns all the member values, regardless of their nesting level.

Refer to JSONPath documentation for the full list of operators. JSONPath also supports a variety of filter expressions.

JSONPath Functions

PostgreSQL 12 provides several functions to use JSONPath to query your JSONB data. From the docs:

  • jsonb_path_exists - Checks whether JSONB path returns any item for the specified JSON valor.
  • jsonb_path_match - Returns the result of JSONB path predicate check for the specified JSONB value. Only the first item of the result is taken into account. If the result is not Boolean, then null is returned.
  • jsonb_path_query - Gets all JSONB items returned by JSONB path for the specified JSONB value. There are also a couple of other variants of this function that handle arrays of objects.

Let's start with a simple query - finding books by publisher:

demo=# select * from books where data @@ '$.publisher == "ktjKEZ1tvq"';
id | author | isbn | rating | data
---------+-----------------+------------+--------+----------------------------------------------------------------------------------------------------------------------------------
1000001 | 4RNsovI2haTgU7l | GwSoX67gLS | 2 | {"tags": {"nk542369": {"ik55240": "iv305393"}}, "keywords": ["abc", "def", "geh"], "publisher": "ktjKEZ1tvq", "criticrating": 0}
(1 row)

demo=# explain analyze select * from books where data @@ '$.publisher == "ktjKEZ1tvq"';
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on books (cost=21.75..1014.25 rows=1000 width=158) (actual time=0.123..0.124 rows=1 loops=1)
Recheck Cond: (data @@ '($."publisher" == "ktjKEZ1tvq")'::jsonpath)
Heap Blocks: exact=1
-> Bitmap Index Scan on datagin (cost=0.00..21.50 rows=1000 width=0) (actual time=0.110..0.110 rows=1 loops=1)
Index Cond: (data @@ '($."publisher" == "ktjKEZ1tvq")'::jsonpath)
Planning Time: 0.137 ms
Execution Time: 0.194 ms
(7 rows)

You can rewrite this expression as a JSONPath filter:

demo=# select * from books where jsonb_path_exists(data,'$.publisher ?(@ == "ktjKEZ1tvq")');

You can also use very complex query expressions. For example, let's select books where print style =hardcover and price =100:

select * from books where jsonb_path_exists(data, '$.prints[*] ?(@.style=="hc" &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; @.price == 100)');

However, index support for JSONPath is very limited at this point - this makes it dangerous to use JSONPath in the where clause. JSONPath support for indexes will be improved in subsequent releases.

demo=# explain analyze select * from books where jsonb_path_exists(data,'$.publisher ?(@ == "ktjKEZ1tvq")');
QUERY PLAN
------------------------------------------------------------------------------------------------------------
Seq Scan on books (cost=0.00..36307.24 rows=333340 width=158) (actual time=0.019..480.268 rows=1 loops=1)
Filter: jsonb_path_exists(data, '$."publisher"?(@ == "ktjKEZ1tvq")'::jsonpath, '{}'::jsonb, false)
Rows Removed by Filter: 1000028
Planning Time: 0.095 ms
Execution Time: 480.348 ms
(5 rows)

Projecting Partial JSON

Another great use case for JSONPath is projecting partial JSONB from the row that matches. Consider the following sample JSONB:

demo=# select jsonb_pretty(data) from books where id = 1000029;
jsonb_pretty
-----------------------------------
{
 "tags": {
 "nk678947": {
      "ik159670": "iv32358
 }
 },
 "prints": [
     {
         "price": 100,
         "style": "hc"
     },
     {
        "price": 50,
        "style": "pb"
     }
 ],
 "braille": false,
 "keywords": [
     "abc",
     "kef",
     "keh"
 ],
 "hardcover": true,
 "publisher": "ppc3YXL8kK",
 "criticrating": 3
}

Select only the publisher field:

demo=# select jsonb_path_query(data, '$.publisher') from books where id = 1000029;
jsonb_path_query
------------------
"ppc3YXL8kK"
(1 row)

Select the prints field (which is an array of objects):

demo=# select jsonb_path_query(data, '$.prints') from books where id = 1000029;
jsonb_path_query
---------------------------------------------------------------
[{"price": 100, "style": "hc"}, {"price": 50, "style": "pb"}]
(1 row)

Select the first element in the array prints:

demo=# select jsonb_path_query(data, '$.prints[0]') from books where id = 1000029;
jsonb_path_query
-------------------------------
{"price": 100, "style": "hc"}
(1 row)

Select the last element in the array prints:

demo=# select jsonb_path_query(data, '$.prints[$.size()]') from books where id = 1000029;
jsonb_path_query
------------------------------
{"price": 50, "style": "pb"}
(1 row)

Select only the hardcover prints from the array:

demo=# select jsonb_path_query(data, '$.prints[*] ?(@.style=="hc")') from books where id = 1000029;
       jsonb_path_query
-------------------------------
 {"price": 100, "style": "hc"}
(1 row)

We can also chain the filters:

demo=# select jsonb_path_query(data, '$.prints[*] ?(@.style=="hc") ?(@.price ==100)') from books where id = 1000029;
jsonb_path_query
-------------------------------
{"price": 100, "style": "hc"}
(1 row)

In summary, PostgreSQL provides a powerful and versatile platform to store and process JSON data. There are several gotcha's that you need to be aware of, but we are optimistic that it will be fixed in future releases.

Más consejos para ti

Which Is the Best PostgreSQL GUI?

PostgreSQL graphical user interface (GUI) tools help these open source database users to manage, manipulate, and visualize their data. In this post, we discuss the top 5 GUI tools for administering your PostgreSQL deployments. Más información

Managing High Availability in PostgreSQL

Managing high availability in your PostgreSQL hosting is very important to ensuring your clusters maintain exceptional uptime and strong operational performance so your data is always available to your application. Más información

PostgreSQL Connection Pooling:Part 1 – Pros &Cons

In modern apps, clients open a lot of connections. Developers are discouraged from holding a database connection while other operations take place. “Open a connection as late as possible, close as soon as possible”. Más información