如何尽可能透明地将现有 Postgres Table 迁移到分区 table?

How to migrate an existing Postgres Table to partitioned table as transparently as possible?

我在 postgres-DB 中有一个现有的 table。为了演示,它是这样的:

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);

然而,与这几个值相比,myTable实际上是巨大的,而且还在不断增长。我正在从这个 table 生成各种报告,但目前我的报告中有 98% 仅在一个月内有效,而其余查询在更短的时间范围内有效。通常我的查询导致 Postgres 对这个巨大的 table 进行 table 扫描,我正在寻找减少问题的方法。 Table partitioning 似乎完全符合我的问题。我可以将 table 分成几个月。但是如何将现有的 table 变成分区的 table?手册明确指出:

It is not possible to turn a regular table into a partitioned table or vice versa

所以我需要开发自己的迁移脚本,它会分析当前的table并进行迁移。需求如下:

如何迁移我的 table 进行分区?

在 Postgres 10 中引入了 "Declarative Partitioning",它可以减轻您的大量工作,例如使用巨大的 if/else 语句生成触发器或规则重定向到正确的 table。 Postgres 现在可以自动执行此操作。让我们从迁移开始:

  1. 重命名旧的 table 并创建一个新的分区 table

    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);
    

这几乎不需要任何解释。旧的 table 被重命名(在数据迁移后我们将删除它)并且我们的分区得到一个 master table,它与我们原来的 table 基本相同,但没有索引)

  1. 创建一个可以根据需要生成新分区的函数:

    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;
    

这个以后会派上用场的。

  1. 创建一个基本上只委托给我们的主人的视图table:

    create or replace view myTable as select * from myTable_master;
    
  2. 创建规则,这样当我们插入规则时,我们不仅会更新分区 table,还会根据需要创建一个新分区:

    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)
        );
    

当然,如果你还需要updatedelete,你还需要一个规则,那些应该是直截了当的。

  1. 实际迁移旧的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;
    

现在 table 的迁移已经完成,不需要知道需要多少分区,而且视图 myTable 将是绝对透明的。您可以像以前一样从 table 中简单地插入和 select,但您可能会从分区中获得性能优势。

请注意,只需要视图,因为分区 table 不能有行触发器。如果您可以在代码需要时手动调用 createPartitionIfNotExists,那么您就不需要视图及其所有规则。在这种情况下,您需要在迁移过程中手动添加分区:

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
$$;

一个建议是,为您的主要 table 访问使用一个视图,执行上述步骤,在其中创建一个新分区 table。完成后,将视图指向新的分区 table,然后进行迁移,最后弃用旧的 table.