問題:
データベースに時間関連のデータがあり、ユーザーが効率的に検索できるようにそのデータを整理、構造化、およびインデックス付けするのに苦労しています。単純なデータベース クエリでさえ、許容範囲を超える時間がかかります。
プロジェクトのコンテキスト:
これは純粋なデータベースの質問ですが、いくつかのコンテキストがデータ モデルを理解するのに役立つ場合があります。
このプロジェクトは、大きくて複雑な機械の研究を中心にしています。私は機械自体についてあまり知りませんが、実験室での噂によると、フラックスコンデンサがどこかにあるということです - そして、昨日、シュレーディンガーの猫の尻尾が横にぶら下がっているのを見つけたと思います;-)
機械の稼働中に、さまざまな測定ポイント (いわゆるスポット) で機械全体に配置されたセンサーを使用して、一定期間にわたって一定の間隔で多くのさまざまなパラメーターを測定します。これらのパラメータを測定するために1 つのデバイスだけでなく、それらの全範囲を使用します。それらは測定データの品質が異なります(これには、サンプルレート、センサーの品質、価格、および私が気にしない他の多くの側面が含まれると思います)。このプロジェクトの実際の目的の 1 つは、これらのデバイス間の比較を確立することです。これらの測定デバイスは、それぞれがマシンに接続された多数のケーブルを持ち、それぞれが測定データを提供する一連のラボ トロリーとして視覚化できます。
データモデル:
すべてのスポットとすべてのデバイスから、すべてのパラメーターの測定データがあります (たとえば、6 日間にわたって 1 分に 1 回)。私の仕事は、そのデータをデータベースに保存し、効率的にアクセスできるようにすることです。
手短に:
- デバイスには一意の名前があります
- パラメータにも名前があります。ただし、それらは一意ではないため、ID も持っています
- スポットにはIDがあります
もちろん、プロジェクト データベースはもっと複雑ですが、これらの詳細は問題とは関係がないようです。
- 測定データインデックスには、ID、測定が行われたときのタイム スタンプ、および測定が実行されたデバイスとスポットへの参照があります。
- 測定データ値には、パラメータと実際に測定された値への参照があります
最初に、測定データ値をモデル化して、独自の ID を主キーとして設定しました。n:m
測定データのインデックスと値の関係は、 index:value
ID ペアのみを格納する別のテーブルでしたが、そのテーブル自体がかなりのハード ドライブ領域を消費するため、それを削除し、値の ID を、データの ID を格納する単純な整数に変更しました。属する測定データ インデックス。測定データ値の主キーは、その ID とパラメーター ID で構成されます。
余談ですが、データ モデルを作成するときは、3NF や適切なテーブル制約 (一意のキーなど) などの一般的な設計ガイドラインに注意深く従っていました。もう 1 つの経験則は、すべての外部キーに対してインデックスを作成することでした。「厳密な」3NF からの測定データ インデックス/値テーブルの偏差が、現在調べているパフォーマンスの問題の理由の 1 つかもしれないという疑いがありますが、データ モデルを元に戻しても問題は解決しませんでした。
DDL のデータ モデル:
注:このコードはさらに下に更新されています。
以下のスクリプトは、データベースと関連するすべてのテーブルを作成します。明示的なインデックスはまだないことに注意してください。これを実行する前に、貴重なデータで呼び出されたデータベースをまだ持っていないことを確認してくださいso_test
...
\c postgres
DROP DATABASE IF EXISTS so_test;
CREATE DATABASE so_test;
\c so_test
CREATE TABLE device
(
name VARCHAR(16) NOT NULL,
CONSTRAINT device_pk PRIMARY KEY (name)
);
CREATE TABLE parameter
(
-- must have ID as names are not unique
id SERIAL,
name VARCHAR(64) NOT NULL,
CONSTRAINT parameter_pk PRIMARY KEY (id)
);
CREATE TABLE spot
(
id SERIAL,
CONSTRAINT spot_pk PRIMARY KEY (id)
);
CREATE TABLE measurement_data_index
(
id SERIAL,
fk_device_name VARCHAR(16) NOT NULL,
fk_spot_id INTEGER NOT NULL,
t_stamp TIMESTAMP NOT NULL,
CONSTRAINT measurement_pk PRIMARY KEY (id),
CONSTRAINT measurement_data_index_fk_2_device FOREIGN KEY (fk_device_name)
REFERENCES device (name) MATCH FULL
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT measurement_data_index_fk_2_spot FOREIGN KEY (fk_spot_id)
REFERENCES spot (id) MATCH FULL
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT measurement_data_index_uk_all_cols UNIQUE (fk_device_name, fk_spot_id, t_stamp)
);
CREATE TABLE measurement_data_value
(
id INTEGER NOT NULL,
fk_parameter_id INTEGER NOT NULL,
value VARCHAR(16) NOT NULL,
CONSTRAINT measurement_data_value_pk PRIMARY KEY (id, fk_parameter_id),
CONSTRAINT measurement_data_value_fk_2_parameter FOREIGN KEY (fk_parameter_id)
REFERENCES parameter (id) MATCH FULL
ON UPDATE NO ACTION ON DELETE NO ACTION
);
テーブルにテスト データを入力するスクリプトも作成しました。
CREATE OR REPLACE FUNCTION insert_data()
RETURNS VOID
LANGUAGE plpgsql
AS
$BODY$
DECLARE
t_stamp TIMESTAMP := '2012-01-01 00:00:00';
index_id INTEGER;
param_id INTEGER;
dev_name VARCHAR(16);
value VARCHAR(16);
BEGIN
FOR dev IN 1..5
LOOP
INSERT INTO device (name) VALUES ('dev_' || to_char(dev, 'FM00'));
END LOOP;
FOR param IN 1..20
LOOP
INSERT INTO parameter (name) VALUES ('param_' || to_char(param, 'FM00'));
END LOOP;
FOR spot IN 1..10
LOOP
INSERT INTO spot (id) VALUES (spot);
END LOOP;
WHILE t_stamp < '2012-01-07 00:00:00'
LOOP
FOR dev IN 1..5
LOOP
dev_name := 'dev_' || to_char(dev, 'FM00');
FOR spot IN 1..10
LOOP
INSERT INTO measurement_data_index
(fk_device_name, fk_spot_id, t_stamp)
VALUES (dev_name, spot, t_stamp) RETURNING id INTO index_id;
FOR param IN 1..20
LOOP
SELECT id INTO param_id FROM parameter
WHERE name = 'param_' || to_char(param, 'FM00');
value := 'd' || to_char(dev, 'FM00')
|| '_s' || to_char(spot, 'FM00')
|| '_p' || to_char(param, 'FM00');
INSERT INTO measurement_data_value (id, fk_parameter_id, value)
VALUES (index_id, param_id, value);
END LOOP;
END LOOP;
END LOOP;
t_stamp := t_stamp + '1 minute'::INTERVAL;
END LOOP;
END;
$BODY$;
SELECT insert_data();
PostgreSQL クエリ プランナーは最新の統計を必要とするため、すべてのテーブルを分析します。バキューム処理は必要ないかもしれませんが、とにかく実行してください:
VACUUM ANALYZE device;
VACUUM ANALYZE measurement_data_index;
VACUUM ANALYZE measurement_data_value;
VACUUM ANALYZE parameter;
VACUUM ANALYZE spot;
サンプルクエリ:
たとえば、特定のパラメーターのすべての値を取得するために非常に単純なクエリを実行すると、データベースはまだそれほど大きくありませんが、すでに数秒かかります。
EXPLAIN (ANALYZE ON, BUFFERS ON)
SELECT measurement_data_value.value
FROM measurement_data_value, parameter
WHERE measurement_data_value.fk_parameter_id = parameter.id
AND parameter.name = 'param_01';
私の開発マシンでの結果の例 (私の環境の詳細については、以下を参照してください):
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------
Hash Join (cost=1.26..178153.26 rows=432000 width=12) (actual time=0.046..2281.281 rows=432000 loops=1)
Hash Cond: (measurement_data_value.fk_parameter_id = parameter.id)
Buffers: shared hit=55035
-> Seq Scan on measurement_data_value (cost=0.00..141432.00 rows=8640000 width=16) (actual time=0.004..963.999 rows=8640000 loops=1)
Buffers: shared hit=55032
-> Hash (cost=1.25..1.25 rows=1 width=4) (actual time=0.010..0.010 rows=1 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 1kB
Buffers: shared hit=1
-> Seq Scan on parameter (cost=0.00..1.25 rows=1 width=4) (actual time=0.004..0.008 rows=1 loops=1)
Filter: ((name)::text = 'param_01'::text)
Buffers: shared hit=1
Total runtime: 2313.615 ms
(12 rows)
データベースには暗黙的なインデックス以外のインデックスはありません。そのため、プランナーがシーケンシャル スキャンのみを実行することは驚くべきことではありません。経験則と思われるものに従って、次のようにすべての外部キーにbtreeインデックスを追加すると
CREATE INDEX measurement_data_index_idx_fk_device_name
ON measurement_data_index (fk_device_name);
CREATE INDEX measurement_data_index_idx_fk_spot_id
ON measurement_data_index (fk_spot_id);
CREATE INDEX measurement_data_value_idx_fk_parameter_id
ON measurement_data_value (fk_parameter_id);
次に、別のバキューム分析を行い (念のため)、クエリを再実行します。プランナーはビットマップ ヒープとビットマップ インデックス スキャンを使用し、合計クエリ時間はいくらか改善されます。
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop (cost=8089.19..72842.42 rows=431999 width=12) (actual time=66.773..1336.517 rows=432000 loops=1)
Buffers: shared hit=55033 read=1184
-> Seq Scan on parameter (cost=0.00..1.25 rows=1 width=4) (actual time=0.005..0.012 rows=1 loops=1)
Filter: ((name)::text = 'param_01'::text)
Buffers: shared hit=1
-> Bitmap Heap Scan on measurement_data_value (cost=8089.19..67441.18 rows=431999 width=16) (actual time=66.762..1237.488 rows=432000 loops=1)
Recheck Cond: (fk_parameter_id = parameter.id)
Buffers: shared hit=55032 read=1184
-> Bitmap Index Scan on measurement_data_value_idx_fk_parameter_id (cost=0.00..7981.19 rows=431999 width=0) (actual time=65.222..65.222 rows=432000 loops=1)
Index Cond: (fk_parameter_id = parameter.id)
Buffers: shared read=1184
Total runtime: 1371.716 ms
(12 rows)
ただし、非常に単純なクエリの実行時間は 1 秒以上です。
私がこれまでに行ったこと:
- PostgreSQL 9.0 High Performanceのコピーを手に入れました- 素晴らしい本です!
- 基本的なPostgreSQLサーバー構成をいくつか行いました。以下の環境を参照してください
- プロジェクトからの実際のクエリを使用して一連のパフォーマンス テストを実行し、結果をグラフィカルに表示するためのフレームワークを作成しました。これらのクエリは、デバイス、スポット、パラメーター、および時間間隔を入力パラメーターとして使用し、一連のテストは、たとえば 5、10 デバイス、5、10 スポット、5、10、15、20 パラメーター、および 1..7 日間にわたって実行されます。基本的な結果は、それらはすべて遅すぎるということですが、それらのクエリ プランは複雑すぎて理解できませんでした。そのため、上記で使用した非常に単純なクエリに戻りました。
値テーブルのパーティション分割を検討しました。データは時間に関連しており、パーティショニングはその種のデータを整理する適切な手段と思われます。PostgreSQL ドキュメントの例でさえ、似たようなものを使用しています。ただし、同じ記事を読みました:
この利点は通常、テーブルが非常に大きくなる場合にのみ価値があります。どの時点でテーブルがパーティショニングの恩恵を受けるかは、アプリケーションによって異なりますが、経験則では、テーブルのサイズはデータベース サーバーの物理メモリを超える必要があります。
テスト データベース全体のサイズは 1 GB 未満であり、8 GB の RAM を搭載した開発マシンと 1 GB の仮想マシン (以下の環境も参照) でテストを実行しているため、テーブルが非常に大きくなったり、それを超えたりすることはありません。物理メモリ。いずれにせよパーティショニングを実装するかもしれませんが、このアプローチはパフォーマンスの問題自体を対象としていないと感じています。
さらに、値テーブルをクラスター化することを検討しています。新しいデータが挿入されるたびにクラスタリングをやり直す必要があり、さらに排他的な読み取り/書き込みロックが必要であるという事実は嫌いですが、このSO の質問を見ると、とにかく利点があり、オプションになる可能性があるようです。ただし、クラスタリングはインデックスに対して行われ、クエリには最大 4 つの選択基準 (デバイス、スポット、パラメーター、および時間) が含まれるため、それらすべてに対してクラスターを作成する必要があります。私は単に正しいインデックスを作成していません...
私の環境:
- 開発は、デュアルコア CPU と 8GB の RAM を搭載した MacBook Pro (mid-2009) で行われています。
- MBP でホストされている 1 GB の RAM を搭載した仮想 Debian 6.0 マシンでデータベース パフォーマンス テストを実行しています。
- PostgreSQL のバージョンは 9.1 です。これがインストール時の最新バージョンだったので、9.2 へのアップグレードが可能です。
- PostgreSQL ドキュメント
shared_buffers
で推奨されているように、両方のマシンでデフォルトの 1600kB から 25% の RAM に変更しました(これには、SHMALL、SHMMAX などのカーネル設定の拡大が含まれます)。 - 同様に、effective_cache_sizeをデフォルトの 128MB から使用可能な RAM の 50% に変更しました。
- さまざまなwork_mem設定でパフォーマンス テストを実行しましたが、パフォーマンスに大きな違いは見られませんでした
注:私が重要だと考える側面の 1 つは、プロジェクトからの実際のクエリを使用した一連のパフォーマンス テストでは、8 GB の MacBook と 1 GB の仮想マシンの間でパフォーマンスの点で違いがないことです。つまり、クエリが MacBook で 10 秒かかる場合、VM でも 10 秒かかります。shared_buffers
また、変更前effective_cache_size
と変更後に同じパフォーマンス テストを実行しましたがwork_mem
、構成の変更によってパフォーマンスが 10% 以上向上することはありませんでした。一部の結果は実際にはさらに悪化しているため、違いは構成の変更ではなく、テストの変動によって引き起こされているようです。これらの観察から、RAM とpostgres.conf
設定はまだここでの制限要因ではないと私は信じています。
私の質問:
異なるインデックスまたは追加のインデックスがクエリを高速化するかどうか、もしそうなら、どのインデックスを作成するかはわかりません。データベースのサイズとクエリの単純さを見ると、データ モデルまたはこれまでのインデックスの選択方法に根本的な問題があるように感じます。
クエリのパフォーマンスを向上させるために、時間関連の構造とインデックスを作成する方法について誰かアドバイスがありますか?
より広く質問されたのは、クエリ パフォーマンスのチューニングです
- 通常、「インシデントベース」で実行されます。つまり、クエリが十分に機能しない場合は? すべてのクエリが遅すぎるようです...
- 主に、クエリプランを見て(そして理解して)、インデックスを追加して、状況が改善したかどうかを測定し、経験を適用してプロセスを加速するという問題ですか?
このデータベースを飛ばすにはどうすればよいですか?
更新 01:
ここまでの回答を見ると、計測データの指標・値表の必要性をきちんと説明できていなかったように思いますので、もう一度おさらいさせてください。ここで問題になるのが収納スペースです。
ノート:
- ここで使用されている数値は、説明を目的としたものであり、比較のみを目的としています。つまり、数値自体は関係ありません。重要なのは、単一のテーブルを使用する場合と、インデックスと値のテーブルを使用する場合のストレージ要件の割合の違いです。
- PostgreSQL のデータ型のストレージ サイズは、この章で説明されています
- これは科学的に正しいと主張するものではありません。たとえば、単位はおそらく数学的な偽物です。ただし、数値は合計されるはずです
仮定
- 1日測定
- 1 分あたり 1 セットの測定
- 10 台のデバイス
- 10 パラメータ
- 10箇所
これは合計すると
1 測定/分 x 60 分/時間 x 24 時間/日 = 1440 測定/日
各測定には、すべてのパラメータのすべてのスポットおよびすべてのデバイスからのデータがあるため、
10 スポット x 10 デバイス x 10 パラメータ = 1000 データセット/測定
だから合計で
1440 測定/日 x 1000 データ セット/測定 = 1 440 000 データ セット/日
Catcallが提案したように、すべての測定値を単一のテーブルに保存すると、たとえば
CREATE TABLE measurement_data
(
device_name character varying(16) NOT NULL,
spot_id integer NOT NULL,
parameter_id integer NOT NULL,
t_stamp timestamp without time zone NOT NULL,
value character varying(16) NOT NULL,
-- constraints...
);
1行の合計は
17 + 4 + 4 + 8 + 17 = 50 バイト/行
最悪の場合、すべての varchar フィールドが完全に埋められます。これは、
50 バイト/行 x 1 440,000 行/日 = 72,000,000 バイト/日
または 1 日あたり最大 69 MB。
これは大したことではないように思えますが、実際のデータベースに必要なストレージ容量は非常に大きくなります (ここで使用されている数値は説明のためだけのものです)。したがって、質問の前半で説明したように、測定データをインデックスと値テーブルに分割しました。
CREATE TABLE measurement_data_index
(
id SERIAL,
fk_device_name VARCHAR(16) NOT NULL,
fk_spot_id INTEGER NOT NULL,
t_stamp TIMESTAMP NOT NULL,
-- constraints...
);
CREATE TABLE measurement_data_value
(
id INTEGER NOT NULL,
fk_parameter_id INTEGER NOT NULL,
value VARCHAR(16) NOT NULL,
-- constraints...
);
ここで、値行の ID は、それが属するインデックスの ID と同じです。
インデックス テーブルと値テーブルの行のサイズは次のとおりです。
インデックス: 4 + 17 + 4 + 8 = 33 バイト 値: 4 + 4 + 17 = 25 バイト
(繰り返しますが、最悪のシナリオです)。行の合計量は
インデックス: 10 デバイス x 10 スポット x 1440 測定/日 = 144,000 行/日 値: 10 個のパラメーター x 144,000 行/日 = 1,440,000 行/日
だから合計は
インデックス: 33 バイト/行 x 144,000 行/日 = 4,752,000 バイト/日 値: 25 バイト/行 x 1 440,000 行/日 = 36,000,000 バイト/日 合計: = 40 752 000 バイト/日
または 1 日あたり最大 39 MB - 単一のテーブル ソリューションの最大 69 MB とは対照的です。
更新 02 (re: wildplassers の応答):
この質問はそのままではかなり長くなるので、上記の元の質問のコードを更新することを検討していましたが、違いをよりよく確認するために、最初の解決策と改善された解決策の両方をここに含めると役立つと思います。
元のアプローチと比較した変更点 (重要度の高い順):
- タイムスタンプとパラメーターを交換します。つまり、テーブルから
t_stamp
フィールドを移動し、フィールドを値からインデックス テーブルに移動します。この変更により、インデックス テーブルのすべてのフィールドは定数になり、新しい測定データは値テーブルのみに書き込まれます。これにより、クエリのパフォーマンスが大幅に向上するとは思っていませんでしたが (私は間違っていました)、測定データ インデックスの概念がより明確になったと感じています。わずかに多くの記憶域スペースが必要ですが (大雑把な見積もりによると)、「静的な」インデックス テーブルを使用すると、読み取り/書き込み要件に従ってテーブルスペースを別のハード ドライブに移動する場合の展開にも役立つ可能性があります。measurement_data_index
measurement_data_value
fk_parameter_id
- デバイステーブルで代理キーを使用する: 私が理解していることから、代理キーは、データベース設計の観点から厳密には必要とされない主キーです (たとえば、デバイス名はすでに一意であるため、PK としても機能する可能性があります)。クエリのパフォーマンスを向上させるのに役立つ場合があります。これを追加したのは、インデックス テーブルが (名前と ID の代わりに) ID のみを参照すると、概念がより明確になると思うからです。
- rewrite
insert_data()
:generate_series()
ネストされたFOR
ループの代わりに使用します。コードをより「機敏」にします。 - これらの変更の副作用として、テスト データの挿入にかかる時間は、最初のソリューションで必要な時間の約 50% に過ぎません。
- wildplasser が提案したように、ビューを追加しませんでした。下位互換性は必要ありません。
- インデックス テーブル内の FK の追加のインデックスは、クエリ プランナーによって無視されるようであり、クエリ プランやパフォーマンスには影響しません。
(この行がないと、以下のコードはSOページのコードとして正しく表示されません...)
\c postgres
DROP DATABASE IF EXISTS so_test_03;
CREATE DATABASE so_test_03;
\c so_test_03
CREATE TABLE device
(
id SERIAL,
name VARCHAR(16) NOT NULL,
CONSTRAINT device_pk PRIMARY KEY (id),
CONSTRAINT device_uk_name UNIQUE (name)
);
CREATE TABLE parameter
(
id SERIAL,
name VARCHAR(64) NOT NULL,
CONSTRAINT parameter_pk PRIMARY KEY (id)
);
CREATE TABLE spot
(
id SERIAL,
name VARCHAR(16) NOT NULL,
CONSTRAINT spot_pk PRIMARY KEY (id)
);
CREATE TABLE measurement_data_index
(
id SERIAL,
fk_device_id INTEGER NOT NULL,
fk_parameter_id INTEGER NOT NULL,
fk_spot_id INTEGER NOT NULL,
CONSTRAINT measurement_pk PRIMARY KEY (id),
CONSTRAINT measurement_data_index_fk_2_device FOREIGN KEY (fk_device_id)
REFERENCES device (id) MATCH FULL
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT measurement_data_index_fk_2_parameter FOREIGN KEY (fk_parameter_id)
REFERENCES parameter (id) MATCH FULL
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT measurement_data_index_fk_2_spot FOREIGN KEY (fk_spot_id)
REFERENCES spot (id) MATCH FULL
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT measurement_data_index_uk_all_cols UNIQUE (fk_device_id, fk_parameter_id, fk_spot_id)
);
CREATE TABLE measurement_data_value
(
id INTEGER NOT NULL,
t_stamp TIMESTAMP NOT NULL,
value VARCHAR(16) NOT NULL,
-- NOTE: inverse field order compared to wildplassers version
CONSTRAINT measurement_data_value_pk PRIMARY KEY (id, t_stamp),
CONSTRAINT measurement_data_value_fk_2_index FOREIGN KEY (id)
REFERENCES measurement_data_index (id) MATCH FULL
ON UPDATE NO ACTION ON DELETE NO ACTION
);
CREATE OR REPLACE FUNCTION insert_data()
RETURNS VOID
LANGUAGE plpgsql
AS
$BODY$
BEGIN
INSERT INTO device (name)
SELECT 'dev_' || to_char(item, 'FM00')
FROM generate_series(1, 5) item;
INSERT INTO parameter (name)
SELECT 'param_' || to_char(item, 'FM00')
FROM generate_series(1, 20) item;
INSERT INTO spot (name)
SELECT 'spot_' || to_char(item, 'FM00')
FROM generate_series(1, 10) item;
INSERT INTO measurement_data_index (fk_device_id, fk_parameter_id, fk_spot_id)
SELECT device.id, parameter.id, spot.id
FROM device, parameter, spot;
INSERT INTO measurement_data_value(id, t_stamp, value)
SELECT index.id,
item,
'd' || to_char(index.fk_device_id, 'FM00') ||
'_s' || to_char(index.fk_spot_id, 'FM00') ||
'_p' || to_char(index.fk_parameter_id, 'FM00')
FROM measurement_data_index index,
generate_series('2012-01-01 00:00:00', '2012-01-06 23:59:59', interval '1 min') item;
END;
$BODY$;
SELECT insert_data();
ある段階で、明示的なs の代わりに inlinePRIMARY KEY
およびREFERENCES
ステートメントを使用するように独自の規則を変更します。CONSTRAINT
今のところ、これをそのままにしておくと、2 つのソリューションを比較しやすくなると思います。
クエリ プランナーの統計を更新することを忘れないでください。
VACUUM ANALYZE device;
VACUUM ANALYZE measurement_data_index;
VACUUM ANALYZE measurement_data_value;
VACUUM ANALYZE parameter;
VACUUM ANALYZE spot;
最初のアプローチと同じ結果を生成するクエリを実行します。
EXPLAIN (ANALYZE ON, BUFFERS ON)
SELECT measurement_data_value.value
FROM measurement_data_index,
measurement_data_value,
parameter
WHERE measurement_data_index.fk_parameter_id = parameter.id
AND measurement_data_index.id = measurement_data_value.id
AND parameter.name = 'param_01';
結果:
Nested Loop (cost=0.00..34218.28 rows=431998 width=12) (actual time=0.026..696.349 rows=432000 loops=1)
Buffers: shared hit=435332
-> Nested Loop (cost=0.00..29.75 rows=50 width=4) (actual time=0.012..0.453 rows=50 loops=1)
Join Filter: (measurement_data_index.fk_parameter_id = parameter.id)
Buffers: shared hit=7
-> Seq Scan on parameter (cost=0.00..1.25 rows=1 width=4) (actual time=0.005..0.010 rows=1 loops=1)
Filter: ((name)::text = 'param_01'::text)
Buffers: shared hit=1
-> Seq Scan on measurement_data_index (cost=0.00..16.00 rows=1000 width=8) (actual time=0.003..0.187 rows=1000 loops=1)
Buffers: shared hit=6
-> Index Scan using measurement_data_value_pk on measurement_data_value (cost=0.00..575.77 rows=8640 width=16) (actual time=0.013..12.157 rows=8640 loops=50)
Index Cond: (id = measurement_data_index.id)
Buffers: shared hit=435325
Total runtime: 726.125 ms
これは、最初のアプローチで必要な約 1.3 秒のほぼ半分です。432K 行を読み込んでいることを考えると、これは今のところ我慢できる結果です。
注:値テーブル PK のフィールドの順序は次のとおりですid, t_stamp
。wildplassers の応答の順序は ですt_stamp, whw_id
。「通常の」フィールドの順序は、フィールドがテーブル宣言にリストされている順序であると感じているためです(「逆」はその逆です)が、それは私が得られないようにする私自身の慣習です混乱している。いずれにせよ、Erwin Brandstetterが指摘したように、この順序はパフォーマンスの向上にとって絶対に重要です。それが間違った方法である場合 (そして、wildplassers ソリューションのような逆インデックスがない場合)、クエリ プランは次のようになり、パフォーマンスは 3 倍以上悪くなります。
Hash Join (cost=22.14..186671.54 rows=431998 width=12) (actual time=0.460..2570.941 rows=432000 loops=1)
Hash Cond: (measurement_data_value.id = measurement_data_index.id)
Buffers: shared hit=63537
-> Seq Scan on measurement_data_value (cost=0.00..149929.58 rows=8639958 width=16) (actual time=0.004..1095.606 rows=8640000 loops=1)
Buffers: shared hit=63530
-> Hash (cost=21.51..21.51 rows=50 width=4) (actual time=0.446..0.446 rows=50 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 2kB
Buffers: shared hit=7
-> Hash Join (cost=1.26..21.51 rows=50 width=4) (actual time=0.015..0.359 rows=50 loops=1)
Hash Cond: (measurement_data_index.fk_parameter_id = parameter.id)
Buffers: shared hit=7
-> Seq Scan on measurement_data_index (cost=0.00..16.00 rows=1000 width=8) (actual time=0.002..0.135 rows=1000 loops=1)
Buffers: shared hit=6
-> Hash (cost=1.25..1.25 rows=1 width=4) (actual time=0.008..0.008 rows=1 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 1kB
Buffers: shared hit=1
-> Seq Scan on parameter (cost=0.00..1.25 rows=1 width=4) (actual time=0.004..0.007 rows=1 loops=1)
Filter: ((name)::text = 'param_01'::text)
Buffers: shared hit=1
Total runtime: 2605.277 ms