PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Um guia para particionar dados no PostgreSQL

O que é particionamento de dados?


Para bancos de dados com tabelas extremamente grandes, o particionamento é um truque maravilhoso e engenhoso para os designers de banco de dados melhorarem o desempenho do banco de dados e tornarem a manutenção muito mais fácil. O tamanho máximo de tabela permitido em um banco de dados PostgreSQL é de 32 TB, no entanto, a menos que seja executado em um computador ainda não inventado no futuro, podem surgir problemas de desempenho em uma tabela com apenas um centésimo desse espaço.

O particionamento divide uma tabela em várias tabelas e geralmente é feito de forma que os aplicativos que acessam a tabela não notem nenhuma diferença, além de serem mais rápidos para acessar os dados de que precisam. Ao dividir a tabela em várias tabelas, a ideia é permitir que a execução das consultas tenha que varrer tabelas e índices muito menores para encontrar os dados necessários. Independentemente da eficiência de uma estratégia de índice, a varredura de um índice para uma tabela de 50 GB sempre será muito mais rápida do que um índice para uma tabela de 500 GB. Isso também se aplica a varreduras de tabela, porque às vezes as varreduras de tabela são inevitáveis.

Ao introduzir uma tabela particionada ao planejador de consultas, há algumas coisas a serem conhecidas e compreendidas sobre o próprio planejador de consultas. Antes que qualquer consulta seja realmente executada, o planejador de consultas pegará a consulta e planejará a maneira mais eficiente de acessar os dados. Ao dividir os dados em diferentes tabelas, o planejador pode decidir quais tabelas acessar e quais tabelas ignorar completamente, com base no que cada tabela contém.

Isso é feito adicionando restrições às tabelas divididas que definem quais dados são permitidos em cada tabela e, com um bom design, podemos fazer com que o planejador de consultas verifique um pequeno subconjunto de dados em vez de tudo.

Uma tabela deve ser particionada?


O particionamento pode melhorar drasticamente o desempenho em uma tabela quando feito corretamente, mas se feito de forma errada ou quando não for necessário, pode piorar o desempenho, até mesmo inutilizável.

Qual ​​é o tamanho da mesa?


Não existe uma regra real sobre o tamanho de uma tabela antes que o particionamento seja uma opção, mas com base nas tendências de acesso ao banco de dados, os usuários e administradores do banco de dados começarão a ver o desempenho em uma tabela específica começar a degradar à medida que aumenta. Em geral, o particionamento só deve ser considerado quando alguém diz “não posso fazer X porque a tabela é muito grande”. Para alguns hosts, 200 GB pode ser o momento certo para particionar, para outros, pode ser hora de particionar quando atingir 1 TB.

Se a tabela for considerada “muito grande”, é hora de observar os padrões de acesso. Seja conhecendo as aplicações que acessam o banco de dados, seja monitorando logs e gerando relatórios de consultas com algo como pgBadger, podemos ver como uma tabela é acessada e, dependendo de como ela é acessada, podemos ter opções para uma boa estratégia de particionamento.

Para saber mais sobre o pgBadger e como usá-lo, confira nosso artigo anterior sobre o pgBadger.

O inchaço da mesa é um problema?


Linhas atualizadas e excluídas resultam em tuplas mortas que precisam ser limpas. A aspiração de tabelas, manual ou automaticamente, passa por todas as linhas da tabela e determina se ela deve ser recuperada ou deixada sozinha. Quanto maior a tabela, mais tempo esse processo leva e mais recursos do sistema são usados. Mesmo que 90% de uma tabela sejam dados imutáveis, eles devem ser verificados sempre que um vácuo for executado. Particionar a tabela pode ajudar a reduzir a tabela que precisa de limpeza para outras menores, reduzindo a quantidade de dados inalteráveis ​​que precisam ser verificados, menos tempo de limpeza geral e mais recursos do sistema liberados para acesso do usuário em vez de manutenção do sistema.

Como os dados são excluídos, se houver?


Se os dados forem excluídos em uma programação, digamos que dados com mais de 4 anos sejam excluídos e arquivados, isso pode resultar em instruções de exclusão de batidas pesadas que podem levar tempo para serem executadas e, como mencionado anteriormente, criando linhas mortas que precisam ser limpas. Se uma boa estratégia de particionamento for implementada, uma instrução DELETE de várias horas com manutenção de limpeza posterior pode ser transformada em uma instrução DROP TABLE de um minuto em uma tabela mensal antiga com manutenção de limpeza zero.

Como a tabela deve ser particionada?


As chaves para padrões de acesso estão na cláusula WHERE e nas condições JOIN. Sempre que uma consulta especifica colunas nas cláusulas WHERE e JOIN, ela informa ao banco de dados “este é o dado que eu quero”. Assim como o design de índices que visam essas cláusulas, as estratégias de particionamento dependem do direcionamento dessas colunas para separar dados e fazer com que a consulta acesse o menor número possível de partições.

Exemplos:
  1. Uma tabela de transações, com uma coluna de data que é sempre usada em uma cláusula where.
  2. Uma tabela de clientes com colunas de local, como país de residência, que é sempre usada nas cláusulas where.

As colunas mais comuns para se concentrar no particionamento geralmente são carimbos de data/hora, pois geralmente uma grande parte dos dados são informações históricas e provavelmente terão dados bastante previsíveis espalhados por diferentes agrupamentos de tempo.

Determinar a propagação de dados


Assim que identificarmos em quais colunas particionar, devemos dar uma olhada na distribuição dos dados, com o objetivo de criar tamanhos de partição que distribuam os dados da maneira mais uniforme possível entre as diferentes partições filhas.
severalnines=# SELECT DATE_TRUNC('year', view_date)::DATE, COUNT(*) FROM website_views GROUP BY 1 ORDER BY 1;
 date_trunc |  count
------------+----------
 2013-01-01 | 11625147
 2014-01-01 | 20819125
 2015-01-01 | 20277739
 2016-01-01 | 20584545
 2017-01-01 | 20777354
 2018-01-01 |   491002
(6 rows)

Neste exemplo, truncamos a coluna timestamp para uma tabela anual, resultando em cerca de 20 milhões de linhas por ano. Se todas as nossas consultas especificarem uma(s) data(s) ou intervalo(s) de datas e as especificadas geralmente abrangerem dados de um único ano, essa pode ser uma ótima estratégia inicial para particionamento, pois resultaria em uma única tabela por ano , com um número gerenciável de linhas por tabela.
Baixe o whitepaper hoje PostgreSQL Management &Automation with ClusterControlSaiba o que você precisa saber para implantar, monitorar, gerenciar e dimensionar o PostgreSQLBaixe o whitepaper

Criando uma tabela particionada


Existem algumas maneiras de criar tabelas particionadas, no entanto, vamos nos concentrar principalmente no tipo mais rico em recursos disponível, particionamento baseado em gatilho. Isso requer configuração manual e um pouco de codificação na linguagem procedural plpgsql para funcionar.

Ele opera tendo uma tabela pai que acabará se tornando vazia (ou permanecerá vazia se for uma nova tabela) e tabelas filhas que HERDAM a tabela pai. Quando a tabela pai é consultada, as tabelas filhas também são pesquisadas por dados devido ao INHERIT aplicado às tabelas filhas. No entanto, como as tabelas filhas contêm apenas subconjuntos dos dados do pai, adicionamos um CONSTRAINT na tabela que faz um CHECK e verifica se os dados correspondem ao que é permitido na tabela. Isso faz duas coisas:primeiro, recusa dados que não pertencem e, segundo, informa ao planejador de consulta que apenas dados correspondentes a essa CHECK CONSTRAINT são permitidos nesta tabela, portanto, se estiver procurando por dados que não correspondam à tabela, não nem me incomodo em procurá-lo.

Por fim, aplicamos um gatilho à tabela pai que executa um procedimento armazenado que decide em qual tabela filho colocar os dados.

Criar tabela


A criação da tabela pai é como qualquer outra criação de tabela.
severalnines=# CREATE TABLE data_log (data_log_sid SERIAL PRIMARY KEY,
  date TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(),
  event_details VARCHAR);
CREATE TABLE

Criar tabelas filhas


A criação das tabelas filhas é semelhante, mas envolve algumas adições. Por questões de organização, teremos nossas tabelas filhas em um esquema separado. Faça isso para cada tabela filho, alterando os detalhes de acordo.

NOTA:O nome da sequência usada no nextval() vem da sequência que o pai criou. Isso é crucial para que todas as tabelas filhas usem a mesma sequência.
severalnines=# CREATE SCHEMA part;
CREATE SCHEMA

severalnines=# CREATE TABLE part.data_log_2018 (data_log_sid integer DEFAULT nextval('public.data_log_data_log_sid_seq'::regclass),
  date TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(),
  event_details VARCHAR)
 INHERITS (public.data_log);
CREATE TABLE

severalnines=# ALTER TABLE ONLY part.data_log_2018
    ADD CONSTRAINT data_log_2018_pkey PRIMARY KEY (data_log_sid);
ALTER TABLE

severalnines=# ALTER TABLE part.data_log_2018 ADD CONSTRAINT data_log_2018_date CHECK (date >= '2018-01-01' AND date < '2019-01-01');
ALTER TABLE

Criar função e gatilho


Por fim, criamos nosso procedimento armazenado e adicionamos o gatilho à nossa tabela pai.
severalnines=# CREATE OR REPLACE FUNCTION 
 public.insert_trigger_table()
  RETURNS trigger
  LANGUAGE plpgsql
 AS $function$
 BEGIN
     IF NEW.date >= '2018-01-01' AND NEW.date < '2019-01-01' THEN
         INSERT INTO part.data_log_2018 VALUES (NEW.*);
         RETURN NULL;
     ELSIF NEW.date >= '2019-01-01' AND NEW.date < '2020-01-01' THEN
         INSERT INTO part.data_log_2019 VALUES (NEW.*);
         RETURN NULL;
     END IF;
 END;
 $function$;
CREATE FUNCTION

severalnines=# CREATE TRIGGER insert_trigger BEFORE INSERT ON data_log FOR EACH ROW EXECUTE PROCEDURE insert_trigger_table();
CREATE TRIGGER

Teste


Agora que está tudo criado, vamos testá-lo. Neste teste, adicionei mais tabelas anuais cobrindo 2013 - 2020.

Observação:A resposta de inserção abaixo é 'INSERIR 0 0', o que sugere que não foi inserido nada. Isso será abordado mais adiante neste artigo.
severalnines=# INSERT INTO data_log (date, event_details) VALUES ('2018-08-20 15:22:14', 'First insert');
INSERT 0 0

severalnines=# SELECT * FROM data_log WHERE date >= '2018-08-01' AND date < '2018-09-01';
 data_log_sid |            date            | event_details
--------------+----------------------------+---------------
            1 | 2018-08-17 23:01:38.324056 | First insert
(1 row)

Ela existe, mas vamos examinar o planejador de consultas para garantir que a linha veio da tabela filha correta e que a tabela pai não retornou nenhuma linha.
severalnines=# EXPLAIN ANALYZE SELECT * FROM data_log;
                                                    QUERY PLAN
------------------------------------------------------------------------------------------------------------------
 Append  (cost=0.00..130.12 rows=5813 width=44) (actual time=0.016..0.019 rows=1 loops=1)
   ->  Seq Scan on data_log  (cost=0.00..1.00 rows=1 width=44) (actual time=0.007..0.007 rows=0 loops=1)
   ->  Seq Scan on data_log_2015  (cost=0.00..21.30 rows=1130 width=44) (actual time=0.001..0.001 rows=0 loops=1)
   ->  Seq Scan on data_log_2013  (cost=0.00..17.80 rows=780 width=44) (actual time=0.001..0.001 rows=0 loops=1)
   ->  Seq Scan on data_log_2014  (cost=0.00..17.80 rows=780 width=44) (actual time=0.001..0.001 rows=0 loops=1)
   ->  Seq Scan on data_log_2016  (cost=0.00..17.80 rows=780 width=44) (actual time=0.001..0.001 rows=0 loops=1)
   ->  Seq Scan on data_log_2017  (cost=0.00..17.80 rows=780 width=44) (actual time=0.001..0.001 rows=0 loops=1)
   ->  Seq Scan on data_log_2018  (cost=0.00..1.02 rows=2 width=44) (actual time=0.005..0.005 rows=1 loops=1)
   ->  Seq Scan on data_log_2019  (cost=0.00..17.80 rows=780 width=44) (actual time=0.001..0.001 rows=0 loops=1)
   ->  Seq Scan on data_log_2020  (cost=0.00..17.80 rows=780 width=44) (actual time=0.001..0.001 rows=0 loops=1)
 Planning time: 0.373 ms
 Execution time: 0.069 ms
(12 rows)

Boas notícias, a única linha que inserimos chegou à tabela de 2018, onde ela pertence. Mas, como podemos ver, a consulta não especifica uma cláusula where usando a coluna de data, portanto, para buscar tudo, o planejador de consulta e a execução fizeram uma varredura sequencial em cada tabela.

Em seguida, vamos testar usando uma cláusula where.
severalnines=# EXPLAIN ANALYZE SELECT * FROM data_log WHERE date >= '2018-08-01' AND date < '2018-09-01';
                                                                   QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------
 Append  (cost=0.00..2.03 rows=2 width=44) (actual time=0.013..0.014 rows=1 loops=1)
   ->  Seq Scan on data_log  (cost=0.00..1.00 rows=1 width=44) (actual time=0.007..0.007 rows=0 loops=1)
         Filter: ((date >= '2018-08-01 00:00:00'::timestamp without time zone) AND (date < '2018-09-01 00:00:00'::timestamp without time zone))
   ->  Seq Scan on data_log_2018  (cost=0.00..1.03 rows=1 width=44) (actual time=0.006..0.006 rows=1 loops=1)
         Filter: ((date >= '2018-08-01 00:00:00'::timestamp without time zone) AND (date < '2018-09-01 00:00:00'::timestamp without time zone))
 Planning time: 0.591 ms
 Execution time: 0.041 ms
(7 rows)

Aqui podemos ver que o planejador de consulta e a execução fizeram uma varredura sequencial em duas tabelas, a tabela pai e a tabela filho para 2018. Existem tabelas filhas para os anos de 2013 - 2020, mas aquelas que não são 2018 nunca foram acessadas porque a cláusula where tem um intervalo pertencente apenas a 2018. O planejador de consulta descartou todas as outras tabelas porque CHECK CONSTRAINT considera impossível que os dados existam nessas tabelas.

Partições de trabalho com ferramentas ORM estritas ou validação de linha inserida


Como mencionado anteriormente, o exemplo que construímos retorna um ‘INSERT 0 0’ mesmo que tenhamos inserido uma linha. Se os aplicativos que inserem dados nessas tabelas particionadas dependerem da verificação de que as linhas inseridas estão corretas, elas falharão. Há uma correção, mas adiciona outra camada de complexidade à tabela particionada, portanto, pode ser ignorada se esse cenário não for um problema para os aplicativos que usam a tabela particionada.

Usando uma visualização em vez da tabela pai.


A correção para esse problema é criar uma exibição que consulte a tabela pai e direcione as instruções INSERT para a exibição. Inserir em uma visualização pode parecer loucura, mas é aí que entra o gatilho na visualização.
severalnines=# CREATE VIEW data_log_view AS 
 SELECT data_log.data_log_sid,
     data_log.date,
     data_log.event_details
    FROM data_log;
CREATE VIEW

severalnines=# ALTER VIEW data_log_view ALTER COLUMN data_log_sid SET default nextval('data_log_data_log_sid_seq'::regclass);
ALTER VIEW

Consultar essa exibição será semelhante a consultar a tabela principal, e as cláusulas WHERE e JOINS funcionarão conforme o esperado.

Visualizar função e gatilho específicos


Em vez de usar a função e o gatilho que definimos antes, ambos serão ligeiramente diferentes. Alterações em negrito.
CREATE OR REPLACE FUNCTION public.insert_trigger_view()
 RETURNS trigger
 LANGUAGE plpgsql
AS $function$
BEGIN
    IF NEW.date >= '2018-01-01' AND NEW.date < '2019-01-01' THEN
        INSERT INTO part.data_log_2018 VALUES (NEW.*);
        RETURN NEW;

    ELSIF NEW.date >= '2019-01-01' AND NEW.date < '2020-01-01' THEN
        INSERT INTO part.data_log_2019 VALUES (NEW.*);
        RETURN NEW;

    END IF;
END;
$function$;

severalnines=# CREATE TRIGGER insert_trigger INSTEAD OF INSERT ON data_log_view FOR EACH ROW EXECUTE PROCEDURE insert_trigger_view();

A definição “INSTEAD OF” assume o comando de inserção na exibição (que não funcionaria de qualquer maneira) e executa a função. A função que definimos tem um requisito muito específico de fazer um 'RETURN NEW;' depois que a inserção nas tabelas filhas for concluída. Sem isso (ou fazendo como fizemos antes com 'RETURN NULL') resultará em 'INSERT 0 0' em vez de 'INSERT 0 1' como seria de esperar.

Exemplo:
severalnines=# INSERT INTO data_log_view (date, event_details) VALUES ('2018-08-20 18:12:48', 'First insert on the view');
INSERT 0 1

severalnines=# EXPLAIN ANALYZE SELECT * FROM data_log_view WHERE date >= '2018-08-01' AND date < '2018-09-01';
                                                                   QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------
 Append  (cost=0.00..2.03 rows=2 width=44) (actual time=0.015..0.017 rows=2 loops=1)
   ->  Seq Scan on data_log  (cost=0.00..1.00 rows=1 width=44) (actual time=0.009..0.009 rows=0 loops=1)
         Filter: ((date >= '2018-08-01 00:00:00'::timestamp without time zone) AND (date < '2018-09-01 00:00:00'::timestamp without time zone))
   ->  Seq Scan on data_log_2018  (cost=0.00..1.03 rows=1 width=44) (actual time=0.006..0.007 rows=2 loops=1)
         Filter: ((date >= '2018-08-01 00:00:00'::timestamp without time zone) AND (date < '2018-09-01 00:00:00'::timestamp without time zone))
 Planning time: 0.633 ms
 Execution time: 0.048 ms
(7 rows)

severalnines=# SELECT * FROM data_log_view WHERE date >= '2018-08-01' AND date < '2018-09-01';
 data_log_sid |        date         |      event_details
--------------+---------------------+--------------------------
            1 | 2018-08-20 15:22:14 | First insert
            2 | 2018-08-20 18:12:48 | First insert on the view
(2 rows)

Testes de aplicativos para 'rowcount' inserido como correto encontrarão essa correção para funcionar conforme o esperado. Neste exemplo, anexamos _view à nossa visão e procedimento armazenado, mas se desejarmos que a tabela seja particionada sem que nenhum usuário saiba/altere o aplicativo, renomeamos a tabela pai para data_log_parent e chamamos a visão pelo antigo nome da tabela pai.

Atualizando uma linha e alterando o valor da coluna particionada


Uma coisa a estar ciente é que, se realizar uma atualização nos dados na tabela particionada e alterar o valor da coluna para algo não permitido pela restrição, resultará em um erro. Se esse tipo de atualização nunca acontecer, então ele pode ser ignorado, mas se for uma possibilidade, um novo gatilho para processos UPDATE deve ser escrito que efetivamente excluirá a linha da partição filho antiga e inserirá uma nova na nova partição filho de destino.

Criando futuras partições


A criação de partições futuras pode ser feita de algumas maneiras diferentes, cada uma com seus prós e contras.

Futuro criador de partições


Um programa externo pode ser escrito para criar partições futuras X vezes antes de serem necessárias. Em um exemplo de particionamento particionado em uma data, a próxima partição necessária para criar (no nosso caso 2019) pode ser configurada para ser criada em dezembro. Este pode ser um script manual executado pelo Administrador de Banco de Dados ou configurado para que o cron o execute quando necessário. Partições anuais significam que ele é executado uma vez por ano, no entanto, partições diárias são comuns, e um cron job diário contribui para um DBA mais feliz.

Criador automático de partições


Com o poder do plpgsql, podemos capturar erros ao tentar inserir dados em uma partição filha que não existe e, na hora, criar a partição necessária e tentar inserir novamente. Esta opção funciona bem, exceto no caso em que muitos clientes diferentes inserindo dados semelhantes ao mesmo tempo, podem causar uma condição de corrida em que um cliente cria a tabela, enquanto outro tenta criar a mesma tabela e obtém um erro dela já existente. A programação inteligente e avançada do plpgsql pode corrigir isso, mas se vale a pena ou não o nível de esforço está em debate. Se essa condição de corrida não ocorrer devido aos padrões de inserção, não há com o que se preocupar.

Apagando partições


Se as regras de retenção de dados determinarem que os dados sejam excluídos após um determinado período de tempo, isso se tornará mais fácil com tabelas particionadas se particionadas em uma coluna de data. Se formos excluir dados com 10 anos, pode ser tão simples quanto:
severalnines=# DROP TABLE part.data_log_2007;
DROP TABLE

Isso é muito mais rápido e eficiente do que uma instrução 'DELETE', pois não resulta em nenhuma tupla morta para limpar com um aspirador.

Observação:ao remover tabelas da configuração da partição, o código nas funções de gatilho também deve ser alterado para não direcionar a data para a tabela descartada.

O que você deve saber antes de particionar


O particionamento de tabelas pode oferecer uma melhoria drástica no desempenho, mas também pode piorá-lo. Antes de enviar para servidores de produção, a estratégia de particionamento deve ser testada extensivamente, para consistência de dados, velocidade de desempenho, tudo. Particionar uma tabela tem algumas partes móveis, todas elas devem ser testadas para garantir que não haja problemas.

Quando se trata de decidir o número de partições, é altamente recomendável manter o número de tabelas filhas abaixo de 1.000 tabelas e ainda menor, se possível. Quando a contagem da tabela filha fica acima de ~1000, o desempenho começa a cair, pois o próprio planejador de consulta acaba demorando muito mais para fazer o plano de consulta. Não é inédito que um plano de consulta leve muitos segundos, enquanto a execução real leva apenas alguns milissegundos. Se atender a milhares de consultas por minuto, vários segundos podem paralisar os aplicativos.

Os procedimentos armazenados do gatilho plpgsql também podem ficar complicados e, se muito complicados, também diminuir o desempenho. O procedimento armazenado é executado uma vez para cada linha inserida na tabela. Se acabar fazendo muito processamento para cada linha, as inserções podem se tornar muito lentas. O teste de desempenho garantirá que ainda esteja na faixa aceitável.

Seja criativo


O particionamento de tabelas no PostgreSQL pode ser tão avançado quanto necessário. Em vez de colunas de data, as tabelas podem ser particionadas em uma coluna de “país”, com uma tabela para cada país. O particionamento pode ser feito em várias colunas, como uma coluna 'data' e uma coluna 'país'. Isso tornará o procedimento armazenado que manipula as inserções mais complexo, mas é 100% possível.

Lembre-se, os objetivos com o particionamento são dividir tabelas extremamente grandes em menores e fazê-lo de uma maneira bem pensada para permitir que o planejador de consultas acesse os dados mais rapidamente do que poderia ter na tabela original maior.

Particionamento Declarativo


No PostgreSQL 10 e posterior, um novo recurso de particionamento ‘Particionamento Declarativo’ foi introduzido. É uma maneira mais fácil de configurar partições, mas tem algumas limitações. Se as limitações forem aceitáveis, provavelmente será mais rápido do que a configuração manual da partição, mas muitos testes verificarão isso.

A documentação oficial do postgresql tem informações sobre o Particionamento Declarativo e como ele funciona. É novo no PostgreSQL 10, e com a versão 11 do PostgreSQL no horizonte no momento da redação deste artigo, algumas das limitações foram corrigidas, mas não todas. À medida que o PostgreSQL evolui, o particionamento declarativo pode se tornar um substituto completo para o particionamento mais complexo abordado neste artigo. Até então, o Particionamento Declarativo pode ser uma alternativa mais fácil se nenhuma das limitações limitar as necessidades de particionamento.

Limitações de particionamento declarativo


A documentação do PostgreSQL aborda todas as limitações desse tipo de particionamento no PostgreSQL 10, mas uma ótima visão geral pode ser encontrada no Wiki Oficial do PostgreSQL que lista as limitações em um formato mais fácil de ler, além de observar quais foram corrigidas em o próximo PostgreSQL 11.

Pergunte à comunidade


Administradores de banco de dados em todo o mundo vêm projetando estratégias de particionamento avançadas e personalizadas há muito tempo, e muitos de nós frequentamos IRC e listas de discussão. Se for necessária ajuda para decidir a melhor estratégia ou apenas resolver um bug em um procedimento armazenado, a comunidade está aqui para ajudar.
  • IRC
    A Freenode tem um canal muito ativo chamado #postgres, onde os usuários se ajudam a entender conceitos, corrigir erros ou encontrar outros recursos.
  • Listas de correspondência
    O PostgreSQL tem um punhado de listas de discussão que podem ser adicionadas. Perguntas/questões mais longas podem ser enviadas aqui e podem alcançar muito mais pessoas do que o IRC a qualquer momento. As listas podem ser encontradas no site do PostgreSQL, e as listas pgsql-general ou pgsql-admin são bons recursos.