Como particionar a tabela existente no postgres?

19

Gostaria de particionar uma tabela com mais de 1 milhão de linhas por período. Como isso é comumente feito sem exigir muito tempo de inatividade ou correr o risco de perder dados? Aqui estão as estratégias que estou considerando, mas abertas a sugestões:

  1. A tabela existente é o mestre e os filhos herdam dela. Com o tempo, mova os dados do mestre para o filho, mas haverá um período em que alguns dados estão na tabela mestre e outros nos filhos.

  2. Crie uma nova tabela mestre e filho. Crie uma cópia dos dados na tabela existente nas tabelas filho (para que os dados residam em dois locais). Depois que as tabelas filho tiverem os dados mais recentes, altere todas as inserções daqui para frente para apontar para a nova tabela mestre e excluir a tabela existente.

Evan Appleby
fonte
11
Aqui estão minhas idéias: se as tabelas tiverem coluna de data e hora -> criar novo mestre + novo filho -> inserir novos dados em NEW + OLD (ex: datetime = 2015-07-06 00:00:00) -> copiar de OLD para NEW base na coluna time (onde: datetime <2015-07-06 00:00:00) -> renomear tabela -> alterar inserção para NEW else -> criar "acionador de partição" para inserir / atualizar no mestre (inserir / atualizar novos dados - > mover para criança, para que novos dados sejam inseridos para criança) -> atualizar mestre, o gatilho moverá dados para criança.
Luan Huynh
@Innnh, então você está sugerindo a segunda opção, mas depois que os dados forem copiados, exclua a tabela antiga e renomeie a nova tabela para ter o mesmo nome da tabela antiga. Isso está certo?
Evan Appleby
renomeie nova tabela para tabela antiga, mas você deve manter a tabela antiga até que as novas tabelas de partição de fluxo fiquem completamente ok.
Luan Huynh
2
Por apenas alguns milhões de linhas, não acho que o particionamento seja realmente necessário. Por que você acha que precisa disso? Que problema você está tentando resolver?
A_horse_with_no_name
11
@EvanAppleby DELETE FROM ONLY master_tableé a solução.
Dezso

Respostas:

21

Como o número 1 exige a cópia de dados do mestre para o filho enquanto ele estiver em um ambiente de produção ativo, eu pessoalmente fui com o número 2 (criando um novo mestre). Isso evita interrupções na tabela original enquanto ela estiver ativamente em uso e, se houver algum problema, eu posso excluir facilmente o novo mestre sem problemas e continuar usando a tabela original. Aqui estão as etapas para fazer isso:

  1. Crie uma nova tabela mestre.

    CREATE TABLE new_master (
        id          serial,
        counter     integer,
        dt_created  DATE DEFAULT CURRENT_DATE NOT NULL
    );
  2. Crie filhos que herdam do mestre.

    CREATE TABLE child_2014 (
        CONSTRAINT pk_2014 PRIMARY KEY (id),
        CONSTRAINT ck_2014 CHECK ( dt_created < DATE '2015-01-01' )
    ) INHERITS (new_master);
    CREATE INDEX idx_2014 ON child_2014 (dt_created);
    
    CREATE TABLE child_2015 (
        CONSTRAINT pk_2015 PRIMARY KEY (id),
        CONSTRAINT ck_2015 CHECK ( dt_created >= DATE '2015-01-01' AND dt_created < DATE '2016-01-01' )
    ) INHERITS (new_master);
    CREATE INDEX idx_2015 ON child_2015 (dt_created);
    
    ...
  3. Copie todos os dados históricos para a nova tabela mestre

    INSERT INTO child_2014 (id,counter,dt_created)
    SELECT id,counter,dt_created
    from old_master
    where dt_created < '01/01/2015'::date;
  4. Pausar temporariamente novas inserções / atualizações no banco de dados de produção

  5. Copie os dados mais recentes para a nova tabela mestre

    INSERT INTO child_2015 (id,counter,dt_created)
    SELECT id,counter,dt_created
    from old_master
    where dt_created >= '01/01/2015'::date AND dt_created < '01/01/2016'::date;
  6. Renomeie as tabelas para que new_master se torne o banco de dados de produção.

    ALTER TABLE old_master RENAME TO old_master_backup;
    ALTER TABLE new_master RENAME TO old_master;
  7. Inclua a função para instruções INSERT em old_master para que os dados sejam passados ​​para a partição correta.

    CREATE OR REPLACE FUNCTION fn_insert() RETURNS TRIGGER AS $$
    BEGIN
        IF ( NEW.dt_created >= DATE '2015-01-01' AND
             NEW.dt_created < DATE '2016-01-01' ) THEN
            INSERT INTO child_2015 VALUES (NEW.*);
        ELSIF ( NEW.dt_created < DATE '2015-01-01' ) THEN
            INSERT INTO child_2014 VALUES (NEW.*);
        ELSE
            RAISE EXCEPTION 'Date out of range';
        END IF;
        RETURN NULL;
    END;
    $$
    LANGUAGE plpgsql;
  8. Adicione gatilho para que a função seja chamada INSERTS

    CREATE TRIGGER tr_insert BEFORE INSERT ON old_master
    FOR EACH ROW EXECUTE PROCEDURE fn_insert();
  9. Defina a exclusão de restrição como ON

    SET constraint_exclusion = on;
  10. Reativar UPDATES e INSERTS no banco de dados de produção

  11. Configure trigger ou cron para que novas partições sejam criadas e a função seja atualizada para atribuir novos dados à correção da partição. Consulte este artigo para exemplos de código

  12. Excluir old_master_backup

Evan Appleby
fonte
11
Boa redação. Seria interessante se isso realmente tornar suas consultas mais rápidas. 10 milhões ainda não são tantas linhas que eu pensaria em particionar. Gostaria de saber se o seu desempenho degradante talvez tenha sido causado por vacuumnão atualizar ou ser impedido devido a sessões "inativas na transação".
A_horse_with_no_name 07/07
@a_horse_with_no_name, até agora ele não fez as consultas significativamente melhor :( Eu uso Heroku que tem configurações de auto-vácuo e parece acontecer diariamente para este grande mesa vai olhar mais para que tho..
Evan Appleby
As inserções nas etapas 3 e 5 não devem ser apresentadas na tabela new_master e deixar o postgresql escolher a tabela / partição filho certa?
Pakman
@pakman a função de atribuir o filho certo não é adicionada até a etapa 7
Evan Appleby
4

Há uma nova ferramenta chamada pg_pathman ( https://github.com/postgrespro/pg_pathman ), que iria fazer isso para você automaticamente.

Então, algo como o seguinte faria isso.

SELECT create_range_partitions('master', 'dt_created', 
   '2015-01-01'::date, '1 day'::interval);
kakoni
fonte