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:
myTable
est inconnue.Comment puis-je migrer ma table à partitionner?
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:
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)
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.
Créez une vue qui délègue simplement à notre table principale:
create or replace view myTable as select * from myTable_master;
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.
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
$$;