PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Usando JSONB no PostgreSQL:como armazenar e indexar efetivamente dados JSON no PostgreSQL

JSON significa JavaScript Object Notation. É um formato padrão aberto que organiza os dados em pares de chave/valor e arrays detalhados na RFC 7159. JSON é o formato mais comum usado por web services para trocar dados, armazenar documentos, dados não estruturados, etc. para mostrar dicas e técnicas sobre como armazenar e indexar dados JSON com eficiência no PostgreSQL.


Você também pode conferir nosso webinar Trabalhando com dados JSON no PostgreSQL vs. MongoDB em parceria com o PostgresConf para saber mais sobre o assunto e confira nossa página do SlideShare para baixar os slides.

Por que armazenar JSON no PostgreSQL?

Por que um banco de dados relacional deveria se preocupar com dados não estruturados? Acontece que existem alguns cenários em que é útil.

  1. Flexibilidade do esquema


    Um dos principais motivos para armazenar dados usando o formato JSON é a flexibilidade do esquema. Armazenar seus dados em JSON é útil quando seu esquema é fluido e está mudando com frequência. Se você armazenar cada uma das chaves como colunas, isso resultará em operações DML frequentes. Isso pode ser difícil quando seu conjunto de dados é grande, por exemplo, rastreamento de eventos, análises, tags etc. Observação:se uma determinada chave estiver sempre presente em seu documento, pode fazer sentido armazená-lo como uma coluna de primeira classe. Discutimos mais sobre essa abordagem na seção "Padrões e antipadrões JSON" abaixo.
  2. Objetos aninhados


    Se seu conjunto de dados tiver objetos aninhados (único ou vários níveis), em alguns casos, será mais fácil manipulá-los em JSON em vez de desnormalizar os dados em colunas ou várias tabelas.
  3. Sincronizando com fontes de dados externas


    Muitas vezes, um sistema externo fornece dados como JSON, portanto, pode ser um armazenamento temporário antes que os dados sejam ingeridos em outras partes do sistema. Por exemplo, transações do Stripe.

Linha do tempo de suporte a JSON no PostgreSQL

O suporte a JSON no PostgreSQL foi introduzido na versão 9.2 e tem melhorado constantemente a cada lançamento.

  • Onda 1:o PostgreSQL 9.2  (2012) adicionou suporte para o tipo de dados JSON


    O banco de dados JSON na versão 9.2 era bastante limitado (e provavelmente exagerado naquele ponto) - basicamente uma string glorificada com alguma validação JSON lançada. É útil validar o JSON recebido e armazenar no banco de dados. Mais detalhes são fornecidos abaixo.
  • Onda 2:o PostgreSQL 9.4 (2014) adicionou suporte para o tipo de dados JSONB


    JSONB significa “JSON Binary” ou “JSON better” dependendo de quem você pergunta. É um formato binário decomposto para armazenar JSON. O JSONB suporta a indexação dos dados JSON e é muito eficiente na análise e consulta dos dados JSON. Na maioria dos casos, ao trabalhar com JSON no PostgreSQL, você deve usar JSONB.
  • Onda 3:PostgreSQL 12 (2019) adicionou suporte para SQL/JSON padrão e consultas JSONPATH


    JSONPath traz um poderoso mecanismo de consulta JSON para o PostgreSQL.


Quando você deve usar JSON vs. JSONB?

Na maioria dos casos, JSONB é o que você deve usar. No entanto, existem alguns casos específicos em que o JSON funciona melhor:

  • O JSON preserva a formatação original (também conhecida como espaço em branco) e a ordem das chaves.
  • JSON preserva chaves duplicadas.
  • JSON é mais rápido de ingerir em comparação com JSONB. No entanto, se você fizer algum processamento adicional, o JSONB será mais rápido.

Por exemplo, se você está apenas ingerindo logs JSON e não os consultando de forma alguma, o JSON pode ser uma opção melhor para você. Para os propósitos deste blog, quando nos referirmos ao suporte JSON no PostgreSQL, iremos nos referir ao JSONB daqui para frente.
Usando JSONB no PostgreSQL:como armazenar e indexar dados JSON com eficiência no PostgreSQLClick To Tweet

Padrões e antipadrões JSONB

Se o PostgreSQL tem um ótimo suporte para JSONB, por que precisamos mais de colunas? Por que não apenas criar uma tabela com um blob JSONB e se livrar de todas as colunas como o esquema abaixo:

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

No final das contas, as colunas ainda são a técnica mais eficiente para trabalhar com seus dados. O armazenamento JSONB tem algumas desvantagens em relação às colunas tradicionais:

  • O PostreSQL não armazena estatísticas de coluna para colunas JSONB


    O PostgreSQL mantém estatísticas sobre as distribuições de valores em cada coluna da tabela - valores mais comuns (MCV), entradas NULL, histograma de distribuição. Com base nesses dados, o planejador de consultas do PostgreSQL toma decisões inteligentes sobre o plano a ser usado para a consulta. Neste ponto, o PostgreSQL não armazena estatísticas para colunas ou chaves JSONB. Isso às vezes pode resultar em escolhas ruins, como usar junções de loop aninhadas versus junções de hash, etc. Um exemplo mais detalhado disso é fornecido nesta postagem do blog – Quando evitar JSONB em um esquema PostgreSQL.
  • O armazenamento JSONB resulta em um espaço de armazenamento maior


    O armazenamento JSONB não desduplica os nomes de chave no JSON. Isso pode resultar em um espaço de armazenamento consideravelmente maior em comparação ao MongoDB BSON no WiredTiger ou no armazenamento de coluna tradicional. Eu executei um teste simples com o modelo JSONB abaixo armazenando cerca de 10 milhões de linhas de dados, e aqui estão os resultados – De certa forma, isso é semelhante ao modelo de armazenamento MongoDB MMAPV1, onde as chaves no JSONB foram armazenadas como estão sem nenhuma compactação. Uma correção de longo prazo é mover os nomes das chaves para um dicionário de nível de tabela e consultar esse dicionário em vez de armazenar os nomes das chaves repetidamente. Até então, a solução alternativa pode ser usar nomes mais compactos (estilo unix) em vez de nomes mais descritivos. Por exemplo, se você estiver armazenando milhões de instâncias de uma chave específica, seria melhor em termos de armazenamento nomeá-la como "pb" em vez de "publisherName".

A maneira mais eficiente de aproveitar JSONB no PostgreSQL é combinar colunas e JSONB. Se uma chave aparecer com muita frequência em seus blobs JSONB, provavelmente será melhor armazená-la como uma coluna. Use JSONB como um “pegar tudo” para lidar com as partes variáveis ​​do seu esquema enquanto aproveita as colunas tradicionais para campos mais estáveis.

Estruturas de dados JSONB



Tanto o JSONB quanto o MongoDB BSON são essencialmente estruturas de árvore, usando nós de vários níveis para armazenar os dados JSONB analisados. O MongoDB BSON tem uma estrutura muito semelhante.

Fonte das imagens


JSONB &TOAST

Outra consideração importante para o armazenamento é como o JSONB interage com o TOAST (The Oversize Attribute Storage Technique). Normalmente, quando o tamanho da sua coluna excede o TOAST_TUPLE_THRESHOLD (padrão de 2kb), o PostgreSQL tentará compactar os dados e caber em 2kb. Se isso não funcionar, os dados serão movidos para o armazenamento fora de linha. Isso é o que eles chamam de "BRINDE" os dados. Quando os dados são buscados, o processo reverso “deTOASTting” precisa acontecer. Você também pode controlar a estratégia de armazenamento TOAST:

  • Estendido – Permite armazenamento e compactação fora de linha (usando pglz). Esta é a opção padrão.
  • Externo – Permite armazenamento fora de linha, mas não compactação.

Se houver atrasos devido à compactação ou descompactação TOAST, uma opção é definir proativamente o armazenamento da coluna como "ESTENDIDO". Para todos os detalhes, consulte este documento do PostgreSQL.

Operadores e funções JSONB

O PostgreSQL fornece uma variedade de operadores para trabalhar em JSONB. Dos documentos:

Operador Descrição
-> Obter elemento de matriz JSON (indexado a partir de zero, números inteiros negativos contam a partir do final)
-> Obter campo de objeto JSON por chave
->> Obter elemento de matriz JSON como texto
->> Obter campo de objeto JSON como texto
#> Obter objeto JSON no caminho especificado
#>> Obter objeto JSON no caminho especificado como texto
@> O valor JSON esquerdo contém as entradas de caminho/valor JSON direito no nível superior?
<@ As entradas de caminho/valor JSON esquerdo estão contidas no nível superior dentro do valor JSON direito?
? A string existe como uma chave de nível superior no valor JSON?
?| Faça qualquer uma dessas strings existem como chaves de nível superior?
?& Faça todos esses arrays strings existem como chaves de nível superior?
|| Concatenar dois valores jsonb em um novo valor jsonb
- Excluir par chave/valor ou string elemento do operando esquerdo. Os pares de chave/valor são correspondidos com base em seu valor de chave.
- Excluir vários pares de chave/valor ou string elementos do operando esquerdo. Os pares de chave/valor são correspondidos com base em seu valor de chave.
- Exclua o elemento da matriz com o índice especificado (inteiros negativos contam a partir do final). Lança um erro se o contêiner de nível superior não for uma matriz.
#- Excluir o campo ou elemento com o caminho especificado (para matrizes JSON, números inteiros negativos contam a partir do final)
@? O caminho JSON retorna algum item para o valor JSON especificado?
@@ Retorna o resultado da verificação de predicado de caminho JSON para o valor JSON especificado. Apenas o primeiro item do resultado é considerado. Se o resultado não for booleano, será retornado null.

O PostgreSQL também fornece uma variedade de Funções de Criação e Funções de Processamento para trabalhar com os dados JSONB.

Índices JSONB

JSONB fornece uma ampla variedade de opções para indexar seus dados JSON. Em alto nível, vamos nos aprofundar em 3 tipos diferentes de índices – GIN, BTREE e HASH. Nem todos os tipos de índice oferecem suporte a todas as classes de operadores, portanto, é necessário planejar para projetar seus índices com base no tipo de operadores e consultas que você planeja usar.

Índices GIN

GIN significa “índices invertidos generalizados”. Dos documentos:

"GIN é projetado para lidar com casos em que os itens a serem indexados são valores compostos e as consultas a serem tratadas pelo índice precisam procurar por elemento valores que aparecem nos itens compostos. Por exemplo, os itens podem ser documentos e as consultas podem ser pesquisas de documentos que contenham palavras específicas.”


GIN suporta duas classes de operadores:

  • jsonb_ops (padrão) – ?, ?|, ?&, @>, @@, @? [Indexar cada chave e valor no elemento JSONB]
  • jsonb_pathops – @>, @@, @? [Indexar apenas os valores no elemento JSONB]
CREATE INDEX datagin ON books USING gin (data);

Operadores de existência (?, ?|, ?&)

Esses operadores podem ser usados ​​para verificar a existência de chaves de nível superior no JSONB. Vamos criar um índice GIN na coluna JSONB de dados. Por exemplo, encontre todos os livros disponíveis em braille. O JSON se parece com isso:

"{"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 você pode ver na saída de explicação, o índice GIN que criamos está sendo usado para a pesquisa. E se quiséssemos encontrar livros em braille ou em capa 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)

O índice GIN suporta os operadores de “existência” apenas em chaves de “nível superior”. Se a chave não estiver no nível superior, o índice não será usado. Isso resultará em uma varredura sequencial:

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)

A maneira de verificar a existência em documentos aninhados é usar “índices de expressão”. Vamos criar um índice em data->tags:

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)

Observação:Uma alternativa aqui é usar o operador @>:

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

No entanto, isso só funciona se o valor for um objeto. Portanto, se você não tiver certeza se o valor é um objeto ou um valor primitivo, isso pode levar a resultados incorretos.

Operadores de caminho @>, <@

O operador “path” pode ser usado para consultas de vários níveis de seus dados JSONB. Vamos usá-lo semelhante ao ? operador acima:

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)

Os operadores de caminho suportam a consulta de objetos aninhados ou objetos de nível 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

As consultas também podem ser de vários níveis:

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)

Classe de operador GIN Index “pathops”

GIN também suporta uma opção “pathops” para reduzir o tamanho do índice GIN. Quando você usa a opção pathops, o único suporte do operador é o “@>” então você precisa ter cuidado com suas consultas. Dos documentos:

“A diferença técnica entre um índice GIN jsonb_ops e um jsonb_path_ops é que o primeiro cria itens de índice independentes para cada chave e valor nos dados, enquanto o último cria itens de índice apenas para cada valor nos dados”

Você pode criar um índice de pathops GIN da seguinte forma:

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

No meu pequeno conjunto de dados de 1 milhão de livros, você pode ver que o índice GIN do pathops é menor. Você deve testar com seu conjunto de dados para entender a economia:

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

Vamos executar novamente nossa consulta anterior com o í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)

No entanto, como mencionado acima, a opção “pathops” não suporta todos os cenários que a classe de operador padrão suporta. Com um índice GIN “pathops”, todas essas consultas não são capazes de alavancar o índice GIN. Para resumir, você tem um índice menor, mas suporta um caso de uso mais 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

Os índices B-tree são o tipo de índice mais comum em bancos de dados relacionais. No entanto, se você indexar uma coluna JSONB inteira com um índice de árvore B, os únicos operadores úteis serão “=”, <, <=,>,>=. Essencialmente, isso só pode ser usado para comparações de objetos inteiros, que têm um caso de uso muito limitado.

Um cenário mais comum é usar "índices de expressão" de árvore B. Para uma cartilha, consulte aqui – Índices em Expressões. Os índices de expressão B-tree podem suportar os operadores de comparação comuns '=', '<', '>', '>=', '<='. Como você deve se lembrar, os índices GIN não suportam esses operadores. Vamos considerar o caso em que queremos recuperar todos os livros com data->criticrating> 4. Então, você construiria uma consulta mais ou menos assim:

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.

Bem, isso não funciona porque o operador '->' retorna um tipo JSONB. Então precisamos usar algo assim:

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

Se você estiver usando uma versão anterior ao PostgreSQL 11, fica mais feio. Você precisa primeiro consultar como texto e depois convertê-lo em inteiro:

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

Para índices de expressão, o índice precisa ser uma correspondência exata com a expressão de consulta. Então, nosso índice ficaria assim:

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 de hash

Se você estiver interessado apenas no operador "=", os índices de hash se tornarão interessantes. Por exemplo, considere o caso em que estamos procurando uma tag específica em um livro. O elemento a ser indexado pode ser um elemento de nível superior ou profundamente aninhado.

Ex. tags->editor =XlekfkLOtL

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

Os índices de hash também tendem a ser menores em tamanho do que os índices B-tree ou GIN. Claro, isso depende, em última análise, do seu conjunto de dados.

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)

Menção especial:Índices de trigramas GIN

O PostgreSQL suporta correspondência de strings usando índices trigramas. Os índices de trigramas funcionam dividindo o texto em trigramas. Trigramas são basicamente palavras divididas em sequências de 3 letras. Mais informações podem ser encontradas na documentação. 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 value.
  • 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.


Mais dicas para você

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. Saber mais

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. Saber mais

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”. Saber mais