Um sistema de particionamento no PostgreSQL foi adicionado pela primeira vez no PostgreSQL 8.1 pelo fundador do 2ndQuadrant Simon Riggs . Ele foi baseado em herança de relação e usou uma nova técnica para excluir tabelas de serem verificadas por uma consulta, chamada “exclusão de restrição”. Embora tenha sido um grande passo à frente na época, hoje em dia é visto como complicado de usar e lento e, portanto, precisando de substituição.
Na versão 10, foi substituído graças aos esforços heróicos de Amit Langote com “particionamento declarativo” de estilo moderno. Essa nova tecnologia significava que você não precisava mais escrever código manualmente para rotear tuplas para suas partições corretas e não precisava mais declarar manualmente as restrições corretas para cada partição:o sistema fazia isso automaticamente para você.
Infelizmente, no PostgreSQL 10 foi praticamente tudo o que fez. Por causa da grande complexidade e das restrições de tempo, havia muitas coisas que faltavam na implementação do PostgreSQL 10. Robert Haas deu uma palestra sobre isso na PGConf.EU de Varsóvia.
Muitas pessoas trabalharam para melhorar a situação do PostgreSQL 11; aqui está minha tentativa de recontagem. Separei em três áreas:
- Novos recursos de particionamento
- Melhor suporte a DDL para tabelas particionadas
- Otimizações de desempenho.
Novos recursos de particionamento
No PostgreSQL 10, suas tabelas particionadas podem ser assim em RANGE e LISTA modos. Estas são ferramentas poderosas para basear muitos bancos de dados do mundo real, mas para muitos outros projetos você precisa do novo modo adicionado no PostgreSQL 11:HASH particionamento . Muitos clientes precisam disso e a Amul Sul trabalhou duro para torná-lo possível. Aqui está um exemplo simples:
CREATE TABLE clients ( client_id INTEGER, name TEXT ) PARTITION BY HASH (client_id); CREATE TABLE clients_0 PARTITION OF clients FOR VALUES WITH (MODULUS 3, REMAINDER 0); CREATE TABLE clients_1 PARTITION OF clients FOR VALUES WITH (MODULUS 3, REMAINDER 1); CREATE TABLE clients_2 PARTITION OF clients FOR VALUES WITH (MODULUS 3, REMAINDER 2);
Não é obrigatório usar o mesmo valor de módulo para todas as partições; isso permite criar mais partições posteriormente e redistribuir as linhas uma partição por vez, se necessário.
Outro recurso muito útil, escrito por Amit Khandekar é a capacidade de permitir UPDATE para mover linhas de uma partição para outra — ou seja, se houver uma alteração nos valores da coluna de particionamento, a linha será movida automaticamente para a partição correta. Anteriormente, essa operação teria gerado um erro.
Outro novo recurso, escrito por Amit Langote e atenciosamente , é INSERIR NA ATUALIZAÇÃO DE CONFLITO pode ser aplicado a tabelas particionadas . Anteriormente, esse comando falharia se visasse uma tabela particionada. Você poderia fazê-lo funcionar sabendo exatamente em qual partição a linha terminaria, mas isso não é muito conveniente. Não vou falar sobre os detalhes desse comando, mas se você já desejou ter UPSERT no Postgres, é isso. Uma ressalva é que o UPDATE ação não pode mover a linha para outra partição.
Finalmente, outro novo recurso fofo no PostgreSQL 11, desta vez por Jeevan Ladhe, Beena Emerson, Ashutosh Bapat, Rahila Syed, eRobert Haas é suporte para uma partição padrão em uma tabela particionada , ou seja, uma partição que recebe todas as linhas que não cabem em nenhuma das partições regulares. No entanto, embora bom no papel, esse recurso não é muito conveniente nas configurações de produção porque algumas operações exigem bloqueio mais pesado com partições padrão do que sem. Exemplo:a criação de uma nova partição requer a verificação da partição padrão para determinar se nenhuma linha existente corresponde aos limites da nova partição. Talvez no futuro esses requisitos de bloqueio sejam reduzidos, mas enquanto isso minha sugestão é não usá-lo.
Melhor suporte a DDL
No PostgreSQL 10, certos DDL se recusavam a funcionar quando aplicados a uma tabela particionada e exigia que você processasse cada partição individualmente. No PostgreSQL 11, corrigimos algumas dessas limitações, conforme anunciado anteriormente por Simon Riggs. Primeiro, agora você pode usar CREATE INDEX em uma tabela particionada , um recurso escrito por você verdadeiramente. Este pode ser visto apenas como uma questão de reduzir o tédio:em vez de repetir o comando para cada partição (e garantir nunca esquecer para cada nova partição), você pode fazê-lo apenas uma vez para a tabela particionada pai e aplica-se automaticamente para todas as partições, existentes e futuras.
Uma coisa legal a ter em mente é a correspondência de índices existentes nas partições. Como você sabe, criar um índice é uma proposta de bloqueio, então quanto menos tempo levar, melhor. Eu escrevi esse recurso para que os índices existentes na partição fossem comparados aos índices que estão sendo criados e, se houver correspondências, não é necessário varrer a partição para criar novos índices:os índices existentes seriam usados.
Junto com isso, também por sua conta, você também pode criar ÚNICO restrições, bem como CHAVE PRIMÁRIA restrições . Duas advertências:primeiro, a chave de partição deve fazer parte da chave primária. Isso permite que as verificações exclusivas sejam feitas localmente por partição, evitando índices globais. Segundo, ainda não é possível ter chaves estrangeiras que façam referência a essas chaves primárias. Estou trabalhando nisso para o PostgreSQL 12.
Outra coisa que você pode fazer (graças à mesma pessoa) é criar FOR EACH ROW gatilhos em uma tabela particionada , e aplique-o a todas as partições (existentes e futuras). Como efeito colateral, você pode ter adiado único restrições em tabelas particionadas. Uma ressalva:somente DEPOIS gatilhos são permitidos, até descobrirmos como lidar com ANTES gatilhos que movem linhas para uma partição diferente.
Por fim, uma tabela particionada pode ter FOREIGN KEY restrições . Isso é muito útil para particionar grandes tabelas de fatos, evitando referências pendentes, que todo mundo detesta. Meu colega Gabriele Bartolini me agarrou pelo colo quando descobriu que eu tinha escrito e cometido isso, gritando que isso era um divisor de águas e como eu poderia ser tão insensível a ponto de não informá-lo disso. Eu, eu continuo a hackear o código por diversão.
Trabalho de Desempenho
Anteriormente, as consultas de pré-processamento para descobrir quais partições não deveriam ser verificadas (exclusão de restrição) eram bastante simplistas e lentas. Isso foi aprimorado pelo admirável trabalho em equipe realizado por Amit Langote, David Rowley, Beena Emerson, Dilip Kumar para introduzir a "poda mais rápida" primeiro e a "poda de tempo de execução" com base nela depois. O resultado é muito mais poderoso e rápido (David Rowley já descrevi isso em um artigo anterior.) Depois de todo esse esforço, a remoção de partição é aplicada em três pontos na vida de uma consulta:
- No momento do plano de consulta,
- Quando os parâmetros de consulta são recebidos,
- Em cada ponto em que um nó de consulta passa valores como parâmetros para outro nó.
Esta é uma melhoria notável em relação ao sistema original que só poderia ser aplicada na hora do plano de consulta, e acredito que agradará a muitos.
Você pode ver esse recurso em ação comparando a saída EXPLAIN para uma consulta antes e depois de desativar o enable_partition_pruning opção. Como um exemplo muito simplista, compare este plano sem poda:
SET enable_partition_pruning TO off; EXPLAIN (ANALYZE, COSTS off) SELECT * FROM clientes WHERE cliente_id = 1234;
QUERY PLAN ------------------------------------------------------------------------- Append (actual time=6.658..10.549 rows=1 loops=1) -> Seq Scan on clientes_1 (actual time=4.724..4.724 rows=0 loops=1) Filter: (cliente_id = 1234) Rows Removed by Filter: 24978 -> Seq Scan on clientes_00 (actual time=1.914..1.914 rows=0 loops=1) Filter: (cliente_id = 1234) Rows Removed by Filter: 12644 -> Seq Scan on clientes_2 (actual time=0.017..1.021 rows=1 loops=1) Filter: (cliente_id = 1234) Rows Removed by Filter: 12570 -> Seq Scan on clientes_3 (actual time=0.746..0.746 rows=0 loops=1) Filter: (cliente_id = 1234) Rows Removed by Filter: 12448 -> Seq Scan on clientes_01 (actual time=0.648..0.648 rows=0 loops=1) Filter: (cliente_id = 1234) Rows Removed by Filter: 12482 -> Seq Scan on clientes_4 (actual time=0.774..0.774 rows=0 loops=1) Filter: (cliente_id = 1234) Rows Removed by Filter: 12400 -> Seq Scan on clientes_5 (actual time=0.717..0.717 rows=0 loops=1) Filter: (cliente_id = 1234) Rows Removed by Filter: 12477 Planning Time: 0.375 ms Execution Time: 10.603 ms
com a produzida com poda:
EXPLAIN (ANALYZE, COSTS off) SELECT * FROM clientes WHERE cliente_id = 1234;
QUERY PLAN ---------------------------------------------------------------------- Append (actual time=0.054..2.787 rows=1 loops=1) -> Seq Scan on clientes_2 (actual time=0.052..2.785 rows=1 loops=1) Filter: (cliente_id = 1234) Rows Removed by Filter: 12570 Planning Time: 0.292 ms Execution Time: 2.822 ms
Tenho certeza que você achará isso atraente. Você pode ver uma tonelada de exemplos mais sofisticados examinando o arquivo esperado de testes de regressão.
Outro item foi a introdução de junções por partição, por Ashutosh Bapat . A idéia aqui é que se você tem duas tabelas particionadas, e elas são particionadas de maneiras idênticas, então quando elas são unidas você pode unir cada partição de um lado à sua partição correspondente do outro lado; isso é muito melhor do que juntar cada partição do lado a cada partição do outro lado. O fato de que os esquemas de partição precisam corresponder exatamente pode parecer improvável que isso tenha muito uso no mundo real, mas, na realidade, existem muitas situações em que isso se aplica. Exemplo:uma tabela de pedidos e sua tabela de itens_pedidos correspondente. Felizmente, já há muito trabalho para relaxar essa restrição.
O último item que quero mencionar são os agregados particionados, de Jeevan Chalke, Ashutosh Bapat, eRobert Haas . Essa otimização significa que uma agregação que inclui as chaves de partição no GROUP BY A cláusula pode ser executada agregando as linhas de cada partição separadamente, o que é muito mais rápido.
Pensamentos finais
Após os desenvolvimentos significativos neste ciclo, o PostgreSQL tem uma história de particionamento muito mais atraente. Embora ainda haja muitas melhorias a serem feitas, principalmente para melhorar o desempenho e a simultaneidade de várias operações envolvendo tabelas particionadas, agora estamos em um ponto em que o particionamento declarativo se tornou uma ferramenta muito valiosa para atender a muitos casos de uso. No 2ndQuadrant continuaremos a contribuir com código para melhorar o PostgreSQL nesta área e em outras, como fizemos para cada lançamento desde o 8.0.