PostgreSQL como Banco NoSQL

Usando NoSQL e SQL ao mesmo tempo com Postgres

PostgreSQL é um dos bancos de dados mais estáveis e completos disponíveis. Além de uma história de 30 anos de desenvolvimento que precede a padronização de SQL como linguagem padrão de consulta para bancos de dados relacionais. PostgreSQL é o sucessor do Ingres (o nome vem de "Post Ingres") e sua história é plena de inovação, como a orientação a objetos aproximando o mundo da orientação a objetos e dos dados estruturados dos bancos relacionais, e além de tudo, é software livre.

PostgreSQL (ou Postgres, mas nunca POSTGRE, por favor!) também suporta dados não estruturados através de seu tipo HSTORE desde 2006 e nas versões mais recentes ganhou o tipo JSONB. Estas características fazem com que o Postgres ganhe suporte nativo a dados no estilo "chave->valor" como Cassandra, CouchDB, etc e também a documentos JSON, exatamente como o MongoDB.

Postgres também possui uma implementação muito completa de suporte a dados geográficos através de sua venerada extensão Postgis.

Todas estas características são apenas uma descrição rasa do trabalho inovador realizado na evolução do PostgreSQL. Este trabalho coroou seu criador, Michael Stonebraker, com o prêmio Turing de 2014, prêmio reservado aos contribuidores dos grandes avanços da computação mundial, tão prestigioso quanto o prêmio Nobel.

Mas que tal colocar a mão na massa e experimentar um pouco das características NoSQL do Postgres? Vamos criar duas tabelas, uma tradicional e uma tabela com apenas uma coluna com dados JSONB. Estes exemplos foram executados em um banco PostgreSQL v9.4.

Digamos que criamos um sistema de banco de dados para gerenciar um Petshop web. Nossos produtos estão armazenados de forma estruturada em uma tabela tradicional, "produtos" e dados obtidos de nossos usuários estão armazenados na forma de documentos json. Criemos a tabela de produtos e vamos inserir uma ração para gatos e uma para cachorros:

macan=# create table produtos ( produto_id int, produto_nome varchar(80), produto_para varchar(80), preco numeric(6,2), estoque int );
CREATE TABLE

macan=# insert into produtos values (1,'Ração Doguina', 'cachorro', 140.23, 5);
INSERT 0 1
macan=# insert into produtos values (1,'Ração Kittehfood', 'gato', 139.99, 15);
INSERT 0 1

macan=# select * from produtos;
produto_id | produto_nome | produto_para | preco | estoque
------------+------------------+--------------+--------+---------
1 | Ração Doguina | cachorro | 140.23 | 5
1 | Ração Kittehfood | gato | 139.99 | 15
(2 rows)

Agora vamos criar uma tabela com documentos JSON, completamente schemaless. Façamos de conta que estes dados foram entrados por usuários via um frontend web que evoluiu de forma a incluir novos campos a cada release. Para isso vamos criar uma tabela com apenas uma coluna, do tipo JSONB e inserir alguns dados com diferentes campos.

macan=# create table user_data ( data JSONB );
CREATE TABLE

Note que poderíamos criar outras colunas, poderíamos ter uma tabela relacional tradicional com user_id, user_name e etc e um campo user_data com um documento json por usuário, mas vamos deixá-la assim para efeito de demonstração. Vamos inserir 3 documentos com conteúdo diferente. Digamos que nossos usuários tenham cadastrado dados de seus bichinhos em nosso site, primeiro em um momento quando isso não era possível e depois quando este recurso foi implementado.

macan=# insert into user_data (data) values ('{"nome": "Ada", "idade" : 5, "tipo": "gato", "dono": "Eduardo Maçan" }' );
INSERT 0 1
macan=# insert into user_data (data) values ('{"nome": "Juca", "idade" : 6, "tipo": "gato", "dono": "Andressa Martins" }' );
INSERT 0 1
macan=# insert into user_data (data) values ('{"nome": "Fred", "idade" : 4, "tipo": "cachorro", "dono": "Elizabete Martins" }' );
INSERT 0 1
macan=# insert into user_data (data) values ('{"nome": "Tutu", "dono": "Elizabete Martins" }' );
INSERT 0 1
macan=# select data from user_data ;
data
-------------------------------------------------------------------------------
{"dono": "Eduardo Maçan", "nome": "Ada", "tipo": "gato", "idade": 5}
{"dono": "Andressa Martins", "nome": "Juca", "tipo": "gato", "idade": 6}
{"dono": "Elizabete Martins", "nome": "Fred", "tipo": "cachorro", "idade": 4}
{"dono": "Elizabete Martins", "nome": "Tutu"}
(4 rows)

Note que um dos bichinhos não teve seu tipo ou idade especificada. Vamos agora fazer uma consulta por todos os documentos que dizem respeito a bichos com menos de 5 anos de idade.

macan=# select data->>'nome' as nome from user_data where (data->>'idade')::int < 5;
 nome
------
Fred
(1 row)

Ou o documento todo:

macan=# select data from user_data where data->>'idade' < '5';
data
-------------------------------------------------------------------------------
{"dono": "Elizabete Martins", "nome": "Fred", "tipo": "cachorro", "idade": 4}
(1 row)

Note que, como era de se esperar, o documento que não possuia um campo "idade" não entrou na consulta, não é como se sua ausência significasse um valor default (zero, por exemplo). Este campo não existe para este documento, logo ele não será incluído em nenhuma consulta que use o campo "idade".

Digamos agora que iremos fazer um spamEmail Marketing para nossos clientes cadastrados oferecendo ração para seus bichos. Como você faria isso se estivesse usando dois gerenciadores de bancos de dados diferentes em seu sistema? MySQL para a parte transacional/relacional e MongoDB para a parte NoSQL? Seja lá como fosse implementado, você deveria fazer múltiplas consultas, armazenar resultados intermediários em memória e depois consolidá-los, possivelmente iterando pela lista de resultados de uma das consultas.

Mas... e se tudo fosse implementado com PostgreSQL?

macan=# select ud.data->>'dono' as dono, ud.data->>'nome' as nome, ud.data->>'tipo' as tipo, p.produto_nome as produto from user_data ud, produtos p where ud.data->>'tipo' = p.produto_para;
dono | nome | tipo | produto
-------------------+------+----------+------------------
Eduardo Maçan | Ada | gato | Ração Kittehfood
Andressa Martins | Juca | gato | Ração Kittehfood
Elizabete Martins | Fred | cachorro | Ração Doguina
(3 rows)

Voilà! Já podemos enviar nosso spamEmail Marketing oferecendo produtos adequados para cada bichinho de nossos clientes, com um template customizado para gatos e outro para cachorros, por que não? Um dos bichos ficou de fora, mas ele foi cadastrado numa versão antiga do nosso sistema, que não possuia ainda uma entrada de  formulário para tipo e idade do bichinho. The schemaless way!

Temos os dois mundos, relacional e não relacional juntos e perfeitamente integrados, sem a necessidade de malabarismos e podendo fazer uso do melhor de cada um. Transações? Sim! Esquema flexível? Também!

Óbvio que nem mencionei a criação de índices e outros tópicos importantes, mas deixo a investigação para o leitor, cuja curiosidade espero ter atiçado.

  3 comments for “PostgreSQL como Banco NoSQL

  1. Giuliano
    04/04/2015 at 19:55

    Muito intessante isso aí...eu até já tinha ouvido falar desse suporte a JSON do Postgres, mas não me interessado em saber como ele tinha sido implementado. Vou dar uma olhada com mais calma nisso aí agora!!!

  2. 05/04/2015 at 07:29

    Dois detalhes: o PostgreSQL é o SGDB mais completo e estável, e não apenas entre os livres. Talvez o IBM DB/2 para OS/360 (seja lá qual for o nome do /mainframe/ hoje) seja mais estável, mas não é tão completo.

    • eduardomacan
      05/04/2015 at 08:37

      Você tem razão, Leandro. Mudei a redação do primeiro parágrafo para refletir isso 🙂

Comente de volta!