Substituindo EAV por JSONB no PostgreSQL

TL DR: JSONB pode simplificar bastante o desenvolvimento do esquema do banco de dados sem sacrificar o desempenho da consulta.

1. Introdução


Vamos dar um exemplo clássico, provavelmente, de um dos casos de uso mais antigos de bancos de dados relacionais (banco de dados): temos uma entidade e é necessário preservar certas propriedades (atributos) dessa entidade. Mas nem todas as instâncias podem ter o mesmo conjunto de propriedades; além disso, no futuro, a possível adição de mais propriedades.

A maneira mais fácil de resolver esse problema é criar uma coluna na tabela do banco de dados para cada valor da propriedade e simplesmente preencher as que são necessárias para uma instância de entidade específica. Ótimo! O problema foi resolvido ... até que sua tabela contenha milhões de registros e você não precise adicionar um novo registro.

Considere o padrão EAV ( Entity-Attribute-Value ), é bastante comum. Uma tabela contém entidades (registros), outra tabela contém nomes de propriedades (atributos) e a terceira tabela associa entidades a seus atributos e contém o valor desses atributos para a entidade atual. Isso oferece a oportunidade de ter diferentes conjuntos de propriedades para diferentes objetos, além de adicionar propriedades rapidamente, sem alterar a estrutura do banco de dados.

No entanto, eu não escreveria esta nota se não houvesse deficiências na abordagem usando o EVA. Assim, por exemplo, para obter uma ou mais entidades com 1 atributo cada, são necessárias 2 junções (junções) na consulta: a primeira é uma junção com a tabela de atributos, a segunda é a junção com uma tabela de valores. Se uma entidade tiver 2 atributos, 4 junções já serão necessárias! Além disso, todos os atributos são geralmente armazenados como seqüências de caracteres, o que leva à conversão de tipos para o resultado e a cláusula WHERE. Se você escrever muitas solicitações, isso será um desperdício em termos de uso de recursos.

Apesar dessas falhas óbvias, o EAV tem sido usado há muito tempo para resolver esses tipos de problemas. Essas eram falhas inevitáveis ​​e simplesmente não havia alternativa melhor.
Mas então uma nova "tecnologia" apareceu no PostgreSQL ...

A partir do PostgreSQL 9.4, um tipo de dados JSONB foi adicionado para armazenar dados binários JSON. Embora o armazenamento do JSON nesse formato normalmente ocupe um pouco mais de espaço e tempo do que o JSON em texto sem formatação, as operações com ele são muito mais rápidas. O JSONB também suporta indexação, o que torna a consulta ainda mais rápida.

O tipo de dados JSONB nos permite substituir o padrão EAV volumoso adicionando apenas uma coluna JSONB à nossa tabela de entidades, o que simplifica bastante o design do banco de dados. Mas muitos argumentam que isso deve ser acompanhado por uma diminuição na produtividade ... É por isso que apareci neste artigo.

Testar a configuração do banco de dados


Para essa comparação, criei um banco de dados em uma nova instalação do PostgreSQL 9.5 na versão $ 80 do DigitalOcean Ubuntu 14.04. Após definir alguns parâmetros no postgresql.conf, executei esse script usando o psql. As tabelas a seguir foram criadas para representar os dados 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 ); 

Abaixo está uma tabela na qual os mesmos dados serão armazenados, mas com atributos na coluna JSONB type - properties .

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

Parece muito mais fácil, certo? Em seguida, 10 milhões de registros foram adicionados às tabelas de entidades ( entity & entity_jsonb ) e, portanto, os mesmos dados da tabela foram preenchidos usando o padrão EAV e a abordagem com a coluna JSONB - entity_jsonb.properties . Assim, recebemos vários tipos de dados diferentes entre todo o conjunto de propriedades. Dados de exemplo:

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

Então, agora temos os mesmos dados, para duas opções. Vamos começar a comparar implementações no trabalho!

Simplificação de design


Já foi dito que o design do banco de dados foi bastante simplificado: uma tabela, usando a coluna JSONB para propriedades, em vez de usar três tabelas para o EAV. Mas como isso se reflete nos pedidos? A atualização de uma propriedade de uma entidade é a seguinte:

 -- EAV UPDATE entity_attribute_value SET value = 'blue' WHERE entity_attribute_id = 1 AND entity_id = 120; -- JSONB UPDATE entity_jsonb SET properties = jsonb_set(properties, '{"color"}', '"blue"') WHERE id = 120; 

Como você pode ver, a última solicitação não parece mais fácil. Para atualizar o valor de uma propriedade em um objeto JSONB, devemos usar a função jsonb_set () e transmitir nosso novo valor como um objeto JSONB. No entanto, não precisamos conhecer nenhum identificador antecipadamente. Observando o exemplo do EAV, precisamos conhecer o entity_id e o entity_attribute_id para atualizar. Se você deseja atualizar uma propriedade em uma coluna JSONB com base no nome do objeto, tudo isso é feito em uma linha simples.

Agora vamos escolher a entidade que acabamos de atualizar, de acordo com a condição de sua nova cor:

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

Penso que podemos concordar que o segundo é mais curto (sem junção!) E, portanto, mais legível. Aqui está a vitória do JSONB! Usamos o operador JSON - >> para obter a cor como valor de texto de um objeto JSONB. Há também uma segunda maneira de obter o mesmo resultado no modelo JSONB usando o operador @>:

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

Isso é um pouco mais complicado: verificamos se o objeto JSON na coluna de propriedades contém o objeto à direita do operador @>. Menos legível, mais produtivo (veja abaixo).

Simplifique ainda mais o uso do JSONB quando precisar selecionar várias propriedades de uma só vez. É aqui que a abordagem JSONB realmente entra: simplesmente selecionamos propriedades como colunas adicionais em nosso conjunto de resultados sem a necessidade de junções:

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

Com o EAV, você precisará de 2 associações para cada propriedade que deseja solicitar. Na minha opinião, as consultas acima mostram uma grande simplificação no design do banco de dados. Veja mais exemplos de como escrever solicitações JSONB, também nesta postagem.
Agora é hora de falar sobre desempenho.

Desempenho


Para comparar o desempenho, usei EXPLAIN ANALYZE nas consultas para calcular o tempo de execução. Cada solicitação foi executada pelo menos três vezes porque a primeira vez que o planejador de consultas demora mais. Inicialmente, executei consultas sem nenhum índice. Obviamente, isso serviu como uma vantagem do JSONB, pois a junção necessária para o EAV não pôde usar índices (os campos de chave estrangeira não foram indexados). Depois disso, criei um índice para 2 colunas de chaves estrangeiras na tabela de valores EAV, bem como um índice GIN para a coluna JSONB.

As atualizações de dados mostraram os seguintes resultados no tempo (em ms). Observe que a escala é logarítmica:

imagem

Vemos que o JSONB é muito (> 50.000-x) mais rápido que o EAV se você não usar índices, pelo motivo indicado acima. Quando indexamos as colunas com chaves primárias, a diferença quase desaparece, mas o JSONB ainda é 1,3 vezes mais rápido que o EAV. Observe que o índice na coluna JSONB não tem efeito aqui, pois não usamos a coluna de propriedades nos critérios de avaliação.

Para selecionar dados com base em um valor da propriedade, obtemos os seguintes resultados (escala normal):

imagem

Você pode perceber que o JSONB é mais rápido do que o EAV sem índices, mas quando o EAV está com índices, ele ainda funciona mais rápido que o JSONB. Mas então vi que o tempo para solicitações JSONB era o mesmo, o que me levou ao fato de que o índice GIN não funcionava. Aparentemente, quando você usa o índice GIN para uma coluna com propriedades preenchidas, ele age apenas ao usar o operador de inclusão @>. Eu usei isso em um novo teste, que teve um enorme impacto no tempo: apenas 0,153 ms! Isso é 15.000 vezes mais rápido que o EAV e 25.000 vezes mais rápido que o operador - >>.

Eu acho que foi rápido o suficiente!

Tamanho da tabela DB


Vamos comparar os tamanhos de tabela para as duas abordagens. No psql, podemos mostrar o tamanho de todas as tabelas e índices usando o comando \ dti +

imagem

Para a abordagem EAV, os tamanhos das tabelas são de cerca de 3068 MB e os índices são de até 3427 MB, o que totaliza 6,43 GB. Usando a abordagem JSONB, 1817 MB para a tabela e 318 MB para os índices são usados, ou seja, 2,08 GB. Acontece 3 vezes menos! Esse fato me surpreendeu um pouco porque armazenamos nomes de propriedades em todos os objetos JSONB.

Mas, mesmo assim, os números falam por si: no EAV, armazenamos 2 chaves estrangeiras inteiras para o valor do atributo, como resultado, obtemos 8 bytes de dados adicionais. Além disso, no EAV, todos os valores de propriedade são armazenados como texto, enquanto o JSONB utilizará valores numéricos e lógicos internamente sempre que possível, resultando em menos volume.

Sumário


Em geral, acho que armazenar propriedades da entidade no formato JSONB pode simplificar bastante o design e a manutenção do seu banco de dados. Se você realizar muitas consultas, tudo o que estiver armazenado na mesma tabela com a entidade funcionará de maneira mais eficiente. E o fato de isso simplificar a interação entre os dados já é uma vantagem, mas o banco de dados resultante é 3 vezes menor em volume.

Além disso, de acordo com o teste, podemos concluir que a perda de desempenho é muito pequena. Em alguns casos, o JSONB ainda funciona mais rápido que o EAV, o que o torna ainda melhor. No entanto, é claro que essa referência não abrange todos os aspectos (por exemplo, entidades com um número muito grande de propriedades, um aumento significativo no número de propriedades dos dados existentes, ...); portanto, se você tiver alguma sugestão sobre como melhorá-las, por favor Sinta-se livre para deixar um comentário!

Source: https://habr.com/ru/post/pt475178/


All Articles