PostgreSQL 14以降における宣言的テーブルパーティショニングの理解と実装
PostgreSQLは長年にわたり強力で多用途なリレーショナルデータベースでしたが、データセットが大きくなるにつれて、巨大なテーブルの管理とクエリが大きな課題となることがあります。パフォーマンスが低下し、メンテナンス作業が煩雑になり、システム全体の効率が損なわれます。PostgreSQL 10で、これらの問題に対処するためのネイティブなソリューションとして宣言的パーティショニングが導入され、特にPostgreSQL 14以降でその機能は成熟し続けています。
宣言的パーティショニングにより、大きなテーブルをパーティションと呼ばれるより小さく管理しやすい断片に分割できます。この戦略は、データベースが関連するパーティションのみをスキャンできるようにすることでクエリパフォーマンスを向上させるだけでなく、データのアーカイブ、削除、インデックス管理などのメンテナンス操作を簡素化します。本記事では、PostgreSQLにおける宣言的パーティショニングのコアコンセプトの理解、さまざまなタイプの探求、そしてデータベースを最適化するための実装方法に関する実用的な例を解説します。
宣言的テーブルパーティショニングとは?
宣言的パーティショニングとは、定義された一連のルールに基づいて、単一の論理テーブル(親テーブルまたはパーティション化されたテーブル)を複数の物理テーブル(子テーブルまたはパーティションテーブル)に分割できるようにするデータベース機能です。各パーティションは、親テーブルからのデータのサブセットを保持します。パーティショニングキーによって、行がどのパーティションに属するかが決定されます。
宣言的パーティショニングの主な利点は以下のとおりです。
- クエリパフォーマンスの向上: パーティショニングキーでフィルタリングするクエリは、PostgreSQLが関連データを含まないパーティションを剪定(除外)できるため、大幅に高速化されます。このプロセスはパーティション剪定と呼ばれます。
- データ管理の容易化: 古いデータの削除やアーカイブなどの操作は、単一の大きなテーブルに対して大規模な
DELETE操作を実行するのではなく、個々のパーティションを切断または削除することで、はるかに効率的に実行できます。 - メンテナンスの簡素化: インデックス作成やVACUUM処理をパーティションごとに行えるため、テーブル全体への影響を軽減できます。
- 可用性の向上: 個々のパーティションに対するメンテナンスは、テーブル全体への影響を最小限に抑えながら実行できることがよくあります。
宣言的パーティショニングのタイプ
PostgreSQLは、データ分布パターンに応じてそれぞれに適した、いくつかの宣言的パーティショニング方法をサポートしています。
1. 範囲パーティショニング (Range Partitioning)
範囲パーティショニングは、特定の列(例:日付、数値)の連続した値の範囲に基づいてデータを分割します。
ユースケース: ログ、イベントデータ、売上記録など、特定の期間や数値範囲内で頻繁にクエリを実行する時系列データに最適です。
例: sale_date列に基づいてsalesテーブルをパーティショニングする。
範囲パーティション化されたテーブルの作成
まず、パーティショニング方法とキーを指定して、親テーブルを作成します。
CREATE TABLE sales (
sale_id SERIAL,
product_name VARCHAR(100),
sale_amount NUMERIC(10, 2),
sale_date DATE NOT NULL
)
PARTITION BY RANGE (sale_date);
次に、個々のパーティションを作成します。各パーティションは、それが含む範囲を指定するFOR VALUES句で定義されます。
-- 2023年1月の売上用パーティション
CREATE TABLE sales_2023_01
PARTITION OF sales ()
FOR VALUES FROM ('2023-01-01') TO ('2023-01-31');
-- 2023年2月の売上用パーティション
CREATE TABLE sales_2023_02
PARTITION OF sales ()
FOR VALUES FROM ('2023-02-01') TO ('2023-02-28');
-- 2023年3月の売上用パーティション
CREATE TABLE sales_2023_03
PARTITION OF sales ()
FOR VALUES FROM ('2023-03-01') TO ('2023-03-31');
ヒント: 範囲を定義する際は、それらが連続しており、すべての可能な値をカバーしていることを確認してください。範囲が重複しないようにします。TOの値は排他的(含まれない)です。
2. リストパーティショニング (List Partitioning)
リストパーティショニングは、列内の離散的な値のリストに基づいてデータを分割します。
ユースケース: 地域、ステータスコード、製品カテゴリなど、固定された既知の値のセットを持つ列に適しています。
例: region列に基づいてordersテーブルをパーティショニングする。
リストパーティション化されたテーブルの作成
PARTITION BY LISTを指定して親テーブルを定義します。
CREATE TABLE orders (
order_id SERIAL,
customer_name VARCHAR(100),
order_total NUMERIC(10, 2),
region VARCHAR(50) NOT NULL
)
PARTITION BY LIST (region);
特定の地域に対してパーティションを作成します。
-- 'North America'の注文用パーティション
CREATE TABLE orders_north_america
PARTITION OF orders ()
FOR VALUES IN ('North America');
-- 'Europe'の注文用パーティション
CREATE TABLE orders_europe
PARTITION OF orders ()
FOR VALUES IN ('Europe');
-- 'Asia'の注文用パーティション
CREATE TABLE orders_asia
PARTITION OF orders ()
FOR VALUES IN ('Asia');
重要: INリストに一致するregionの値で挿入しようとした値があり、かつDEFAULTパーティションが存在しない場合、挿入は失敗します。他のすべての値をキャッチするためにDEFAULTパーティションを作成できます。
デフォルトパーティションの作成
-- 明示的にリストされていないすべての地域のためのデフォルトパーティション
CREATE TABLE orders_other
PARTITION OF orders ()
DEFAULT;
3. ハッシュパーティショニング (Hash Partitioning)
ハッシュパーティショニングは、パーティショニングキーのハッシュ値に基づいて、指定された数のパーティションにデータを分散させます。
ユースケース: 大量のデータがあり、明確な範囲やリストベースの分布なしにパーティション全体に均等に分散させたい場合に役立ちます。負荷分散に適しています。
例: user_idに基づいてusersテーブルをパーティショニングする。
ハッシュパーティション化されたテーブルの作成
PARTITION BY HASHを指定し、パーティション数を指定して親テーブルを定義します。
CREATE TABLE users (
user_id BIGSERIAL,
username VARCHAR(50) NOT NULL,
email VARCHAR(100)
)
PARTITION BY HASH (user_id);
パーティションを明示的に指定しない場合、PostgreSQLは自動的にパーティションを作成しますが、特にパーティションの数と名前を制御したい場合は、明示的に作成することが推奨されます。
明示的なハッシュパーティションの作成
-- 4つのハッシュパーティションを作成
CREATE TABLE users_p0
PARTITION OF users
FOR VALUES WITH (modulus 4, remainder 0);
CREATE TABLE users_p1
PARTITION OF users
FOR VALUES WITH (modulus 4, remainder 1);
CREATE TABLE users_p2
PARTITION OF users
FOR VALUES WITH (modulus 4, remainder 2);
CREATE TABLE users_p3
PARTITION OF users
FOR VALUES WITH (modulus 4, remainder 3);
注: ハッシュパーティショニングを使用する場合、modulus(パーティションの総数)とremainder(このパーティションがどの番号であるか)を指定する必要があります。
宣言的パーティショニングの実装:ベストプラクティス
- 適切なパーティショニングキーの選択: パーティショニングキーは、最も頻繁に行うクエリフィルタおよびデータ管理操作と一致させるべきです。適切なキーはパフォーマンスを大幅に向上させます。
- パーティション数の考慮: パーティションが少なすぎると十分なメリットが得られない可能性があり、多すぎるとオーバーヘッドが増加する可能性があります。管理性とパフォーマンスのバランスを取る数を目指します。範囲パーティショニングについては、データ成長率と保持ポリシーを考慮してください。
- 自動化のための
pg_partmanの使用: 特に時系列データの場合、範囲パーティショニングについては、pg_partmanのような拡張機能を検討してください。新しいパーティションの作成と古いパーティションのアーカイブ/削除を自動化し、手動での労力を大幅に削減します。 - 戦略的なインデックス作成: 子テーブルのインデックスは独立しています。必要に応じて個々のパーティションにインデックスを作成できます。効率的な剪定のために、パーティショニングキーにインデックスを作成することを検討してください。
- パーティション剪定の活用:
WHERE句にパーティショニングキーを含めることで、クエリがパーティション剪定を活用するように記述されていることを確認してください。EXPLAINコマンドで剪定が発生しているかを確認できます。 DEFAULTパーティション: リストパーティショニングの場合、予期しない新しい値が挿入された場合のエラーを防ぐために、DEFAULTパーティションが不可欠です。- データ型: パーティショニングキーのデータ型が、親テーブルと子テーブル間で適切で一貫していることを確認してください。
パーティションの管理
パーティションの接続と切断
パーティションはCREATE TABLE ... PARTITION OF ...を通じて直接作成されますが、既存のテーブルを切断したり接続したりすることもできます。これは、データの移行や大規模なデータセットの管理に役立ちます。
パーティションの切断: パーティションを切断するには、まずそれを通常のテーブルにする必要がありますが、最近のPostgreSQLバージョンでは直接切断できます。
-- sales_2023_01パーティションを切断
ALTER TABLE sales DETACH PARTITION sales_2023_01;
テーブルをパーティションとして接続: スキーマが親テーブルと一致する通常のテーブルを新しいパーティションとして接続できます。
-- 'old_sales_data'が'sales'と同じスキーマを持つ通常のテーブルであると仮定します
CREATE TABLE sales_2022_12
PARTITION OF sales ()
FOR VALUES FROM ('2022-12-01') TO ('2022-12-31');
-- 既存のテーブルを新しいパーティションスロットに接続します
ALTER TABLE sales ATTACH PARTITION sales_2022_12
FOR VALUES FROM ('2022-12-01') TO ('2022-12-31');
-- 事前に作成されたテーブルがあった場合、まずパーティションにする必要があります:
-- CREATE TABLE sales_2022_12 (LIKE sales INCLUDING ALL);
-- ... sales_2022_12 にデータを投入 ...
-- ALTER TABLE sales ATTACH PARTITION sales_2022_12 FOR VALUES FROM ('2022-12-01') TO ('2022-12-31');
パーティションの削除
パーティションの削除は、パーティションテーブルのみを削除し(明示的に指定しない限り)その内部のデータを削除しないため、高速な操作です。これはDELETEよりもはるかに高速です。
-- パーティションを削除するには、子テーブルを単純に削除します
DROP TABLE sales_2023_01;
例:パーティション剪定によるクエリパフォーマンスの改善
前述のようにsale_dateでパーティショニングされたsalesテーブルを考えます。
パーティション剪定なしのクエリ(パーティション化されていないテーブルでの仮定のクエリ):
SELECT SUM(sale_amount)
FROM sales
WHERE sale_date >= '2023-01-15' AND sale_date < '2023-01-20';
もしsalesが巨大な非パーティション化されたテーブルであった場合、このクエリはテーブル全体をスキャンします。しかし、宣言的パーティショニングを使用すると:
-- このクエリは sales_2023_01 パーティションのみをスキャンします
SELECT SUM(sale_amount)
FROM sales
WHERE sale_date >= '2023-01-15' AND sale_date < '2023-01-20';
PostgreSQLのクエリオプティマイザは、sale_dateがパーティショニングキーであり、指定された範囲が完全にsales_2023_01パーティション内に収まっていることを認識します。したがって、そのパーティションのみをスキャンするため、I/Oが劇的に減少し、パフォーマンスが向上します。
これを検証するには、EXPLAINを使用します。
EXPLAIN SELECT SUM(sale_amount) FROM sales WHERE sale_date >= '2023-01-15' AND sale_date < '2023-01-20';
出力にはPartitionPruneステップが表示され、無関係なパーティションが除外されたことを示します。
結論
PostgreSQL 14以降の宣言的パーティショニングは、大規模なデータセットの管理と最適化のための強力な機能です。範囲、リスト、またはハッシュ戦略に基づいてテーブルをインテリジェントに分割することにより、クエリパフォーマンス、データ管理効率、および全体的なデータベースの保守性において大幅な改善を達成できます。利用可能なパーティショニングのタイプを理解し、実装時にベストプラクティスを適用することが、アプリケーションでこの機能の真の可能性を引き出す鍵となります。