私は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の両方」のマッチングを可能にする他の方法はありますか?
このクエリは効率的ですか?
いいえ、それは非常に非効率的です複数の理由があります。
この単純化されたスキーマに基づいて私の答え:
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;
);
テストスキーマに適合した元のクエリ:
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に解決します。
以前よりはるかに高速ですが、重要なサイズのテーブルでは依然として非常に非効率的です。
上記のアプローチではインデックスを使用できません。動的に生成された配列はインデックスで機能します。述語は、集約の後にの後に来ます。
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
それ以外の場合は、インデックスを操作できる完全に異なるクエリテクニックが必要です。 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 ここ