Substituindo EAV por JSONB no PostgreSQL

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

Introdução

Vamos dar um exemplo clássico de provavelmente um dos casos de uso mais antigos do mundo de um banco de dados relacional (banco de dados): temos uma entidade e precisamos salvar certas propriedades (atributos) dessa entidade. Mas nem todas as instâncias podem ter o mesmo conjunto de propriedades e mais propriedades poderão ser adicionadas no futuro.

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

Considere o padrão EAV (Entidade-Atributo-Valor), isso ocorre com bastante frequência. Uma tabela contém entidades (registros), outra tabela contém nomes de propriedades (atributos) e uma terceira tabela associa entidades aos seus atributos e contém o valor desses atributos para a entidade atual. Isso lhe dá a capacidade de ter diferentes conjuntos de propriedades para diferentes objetos e também adicionar propriedades dinamicamente sem alterar a estrutura do banco de dados.

No entanto, eu não estaria escrevendo este post se não houvesse algumas desvantagens na abordagem EVA. Assim, por exemplo, para obter uma ou mais entidades que possuam 1 atributo cada, são necessárias 2 junções na consulta: a primeira é uma junção com a tabela de atributos, a segunda é uma junção com a tabela de valores. Se uma entidade tiver 2 atributos, serão necessárias 4 junções! Além disso, todos os atributos são normalmente armazenados como strings, o que resulta na conversão de tipo tanto para o resultado quanto para a cláusula WHERE. Se você escrever muitas consultas, isso será um desperdício em termos de uso de recursos.

Apesar destas deficiências óbvias, o EAV tem sido utilizado há muito tempo para resolver estes tipos de problemas. Estas eram deficiências inevitáveis ​​e simplesmente não havia alternativa melhor.
Mas então uma nova “tecnologia” apareceu no PostgreSQL...

A partir do PostgreSQL 9.4, o tipo de dados JSONB foi adicionado para armazenar dados binários JSON. Embora armazenar JSON nesse formato normalmente ocupe um pouco mais de espaço e tempo do que JSON de texto simples, executar operações nele é muito mais rápido. JSONB também suporta indexação, o que torna as consultas ainda mais rápidas.

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

Configurando um banco de dados de teste

Para esta comparação, criei o banco de dados em uma nova instalação do PostgreSQL 9.5 na versão de $80 DigitalOcean Ubuntu 14.04. Depois de definir alguns parâmetros no postgresql.conf eu executei este script usando psql. As tabelas a seguir foram criadas para apresentar os dados na forma 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 segue uma tabela onde os mesmos dados serão armazenados, mas com atributos em uma coluna do tipo JSONB – Propriedades.

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

Parece muito mais simples, não é? Em seguida, foi adicionado às tabelas de entidades (entidade & entidade_jsonb) 10 milhões de registros e, consequentemente, a tabela foi preenchida com os mesmos dados usando o padrão EAV e a abordagem com uma coluna JSONB - entidade_jsonb.properties. Assim, recebemos diversos 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"
  } 
}

Portanto, agora temos os mesmos dados para ambas as opções. Vamos começar a comparar as implementações no trabalho!

Simplifique seu projeto

Foi afirmado anteriormente que o design do banco de dados foi bastante simplificado: uma tabela, usando uma coluna JSONB para propriedades, em vez de usar três tabelas para EAV. Mas como isso se reflete nas solicitações? A atualização de uma propriedade de entidade é assim:

-- 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 simples. Para atualizar o valor de uma propriedade em um objeto JSONB temos que usar a função jsonb_set()e deve passar nosso novo valor como um objeto JSONB. No entanto, não precisamos conhecer nenhum identificador antecipadamente. Observando o exemplo do EAV, precisamos saber o entidade_id e o entidade_attribute_id para realizar a atualização. Se você deseja atualizar uma propriedade em uma coluna JSONB com base no nome do objeto, tudo será feito em uma linha simples.

Agora vamos selecionar a entidade que acabamos de atualizar com base em 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';

Acho que podemos concordar que o segundo é mais curto (sem junção!) e, portanto, mais legível. JSONB vence aqui! Usamos o operador JSON ->> para obter a cor como um valor de texto do 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 em sua coluna de propriedades contém um objeto que está à direita do operador @>. Menos legível, mais produtivo (veja abaixo).

Vamos tornar o uso do JSONB ainda mais fácil quando você precisar selecionar várias propriedades de uma só vez. É aqui que entra a abordagem JSONB: 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 joins para cada propriedade que deseja consultar. Na minha opinião, as consultas acima mostram uma grande simplificação no design do banco de dados. Veja mais exemplos de como escrever consultas JSONB, também em esta publicar.
Agora é hora de falar sobre desempenho.

Desempenho

Para comparar o desempenho eu usei EXPLICAR ANALISE em consultas, para calcular o tempo de execução. Cada consulta foi executada pelo menos três vezes porque o planejador de consultas demora mais na primeira vez. Primeiro executei as consultas sem nenhum índice. Obviamente, isso era uma vantagem do JSONB, já que as junções necessárias para o EAV não podiam utilizar índices (os campos-chave estrangeiros não eram indexados). Depois disso criei um índice nas 2 colunas de chave estrangeira da tabela de valores EAV, bem como um índice GIN para uma coluna JSONB.

A atualização dos dados mostrou os seguintes resultados em termos de tempo (em ms). Observe que a escala é logarítmica:

Substituindo EAV por JSONB no PostgreSQL

Vemos que o JSONB é muito (> 50000-x) mais rápido que o EAV se você não usar índices, pelo motivo exposto acima. Quando indexamos 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 estamos usando a coluna de propriedade nos critérios de avaliação.

Ao selecionar dados com base no valor da propriedade, obtemos os seguintes resultados (escala normal):

Substituindo EAV por JSONB no PostgreSQL

Você pode notar que o JSONB novamente funciona mais rápido que o EAV sem índices, mas quando o EAV com índices, ele ainda funciona mais rápido que o JSONB. Mas então vi que os tempos das consultas JSONB eram os mesmos, isso me levou ao fato de que os índices GIN não funcionam. Aparentemente, quando você usa um índice GIN em uma coluna com propriedades preenchidas, ele só entra em vigor ao usar o operador include @>. Usei isso em um novo teste e teve um impacto enorme no tempo: apenas 0,153ms! Isso é 15000 vezes mais rápido que o EAV e 25000 vezes mais rápido que o operador ->>.

Acho que foi rápido o suficiente!

Tamanho da tabela do banco de dados

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

Substituindo EAV por JSONB no PostgreSQL

Para a abordagem EAV, os tamanhos das tabelas são em torno de 3068 MB e os índices de até 3427 MB, totalizando 6,43 GB. A abordagem JSONB usa 1817 MB para a tabela e 318 MB para os índices, ou seja, 2,08 GB. Acontece 3 vezes menos! Esse fato me surpreendeu um pouco porque armazenamos nomes de propriedades em cada objeto JSONB.

Mesmo assim, os números falam por si: no EAV armazenamos 2 chaves estrangeiras inteiras por valor de atributo, resultando em 8 bytes de dados adicionais. Além disso, o EAV armazena todos os valores de propriedade como texto, enquanto o JSONB usará valores numéricos e booleanos internamente sempre que possível, resultando em um espaço menor.

Resultados de

No geral, acho que salvar as propriedades da entidade no formato JSONB pode facilitar muito o design e a manutenção do seu banco de dados. Se você estiver executando muitas consultas, manter tudo na mesma tabela da entidade funcionará com mais eficiência. E o fato de isso simplificar a interação entre os dados já é uma vantagem, mas o volume do banco de dados resultante é 3 vezes menor.

Além disso, com base nos testes realizados, podemos concluir que as perdas de desempenho são muito insignificantes. Em alguns casos, o JSONB é ainda mais rápido que o EAV, o que o torna ainda melhor. No entanto, é claro que este benchmark não cobre 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,...), por isso, se tiver alguma sugestão sobre como melhorá-las , fique à vontade para deixar nos comentários!

Fonte: habr.com

Adicionar um comentário