web-dev-qa-db-fra.com

Comment migrer une table Postgres existante vers une table partitionnée de la manière la plus transparente possible?

J'ai une table existante dans un postgres-DB. À des fins de démonstration, voici à quoi cela ressemble:

create table myTable(
    forDate date not null,
    key2 int not null,
    value int not null,
    primary key (forDate, key2)
);

insert into myTable (forDate, key2, value) values
    ('2000-01-01', 1, 1),
    ('2000-01-01', 2, 1),
    ('2000-01-15', 1, 3),
    ('2000-03-02', 1, 19),
    ('2000-03-30', 15, 8),
    ('2011-12-15', 1, 11);

Cependant, contrairement à ces quelques valeurs, myTable est en fait ÉNORME et ne cesse de croître. Je génère divers rapports à partir de ce tableau, mais actuellement 98% de mes rapports fonctionnent avec un seul mois et les requêtes restantes fonctionnent avec un délai encore plus court. Souvent, mes requêtes obligent Postgres à effectuer des analyses de table sur cette immense table et je cherche des moyens de réduire le problème. Partitionnement de table semble parfaitement correspondre à mon problème. Je pourrais simplement partitionner ma table en mois. Mais comment transformer ma table existante en table partitionnée? Le manuel indique explicitement:

Il n'est pas possible de transformer une table standard en table partitionnée ou vice versa

Je dois donc développer mon propre script de migration, qui analysera la table actuelle et la migrera. Les besoins sont les suivants:

  • Au moment de la conception, la période couverte par myTable est inconnue.
  • Chaque partition doit couvrir un mois du premier jour de ce mois au dernier jour de ce mois.
  • La table augmentera indéfiniment, donc je n'ai pas de "valeur d'arrêt" sensée pour le nombre de tables à générer
  • Le résultat doit être aussi transparent que possible, ce qui signifie que je veux toucher le moins possible à mon code existant. Dans le meilleur des cas, cela ressemble à une table normale dans laquelle je peux insérer et sélectionner sans spécial.
  • Un temps d'arrêt de la base de données pour la migration est acceptable
  • Il est hautement préférable de s'entendre avec de purs Postgres sans plugins ou autres choses qui doivent être installés sur le serveur.
  • La base de données est PostgreSQL 10, la mise à niveau vers une version plus récente se fera de toute façon tôt ou tard, c'est donc une option si cela aide

Comment puis-je migrer ma table à partitionner?

8
yankee

Dans Postgres 10, le "partitionnement déclaratif" a été introduit, ce qui peut vous soulager de beaucoup de travail, comme la génération de déclencheurs ou de règles avec d'énormes instructions if/else redirigeant vers la bonne table. Postgres peut le faire automatiquement maintenant. Commençons par la migration:

  1. Renommez l'ancienne table et créez une nouvelle table partitionnée

    alter table myTable rename to myTable_old;
    
    create table myTable_master(
        forDate date not null,
        key2 int not null,
        value int not null
    ) partition by range (forDate);
    

Cela ne devrait guère exiger d'explication. L'ancienne table est renommée (après la migration des données, nous la supprimons) et nous obtenons une table principale pour notre partition qui est fondamentalement la même que notre table d'origine, mais sans index)

  1. Créez une fonction qui peut générer de nouvelles partitions selon nos besoins:

    create function createPartitionIfNotExists(forDate date) returns void
    as $body$
    declare monthStart date := date_trunc('month', forDate);
        declare monthEndExclusive date := monthStart + interval '1 month';
        -- We infer the name of the table from the date that it should contain
        -- E.g. a date in June 2005 should be int the table mytable_200506:
        declare tableName text := 'mytable_' || to_char(forDate, 'YYYYmm');
    begin
        -- Check if the table we need for the supplied date exists.
        -- If it does not exist...:
        if to_regclass(tableName) is null then
            -- Generate a new table that acts as a partition for mytable:
            execute format('create table %I partition of myTable_master for values from (%L) to (%L)', tableName, monthStart, monthEndExclusive);
            -- Unfortunatelly Postgres forces us to define index for each table individually:
            execute format('create unique index on %I (forDate, key2)', tableName);
        end if;
    end;
    $body$ language plpgsql;
    

Cela vous sera utile plus tard.

  1. Créez une vue qui délègue simplement à notre table principale:

    create or replace view myTable as select * from myTable_master;
    
  2. Créez une règle pour que lorsque nous l'insérons dans la règle, nous ne mettions pas seulement à jour la table partitionnée, mais nous créons également une nouvelle partition si nécessaire:

    create or replace rule autoCall_createPartitionIfNotExists as on insert
        to myTable
        do instead (
            select createPartitionIfNotExists(NEW.forDate);
            insert into myTable_master (forDate, key2, value) values (NEW.forDate, NEW.key2, NEW.value)
        );
    

Bien sûr, si vous avez également besoin de update et delete, vous avez également besoin d'une règle pour celles qui doivent être simples.

  1. Migrer réellement l'ancienne table:

    -- Finally copy the data to our new partitioned table
    insert into myTable (forDate, key2, value) select * from myTable_old;
    
    -- And get rid of the old table
    drop table myTable_old;
    

La migration de la table est maintenant terminée sans qu'il soit nécessaire de savoir combien de partitions sont nécessaires et la vue myTable sera absolument transparente. Vous pouvez simplement insérer et sélectionner à partir de ce tableau comme précédemment, mais vous pourriez bénéficier des performances du partitionnement.

Notez que la vue est uniquement nécessaire, car une table partitionnée ne peut pas avoir de déclencheurs de ligne. Si vous pouvez vous entendre en appelant createPartitionIfNotExists manuellement chaque fois que cela est nécessaire à partir de votre code, vous n'avez pas besoin de la vue et de toutes ses règles. Dans ce cas, vous devez ajouter manuellement les partitions lors de la migration:

do
$$
declare rec record;
begin
    -- Loop through all months that exist so far...
    for rec in select distinct date_trunc('month', forDate)::date yearmonth from myTable_old loop
        -- ... and create a partition for them
        perform createPartitionIfNotExists(rec.yearmonth);
    end loop;
end
$$;
12
yankee