{"id":8429,"date":"2019-01-21T15:02:59","date_gmt":"2019-01-21T17:02:59","guid":{"rendered":"http:\/\/blog.plataformatec.com.br\/?p=8429"},"modified":"2019-10-28T11:55:46","modified_gmt":"2019-10-28T14:55:46","slug":"indices-para-jsonb-no-postgres","status":"publish","type":"post","link":"http:\/\/blog.plataformatec.com.br\/2019\/01\/indices-para-jsonb-no-postgres\/","title":{"rendered":"\u00cdndices para JSONB no Postgres"},"content":{"rendered":"
No Postgres existe a op\u00e7\u00e3o de salvar dados como JSON<\/a> – o que pode ser muito \u00fatil especialmente quando temos muitas incertezas quanto aos requisitos de neg\u00f3cio que ajudariam na modelagem das tabelas.<\/p>\n A flexibilidade de poder armazenar os dados sem se preocupar com a estrutura das tabelas parece interessante, por\u00e9m qual o impacto disso na performance?<\/p>\n A verdade \u00e9 o que os dados do tipo JSON s\u00e3o armazenados da maneira como foram inseridos – isto \u00e9, em texto – o que deixa as consultas um pouco mais lentas j\u00e1 que os dados precisam ser parseados<\/em> novamente. Por\u00e9m, existe uma outra varia\u00e7\u00e3o: o JSONB. Esse formato processa o texto no momento da inser\u00e7\u00e3o e o armazena em formato bin\u00e1rio. Isso faz com que a inser\u00e7\u00e3o demore um pouco mais, em troca de consultas mais r\u00e1pidas. Ah, e o JSONB tamb\u00e9m suporta \u00edndices!<\/p>\n Nesse artigo voc\u00ea vai encontrar uma compara\u00e7\u00e3o dos tipos de \u00edndices dispon\u00edveis para JSONB no Postgres, um benchmark<\/em> e como us\u00e1-los em aplica\u00e7\u00f5es Rails.<\/p>\n Para testar a performance de \u00edndices, \u00e9 importante ter um banco populado com uma quantidade razo\u00e1vel de registros. Abaixo voc\u00ea encontrar\u00e1 as etapas necess\u00e1rias para criar o banco de dados utilizado nesse benchmark<\/em>. Iremos importar cerca de 600 mil registros na estrutura abaixo:<\/p>\n Para importar esses dados, fa\u00e7a o download do arquivo<\/a> e siga as instru\u00e7\u00f5es abaixo:<\/p>\n Vamos criar a tabela Por fim, vamos importar os dados do JSON que acabamos de baixar utilizando o comando O \u00edndice do formato B-tree<\/strong> funciona somente com uma chave do documento e para um operador espec\u00edfico: isto \u00e9, o operador usado na defini\u00e7\u00e3o do \u00edndice dever\u00e1 ser o mesmo utilizado na consulta:<\/p>\n Para que o \u00edndice seja utilizado, devemos fazer a consulta com o mesmo operador:<\/p>\n Se utilizarmos outros operadores o \u00edndice n\u00e3o ser\u00e1 aplicado, e consequentemente a busca ficar\u00e1 mais lenta:<\/p>\n O \u00edndice para a consulta acima seria o seguinte:<\/p>\n Se for necess\u00e1rio responder perguntas como \u201cquais as reviews<\/em> tem rating<\/em> Por\u00e9m fica uma observa\u00e7\u00e3o aqui: nos testes feitos nesse benchmark<\/em>, os \u00edndices para esses operadores acabaram n\u00e3o<\/strong> valendo a pena, o tempo de execu\u00e7\u00e3o foi o mesmo. Mas pode ser que esse n\u00e3o seja sempre o caso, por isso \u00e9 importante fazer benchmarks<\/em> no seu ambiente para ter uma escolha mais assertiva.<\/p>\n Apenas uma men\u00e7\u00e3o honrosa. Sua funcionalidade \u00e9 muito similar ao B-tree<\/strong>, por\u00e9m o pr\u00f3prio banco j\u00e1 lan\u00e7a um warning<\/em> ao cri\u00e1-lo. O warning<\/em> informa que o Hash<\/strong> n\u00e3o suport algo chamado WAL<\/strong> e, portanto, seu uso \u00e9 desencorajado:<\/p>\n O Write-Ahead Logging<\/em> (WAL) \u00e9 um padr\u00e3o que garante a integridade dos dados. De forma resumida, ele garante que as altera\u00e7\u00f5es nos arquivos do banco de dados s\u00f3 sejam salvas depois que essas mudan\u00e7as forem salvas em um log. Isso permite que, se houver um crash<\/em> no banco, seja poss\u00edvel aplicar as mudan\u00e7as perdidas com base no log. Por n\u00e3o suportar o WAL, o Hash<\/strong> acaba sendo um pouco mais r\u00e1pido que o B-tree<\/strong>, por\u00e9m ele n\u00e3o foi inclu\u00eddo no benchmark<\/em> por conta de seu uso ser desencorajado.<\/p>\n Atualiza\u00e7\u00e3o:<\/strong> a partir do Postgres 10, os \u00edndices de tipo Hash<\/strong> passaram a suportar o WAL. Portanto, pode valer a pena considerar essa op\u00e7\u00e3o na hora de escolher qual \u00edndice utilizar.<\/p>\n A partir do Postgres 9.4<\/strong>, podemos utilizar esse novo formato de \u00edndice para os documentos. Esse \u00edndice pode ser usado em todo o documento (com qualquer chave).<\/p>\n Nesse caso podemos realizar a consulta dessa forma:<\/p>\n Tamb\u00e9m \u00e9 poss\u00edvel utilizar os operadores Normalmente o operador mais usado \u00e9 o Por suportar somente esse operador, o \u00edndice fica um pouco menor e as consultas mais perform\u00e1ticas.<\/p>\n Os testes foram realizados em um MacBook Pro 13\u2019 Early 2015 com 8GB de RAM, Core i5 2.7GHz e 120GB SSD.Setup<\/h2>\n
\nPara acompanhar as etapas \u00e0 seguir, precisaremos do Postgres devidamente instalado e rodando<\/a>.<\/p>\n{\n\n \"customer_id\": \"ATVPDKIKX0DER\",\n \"product\": {\n \"category\": \"Arts & Photography\",\n \"group\": \"Book\",\n \"id\": \"1854103040\",\n \"sales_rank\": 72019,\n \"similar_ids\": [\n \"1854102664\",\n \"0893815381\",\n \"0893816493\",\n \"3037664959\",\n \"089381296X\"\n ],\n \"subcategory\": \"Art\",\n \"title\": \"The Age of Innocence\"\n },\n \"review\": {\n \"date\": \"1995-08-10\",\n \"helpful_votes\": 5,\n \"rating\": 5,\n \"votes\": 12\n }\n }\n<\/code><\/pre>\n
1. Criar banco de dados<\/h3>\n
\u276f psql\n<\/code><\/pre>\n
CREATE DATABASE store;\n<\/code><\/pre>\n
2. Criar tabela<\/h3>\n
reviews<\/code> com um \u00fanico campo (
content<\/code>) para armazenar o conte\u00fado de cada registro:<\/p>\n
\u276f psql store\n<\/code><\/pre>\n
CREATE TABLE reviews(content jsonb);\n<\/code><\/pre>\n
3. Importar os dados<\/h3>\n
copy<\/code> dentro do console do Postgres:<\/p>\n
copy reviews (content) FROM 'customer_reviews_nested_1998.json'\n<\/code><\/pre>\n
B-tree<\/h2>\n
CREATE INDEX ON reviews((content #>> '{product,category}'));\n<\/code><\/pre>\n
SELECT COUNT(*) FROM reviews WHERE content #>> '{product,category}' = 'Arts & Photography';\n<\/code><\/pre>\n
SELECT COUNT(*) FROM reviews WHERE content->'product'->>'category' = 'Arts & Photography';\n<\/code><\/pre>\n
CREATE INDEX ON reviews((content->'product'->>'category'));\n<\/code><\/pre>\n
4<\/code> ou mais\u201d ou \u201cquais reviews<\/em> n\u00e3o s\u00e3o de livros\u201d – operadores
><\/code>,
=<\/code>,
<<\/code>,
>=<\/code>,
<=<\/code> e
!=<\/code> – esse \u00e9 o \u00edndice mais indicado.<\/p>\n
Pr\u00f3s:<\/h3>\n
\n
><\/code>,
=<\/code>,
<<\/code>,
>=<\/code>,
<=<\/code> e
!=<\/code>.<\/li>\n<\/ul>\n
Contras:<\/h3>\n
\n
Hash<\/h2>\n
CREATE INDEX ON reviews USING HASH((content #>> '{product,category}'));\nWARNING: hash indexes are not WAL-logged and their use is discouraged\n<\/code><\/pre>\n
\nCaso queira saber mais sobre WAL<\/strong>, recomendo ler a documenta\u00e7\u00e3o de Postgres<\/a>.<\/p>\nGIN (General inverted Index)<\/h2>\n
CREATE INDEX on reviews USING GIN(content);\n<\/code><\/pre>\n
SELECT COUNT(*) FROM reviews WHERE content @> '{\"product\": {\"category\": \"Arts & Photography\"}}';\n<\/code><\/pre>\n
?<\/code>,
?|<\/code> e
?&<\/code>\u00a0– que s\u00e3o usados para saber se o documento cont\u00e9m alguma chave.<\/p>\n
GIN (jsonb_path_ops)<\/h3>\n
@><\/code> , para saber se o documento \u201ccont\u00e9m o peda\u00e7o\u201d que for passado como argumento.
\nSe na sua aplica\u00e7\u00e3o for poss\u00edvel utilizar apenas esse operador, \u00e9 interessante usar a varia\u00e7\u00e3o jsonb_path_ops<\/code>:<\/p>\n
CREATE INDEX ON reviews USING GIN(content jsonb_path_ops);\n<\/code><\/pre>\n
Pr\u00f3s:<\/h3>\n
\n
Contras:<\/h3>\n
\n
><\/code>,
=<\/code>,
<<\/code>,
>=<\/code>,
<=<\/code> e
!=<\/code>.<\/li>\n<\/ul>\n
Benchmarks<\/h2>\n
\nO banco possu\u00ed 589.859 registros.<\/p>\nCria\u00e7\u00e3o do \u00edndice<\/h3>\n\n