web-dev-qa-db-ja.com

"all"と "any"を含むn:m関係の任意のクエリ

私はpostgres> = 9.6を使用しています。タスクとタグの間の典型的なn:m関係のために、タスク、タグ、およびtask_tagsテーブルがあります。

タスクの実際のフィールドに対するクエリだけでなく、タスクのタグ(タグ名)に対するクエリをサポートするタスクテーブルに対するクエリをプログラムで作成できるようにしたいと思います。

タスクフィールド自体のクエリは簡単です。 「タグAはありますか?」のタグに対するクエリまた、単純明快です。私が苦労しているのは、「タグAとタグBがあるか」のようなものにも一致させる選択/クエリ構造を考え出すことですか?

私が思いついた最高の方法は、配列集約を使用したサブクエリのラテラル結合であり、次に配列マッチング関数を使用します。

SELECT DISTINCT ON (tasks.id) tasks.* 
FROM   tasks, LATERAL (SELECT array_agg(tags.name) AS tags 
                       FROM   task_tags 
                       INNER JOIN tags 
                       ON task_tags.tag_id = tags.id 
                       WHERE task_tags.task_id = tasks.id 
                       GROUP BY task_tags.task_id) tt
WHERE  tt.tags @> array['tag1'::varchar, 'tag3'::varchar];

このようにして、ユーザーが提供する「クエリ」のすべての条件を満たすWHERE句を(tasks。*およびtt.tagsを使用して)プログラムで構築できるはずです。

しかし、これがそれを行う最善の方法であるかどうかはわかりません-考えですか?このクエリは効率的ですか?それを改善するために作成できるインデックスはありますか?

同様に、タグ名に対してワイルドカードを使用する方法はありますか?通常の配列マッチングではそれは不可能であり、私が見たソリューションではunnestを使用することをお勧めします(または、そもそも配列を使用しないことをお勧めします)。しかし、「tagAとtagB "。

これらのリレーションシップに対してクエリを作成して、そのような「タグAとタグBの両方」のマッチングを可能にする他の方法はありますか?

2
Alex Hornung

このクエリは効率的ですか?

いいえ、それは非常に非効率的です複数の理由があります。

0.テストスキーマ

この単純化されたスキーマに基づいて私の答え:

CREATE TABLE tag (
  tag_id serial PRIMARY KEY
, tag    text NOT NULL
);

CREATE TABLE task (
  task_id  serial PRIMARY KEY
, task     text NOT NULL
);

CREATE TABLE task_tag (
  task_id    int
, tag_id     int
, PRIMARY KEY (tag_id, task_id)  -- columns in this order
, FOREIGN KEY (task_id) REFERENCES task
, FOREIGN KEY (tag_id)  REFERENCES tag;
);

1.クエリは非常に非効率的です

テストスキーマに適合した元のクエリ:

SELECT DISTINCT ON (task.task_id) task.*  -- DISTINCT is dead freight
FROM   task, LATERAL (
   SELECT array_agg(tag.tag) AS tags  -- array_agg more expensive than array constructor
   FROM   task_tag 
   JOIN   tag ON task_tag.tag_id = tag.tag_id 
   WHERE  task_tag.task_id = task.task_id 
   GROUP  BY task_tag.task_id  -- redundant noise
   ) tt
WHERE  tt.tags @> array['tag1'::text, 'tag3'::text];  -- or array literal.

どうしましたか?手始めに:

  • DISTINCT ONを追加する理由はありません。横方向の結合は、集計後に単一の行にのみ結合します。
  • この単純な配列集約の場合、配列コンストラクターの方が安価です。
  • WHERE句が既にフィルターをかけた後、GROUP BYを追加する必要はありませんonetask_id
  • クエリの呼び出し方によっては、配列コンストラクタの代わりに単一の配列リテラルを渡す方が便利な場合があります。

同等のクエリ1:

SELECT ts.* 
FROM   task ts
JOIN   LATERAL (
   SELECT ARRAY (
      SELECT tg.tag
      FROM   task_tag tt
      JOIN   tag      tg USING (tag_id)
      WHERE  tt.task_id = ts.task_id
      ) AS tags 
   ) tt ON tt.tags @> '{tag1, tag3}'::text[];

しかし、それはまだ無駄です。とにかくすべての行を集約する必要がある一方で、LATERALは害よりも害が大きくなります。

同等のクエリ2:

SELECT ts.* 
FROM   task ts
JOIN  (
   SELECT task_id
   FROM   task_tag
   GROUP  BY 1
   HAVING array_agg(tag_id) @> '{1, 3}'::int[];
   ) AS tags USING (task_id);

ご覧のとおり、私はタグの配列ではなく、tag_idを作成しています。はるかに効率的です。クエリに渡す前に、入力タグをIDに解決します。

以前よりはるかに高速ですが、重要なサイズのテーブルでは依然として非常に非効率的です

2.アプローチ全体が非常に非効率的です

上記のアプローチではインデックスを使用できません。動的に生成された配列はインデックスで機能します。述語は、集約の後にの後に来ます。

2.1 MATERIALIZED VIEWは読み取り専用(またはほとんど)のテーブル用

インデックスで機能させるには、MATERIALIZED VIEWごとに容易に集約されたタグ配列(またはできればID)を使用してtask_idを追加する必要があります。配列の列のGINインデックス。 MVは単なるスナップショットなので、読み取り専用(またはほとんど)のテーブルにのみ適しています。

関連:

MATERIALIZED VIEW

CREATE MATERIALIZED VIEW task_tags_mv AS
SELECT task_id, array_agg(tag_id) AS tag_ids
FROM   task_tag
GROUP  BY 1;

もちろん、未加工のタグではなく、tag_idを使用します。また、integer配列の場合、追加モジュール intarray 。関連:

したがって、この特殊なインデックスを使用します。

CREATE INDEX task_tags_mv_arr_idx ON task_tags_mv USING GIN(tag_ids gin__int_ops);

そしてこのクエリ:

SELECT ts.* 
FROM   task ts
JOIN   task_tags_mv mv USING (task_id)
WHERE  mv.tag_ids @> '{1, 3}'::int[];  -- uses intarray operator

これはfastです。そして、同じ設定が[〜#〜] any [〜#〜]タグに一致する場合に機能し、 overlap演算子&& の代わりにcontains演算子:

...
WHERE  mv.tag_ids && '{1, 3}'::int[];  -- uses intarray operator

2.2一般的なクエリ

それ以外の場合は、インデックスを操作できる完全に異なるクエリテクニックが必要です。 Yper *は、SO 彼のコメント

特定の要件を作成するのは簡単です一般的なクエリにタグのリストを提供します

再帰CTEは、動的SQLを使用しない場合、oneの方法です。 (他にもたくさんあります。)便宜上、VARIADICパラメータを使用してSQL関数にネストします。

CREATE OR REPLACE FUNCTION f_tasks_with_tags(VARIADIC _tags text[])
  RETURNS SETOF task AS
$func$
   WITH RECURSIVE cte AS (
      SELECT task_id, 1 AS idx
      FROM   task_tag
      WHERE  tag_id = (SELECT tag_id FROM tag WHERE tag = _tags[1])

      UNION
      SELECT task_id, c.idx + 1
      FROM   cte c
      JOIN   task_tag tt USING (task_id)
      WHERE  tag_id = (SELECT tag_id FROM tag WHERE tag = _tags[c.idx + 1])
      )
   SELECT t.*
   FROM   cte  c
   JOIN   task t USING (task_id)
   WHERE  c.idx = array_length(_tags, 1)
$func$  LANGUAGE sql STABLE;

コール:

SELECT * FROM f_tasks_with_tags('tag1', 'tag3'); -- add any number of tags

VARIADICについて(詳細はリンク先をご覧ください):

値の頻度に応じて、スパースタグを最初に渡すとパフォーマンスが大幅に向上します。適格でないタスクを早期に排除する方が安価です。

別の潜在的にさらに高速なアプローチ:動的SQL。次のようなクエリを作成して実行します。

SELECT t.*
FROM  (
   SELECT task_id
   FROM   task_tag t1
   JOIN   task_tag t2 USING (task_id)
-- JOIN   task_tag t3 USING (task_id)
-- more ...
   WHERE  t1.tag_id  = 1
   AND    t2.tag_id  = 3
-- AND    t3.tag_id  = 789
-- more ...
   ) tt
JOIN  task t USING (task_id);

このクエリでは、Postgresは結合のシーケンスを自動的に最適化して、最初にスパースタグを評価します-最大join_collapse_limitテーブルまで。見る:

このサイトのplpgsql関数で動的にクエリを構築するためのコード例が多数あります。 検索してみてください。

SQL Fiddle(intarrayモジュールもあります)。

dbfiddle ここ

1