16

問題:

データベースに時間関連のデータがあり、ユーザーが効率的に検索できるようにそのデータを整理、構造化、およびインデックス付けするのに苦労しています。単純なデータベース クエリでさえ、許容範囲を超える時間がかかります。

プロジェクトのコンテキスト:

これは純粋なデータベースの質問ですが、いくつかのコンテキストがデータ モデルを理解するのに役立つ場合があります。

このプロジェクトは、大きくて複雑な機械の研究を中心にしています。私は機械自体についてあまり知りませんが、実験室での噂によると、フラックスコンデンサがどこかにあるということです - そして、昨日、シュレーディンガーの猫の尻尾が横にぶら下がっているのを見つけたと思います;-)

機械の稼働中に、さまざまな測定ポイント (いわゆるスポット) で機械全体に配置されたセンサーを使用して、一定期間にわたって一定の間隔で多くのさまざまなパラメーターを測定します。これらのパラメータを測定するために1 つのデバイスだけでなく、それらの全範囲を使用します。それらは測定データの品質が異なります(これには、サンプルレート、センサーの品質、価格、および私が気にしない他の多くの側面が含まれると思います)。このプロジェクトの実際の目的の 1 つは、これらのデバイス間の比較を確立することです。これらの測定デバイスは、それぞれがマシンに接続された多数のケーブルを持ち、それぞれが測定データを提供する一連のラボ トロリーとして視覚化できます。

データモデル:

すべてのスポットとすべてのデバイスから、すべてのパラメーターの測定データがあります (たとえば、6 日間にわたって 1 分に 1 回)。私の仕事は、そのデータをデータベースに保存し、効率的にアクセスできるようにすることです。

手短に:

  • デバイスには一意の名前があります
  • パラメータにも名前があります。ただし、それらは一意ではないため、ID も持っています
  • スポットにはIDがあります

もちろん、プロジェクト データベースはもっと複雑ですが、これらの詳細は問題とは関係がないようです。

  • 測定データインデックスには、ID、測定が行われたときのタイム スタンプ、および測定が実行されたデバイスとスポットへの参照があります。
  • 測定データには、パラメータと実際に測定された値への参照があります

最初に、測定データ値をモデル化して、独自の ID を主キーとして設定しました。n:m測定データのインデックスと値の関係は、 index:valueID ペアのみを格納する別のテーブルでしたが、そのテーブル自体がかなりのハード ドライブ領域を消費するため、それを削除し、値の 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_indexmeasurement_data_valuefk_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
4

4 に答える 4

6

基本的にセットアップ全体を修正しました。PostgreSQL 9.1.5 でテスト済み。

DB スキーマ

  • あなたのテーブルレイアウトには大きな論理的欠陥があると思います(@Catcallでも指摘されています)。あるべきだと思われる方法で変更しました:
    最後のテーブルmeasurement_data_value(名前を に変更しました) は、 (now: ) のすべての行に対して (now: ) ごとに値measure_valを保存することになっています。下記参照。parameterparammeasurement_data_indexmeasure

  • 「デバイスには一意の名前があります」が、とにかく整数の代理主キーを使用します。テキスト文字列は本質的に大きく、大きなテーブルで外部キーとして使用するには時間がかかります。これらはcollat​​ionの対象にもなるため、クエリが大幅に遅くなる可能性があります。

    この関連する質問の下で、中規模のtext列での結合と並べ替えが主な速度低下であることがわかりました。テキスト文字列を主キーとして使用することを主張する場合は、PostgreSQL 9.1 以降での照合サポートについてお読みください。

  • id主キーの名前として使用するというアンチパターンに陥らないでください。いくつかのテーブルを結合すると (多くのことをしなければならないように!)、いくつかの列名ができてしまいますid。(残念ながら、一部の ORM はそれを使用しています。)

    代わりに、テーブルにちなんで代理主キー列に名前を付けて、それ自体が意味を持つようにします。次に、それを参照する外部キーに同じ名前を付けることができます(同じデータが含まれているため、これは良いことです)。

    CREATE TABLE spot
    ( spot_id SERIAL PRIMARY KEY);
  • 超長い識別子を使用しないでください。それらはタイプしにくく、読みにくいです。経験則: 明確にするために必要な限り長く、できるだけ短くします。

  • varchar(n)やむを得ない理由がない場合は使用しないでください。を使用するvarcharか、より簡単に: をtext使用します。

これらすべてと、より優れた db スキーマに関する私の提案は次のとおりです。

CREATE TABLE device
( device_id serial PRIMARY KEY 
 ,device text NOT NULL
);

CREATE TABLE param
( param_id serial PRIMARY KEY
 ,param text NOT NULL
);
CREATE INDEX param_param_idx ON param (param); -- you are looking up by name!

CREATE TABLE spot
( spot_id  serial PRIMARY KEY);

CREATE TABLE measure
( measure_id serial PRIMARY KEY
 ,device_id int NOT NULL REFERENCES device (device_id) ON UPDATE CASCADE
 ,spot_id int NOT NULL REFERENCES spot (spot_id) ON UPDATE CASCADE
 ,t_stamp timestamp NOT NULL
 ,CONSTRAINT measure_uni UNIQUE (device_id, spot_id, t_stamp)
);

CREATE TABLE measure_val   -- better name? 
( measure_id int NOT NULL REFERENCES measure (measure_id)
                 ON UPDATE CASCADE ON DELETE CASCADE  -- guessing it fits
 ,param_id int NOT NULL REFERENCES param (param_id)
                 ON UPDATE CASCADE ON DELETE CASCADE  -- guessing it fits
 ,value text NOT NULL
 ,CONSTRAINT measure_val_pk PRIMARY KEY (measure_id, param_id)
);
CREATE INDEX measure_val_param_id_idx ON measure_val (param_id);  -- !crucial!

バルキーの名前を に変更しmeasurement_data_valueましmeasure_valた。それが表にあるものだからです: 測定値のパラメーター値。ここで、複数列の pkも理にかなっています。

しかし、私は別のインデックスparam_idを追加しました. あなたが持っていた方法では、列param_idは複数列インデックスの 2 番目の列であり、param_id. dba.SE のこの関連する質問の下で、それに関するすべての詳細を読んでください。

これを単独で実装した後は、クエリが高速になるはずです。しかし、できることは他にもあります。

テストデータ

これにより、データの入力がはるかに高速になります。ポイントは、セットベースの DML コマンドを使用して、個別の挿入を実行するループの代わりに一括挿入を実行することです。挿入するかなりの量のテスト データにかなりの違いがあります。また、はるかに短くシンプルです。

さらに効率的にするために、最後のステップで大量の行を即座に再利用するデータ変更 CTE (Postgres 9.1 の新機能) を使用します。

CREATE OR REPLACE FUNCTION insert_data()
RETURNS void LANGUAGE plpgsql AS
$BODY$
BEGIN
   INSERT INTO device (device)
   SELECT 'dev_' || to_char(g, 'FM00')
   FROM generate_series(1,5) g;

   INSERT INTO param (param)
   SELECT 'param_' || to_char(g, 'FM00')
   FROM generate_series(1,20) g;

   INSERT INTO spot (spot_id)
   SELECT nextval('spot_spot_id_seq'::regclass)
   FROM generate_series(1,10) g; -- to set sequence, too

   WITH x AS (
      INSERT INTO measure (device_id, spot_id, t_stamp)
      SELECT d.device_id, s.spot_id, g
      FROM   device    d
      CROSS  JOIN spot s
      CROSS  JOIN generate_series('2012-01-06 23:00:00' -- smaller set
                                 ,'2012-01-07 00:00:00' -- for quick tests
                                 ,interval '1 min') g
      RETURNING *
      )
   INSERT INTO measure_val (measure_id, param_id, value)
   SELECT x.measure_id
         ,p.param_id
         ,x.device_id || '_' || x.spot_id || '_' || p.param
   FROM  x
   CROSS JOIN param p;
END
$BODY$;

電話:

SELECT insert_data();

クエリ

  • 明示的なJOIN構文とテーブルのエイリアスを使用して、クエリを読みやすくデバッグしやすくします。
SELECT v.value
FROM   param p
JOIN   measure_val v USING (param_id)
WHERE  p.param = 'param_01';

このUSING句は構文を単純化するためだけのものですが、他の方法よりも優れているわけではありませんON

これは、次の 2 つの理由により、はるかに高速になるはずです。

  • param_param_idxのインデックスparam.param
  • ここで詳細に説明されているように、 に索引measure_val_param_id_idxを付けます。measure_val.param_id

フィードバック後に編集

measurement_data_value_idx_fk_parameter_id私の主な見落としは、質問のさらに下の形で重要なインデックスをすでに追加していたことです。(不可解な名前のせいです! :p ) よく調べてみると、テスト セットアップに 10M (7 * 24 * 60 * 5 * 10 * 20) を超える行があり、クエリは 500K を超えて取得します。私ははるかに小さなサブセットでのみテストしました。

また、テーブル全体の 5% を取得すると、インデックスはそこまでしか進みません。私は楽観的でしたが、そのような量のデータには時間がかかります。50 万行をクエリすることは現実的な要件ですか? 実際のアプリケーションで集計すると思いますか?

その他のオプション

  • パーティショニング
  • より多くの RAM とそれを利用する設定。

    1 GB の RAM を搭載した仮想 Debian 6.0 マシン

    必要なものをはるかに下回っています。

  • 特にPostgreSQL 9.2 のインデックスのみのスキャンに関連する部分インデックス。

  • 集約されたデータの実体化されたビュー。明らかに、500K 行を表示するのではなく、何らかの集計を表示します。一度計算して、結果を具体化されたビューに保存すると、そこからデータをはるかに高速に取得できます。
  • CLUSTERクエリが主にパラメーターによるものである場合 (例のように)、インデックスに従ってテーブルを物理的に書き換えることができます。

    CLUSTER measure_val USING measure_val_param_id_idx
    

    このようにして、1 つのパラメーターのすべての行が連続して格納されます。読み取るブロックが少なくなり、キャッシュしやすくなります。手元のクエリをはるかに高速にする必要があります。またはINSERT、同じ効果を得るために、最初から好ましい順序で行を並べます。毎回 (巨大な) テーブル全体を書き直す必要がないため、
    パーティショニングは とうまく組み合わせることができます。CLUSTERデータは明らかに挿入されたばかりで更新されていないため、パーティションはCLUSTER.

  • 一般に、PostgreSQL 9.2は、その改善点がビッグ データのパフォーマンスに重点を置いているため、優れているはずです。

于 2012-09-22T16:08:30.850 に答える
4

この「ソリューション」の背後にある考え方は、{device、spot、paramater}の個別のキードメインを回避することです。これら3つの可能な組み合わせは1000個だけです。(BCNF違反の悪いケースと見なされる可能性があります)。そこで、それらを1つのwhat_how_whereテーブルに結合します。このテーブルは、ツリーの個別のドメインを参照します。Measurement(data)テーブルのキー要素の数が4から2に減り、代理キーが省略されます(使用されていないため)what_how_whereテーブルには代理キーがあります。私の意味は次のように表すことができます。このテーブルにタプルが存在する場合:パラメータ「what」はデバイス「how」「onlocation」「where」で測定できます。

-- temp schema for scratch
DROP SCHEMA tmp CASCADE;
CREATE SCHEMA tmp;
SET search_path=tmp;

        -- tables for the three "key domain"s
CREATE TABLE device
        ( id SERIAL NOT NULL PRIMARY KEY
        , dname VARCHAR NOT NULL -- 'name' might be a reserve word
        , CONSTRAINT device_name UNIQUE (dname)
        );

CREATE TABLE parameter
        ( id SERIAL PRIMARY KEY -- must have ID as names are not unique
        , pname VARCHAR NOT NULL
        );

CREATE TABLE spot
        ( id SERIAL PRIMARY KEY
        , sname VARCHAR NOT NULL
        );
        -- One table to combine the three "key domain"s
CREATE TABLE what_how_where
        ( id SERIAL NOT NULL PRIMARY KEY
        , device_id INTEGER NOT NULL REFERENCES device(id)
        , spot_id INTEGER NOT NULL REFERENCES spot(id)
        , parameter_id INTEGER NOT NULL REFERENCES parameter(id)
        , CONSTRAINT what_natural UNIQUE (device_id,spot_id,parameter_id)
        );

CREATE TABLE measurement
        ( whw_id INTEGER NOT NULL REFERENCES what_how_where(id)
        , t_stamp TIMESTAMP NOT NULL
        , value VARCHAR(32) NOT NULL
        , CONSTRAINT measurement_natural PRIMARY KEY (t_stamp,whw_id)
        );

INSERT INTO device (dname)
SELECT 'dev_' || d::text
FROM generate_series(1,10) d;

INSERT INTO parameter (pname)
SELECT 'param_' || p::text
FROM generate_series(1,10) p;

INSERT INTO spot (sname)
SELECT 'spot_' || s::text
FROM generate_series(1,10) s;

INSERT INTO what_how_where (device_id,spot_id,parameter_id)
SELECT d.id,s.id,p.id
FROM device d
JOIN spot s ON(1=1)
JOIN parameter p ON(1=1)
        ;
ANALYSE what_how_where;

INSERT INTO measurement(whw_id, t_stamp, value)
SELECT w.id
        , g
        , random()::text
FROM what_how_where w
JOIN generate_series('2012-01-01'::date, '2012-09-23'::date, '1 day'::interval) g
        ON (1=1)
        ;

CREATE UNIQUE INDEX measurement_natural_reversed ON measurement(whw_id,t_stamp);
ANALYSE measurement;

        -- A view to *more or less* emulate the original behaviour
DROP VIEW measurement_data ;
CREATE VIEW measurement_data AS (
        SELECT d.dname AS dname
        , p.pname AS pname
        , w.spot_id AS spot_id
        , w.parameter_id AS parameter_id
        , m.t_stamp AS t_stamp
        , m.value AS value
        FROM measurement m
        JOIN what_how_where w ON m.whw_id = w.id
        JOIN device d ON w.device_id = d.id
        JOIN parameter p ON w.parameter_id = p.id
        );


EXPLAIN (ANALYZE ON, BUFFERS ON)
SELECT md.value
  FROM measurement_data md
 WHERE md.pname = 'param_8'
   AND md.t_stamp >= '2012-07-01'
   AND md.t_stamp < '2012-08-01'
        ;

更新:1つの実際的な問題があります。これは、ある種のクラスタリングによってのみ解決できます。

  • 推定行サイズが50バイトの場合
  • パラメータの5%(1/20)のみのクエリの特異性が必要です
  • これは、約4つの「必要な」タプルがOSディスクページに存在することを意味します(+76の不要なタプル)

クラスタリングを使用しない場合、これはすべてのページをプルインして検査する必要があることを意味します。インデックスはここでは役に立ちません(ページが引き込まれないようにする場合にのみ役立ちます。これ最初のキー列の(範囲)検索の場合に当てはまります)インデックスは、メモリ内のスキャンに少し役立つ場合がありますこれらがフェッチされたのページ。

結果として、これは(クエリのフットプリントが使用可能なバッファスペースよりも大きくなると)クエリが実際にマシンのI/O速度を測定することを意味します。

于 2012-09-23T15:09:36.587 に答える
2

特定の測定値をデバイス、スポット、時間の特定の組み合わせとどのように関連付けているのかわかりません。明らかな何かが欠けていますか?

別の見方をしましょう。

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,
  CONSTRAINT measurement_data_pk PRIMARY KEY (device_name , spot_id , t_stamp , parameter_id ),
  CONSTRAINT measurement_data_fk_device FOREIGN KEY (device_name)
      REFERENCES device (name) MATCH FULL
      ON UPDATE NO ACTION ON DELETE NO ACTION,
  CONSTRAINT measurement_data_fk_parameter FOREIGN KEY (parameter_id)
      REFERENCES parameter (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION,
  CONSTRAINT measurement_data_fk_spot FOREIGN KEY (spot_id)
      REFERENCES spot (id) MATCH FULL
      ON UPDATE NO ACTION ON DELETE NO ACTION
);

(このテーブルのさらに適切な名前は「測定値」です。すべてのテーブルにはデータが含まれています。)

この種のテーブルでは、はるかに優れたパフォーマンスが期待できます。しかし、非常に多くの行を返すクエリでは、パフォーマンスが低下することも予想されます。(ハードウェアとネットワークがタスクに一致しない限り。)

于 2012-09-22T16:21:29.367 に答える
1

数字からすると、タイミングのオーバーヘッドに見舞われているようです。これは、 pg_test_timingを使用するか、explainパラメーターに追加timing offすることで確認できます(どちらもPostgreSQLバージョン9.2で導入されています)。クロックソースをTSCではなくHPETに設定することで、結果をほぼ再現できます。

HPETの場合:

 Nested Loop  (cost=8097.73..72850.98 rows=432000 width=12) (actual time=29.188..905.765 rows=432000 loops=1)
   Buffers: shared hit=56216
   ->  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)
         Rows Removed by Filter: 19
         Buffers: shared hit=1
   ->  Bitmap Heap Scan on measurement_data_value  (cost=8097.73..68529.73 rows=432000 width=16) (actual time=29.180..357.848 rows=432000 loops=1)
         Recheck Cond: (fk_parameter_id = parameter.id)
         Buffers: shared hit=56215
         ->  Bitmap Index Scan on measurement_data_value_idx_fk_parameter_id  (cost=0.00..7989.73 rows=432000 width=0) (actual time=21.710..21.710 rows=432000 loops=1)
               Index Cond: (fk_parameter_id = parameter.id)
               Buffers: shared hit=1183
 Total runtime: 1170.409 ms

HPETとタイミングオフの場合:

 Nested Loop  (cost=8097.73..72850.98 rows=432000 width=12) (actual rows=432000 loops=1)
   Buffers: shared hit=56216
   ->  Seq Scan on parameter  (cost=0.00..1.25 rows=1 width=4) (actual rows=1 loops=1)
         Filter: ((name)::text = 'param_01'::text)
         Rows Removed by Filter: 19
         Buffers: shared hit=1
   ->  Bitmap Heap Scan on measurement_data_value  (cost=8097.73..68529.73 rows=432000 width=16) (actual rows=432000 loops=1)
         Recheck Cond: (fk_parameter_id = parameter.id)
         Buffers: shared hit=56215
         ->  Bitmap Index Scan on measurement_data_value_idx_fk_parameter_id  (cost=0.00..7989.73 rows=432000 width=0) (actual rows=432000 loops=1)
               Index Cond: (fk_parameter_id = parameter.id)
               Buffers: shared hit=1183
 Total runtime: 156.537 ms

TSCの場合:

 Nested Loop  (cost=8097.73..72850.98 rows=432000 width=12) (actual time=29.090..156.233 rows=432000 loops=1)
   Buffers: shared hit=56216
   ->  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)
         Rows Removed by Filter: 19
         Buffers: shared hit=1
   ->  Bitmap Heap Scan on measurement_data_value  (cost=8097.73..68529.73 rows=432000 width=16) (actual time=29.083..114.908 rows=432000 loops=1)
         Recheck Cond: (fk_parameter_id = parameter.id)
         Buffers: shared hit=56215
         ->  Bitmap Index Scan on measurement_data_value_idx_fk_parameter_id  (cost=0.00..7989.73 rows=432000 width=0) (actual time=21.667..21.667 rows=432000 loops=1)
               Index Cond: (fk_parameter_id = parameter.id)
               Buffers: shared hit=1183
 Total runtime: 168.869 ms

したがって、速度の低下は主に計装のオーバーヘッドが原因であるように思われます。ただし、PostgreSQLでは大量の行を選択するのはそれほど速くありません。大量のデータに対して数値計算を行う必要がある場合は、データをより大きなチャンクでフェッチできるように構造化することをお勧めします。(たとえば、常に少なくとも1日分のデータを処理する必要がある場合は、1日のすべての測定値を配列に集約します)

一般に、チューニングを行うためにワークロードがどのようになるかを理解する必要があります。ある場合の勝利は、他の場合には大きな損失になる可能性があります。ボトルネックがどこにあるかを把握するために、pg_stat_statementsをチェックすることをお勧めします。

于 2012-09-23T14:07:37.910 に答える