web-dev-qa-db-ja.com

既存のPostgresテーブルを可能な限り透過的にパーティションテーブルに移行する方法

Postgres-DBに既存のテーブルがあります。デモのために、これは次のようになります。

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は実際には巨大であり、継続的に増加しています。このテーブルからさまざまなレポートを生成していますが、現在、レポートの98%は1か月で動作し、残りのクエリはさらに短い時間で動作します。多くの場合、私のクエリにより、Postgresはこの巨大なテーブルに対してテーブルスキャンを実行します。問題を軽減する方法を探しています。 テーブル分割 は私の問題に完全に適合しているようです。テーブルを数か月に分割できました。しかし、既存のテーブルをパーティションテーブルにするにはどうすればよいですか?マニュアルは明示的に述べています:

通常のテーブルを分割テーブルに、またはその逆に変換することはできません

したがって、現在のテーブルを分析して移行する独自の移行スクリプトを開発する必要があります。ニーズは次のとおりです。

  • 設計時には、myTableがカバーする時間枠は不明です。
  • 各パーティションは、その月の最初の日からその月の最後の日まで1か月をカバーする必要があります。
  • テーブルは無制限に大きくなるので、生成するテーブルの数についての適切な「ストップ値」はありません
  • 結果は可能な限り透過的である必要があります。つまり、既存のコードにはできるだけ触れないようにします。最良の場合、これは通常のテーブルのように感じられ、特別なものなしで挿入および選択できます。
  • 移行のためのデータベースのダウンタイムは許容範囲です
  • サーバーにインストールする必要のあるプラグインやその他のものを使用せずに、純粋なPostgresとやり取りすることを強くお勧めします。
  • データベースはPostgreSQL 10ですが、いずれにしても新しいバージョンへのアップグレードは遅かれ早かれ発生するため、役立つ場合はこれがオプションです。

パーティション化するためにテーブルをどのように移行できますか?

8
yankee

Postgres 10で導入された「宣言型パーティショニング」は、正しいテーブルにリダイレクトする巨大なif/elseステートメントでトリガーやルールを生成するなどの多くの作業を軽減します。 Postgresはこれを自動的に行うことができます。移行から始めましょう:

  1. 古いテーブルの名前を変更して、新しいパーティションテーブルを作成します。

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

説明はほとんど必要ありません。古いテーブルの名前が変更され(データの移行後は削除します)、パーティションのマスターテーブルを取得します。これは基本的に元のテーブルと同じですが、インデックスがありません)

  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. 基本的にマスターテーブルに委任するだけのビューを作成します。

    create or replace view myTable as select * from myTable_master;
    
  2. ルールを挿入すると、パーティションテーブルが更新されるだけでなく、必要に応じて新しいパーティションも作成されるように、ルールを作成します。

    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. 古いテーブルを実際に移行します。

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

これでテーブルの移行は完了しました。パーティションがいくつ必要かを知る必要はなく、ビューmyTableは完全に透過的です。以前と同じようにそのテーブルから簡単に挿入して選択できますが、パーティション分割によってパフォーマンスが向上する可能性があります。

パーティションテーブルは行トリガーを持つことができないため、ビューが必要なだけであることに注意してください。コードから必要なときにいつでも手動で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
$$;
12
yankee