データパーティショニングとは何ですか?
非常に大きなテーブルを持つデータベースの場合、データベースのパフォーマンスを向上させ、メンテナンスをはるかに簡単にするために、データベース設計者にとってパーティション分割は素晴らしく巧妙なトリックです。 PostgreSQLデータベースで許可される最大テーブルサイズは32TBですが、将来的にはまだ発明されていないコンピューターで実行されていない限り、その100分の1のスペースしかないテーブルでパフォーマンスの問題が発生する可能性があります。
パーティション分割は、テーブルを複数のテーブルに分割します。通常、テーブルにアクセスするアプリケーションは、必要なデータへのアクセスが高速である以外は、違いに気付かないように行われます。テーブルを複数のテーブルに分割することで、クエリの実行で必要なデータを見つけるためにはるかに小さなテーブルとインデックスをスキャンする必要があるようになります。インデックス戦略の効率に関係なく、50 GBのテーブルのインデックスをスキャンすると、500GBのテーブルのインデックスよりも常にはるかに高速になります。これはテーブルスキャンにも当てはまります。テーブルスキャンが避けられない場合があるからです。
パーティションテーブルをクエリプランナーに導入する場合、クエリプランナー自体について知っておくべきことがいくつかあります。クエリが実際に実行される前に、クエリプランナーはクエリを取得し、データにアクセスする最も効率的な方法を計画します。データを異なるテーブルに分割することで、プランナーは、各テーブルに含まれる内容に基づいて、アクセスするテーブルと完全に無視するテーブルを決定できます。
これは、各テーブルで許可されるデータを定義する制約を分割テーブルに追加することによって行われます。優れた設計により、クエリプランナーにデータ全体ではなくデータの小さなサブセットをスキャンさせることができます。
テーブルを分割する必要がありますか?
パーティショニングは、正しく実行するとテーブルのパフォーマンスを大幅に向上させることができますが、間違って実行したり、不要な場合は、パフォーマンスを低下させたり、使用できなくなったりする可能性があります。
テーブルの大きさはどれくらいですか?
パーティション化がオプションになる前にテーブルをどれだけ大きくする必要があるかについての実際の強硬なルールはありませんが、データベースアクセスの傾向に基づいて、データベースユーザーと管理者は、特定のテーブルが大きくなるにつれてパフォーマンスが低下し始めるのを確認し始めます。一般に、パーティショニングは、誰かが「テーブルが大きすぎるためにXを実行できない」と言った場合にのみ検討する必要があります。一部のホストでは、200 GBがパーティション分割に適した時間である場合もあれば、1TBに達したときにパーティション分割する時間である場合もあります。
テーブルが「大きすぎる」と判断された場合は、アクセスパターンを確認します。データベースにアクセスするアプリケーションを知るか、ログを監視してpgBadgerなどを使用してクエリレポートを生成することで、テーブルへのアクセス方法を確認できます。また、アクセス方法に応じて、適切なパーティション戦略のオプションを選択できます。
pgBadgerとその使用方法の詳細については、pgBadgerに関する以前の記事をご覧ください。
テーブルの膨張は問題ですか?
行を更新および削除すると、タプルがデッドになり、最終的にクリーンアップする必要があります。テーブルをバキュームすることは、手動か自動かにかかわらず、テーブル内のすべての行を調べて、それを再利用するか、そのままにするかを決定します。テーブルが大きいほど、このプロセスにかかる時間が長くなり、使用されるシステムリソースが多くなります。テーブルの90%が不変のデータであっても、バキュームを実行するたびにスキャンする必要があります。テーブルを分割すると、バキュームが必要なテーブルを小さなテーブルに減らし、スキャンする必要のある不変のデータの量を減らし、全体的なバキュームの時間を短縮し、システムメンテナンスではなくユーザーアクセスのために解放されるシステムリソースを増やすことができます。
データはどのように削除されますか?
データがスケジュールに従って削除された場合、たとえば4年以上前のデータが削除されてアーカイブされた場合、実行に時間がかかる可能性のある大ヒットの削除ステートメントが発生し、前述のように、バキュームする必要のあるデッド行が作成される可能性があります。適切なパーティショニング戦略が実装されている場合、後でバキュームを維持する数時間のDELETEステートメントは、バキュームを維持しない古い月次テーブルで1分間のDROPTABLEステートメントに変換できます。
テーブルをどのように分割する必要がありますか?
アクセスパターンのキーは、WHERE句とJOIN条件にあります。クエリがWHERE句とJOIN句の列を指定するときはいつでも、データベースに「これは必要なデータです」と通知します。これらの句をターゲットとするインデックスを設計するのと同じように、パーティション化戦略は、これらの列をターゲットにしてデータを分離し、クエリができるだけ少ないパーティションにアクセスできるようにすることに依存しています。
例:
- where句で常に使用される日付列を持つトランザクションテーブル。
- where句で常に使用される居住国などの場所列を含む顧客テーブル。
通常、データの大部分は履歴情報であり、さまざまな時間グループにまたがってかなり予測可能なデータが存在する可能性があるため、パーティショニングで注目する最も一般的な列は通常、タイムスタンプです。
データスプレッドを決定する
パーティション化する列を特定したら、さまざまな子パーティションにデータをできるだけ均等に分散するパーティションサイズを作成することを目的として、データの分散を確認する必要があります。
severalnines=# SELECT DATE_TRUNC('year', view_date)::DATE, COUNT(*) FROM website_views GROUP BY 1 ORDER BY 1;
date_trunc | count
------------+----------
2013-01-01 | 11625147
2014-01-01 | 20819125
2015-01-01 | 20277739
2016-01-01 | 20584545
2017-01-01 | 20777354
2018-01-01 | 491002
(6 rows)
この例では、タイムスタンプ列を年次テーブルに切り捨てて、年間約2,000万行になります。すべてのクエリで日付または日付範囲が指定されており、指定されたクエリが通常1年以内のデータを対象としている場合、これは1年に1つのテーブルになるため、パーティション分割の優れた開始戦略となる可能性があります。 、テーブルごとの管理可能な行数。
今日のホワイトペーパーをダウンロードするClusterControlを使用したPostgreSQLの管理と自動化PostgreSQLの導入、監視、管理、スケーリングを行うために知っておくべきことについて学ぶホワイトペーパーをダウンロードするパーティションテーブルの作成
パーティション化されたテーブルを作成する方法はいくつかありますが、主に、利用可能な最も機能が豊富なタイプのトリガーベースのパーティション化に焦点を当てます。これを機能させるには、手動セットアップとplpgsql手続き型言語でのコーディングが必要です。
これは、最終的に空になる(または、新しいテーブルの場合は空のままになる)親テーブルと、親テーブルを継承する子テーブルを持つことによって動作します。親テーブルが照会されると、子テーブルにINHERITが適用されているため、子テーブルでもデータが検索されます。ただし、子テーブルには親のデータのサブセットしか含まれていないため、テーブルにCONSTRAINTを追加して、チェックを実行し、データがテーブルで許可されているものと一致することを確認します。これは2つのことを行います。1つは、属していないデータを拒否し、2つ目は、このCHECK CONSTRAINTに一致するデータのみがこのテーブルで許可されることをクエリプランナーに通知するため、テーブルに一致しないデータを検索する場合は、わざわざ検索することもできます。
最後に、データを配置する子テーブルを決定するストアドプロシージャを実行する親テーブルにトリガーを適用します。
テーブルの作成
親テーブルの作成は、他のテーブル作成と同じです。
severalnines=# CREATE TABLE data_log (data_log_sid SERIAL PRIMARY KEY,
date TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(),
event_details VARCHAR);
CREATE TABLE
子テーブルの作成
子テーブルの作成も同様ですが、いくつかの追加が必要です。組織的な理由から、子テーブルは別のスキーマに存在します。子テーブルごとにこれを行い、それに応じて詳細を変更します。
注:nextval()で使用されるシーケンスの名前は、親が作成したシーケンスに由来します。これは、すべての子テーブルが同じシーケンスを使用するために重要です。
severalnines=# CREATE SCHEMA part;
CREATE SCHEMA
severalnines=# CREATE TABLE part.data_log_2018 (data_log_sid integer DEFAULT nextval('public.data_log_data_log_sid_seq'::regclass),
date TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(),
event_details VARCHAR)
INHERITS (public.data_log);
CREATE TABLE
severalnines=# ALTER TABLE ONLY part.data_log_2018
ADD CONSTRAINT data_log_2018_pkey PRIMARY KEY (data_log_sid);
ALTER TABLE
severalnines=# ALTER TABLE part.data_log_2018 ADD CONSTRAINT data_log_2018_date CHECK (date >= '2018-01-01' AND date < '2019-01-01');
ALTER TABLE
関数とトリガーの作成
最後に、ストアドプロシージャを作成し、親テーブルにトリガーを追加します。
severalnines=# CREATE OR REPLACE FUNCTION
public.insert_trigger_table()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
IF NEW.date >= '2018-01-01' AND NEW.date < '2019-01-01' THEN
INSERT INTO part.data_log_2018 VALUES (NEW.*);
RETURN NULL;
ELSIF NEW.date >= '2019-01-01' AND NEW.date < '2020-01-01' THEN
INSERT INTO part.data_log_2019 VALUES (NEW.*);
RETURN NULL;
END IF;
END;
$function$;
CREATE FUNCTION
severalnines=# CREATE TRIGGER insert_trigger BEFORE INSERT ON data_log FOR EACH ROW EXECUTE PROCEDURE insert_trigger_table();
CREATE TRIGGER
テストする
すべて作成されたので、テストしてみましょう。このテストでは、2013年から2020年までの年間表を追加しました。
注:以下の挿入応答は「INSERT0 0」です。これは、何も挿入されていないことを示しています。これについては、この記事の後半で説明します。
severalnines=# INSERT INTO data_log (date, event_details) VALUES ('2018-08-20 15:22:14', 'First insert');
INSERT 0 0
severalnines=# SELECT * FROM data_log WHERE date >= '2018-08-01' AND date < '2018-09-01';
data_log_sid | date | event_details
--------------+----------------------------+---------------
1 | 2018-08-17 23:01:38.324056 | First insert
(1 row)
存在しますが、クエリプランナーを見て、行が正しい子テーブルからのものであり、親テーブルが行をまったく返さないことを確認しましょう。
severalnines=# EXPLAIN ANALYZE SELECT * FROM data_log;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------
Append (cost=0.00..130.12 rows=5813 width=44) (actual time=0.016..0.019 rows=1 loops=1)
-> Seq Scan on data_log (cost=0.00..1.00 rows=1 width=44) (actual time=0.007..0.007 rows=0 loops=1)
-> Seq Scan on data_log_2015 (cost=0.00..21.30 rows=1130 width=44) (actual time=0.001..0.001 rows=0 loops=1)
-> Seq Scan on data_log_2013 (cost=0.00..17.80 rows=780 width=44) (actual time=0.001..0.001 rows=0 loops=1)
-> Seq Scan on data_log_2014 (cost=0.00..17.80 rows=780 width=44) (actual time=0.001..0.001 rows=0 loops=1)
-> Seq Scan on data_log_2016 (cost=0.00..17.80 rows=780 width=44) (actual time=0.001..0.001 rows=0 loops=1)
-> Seq Scan on data_log_2017 (cost=0.00..17.80 rows=780 width=44) (actual time=0.001..0.001 rows=0 loops=1)
-> Seq Scan on data_log_2018 (cost=0.00..1.02 rows=2 width=44) (actual time=0.005..0.005 rows=1 loops=1)
-> Seq Scan on data_log_2019 (cost=0.00..17.80 rows=780 width=44) (actual time=0.001..0.001 rows=0 loops=1)
-> Seq Scan on data_log_2020 (cost=0.00..17.80 rows=780 width=44) (actual time=0.001..0.001 rows=0 loops=1)
Planning time: 0.373 ms
Execution time: 0.069 ms
(12 rows)
朗報です。挿入した1つの行は、それが属する2018年のテーブルに表示されました。ただし、ご覧のとおり、クエリでは日付列を使用してwhere句を指定していないため、すべてをフェッチするために、クエリプランナーと実行はすべてのテーブルに対して順次スキャンを実行しました。
次に、where句を使用してテストしましょう。
severalnines=# EXPLAIN ANALYZE SELECT * FROM data_log WHERE date >= '2018-08-01' AND date < '2018-09-01';
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------
Append (cost=0.00..2.03 rows=2 width=44) (actual time=0.013..0.014 rows=1 loops=1)
-> Seq Scan on data_log (cost=0.00..1.00 rows=1 width=44) (actual time=0.007..0.007 rows=0 loops=1)
Filter: ((date >= '2018-08-01 00:00:00'::timestamp without time zone) AND (date < '2018-09-01 00:00:00'::timestamp without time zone))
-> Seq Scan on data_log_2018 (cost=0.00..1.03 rows=1 width=44) (actual time=0.006..0.006 rows=1 loops=1)
Filter: ((date >= '2018-08-01 00:00:00'::timestamp without time zone) AND (date < '2018-09-01 00:00:00'::timestamp without time zone))
Planning time: 0.591 ms
Execution time: 0.041 ms
(7 rows)
ここでは、クエリプランナーと実行が2018年の親テーブルと子テーブルの2つのテーブルでシーケンシャルスキャンを実行したことがわかります。2013年から2020年までの子テーブルがありますが、where句があるため、2018年以外のテーブルにはアクセスされませんでした。範囲は2018年以内にのみ属します。CHECKCONSTRAINTはデータがこれらのテーブルに存在することは不可能であると見なすため、クエリプランナーは他のすべてのテーブルを除外しました。
厳密なORMツールまたは挿入された行の検証を使用したパーティションの操作
前述のように、作成した例では、行を挿入しても「INSERT00」が返されます。これらのパーティションテーブルにデータを挿入するアプリケーションが、挿入された行が正しいことの確認に依存している場合、これらは失敗します。修正はありますが、パーティションテーブルにさらに複雑なレイヤーが追加されるため、このシナリオがパーティションテーブルを使用するアプリケーションにとって問題にならない場合は、無視できます。
親テーブルの代わりにビューを使用します。
この問題の修正は、親テーブルを照会するビューを作成し、INSERTステートメントをビューに送信することです。ビューに挿入するのはおかしなことに聞こえるかもしれませんが、そこでトリガーが表示されます。
severalnines=# CREATE VIEW data_log_view AS
SELECT data_log.data_log_sid,
data_log.date,
data_log.event_details
FROM data_log;
CREATE VIEW
severalnines=# ALTER VIEW data_log_view ALTER COLUMN data_log_sid SET default nextval('data_log_data_log_sid_seq'::regclass);
ALTER VIEW
このビューのクエリはメインテーブルのクエリと同じように見え、WHERE句とJOINSは期待どおりに機能します。
特定の機能とトリガーを表示
前に定義した関数とトリガーを使用する代わりに、両方ともわずかに異なります。太字の変更。
CREATE OR REPLACE FUNCTION public.insert_trigger_view()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
IF NEW.date >= '2018-01-01' AND NEW.date < '2019-01-01' THEN
INSERT INTO part.data_log_2018 VALUES (NEW.*);
RETURN NEW;
ELSIF NEW.date >= '2019-01-01' AND NEW.date < '2020-01-01' THEN
INSERT INTO part.data_log_2019 VALUES (NEW.*);
RETURN NEW;
END IF;
END;
$function$;
severalnines=# CREATE TRIGGER insert_trigger INSTEAD OF INSERT ON data_log_view FOR EACH ROW EXECUTE PROCEDURE insert_trigger_view();
「INSTEADOF」定義は、ビューの挿入コマンドを引き継ぎ(とにかく機能しません)、代わりに関数を実行します。定義した関数には、子テーブルへの挿入が完了した後に「RETURNNEW;」を実行するという非常に具体的な要件があります。これがないと(または「RETURNNULL」で以前と同じように実行すると)、予想どおり「INSERT01」ではなく「INSERT00」になります。
例:
severalnines=# INSERT INTO data_log_view (date, event_details) VALUES ('2018-08-20 18:12:48', 'First insert on the view');
INSERT 0 1
severalnines=# EXPLAIN ANALYZE SELECT * FROM data_log_view WHERE date >= '2018-08-01' AND date < '2018-09-01';
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------
Append (cost=0.00..2.03 rows=2 width=44) (actual time=0.015..0.017 rows=2 loops=1)
-> Seq Scan on data_log (cost=0.00..1.00 rows=1 width=44) (actual time=0.009..0.009 rows=0 loops=1)
Filter: ((date >= '2018-08-01 00:00:00'::timestamp without time zone) AND (date < '2018-09-01 00:00:00'::timestamp without time zone))
-> Seq Scan on data_log_2018 (cost=0.00..1.03 rows=1 width=44) (actual time=0.006..0.007 rows=2 loops=1)
Filter: ((date >= '2018-08-01 00:00:00'::timestamp without time zone) AND (date < '2018-09-01 00:00:00'::timestamp without time zone))
Planning time: 0.633 ms
Execution time: 0.048 ms
(7 rows)
severalnines=# SELECT * FROM data_log_view WHERE date >= '2018-08-01' AND date < '2018-09-01';
data_log_sid | date | event_details
--------------+---------------------+--------------------------
1 | 2018-08-20 15:22:14 | First insert
2 | 2018-08-20 18:12:48 | First insert on the view
(2 rows)
挿入された「行数」が正しいかどうかをテストするアプリケーションでは、この修正が期待どおりに機能することがわかります。この例では、ビューとストアドプロシージャに_viewを追加しましたが、ユーザーが知らないうちにテーブルをパーティション化したい場合やアプリケーションを変更したい場合は、親テーブルの名前をdata_log_parentに変更し、古いビューを呼び出します。親テーブルの名前。
行の更新とパーティション化された列の値の変更
注意すべき点の1つは、パーティションテーブルのデータを更新し、列の値を制約で許可されていない値に変更すると、エラーが発生することです。このタイプの更新が発生しない場合は無視できますが、可能性がある場合は、古い子パーティションから行を効果的に削除し、新しい子パーティションに新しい行を挿入するUPDATEプロセスの新しいトリガーを作成する必要があります。新しいターゲットの子パーティション。
将来のパーティションの作成
将来のパーティションの作成は、それぞれ長所と短所があるいくつかの異なる方法で行うことができます。
Future Partition Creator
外部プログラムを作成して、必要になるX時間前に将来のパーティションを作成できます。日付でパーティション化されたパーティション化の例では、次に作成する必要のあるパーティション(この場合は2019)を12月のいつか作成するように設定できます。これは、データベース管理者が手動で実行するスクリプトにすることも、必要に応じてcronで実行するように設定することもできます。毎年のパーティションは、1年に1回実行されることを意味しますが、毎日のパーティションが一般的であり、毎日のcronジョブはより幸せなDBAになります。
自動パーティションクリエーター
plpgsqlの機能を使用すると、存在しない子パーティションにデータを挿入しようとした場合にエラーをキャプチャし、その場で必要なパーティションを作成してから、もう一度挿入してみることができます。このオプションは、多くの異なるクライアントが同様のデータを同時に挿入する場合を除いて、1つのクライアントがテーブルを作成し、別のクライアントが同じテーブルを作成しようとして既存のエラーが発生するという競合状態を引き起こす可能性がある場合を除いて、うまく機能します。巧妙で高度なplpgsqlプログラミングでこれを修正できますが、努力する価値があるかどうかは議論の余地があります。挿入パターンが原因でこの競合状態が発生しない場合は、心配する必要はありません。
パーティションの削除
データ保持ルールで一定時間後にデータを削除するように指示されている場合、日付列でパーティション化すると、パーティション化されたテーブルでこれが簡単になります。 10年前のデータを削除する場合は、次のように簡単にできます。
severalnines=# DROP TABLE part.data_log_2007;
DROP TABLE
これは、「DELETE」ステートメントよりもはるかに高速で効率的です。これは、掃除機でクリーンアップするために死んだタプルが発生しないためです。
注:パーティション設定からテーブルを削除する場合は、トリガー関数のコードも変更して、削除されたテーブルに日付を送信しないようにする必要があります。
分割する前に知っておくべきこと
テーブルをパーティション化すると、パフォーマンスが大幅に向上しますが、パフォーマンスが低下する可能性もあります。本番サーバーにプッシュする前に、データの一貫性、パフォーマンスの速度など、すべてについて、パーティショニング戦略を広範囲にテストする必要があります。テーブルのパーティション分割にはいくつかの可動部分があり、問題がないことを確認するためにすべてをテストする必要があります。
パーティションの数を決定する場合は、子テーブルの数を1000テーブル未満に保ち、可能であればさらに少なくすることを強くお勧めします。子テーブルの数が約1000を超えると、クエリプランナー自体がクエリプランを作成するためだけに時間がかかるため、パフォーマンスが低下し始めます。クエリプランに数秒かかることは前例のないことではありませんが、実際の実行には数ミリ秒しかかかりません。 1分間に数千のクエリを処理する場合、数秒でアプリケーションが停止する可能性があります。
plpgsqlトリガーのストアドプロシージャも複雑になる可能性があり、複雑すぎるとパフォーマンスが低下する可能性があります。ストアドプロシージャは、テーブルに挿入された行ごとに1回実行されます。すべての行に対して処理が多すぎると、挿入が遅くなる可能性があります。パフォーマンステストにより、許容範囲内にあることが確認されます。
クリエイティブに
PostgreSQLでのテーブルのパーティション分割は、必要に応じて高度にすることができます。日付列の代わりに、テーブルを「国」列に分割して、国ごとにテーブルを作成することができます。パーティション化は、「日付」列と「国」列の両方など、複数の列で実行できます。これにより、挿入を処理するストアドプロシージャがより複雑になりますが、100%可能です。
パーティション分割の目標は、非常に大きなテーブルを小さなテーブルに分割し、クエリプランナーが大きな元のテーブルよりも速くデータにアクセスできるように、よく考えられた方法で行うことです。
宣言型パーティショニング
PostgreSQL 10以降では、新しいパーティショニング機能「宣言型パーティショニング」が導入されました。パーティションを設定するのは簡単な方法ですが、いくつかの制限があります。制限が許容できる場合は、手動のパーティション設定よりも高速に実行される可能性がありますが、大量のテストでそれを確認できます。
公式のpostgresqlドキュメントには、宣言型パーティショニングとその仕組みに関する情報があります。これはPostgreSQL10の新機能であり、この記事の執筆時点でPostgreSQLのバージョン11が近づいており、一部の制限は修正されていますが、すべてではありません。 PostgreSQLが進化するにつれて、宣言型パーティショニングは、この記事で取り上げたより複雑なパーティショニングの完全な代替になる可能性があります。それまでは、どの制限もパーティショニングのニーズを制限しない場合は、宣言型パーティショニングがより簡単な代替手段になる可能性があります。
宣言型パーティションの制限
PostgreSQLのドキュメントでは、PostgreSQL 10でのこのタイプのパーティショニングに関するすべての制限に対処していますが、公式のPostgreSQL Wikiに概要が記載されており、読みやすい形式で制限がリストされており、どの制限が修正されているかが示されています。今後のPostgreSQL11。
コミュニティに質問する
世界中のデータベース管理者は、長い間、高度なカスタムパーティショニング戦略を設計してきました。私たちの多くは、IRCやメーリングリストに参加しています。最善の戦略を決定するための支援が必要な場合、またはストアドプロシージャのバグを解決するためだけに支援が必要な場合は、コミュニティが支援します。
- IRC
Freenodeには#postgresと呼ばれる非常にアクティブなチャネルがあり、ユーザーはお互いに概念を理解したり、エラーを修正したり、他のリソースを見つけたりすることができます。 - メーリングリスト
PostgreSQLには、参加できるメーリングリストがいくつかあります。より長い形式の質問/問題をここに送信することができ、いつでもIRCよりもはるかに多くの人々に到達することができます。リストはPostgreSQLWebサイトにあり、pgsql-generalまたはpgsql-adminのリストは優れたリソースです。