web-dev-qa-db-ja.com

EAV構造のビューを使用したクエリの最適化

アプリケーションは、次のようなEAV構造に従うデータベースに書き込んでいます。

_CREATE TABLE item (
    id INTEGER PRIMARY KEY,
    description TEXT
);

CREATE TABLE item_attr (
    item INTEGER REFERENCES item(id),
    name TEXT,
    value INTEGER,
    PRIMARY KEY (item, name)
);

INSERT INTO item VALUES (1, 'Item 1');
INSERT INTO item_attr VALUES (1, 'height', 20);
INSERT INTO item_attr VALUES (1, 'width', 30);
INSERT INTO item_attr VALUES (1, 'weight', 40);
INSERT INTO item VALUES (2, 'Item 2');
INSERT INTO item_attr VALUES (2, 'height', 10);
INSERT INTO item_attr VALUES (2, 'weight', 35);
_

(EAVは少し物議を醸していますが、この質問はEAVに関するものではありません。このレガシーアプリケーションは変更できません。)

多くの属性が存在する可能性がありますが、通常、アイテムごとに最大200の属性(多くの場合類似)があります。これらの200の属性のうち、他よりも一般的で、クエリでより頻繁に使用される約25のグループがあります。

これらの25の属性のいくつかに基づいて新しいクエリを簡単に作成できるようにするため(要件は変化する傾向があり、柔軟にする必要があります)、これらの25の属性の属性テーブルを結合するビューを作成しました。上記の例に従うと、これは次のようになります。

_CREATE VIEW exp_item AS SELECT
   i.id AS id,
   i.description AS description,
   ia_height.value AS height,
   ia_width.value AS width,
   ia_weight.value AS weight,
   ia_depth.value AS depth
FROM item i
  LEFT JOIN item_attr ia_height ON i.id=ia_height.item AND ia_height.name='height'
  LEFT JOIN item_attr ia_width ON i.id=ia_width.item AND ia_width.name='width'
  LEFT JOIN item_attr ia_weight ON i.id=ia_weight.item AND ia_weight.name='weight'
  LEFT JOIN item_attr ia_depth ON i.id=ia_depth.item AND ia_depth.name='depth';
_

典型的なレポートは、これらの25の属性のいくつかだけを使用します。次に例を示します。

_SELECT id, description, height, width FROM exp_item;
_

これらのクエリの一部は、思ったほど高速ではありません。 EXPLAINを使用して、未使用の列の結合がまだ行われていることを確認しました。これにより、3つまたは4つの属性のみが使用される約25の結合で、パフォーマンスが不必要に低下します。

もちろん、ビューですべての_LEFT JOIN_ sを実行するのは正常ですが、このビューを保持する方法(または同様の方法)があるかどうか疑問に思っています。方法を簡略化するために、ビューを使用することに主に関心があります私は、属性を多かれ少なかれそれらが列であるかのように参照し、特定のクエリで未使用の属性に結合を使用することを(自動的に)回避します。

これまでに見つけた唯一の回避策は、これらの各クエリに対して特定のビューを定義することです。これは、使用される属性に基づいて結合するだけです。 (これにより、予想どおりに速度が向上しますが、毎回ビューのプログラミングが増えるため、柔軟性が少し低下します。)

これを行うより良い方法はありますか? (クエリを作成する観点から、EAV構造を「ふり」にして単一の適切に構造化されたテーブルであり、これらの不要な左結合を行う必要がないようにするためのより良い方法はありますか?)

PostgreSQL 8.4を使用しています。 itemには約10K行、_item_attr_には約500K行あります。 itemには8万行以上、_item_attr_には4M行以上は期待できません。これは、現代のシステムがあまり問題なく処理できると思います。 (他のRDBMS /バージョンに関するコメントも歓迎します。)

[〜#〜] edit [〜#〜]:この例では、インデックスの使用法を詳しく説明します。

CREATE TABLE ドキュメントに記載されているように、PRIMARY KEY (item, name)は_(item, name)_にインデックスを暗黙的に作成します。 itemnameの両方がJOINで等式制約とともに使用されていることを考えると、このインデックスは、複数列に関する のドキュメントに従って適切と思われます。インデックス

次の例は、明示的な追加のインデックスなしで、このインデックスが期待どおりに使用されているように見えることを示しています。

_EXPLAIN SELECT id, description, height, width FROM exp_item WHERE width < 100;

                                                QUERY PLAN                                                 
-----------------------------------------------------------------------------------------------------------
 Nested Loop Left Join  (cost=28.50..203.28 rows=10 width=20)
   ->  Nested Loop Left Join  (cost=28.50..196.73 rows=10 width=16)
         ->  Nested Loop Left Join  (cost=28.50..190.18 rows=10 width=16)
               ->  Hash Join  (cost=28.50..183.64 rows=10 width=16)
                     Hash Cond: (ia_width.item = i.id)
                     ->  Seq Scan on item_attr ia_width  (cost=0.00..155.00 rows=10 width=8)
                           Filter: ((value < 100) AND (name = 'width'::text))
                     ->  Hash  (cost=16.00..16.00 rows=1000 width=12)
                           ->  Seq Scan on item i  (cost=0.00..16.00 rows=1000 width=12)
               ->  Index Scan using item_attr_pkey on item_attr ia_depth  (cost=0.00..0.64 rows=1 width=4)
                     Index Cond: ((i.id = ia_depth.item) AND (ia_depth.name = 'depth'::text))
         ->  Index Scan using item_attr_pkey on item_attr ia_weight  (cost=0.00..0.64 rows=1 width=4)
               Index Cond: ((i.id = ia_weight.item) AND (ia_weight.name = 'weight'::text))
   ->  Index Scan using item_attr_pkey on item_attr ia_height  (cost=0.00..0.64 rows=1 width=8)
         Index Cond: ((i.id = ia_height.item) AND (ia_height.name = 'height'::text))
_
5
Bruno

これはEAV設計の(多くの)欠点の1つです。

本当にJOINを改善することはできません。必要な複雑さのために、コストベースのオプティマイザは完璧な計画に到達しません。 「十分に良い」と判明

提案:

  • ビューを使用しない:集計タイプのクエリを使用します(例:身長と体重の両方に一致する場合、COUNT(*)= 2)
  • トリガーを使用して実際の(またはスパース)テーブルを維持し、そのクエリを実行する

最初のオプションは、メインのEAVファクトテーブルのいくつかのインデックスがすべてのクエリを適切にカバーできるため、より適切にスケーリングされます。

7
gbn

Eavテーブルのインデックスについては言及していません。そのため、インデックスがないことを想定しています。

いくつかの部分的なものを追加することは意味があるかもしれません。実行するクエリの種類によっては、次のいずれかまたは両方が役立つ場合があります。

create index item_attr_weight_item_idx
  on item_attr(item)
  where (name = 'weight');

create index item_attr_weight_value_idx
  on item_attr(value)
  where (name = 'weight');

または、行数が少ないため、(name, value)または(name, item)の大きなファットインデックスが機能する場合があります。後者も部分的にすることができます、例えば:

create index item_attr_freq_item_idx
  on item_attr(name, item)
  where (name in ('weight', 'height', 'width'));

そうすれば、少なくともクエリプランナーには、さらに多くの資料を扱うことができます。

2

PostgreSQLのhstoreモジュール を試すことを検討します。