Fabrízio de Royes Mello

Blog sobre Tecnologia da Informação

PostgreSQL (Aguardando Pela 9.4) - Introduzindo JSONB, Um Formato Estruturado Para Armazenar Objetos JSON

| Comments

ATENÇÃO! Este post é uma tradução para Português Brasil do blog do Sr. Hubert Depesz Lubaczewski. Fique a vontade para comentar quaisquer problemas na tradução.

No dia 23 de Março de 2014, Andrew Dunstan commitou o seguinte patch:

Introduzindo jsonb, um formato estruturado para armazenar objetos json.

O novo formato aceita exatamente os mesmos dados como o tipo json já existente. Entretanto este é armazenado em um formato que não requer a realização de um parse do text original para processá-lo, tornando-o muito mais adequado para indexação e outras operações. Espaços em brancos desnecessários são descartados, e a ordem das chaves dos objetos não é preservada. Nem são mantidas chaves de objetos duplicadas – o valor para uma determinada chave é armazenada uma única vez.

O novo tipo possui todas as funções e operadores que o tipo json tem, com excessão das funções para geração de objetos json (to_json, json_agg, etc.), e também possui a mesma semântica. Adicionalmente existem classes de operadores para índices to tipo hash e btree, e duas classes para índices GIN o qual não existe para o tipo json.

Essa funcionalidade evoluiu de um trabalho anterior do Oleg Bartunov e Teodor Sigaev, que se destinava a fornecer facilidades semelhantes ao hstore aninhado, mas que no final provou ter alguns problemas de compatibilidade significativos.

Autores: Oleg Bartunov, Teodor Sigaev, Peter Geoghegan e Andrew Dunstan. Revisão: Andres Freund

Depois que ele foi commitado, ele foi explicado muito bem, mas eu decidi escrever sobre isso também, com alguns exemplos. Primeiro, vamos ver como ele funciona.

Vou começar com alguns valores de teste:

{"a":"abc","d":"def","z":[1,2,3]}

{"a":"abc","d";"def","z":[1x2,3]}

{
    "a": "abc",
    "d": "def",
    "z": [1, 2, 3]
}

{"a":"abc","d":"def","z":[1,2,3],"d":"overwritten"}

Primeiro, vamos ver o que acontece após transformar esses valores em json e jsonb:

select '{"a":"abc","d":"def","z":[1,2,3]}'::json;
               json                
-----------------------------------
 {"a":"abc","d":"def","z":[1,2,3]}
(1 row)

select '{"a":"abc","d":"def","z":[1,2,3]}'::jsonb;
                  jsonb                   
------------------------------------------
 {"a": "abc", "d": "def", "z": [1, 2, 3]}
(1 row)

Tudo parece correto aqui, porém a saída do jsonb foi reformatada. Aqui ele não fez muita coisa, mas adicionou alguns espaços em branco. Isso é bom.

select '{"a":"abc","d";"def","z":[1x2,3]}'::json;
ERROR:  invalid input syntax for type json
LINE 1: select '{"a":"abc","d";"def","z":[1x2,3]}'::json;
               ^
DETAIL:  Token ";" is invalid.
CONTEXT:  JSON data, line 1: {"a":"abc","d";...

select '{"a":"abc","d";"def","z":[1x2,3]}'::jsonb;
ERROR:  invalid input syntax for type json
LINE 1: select '{"a":"abc","d";"def","z":[1x2,3]}'::jsonb;
               ^
DETAIL:  Token ";" is invalid.
CONTEXT:  JSON data, line 1: {"a":"abc","d";...

Em ambos os casos o erro foi reportado corretamente, mas no caso do jsonb ele disse: “invalid input syntax for type json”. É provavelmente devido a ordem do cast, e normalmente isso deve estar correto. De qualquer maneira JSON e JSONB são similares o suficiente para não causar problemas.

select '{
    "a": "abc",
    "d": "def",
    "z": [1, 2, 3]
}'::json;
        json        
--------------------
 {                 +
     "a": "abc",   +
     "d": "def",   +
     "z": [1, 2, 3]+
 }
(1 row)

select '{
    "a": "abc",
    "d": "def",
    "z": [1, 2, 3]
}'::jsonb;
                  jsonb                   
------------------------------------------
 {"a": "abc", "d": "def", "z": [1, 2, 3]}
(1 row)

Aqui nós vemos a remoção de espaços em branco. Eu diria que é muito legal. É claro, a não ser que (por qualquer motivo) você queira manter os espaços, mas isso não deveria ser significativo, então dependendo de eles estarem lá não parece sensato.

select '{"a":"abc","d":"def","z":[1,2,3],"d":"overwritten"}'::json;
                        json                         
-----------------------------------------------------
 {"a":"abc","d":"def","z":[1,2,3],"d":"overwritten"}
(1 row)

select '{"a":"abc","d":"def","z":[1,2,3],"d":"overwritten"}'::jsonb;
                      jsonb                       
--------------------------------------------------
 {"a": "abc", "d": "overwritten", "z": [1, 2, 3]}
(1 row)

E o valor está sendo sobrescrito. Eu digo que isso é ótimo.

Quanto a utilização do espaço em disco, isso irá definitivamente depender do caso, mas um rápido teste mostra que jsonb pode utilizar bem mais espaço em disco:

select pg_column_size('{"a":"abc","d":"def","z":[1,2,3]}'::jsonb);
 pg_column_size 
----------------
             84
(1 row)

select pg_column_size('{"a":"abc","d":"def","z":[1,2,3]}'::json);
 pg_column_size 
----------------
             37
(1 row)

Por outro lado, para este JSON:

{"widget": {
    "debug": "on",
    "window": {
        "title": "Sample Konfabulator Widget",
        "name": "main_window",
        "width": 500,
        "height": 500
    },
    "image": { 
        "src": "Images/Sun.png",
        "name": "sun1",
        "hOffset": 250,
        "vOffset": 250,
        "alignment": "center"
    },
    "text": {
        "data": "Click Here",
        "size": 36,
        "style": "bold",
        "name": "text1",
        "hOffset": 250,
        "vOffset": 100,
        "alignment": "center",
        "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;"
    }
}}

O valor do JSON tem 605 bytes e o JSONb 524 bytes.

O que há de mais …

Para JSONb temos mais operadores. Por exemplo – igualdade:

select '{"a":1, "b":2}'::jsonb = '{"b":2, "a":1}'::jsonb;
 ?column? 
----------
 t
(1 row)

Mais operadores estão descritos na documentação.

O que mais – o novo tipo de dados pode utilizar índices para pesquisar por elementos.

Eu criei uma tabela para testes:

create table test (v jsonb);

e inseri nela 100 mil linhas, parecidas com:

{"i": 42, "s": "ryzdaoop"}

Algumas das linhas (~ 1%) tem um valor adicional no json – chave “r” com valor 1.

Agora eu posso criar um índice nesta tabela:

create index whatever on test using gin (v);
CREATE INDEX

e agora:

explain analyze select * from test where v ? 'r';
                                                      QUERY PLAN                                                       
-----------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on test  (cost=16.77..307.23 rows=100 width=42) (actual time=0.554..2.670 rows=1024 loops=1)
   Recheck Cond: (v ? 'r'::text)
   Heap Blocks: exact=644
   ->  Bitmap Index Scan on whatever  (cost=0.00..16.75 rows=100 width=0) (actual time=0.416..0.416 rows=1024 loops=1)
         Index Cond: (v ? 'r'::text)
 Planning time: 0.475 ms
 Total runtime: 2.788 ms
(7 rows)

Eu também poderia fazer:

explain analyze select * from test where v @> '{"i":42}';
                                                      QUERY PLAN                                                      
----------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on test  (cost=28.77..319.23 rows=100 width=42) (actual time=1.132..1.707 rows=103 loops=1)
   Recheck Cond: (v @> '{"i": 42}'::jsonb)
   Heap Blocks: exact=99
   ->  Bitmap Index Scan on whatever  (cost=0.00..28.75 rows=100 width=0) (actual time=1.089..1.089 rows=103 loops=1)
         Index Cond: (v @> '{"i": 42}'::jsonb)
 Planning time: 0.482 ms
 Total runtime: 1.783 ms
(7 rows)

Ou mesmo:

explain analyze select * from test where v @> '{"i":42, "r":1}';
                                                     QUERY PLAN                                                     
--------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on test  (cost=52.77..343.23 rows=100 width=42) (actual time=1.171..1.191 rows=3 loops=1)
   Recheck Cond: (v @> '{"i": 42, "r": 1}'::jsonb)
   Heap Blocks: exact=3
   ->  Bitmap Index Scan on whatever  (cost=0.00..52.75 rows=100 width=0) (actual time=1.143..1.143 rows=3 loops=1)
         Index Cond: (v @> '{"i": 42, "r": 1}'::jsonb)
 Planning time: 0.530 ms
 Total runtime: 1.256 ms
(7 rows)

Isso é muito legal.

Infelizmente você não pode utilizar índices para realizar uma pesquisa mais “profunda”. O que significa isso?

Vamos supor que você tem um valor JSON como este:

{"a": [1,2,3,4]}

Você pode indexá-lo, e pesquisar por “value ? ‘a’”, ou “value @> ‘{“a”:[1,2,3,4]}’“, mas você não pode pesquisar (utilizando índice) por “linhas que tenha 3 na matriz que está sob a chave ‘a’ no json”.

É claro que você pode trabalhar em torno dele criando índice em ((value –> ‘a’)), se for isso que você realmente precisa.

De qualquer maneira eu realmente gostei disso. Parece que funciona muito bem, e eu espero que tenhamos mais recursos no futuro.

Valeu pessoal.

Projeto Aceito No Google Summer of Code 2014 (GSoC2014)

| Comments

Amigos, gostaria de compartilhar um pouco de minha emoção e alegria, pois minha proposta de projeto foi aceita para o “Google Summer of Code 2014”.

Nos próximos meses estarei desenvolvendo uma nova funcionalidade para o PostgreSQL financiado pelo Google, como projeto de pesquisa do curso de Pós-Graduação em Tecnologias Aplicadas a Sistemas de Informação com Métodos Ágeis pela Uniritter Porto Alegre/RS.

O principal objetivo deste projeto é permitir que “unlogged tables” (tabelas que não geram registros no WAL) sejam transformadas em tabelas regulares (que geram registros no WAL) e vice-versa. Para que isso aconteça será adicionado mais duas cláusulas ao comando sql “ALTER TABLE”:

ALTER TABLE table_name SET LOGGED;
ALTER TABLE table_name SET UNLOGGED;

Quem tiver interesse em acompanhar a realização do projeto pode visitar a página do projeto no Wiki da comunidade PostgreSQL e também o código fonte no github.

Novidades

Em breve teremos novidades por aqui, pois começarei um trabalho de tradução dos posts do blog http://www.depesz.com/ do amigo Hubert Depesz Lubaczewski, da Polônia.

O blog do Hubert é bem ativo e possui inúmeros posts interessantes e conversando com ele achamos válida a idéia de ter uma versão em PT-BR do mesmo.

Logo teremos o primeiro post aqui… aguardem…

Database Refactoring

Contextualização

Refatoração de código (Code Refactoring) é uma disciplina/processo que consiste em melhorar a estrutura interna de um software sem modificar seu comportamento externo, e uma Refatoração de Banco de Dados (Database Refactoring) parte do mesmo princípio, porém além de manter o comportamento externo também deve manter a semântica da informação que ele mantém/armazena, e por esse motivo é considerada mais difícil.

Um outro conceito que posso destacar a respeito de Database Refactoring é:
“Mudança disciplinada na estrutura de uma base de dados que não altera sua semântica, porém melhora seu projeto e minimiza a introdução de dados inconsistentes”

O ponto interessante deste último é o texto ”minimiza a introdução de dados inconsistentes”, pois esse é o grande objetivo de realizarmos um refactoring na estrutura de um banco de dados, ou seja, melhorar o desing atual para melhorar a consistência dos dados e também a qualidade dos novos dados que serão adicionados ao seu banco de dados.

E esta tarefa não é das mais simples, pois existe um fator preponderante no que diz respeito a dificuldade de execução deste tipo de refactoring que é o acoplamento, que será visto logo a seguir.

Acoplamento

Figura 1. Baixo Acoplamento
É a medida de dependência entre dois elementos. Quanto mais acoplados dois elementos estiverem, maior a chance que a mudança em um implique na mudança em outro.

Figura 2. Alto Acoplamento
Simples assim, quanto mais o seu banco de dados estiver acoplado, ou seja, dependente de diversas aplicações externas, mais difícil será a aplicação de um refactoring.

A Figura 1 demonstra um cenário ”Single-Database Application” que é bem simplificado, onde a aplicação de um refactoring será mais tranquilo.

Com certeza o cenário da Figura 2, o ”Multi-Database Application“ é o pior caso, pois exige muito cuidado e planejamento para execução do refactoring, então veremos a seguir uma sugestão de processo para execução.

Processo de Refatoração

Figura 3. Processo de Database Refactoring
Um processo é um conjunto organizado de atividades com um objetivo em comum. Executar um database refactoring em um cenário ”Single-Database Application” ou ”Multi-Application Database” requer um processo, por mais simples que seja. A grande diferença na execução em ambos cenários é que no caso do ”Multi-Application Database” o período de transição (mais abaixo falaremos) geralmente será mais longo.

É bom sempre ter em mente que um database refactoring, como já vimos, não é uma atividade simples então caso seja identificada a real necessidade de refatorar um banco de dados então podemos usar o seguinte roteiro (processo) para se guiar:
  • Escolher o refactoring mais apropriado;
  • Depreciar o esquema original;
  • Testar antes, durante e após;
  • Modificar esquema;
  • Migrar os dados;
  • Modificar código externo;
  • Executar testes de regressão;
  • Versionar seu trabalho;
  • Anunciar o refactoring.
Figura 4. Regra Geral Processo Refatoração

Na Figura 4 é demonstrado um pequeno processo descrevendo um fluxo básico para aplicação de um refactoring.

Atente bem para o “Período de Transição”, que é a fase mais importante, principalmente para cenários ”Multi-Database Application” (Figura 2), onde você precisa ter em mente que não conseguirá realizar o refactoring e fazer o deploy em produção de todas as aplicações ao mesmo tempo. Na verdade você nem conseguirá alterar todas as aplicações ao mesmo tempo, principalmente se você tiver dependência de terceiros, então você precisará suportar o esquema original e o esquema resultante ao mesmo tempo, para somente quando todas aplicações estiverem suportando apenas o esquema resultante, ou novo esquema, você poderá aposentar de vez o antigo esquema e assim finalizar este período.

Estratégias de Database Refactorings

Existem alguns pontos a considerar com estratégias para adoção de um database refactoring:

  • Pequenas mudanças são mais fáceis de aplicar;
  • Identifique unicamente cada refactoring;
  • Implemente uma grande mudança realizando várias pequenas mudanças;
  • Tenha uma tabela de configuração/versionamento do seu banco de dados;
  • Priorize triggers ao invés de views ou sincronizações em lote;
  • Escolha um período de transição suficiente para realizar as mudanças;
  • Simplifique sua estratégia de controle de versão de banco de dados;
  • Simplifique negociações com outros times;
  • Encapsule acesso ao banco de dados;
  • Habilite-se a montar facilmente um ambiente de banco de dados;
  • Não duplique SQL;
  • Coloque os ativos de banco de dados sobre controle de mudanças;
  • Seja cuidadoso com políticas.
Os items acima mostram apenas algumas sugestões, em forma de “lições aprendidas”, de algumas estratégias que você pode considerar quando tiver a necessidade de realizar um refactoring.
Para apoiar essas estratégias existe um catálogo que descrevem diversos tipos de refactorings em bancos de dados e exemplos de uso, que veremos a seguir.

Catálogo de Database Refactorings

Este catálogo é dividido em algumas categorias:

  • Structural: são mudanças na estrutura do banco de dados (tabelas, colunas, visões, etc).
  • Data Quality: são mudanças que melhoram a qualidade das informações contidas em um banco de dados.
  • Referential Integrity: são mudanças que asseguram que uma linha referenciada exista em outra relação e/ou assegura que uma linha que não é mais necessária seja removida apropriadamente.
  • Architectural: são mudanças que melhoram a maneira que programas externos interagem com a base de dados.
  • Method: são mudanças que melhoram a qualidade de uma Procedure um Função.
  • Transformations: mudanças que alteram a semântica do esquema do banco pela adição de novas funcionalidades.

No meu github é possível encontrar exemplos práticos de aplicação passo-a-passo de um refactoring em um modelo inicial, passando por um período de transição e chegando ao modelo final

Considerações Finais

Devemos levar em consideração que apesar destas técnicas serem direcionadas para refatoração, ou seja, mudar estrutura sem mudar sua semântica, as mesmas podem e devem ser utilizadas para evolução da sua aplicação, ou seja, se você precisa construir uma nova feature em sua aplicação que está em produção, você poderá recorrer das práticas aqui apresentadas para evoluir seu esquema de forma mais consistente e segura.

Baseado no exposto podemos facilmente responder a pergunta “Por quê Refatorar?”:

  • aceitar mudança de escopo;
  • fornecer feedback rápido;
  • melhoria contínua;
  • aumentar simplicidade para facilitar entendimento;
  • tornar os modelos mais próximos do mundo real;
  • termos modelos simples para facilitar:
    • manutenção e
    • evolução da aplicação
E para refatorarmos precisamos ter conhecimento, disciplina, simplicidade, bom senso e persistência, sem contar no ponto fundamental que é organização.

Referências

    Bad Smells (Mal Cheiros) Em Bancos De Dados

    Uma estrutura de um banco de dados, diferentemente da estrutura de um software, tende a deteriorar naturalmente com o passar do tempo. Dentre várias causas de deterioração podemos citar o crescimento progressivo do volume de dados devido ao aumento natural de usuários que o utilizam e também ao seu próprio tempo de uso, tornando um modelo de dados que no início era eficiente para solução proposta em um modelo ineficiente e defasado.

    Essa deterioração natural aliada a mudanças em requisitos de negócio exigem modificações e refatorações tanto no software que os implementa quanto em seus bancos de dados. Entretanto a refatoração de um banco de dados é mais complexa que a de um software devido aos seguintes motivos: (i) além de manter comportamento também é necessário manter as informações (dados) e (ii) acoplamento com diversas origens (outras aplicações, frameworks, integrações, etc).

    Devido a essas dificuldades a evolução de uma estrutura de banco de dados torna-se um desafio, ocorrendo assim um fenômeno conhecido como Bad Smells (mal cheiros), da mesma forma que ocorre com o código de um software. Em software um code smell (bad smell) é uma categoria comum de problema no código fonte que indica a necessidade de refatoração, e o mesmo ocorre com bancos de dados, onde são chamados database smells.

    Abaixo segue uma lista de alguns database smells:
    • Multi-purpose column (coluna com mais de uma função)
    • Multi-purpose table (tabela com mais de uma função)
    • Redundant data (dados redundantes/repetidos)
    • Tables with many columns (tabelas com muitas colunas)
    • “Smart” columns (colunas “espertas”)
    • Lack of constraints (falta de restrições)
    • Fear of change (medo da mudança)
    Dentre os database smells citados acima, devemos ter atenção especial ao ”Fear of change”, pois pode ser considerado o pior de todos, pois:
    • inibe a inovação, 
    • reduz a efetividade, 
    • produz ainda mais bagunça e 
    • ao longo do tempo a situação fica cada vez pior. 
    Lembra do valor ”Coragem” do XP (eXtreme Programming)? Mudança é algo que deve ser considerado natural e irá acontecer em projetos de software, isso é um fato, não podemos fugir disso, então precisamos das práticas e ferramentas adequadas para que a mudança não seja um entrave na evolução do seu projeto, e sim algo natural e, de certa forma, trivial para sua equipe, e o mesmo deve ocorrer com seu banco de dados.

    Esse post é apenas uma introdução a este assunto pouco explorado e que tenho como proposta para conclusão de Pós-Graduação em Tecnologias Aplicadas a Sistemas de Informações com Métodos Ágeis que estou realizando na http://www.uniritter.edu.br.

    A idéia é montar uma taxonomia de Database Smells através de um catálogo, inclusive sugerindo os Database Refactoring que podem ser utilizados em sua solução, e futuramente, quem sabe em um futuro trabalho de Mestrado fazer um paralelo dos Database Smells com Code Smells, principalmente se os mal cheiros em códigos podem refletir mal cheiros no seu banco de dados. ;-)

    Comments

    Gabriel Prestes
    Excelente o post Fabrízio, pela introdução ao assunto, aguardo novos posts.
    Fabrízio de Royes Mello
    O formato do trabalho será de um artigo científico e não de um TCC, porque é para conclusão de uma Pós-Graduação e não Graduação.

    Mas com certeza vou compartilhar quando estiver pronto.

    Obrigado.
    Webeople
    Bom trabalho, meu caro.

    Quando o tcc estiver pronto, compartilha aí. Gostaria muito de ler.

    Abs,
    Eduardo A. Fiorezi
    Muito bom Fabrízio. Ainda não tive a oportunidade de ler o Database Refactoring, mas na época que foi lançado eu estava escrevendo meu tcc.. Ótimo artigo. Abraços

    PostgreSQL 9.3devel - Nova Extensão “Postgres_fdw”

    Apartir da release 9.1 o PostgreSQL adicionou suporte a foreign tables, viabilizando assim uma forma simples de gerenciar fontes de dados externas dentro do PostgreSQL.

    Através dessa infraestrutura é possível a implementação dos FDW (Foreign Data Wrapper), que são uma espécie de driver para acessar uma fonte de dados externa.

    Já existem diversos FDW implementados que permitem acessar outros bancos de dados (oracle, mysql, etc), arquivos (texto, csv, etc), bases NoSQL (mongodb, couchdb, redis, etc) e outras fontes de dados diferentes, tais como: twitter, ldap, www, etc.

    Hoje foi commitado no git do PostgreSQL um FDW específico para acessar bases PostgreSQL, chamado ”postgres_fdw”, então vou demonstrar como instalar/configurar de uma forma muito simples.


    1) Instalar o “postgres_fdw” apartir do git

    Nesse exemplo vou mostrar como instalar/compilar o PostgreSQL apartir do git oficial, mas se vc tiver uma conta no github pode usar tb o nosso clone do repositório oficial.

    git clone git://git.postgresql.org/git/postgresql.git
    cd postgresql
    ./configure --prefix=$HOME/pgsql
    make
    make install
    cd contrib/postgres_fdw
    make
    make install


    2) Inicializar um novo cluster e colocar o PostgreSQL para executar

    $ cd $HOME/pgsql
    $ mkdir data
    $ chmod 700 data
    $ ./bin/initdb -D data
    $ ./bin/pg_ctl -D data -l startup.log start


    3) Criar bases e tabela para testes

    $ cd $HOME/pgsql
    $ ./bin/createdb bd1
    $ ./bin/createdb bd2
    $ ./bin/psql bd2 -c "create table foo(bar integer);"
    $ ./bin/psql bd2 -c "insert into foo(bar) select * from generate_series(1, 10);"

    Para explicar, criamos 2 (duas) bases de dados “bd1” e “bd2”, e no “bd2” criamos uma tabela chamada “foo” a qual iremos acessar apartir do “bd1” criando uma foreign table como explicarei em seguida.


    4) Criar a extensão “postgres_fdw” no “bd1”

    $ ./bin/psql bd1
    psql (9.3devel)
    Type "help" for help.

    bd1=# CREATE EXTENSION postgres_fdw ;
    CREATE EXTENSION

    bd1=# \dx
    List of installed extensions
    Name | Version | Schema | Description
    --------------+---------+------------+----------------------------------------------------
    plpgsql | 1.0 | pg_catalog | PL/pgSQL procedural language
    postgres_fdw | 1.0 | public | foreign-data wrapper for remote PostgreSQL servers
    (2 rows)


    5) Criar conexão com “bd2” no “bd1”

    bd1=# CREATE SERVER conexao_bd2 FOREIGN DATA WRAPPER postgres_fdw OPTIONS (dbname 'bd2');
    CREATE SERVER

    bd1=# \des+
    List of foreign servers
    Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description
    -------------+----------+----------------------+-------------------+------+---------+----------------+-------------
    conexao_bd2 | fabrizio | postgres_fdw | | | | (dbname 'bd2') |
    (1 row)

    bd1=# CREATE USER MAPPING FOR current_user SERVER conexao_bd2 ;
    CREATE USER MAPPING

    bd1=# \deu+
    List of user mappings
    Server | User name | FDW Options
    -------------+-----------+-------------
    conexao_bd2 | fabrizio |
    (1 row)



    6) Acessar a tabela “foo” do “bd2” apartir do “bd1”

    bd1=# CREATE FOREIGN TABLE foo (bar integer) SERVER conexao_bd2 ;
    CREATE FOREIGN TABLE

    bd1=# \d
    List of relations
    Schema | Name | Type | Owner
    --------+------+---------------+----------
    public | foo | foreign table | fabrizio
    (1 row)

    bd1=# SELECT * FROM foo;
    bar
    -----
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    (10 rows)


    Considerações

    Segundo a própria documentação oficial, o ”postgres_fdw” é uma alternativa mais robusta em relação ao antigo ”dblink” pois nos oferece uma sintaxe mais padronizada e simplificada para acessar tabelas remotas, inclusive com melhor desempenho em muitos casos.

    E pelo que pude acompanhar do seu desenvolvimento, parece que essa FDW deve servir como modelo para o desenvolvimento de outras FDW para acessar outras bases de dados relacionais.

    De uma forma muito simples é possível acessar tabelas de outra base de dados PostgreSQL, e o mesmo ocorre com outras fontes de dados, mas por enquanto apenas para leitura (SELECT), entretanto já está em revisão um patch para permitir escrita (INSERT/UPDATE/DELETE) em foreign tables, vamos aguardar.

    PostgreSQL + Unlogged Tables + Partitioning + Parallel Programming = ETL Reescrito Passando De ~8h Para ~25min De Execução

    Já faz algum tempo que não escrevo nada por aqui, mas não é por falta de tempo ou coisa parecida, é que na realidade não tenho muita intimidade com artigos ou posts em blogs, mas resolvi escrever um “causo” a pedido do amigo Fernando Ike sobre um tweet que lancei há algum tempo depois de obter sucesso em um projeto.

    Aviso antecipadamente que o post é um pouco longo, então se não estiver com paciência agora recomendo vc sair tomar um café (ou uma cerveja) e voltar outra hora… desculpe mesmo, tentei reduzir o máximo… :-(

    Contextualização

    Para vcs entenderem porque cheguei aqui, vou começar dos primórdios… eu tenho (ou tinha…hehehe…) um problema com um ETL em uma aplicação de um cliente (não tenho autorização para “dar nome aos bois”) que basicamente processava os registros de uma grande tabela com dados financeiros e gerava uma “posição” da mesma calculando correção monetária, juros, multas, descontos, etc…

    Esse ETL sempre foi e ainda é ridiculamente simples, porque basicamente é uma PL/pgSQL dentro do PostgreSQL que faz todo esse trabalho de ler os dados de uma tabela, processar e carregar os mesmos em outra tabela. Até aqui tudo bem, sempre funcionou maravilhosamente bem, mas com bases pequenas… mas também não queremos maravilhas de desempenho processando milhares de registros em uma única transação né, é óbvio que isso gera problemas.

    Primeira tentativa… e um sucesso, digamos, opaco…

    Há alguns anos eu já tinha melhorado essa rotina dividindo o processamento em lotes menores, através de um shell script que fazia esse trabalho de divisão em lotes por uma coluna da tabela que categorizava os registros em um determinado tipo, então primeiro identificamos os tipos existentes na tabela origem e processava os mesmos gerando tabelas individuais para cada tipo, e ao final juntava tudo na tabela de destino e removia as tabelas temporárias… e vejam só, SEMPRE a MESMA tabela de destino, e para isso precisamos remover os índices, executar o ETL e depois criar novamente os índices para termos um desempenho decente. Claro que junto dessa rotina implementamos também uma outra para expurgo de registros desnecessários/obsoletos (antigos), o que também sempre foi uma rotina que onerava bastante o servidor pois era SEMPRE a MESMA tabela de destino, então imaginem precisar remover uma porção de registros de uma tabela com mais de 100milhões de registros… isso me lembra um post do Fábio Telles: “Não use DELETE use INSERT” que ajudou muito para torná-la “menos pior”.

    Na época (2007/2008) essa melhoria ajudou pois desafogou bastante a carga de processamento desse ETL, porém com o passar do tempo e a tabela com dados financeiros crescendo constantemente, o ETL foi ficando cada vez mais oneroso chegando ao seu ápice (final de 2012) de ~8h de execução para processar ~8.5milhões de registros. Eu sei que esse número não é tão expressivo assim, mas a complexidade do processamento envolvido para fazer os cálculos de corrreção, juros e multas e as diversas configurações existentes para cada um justificam, de certa forma, todo esse tempode processamento, sem contar que o coitado do servidor ficava “imprestável”.

    Mas vejam bem, estou falando de dados *financeiros* que sob ponto de vista do negócio se fazem muito necessários para vários tipos de procedimentos e análises. Ainda existem instâncias com bases menores (outros clientes) que rodam esse ETL _diariamente_ por necessidade de negócio, mas nesse cliente em especial não é possível fazer isso, nem que eles quisessem  pois o tempo total de execução consume 1/3 de um dia, então os finais de semana são usados :-)

    A hora da verdade …

    Após todos os problemas e sem muitas perspectivas, discutimos sobre a re-implementação da PL/pgSQL que executava o ETL, porém isso não é algo trivial, ainda mais em um ERP complexo onde tal iniciativa teria um impacto de grandes proporções visto que seria necessária uma re-modelagem em  alguns pontos criticos. Apesar de ser uma idéia interessante,  não existe tempo hábil para tanto, pois o cliente não pode mais aguardar uma solução, pois qto mais tempo demorar pior fica.

    Como eu já tenho algum tempo de estrada com PostgreSQL e conheço bem a estrtutura do ETL e do ERP em questão, sugeri a equipe que eu poderia re-implementar o antigo shell script reaproveitando a PL/pgSQL do ETL existente (sem mudar regras de negócio), usando tecnologias e técnicas conhecidas. Então o que fiz:

    1) Particionamento da tabela: esse foi o ponto fundamental, pois dividimos a grande tabela em outras menores tomando como base uma coluna que indica a “data” em que os dados financeiros foram calculados, e que o ERP usa constantemente para ler informações da mesma, portanto as queries iriam se beneficiar do recurso. Sobre esse assunto, além da documentação oficial, vcs podem dar uma olhada em alguns artigos recém lançados pelo Fábio Telles sobre esse assunto.

    2) Implementação de um script PHP (não estou de sacanagem… é PHP mesmo, mas no console) que tivesse a habilidade de gerar processos filhos (fork) para processamento em paralelo, e para isso usei uma classe para realizar esse trabalho. Confesso que no inicio tive um certo receio em implementar essa rotina em PHP, inclusive cogitei a possibilidade de fazê-la em Perl, Python ou Ruby, mas como eu domino mais esta do que as outras e o tempo era curto implementei nela mesmo, e os resultados foram muito satisfatórios.


    COPY no lugar de INSERT

    A primeira coisa que fiz para continuar esse projeto foi *abolir* o INSERT… isso mesmo… não tem INSERT… vc deve estar pensando que estou maluco e se perguntando: “Tá e como adicionar linhas a uma tabela então?” R: usando COPY, ao invés de INSERT… na realidade implementei uma classe que armazena uma coleção (linhas) em memória, e quando eu preciso uso um método para persistir os dados em uma tabela usando COPY… simples assim… então o código usado para INSERT é algo do tipo:

    DDL da tabela exemplo:
    create table foo (
    bar integer
    );

    PHP:
    $tabela = new PgCopy('foo');
    for ($i=0; $i<10; $i++) {
    $tabela->bar = $i;
    $tabela->insertValue(); // adiciona em memória
    }
    $tabela->persist(); // realiza COPY dos dados em memória


    Dividir para conquistar

    Um dos problemas que tinhamos com o processo antigo era justamente que ele era linear, ou seja, um processo apenas com inicio, meio e fim. Então resolvi investir em programação paralela, dividindo o grande volume de registros a processar em vários trabalhos menores sendo capaz de executar alguns em paralelo, de acordo com o nro de núcleos do servidor.

    Para tal atividades crio uma tabela que planeja a execução do trabalho, ou seja, cria lotes para que o script possa processar em paralelo, isso baseado em uma chave artifical (sequencial) que existe no modelo e facilitou a criação de trabalhos com lotes de N registros (neste caso usei 1000).

    A tabela que crio para planejar a execução do ETL é algo do tipo:
    create table jobs (
    id_start bigint,
    id_end bigint,
    status varchar,
    constraint jobs_pk primary key (id_start, id_end),
    constraint jobs_status_ck check (status in ('NOT RUNNING', 'RUNNING', 'FINISHED'))
    );

    Dessa forma utilizo o “id_start” e “id_end” para buscar as informações na origem em “lotes” de 1000 (mil) registros, e com isso consigo disparar vários processos em paralelo, e dessa forma conseguimos aproveitar melhor os recursos do servidor e assim agilizar bastante o processo.


    Unlogged Tables são bem legais

    Esse novo recurso presente apartir da versão 9.1 permite criar tabelas que não são escritas no log de transações (WAL), acelerando e muito a inserção de registros na mesma.

    Assim cada processo disparado pelo script gera e escreve em uma unlogged table os dados, e junto com os processos de trabalho (workers) implementei um processo especial que serve com um tipo de coletor de lixo (garbage collector) para ir gradativamente lendo os lotes processados (unlogged tables geradas) e inserindo (com copy claro) na partição de destino.

    Com essa estratégia posso ter um certo nível de escala na escrita pois consigo separar as tabelas em tablespaces distintas. Claro que se algum imprevisto ocorrer, tipo um desligamento não previsto do servidor, o próprio script tem habilidade de detectar essa situação e fazer uma limpeza geral antes de inicar um novo processo, até mesmo porque as unlogged tables tem seu conteúdo eliminado nessas situações, e não queremos perder parte dos registros não é mesmo… :-)

    Finalizando…

    Resumindo o que fiz foi:
    - Particionar uma tabela grande em outras menores
    - Planejar o processamento dividindo em lotes menores para poder fazer processamento paralelo
    - Utilizar unlogged tables para receber os dados oriundos dos lotes que são processados em paralelo
    - Implementar um processo que irá ler os lotes já processados e inserir os registros na partição de destino.

    Existem outras coisas que foram feitas para melhorar o desempenho, tipo desligar o autovacuum nas tabelas, aumentar o work_mem, criar índices necessários ao final do processamento, e outros que podem ser feitos e que vcs podem visualizar neste post do Fábio.

    Bom, se vc chegou até aqui então obrigado pela paciência e se quiser mais informações fico a disposição.



    Fabrízio de Royes Mello
    fabriziomello [at] gmail.com

    Comments

    Fabrízio de Royes Mello
    Há mto tempo atrás (Ago/2009), segui as instruções contidas em [1] para se cadastrar no Planeta (enviei email e td mais), porém nada aconteceu… nenhuma resposta, retorno, nada… :-(


    [1] http://wiki.postgresql.org.br/PlanetaPostgreSQLBR
    Unknown
    O artigo ficou muito massa. Só tem uma coisa que me preocupa aqui…. qual o motivo do seu blog ainda não estar no nosso planeta.postgresql.org.br ???

    PGBR2013 - Chamada De Trabalhos

    Pessoal,

    Está aberta a chamada de trabalhos para o PGBR2013. Vamos lá, se inscreva… estamos ansiosos para saber o que você tem feito com o PostgreSQL… nos encontramos em Porto Velho/RO de 15 a 17 de agosto de 2013.


    Fabrízio de Royes Mello
    fabriziomello [at] gmail.com

    Patch Sobre CREATE SCHEMA if NOT EXISTS Aceito Para O PostgreSQL 9.3devel

    Após algumas discussões [1] meu patch para adicionar a cláusula “IF NOT EXISTS” no “CREATE SCHEMA” foi aceito e efetuado o commit na branch master do git oficial do PostgreSQL [2] pelo commiter Mr. Tom Lane.

    Agora na 9.3 poderemos usar a cláusula “IF NOT EXISTS” no “CREATE SCHEMA” para que não gere um erro (cancelando transação atual) caso o esquema que está sendo criado já exista, por exemplo:

    BEGIN;
      CREATE SCHEMA IF NOT EXISTS foo;
      CREATE TABLE foo.bar();
    COMMIT;

    O exemplo acima caso o esquema “foo” já exista não será gerado um erro e a transação irá prosseguir normalmente. O comportamento é similar ao já existente “IF NOT EXISTS” do “CREATE TABLE” [3].

    [1] https://commitfest.postgresql.org/action/patch_view?id=907
    [2] http://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=fb34e94d214d6767910df47aa7c605c452d11c57
    [3] http://www.postgresql.org/docs/9.2/interactive/sql-createtable.html


    Fabrízio de Royes Mello
    fabriziomello [at] gmail.com

    Instalar/Configurar “Debugador” De PL/pgSQL (Pldebugger) No PostgreSQL 9.1 Usando Ubuntu


    O passo-a-passo abaixo foi realizado no Ubuntu 10.04LTS com o PostgreSQL 9.1 instalado via ppa do Martin Pitti, mas creio que pode ser executado tranquilamente utilizando a última versão LTS do Ubuntu, a 12.04LTS.

    1) Download dos fontes do PostgreSQL

    $ wget -c http://ftp.postgresql.org/pub/source/v9.1.3/postgresql-9.1.3.tar.bz2


    2) Precisamos instalar ferramentas necessárias para compilação da extensão

    $ sudo apt-get update
    $ sudo apt-get install build-essential libreadline-dev zlib1g-dev flex bison libssl-dev git-core


    3) Compilar o PostgreSQL e a extensao pldebugger

    $ tar jxvf postgresql-9.1.3.tar.bz2
    $ cd postgresql-9.1.3
    $ ./configure –prefix=/usr –includedir=/usr/include –mandir=/usr/share/man –infodir=/usr/share/info –sysconfdir=/etc –localstatedir=/var –libexecdir=/usr/lib/postgresql-9.1 –srcdir=. –mandir=/usr/share/postgresql/9.1/man –docdir=/usr/share/doc/postgresql-doc-9.1 –datadir=/usr/share/postgresql/9.1 –bindir=/usr/lib/postgresql/9.1/bin –libdir=/usr/lib/postgresql/9.1/lib –includedir=/usr/include/postgresql/ –enable-integer-datetimes –with-openssl
    $ make
    $ cd contrib
    $ git clone git://git.postgresql.org/git/pldebugger.git
    $ cd pldebugger
    $ make
    $ sudo make install

    Obs: lembrando que, apesar de extenso, o “./configure” deve ser todo escrito na mesma linha

    4) Após compilar e instalar a extensao pldebugger precisamos ativa-a no PostgreSQL

    $ sudo vim /etc/postgresql/9.1/main/postgresql.conf

    alterar
     
      #shared_preload_libraries = ”

    para
     
      shared_preload_libraries = ‘$libdir/plugin_debugger’
     

    5) Reiniciar PostgreSQL

    $ sudo /etc/init.d/postgresql restart 9.1


    6) Criar a EXTENSAO “pldbgpapi” na(s) base(s) de dados que deseja fazer debug de pls

    $ psql -U postgres bdteste
    psql (9.1.3)
    Digite “help” para ajuda.


    bdteste=# CREATE EXTENSION pldbgapi;
    CREATE EXTENSION
    bdteste=#


    7) Pronto, agora vc pode usar o PGAdmin para “debugar” suas funções PL/pgSQL




    Fabrízio Mello
    fabriziomello [at] gmail.com


    Comments

    Fabrízio de Royes Mello
    Olá Fábio,

    Minha experiência com PostgreSQL nessa plataforma (Windows) é limitada, mas parece que nesse seu caso o problema é no seu "pg_hba.conf" (Host Based Authentication), ou seja, o arquivo de configuração das permissões para autenticação dos usuários na base de dados. Tente editá-lo e alterar os métodos de autenticação das regras (última coluna) para "trust", depois faça um reload no processo do PostgreSQL e tente novamente.

    Também gostaria de comentar que esse não é o melhor local para vc tirar esse tipo de dúvida, então o convido para assinar nossa lista de discussão [1] da Comunidade Brasileira de PostgreSQL, onde temos muitas pessoas observando as dúvidas e ajudando. Faça um teste, se cadastre lá e envie um email com essa dúvida e vc verá como será.

    Abraços,

    [1] https://listas.postgresql.org.br/cgi-bin/mailman/listinfo/pgbr-geral


    Fabrízio de Royes Mello
    Comunidade Brasileira de PostgreSQL
    Fabio
    Olá Fabrízio !! Estou com um problema no postgresql e estou na minha primeira instalação para começar a aprender…Na hora que estou criando o banco pela linha de comando ele pede senha e eu digito, mas apareçe uma mensagem tipo "postgres não conseguiu conectar com o database erro fatal, falha na autenticação da senha", no entanto já troquei a senha do usuario postgres diversas vezes e o erro persiste. Estou usando a ferramenta no windows seven 64bits(já tentei no XP tambem)… Poderia me ajudar? Desde já muito obrigado e valeu pela atenção!!!