web-dev-qa-db-ja.com

効率的な順序付け/ページ付けのためにジャンクションテーブルを結合するための推奨される方法は何ですか?

概要:単純なデータベーススキーマがありますが、数十、数千のレコードでも、基本的なクエリのパフォーマンスは既に問題になっています。

データベース:PostgreSQL 9.6

簡略化されたスキーマ

CREATE TABLE article (
  id bigint PRIMARY KEY,
  title text NOT NULL,
  score int NOT NULL
);
CREATE TABLE tag (
  id bigint PRIMARY KEY,
  name text NOT NULL
);
CREATE TABLE article_tag (
  article_id bigint NOT NULL REFERENCES article (id),
  tag_id bigint NOT NULL REFERENCES tag (id),
  PRIMARY KEY (article_id, tag_id)
);
CREATE INDEX ON article (score);

生産データ情報

すべてのテーブルは読み取り/書き込み可能です。書き込み量が少なく、数分ごとに新しいレコードのみ。

おおよそのレコード数:

  • 〜66,000件の記事
  • 〜63,000個のタグ
  • 〜147Kのarticle_tags

記事ごとに平均5つのタグ。

質問:すべての記事レコードのタグの配列を含むビューarticle_tagsを作成します。article.scoreで順序付けでき、追加のフィルタリングの有無にかかわらずページ番号を付けられます。

初めての試みで、クエリの実行に350ミリ秒かかり、インデックスを使用していないことに驚いた。その後の試行で、それを5ミリ秒まで下げることができましたが、何が起こっているのか理解できません。これらすべてのクエリに同じ時間がかかると思います。 ここで欠けている重要な概念は何ですか?

試行(SQLフィドル):

  1. マルチテーブル結合(〜350ミリ秒)、(article.idで注文した場合は〜5ミリ秒)-最も自然な解決策のように見えた
  2. サブクエリ結合(〜300 ms)-これも自然な解決策のように思われた
  3. 制限付きサブクエリ結合(〜5ミリ秒)-非常に扱いにくい、ビューには使用できません
  4. ラテラル結合(〜5ミリ秒)-これは本当に私が使用すべきものですか?ラテラルの誤用のようです
  5. ...他に何か?
6
Lucifer Sam

ページネーション

paginationの場合、LIMIT(およびOFFSET)は単純ですが、通常、大きなテーブルには非効率的なツールです。 _LIMIT 10_を使用したテストでは、氷山の一角のみが表示されます。どのクエリを選択しても、パフォーマンスはOFFSETとともに低下します。

同時書き込みアクセスがないか、ほとんどない場合、優れたソリューションは、行番号とそれにインデックスを追加した_MATERIALIZED VIEW_です。そして、すべてのクエリは行番号で行を選択します。

同時書き込み負荷では、そのようなMVはすぐに古くなります(ただし、MV CONCURRENTLYをN分ごとに更新するなどの妥協案は許容される場合があります)。
LIMIT/OFFSETは、「次のページ」がそこに移動するターゲットであるため、まったく正しく機能しません。また、LIMIT/OFFSETはそれに対応できません。最良の手法は、非公開の情報に依存します。

関連:

インデックス

インデックスは一般的に見栄えがします。しかし、あなたのコメントは、テーブルtag多くの行があることを示しています。通常、tagのようなテーブルへの書き込み負荷はほとんどなく、インデックスのみのサポートに最適です。したがって、複数列(「カバリング」)インデックスを追加します。

_CREATE INDEX ON tag(id, name);
_

関連:

上位N行のみ

実際にそれ以上ページが必要ない場合(厳密には「ページング」ではありません)、該当する行をarticlebeforeから削減する任意のクエリスタイルが適切です。関連するテーブルから詳細を取得する(高額)。 「限定サブクエリ」(3.)と「ラテラル結合」(4.)のソリューションは優れています。しかし、あなたはもっとうまくやることができます:

ARRAYバリアントにはLATERALコンストラクタを使用します。

_SELECT a.id, a.title, a.score, tags.names
FROM   article a
LEFT   JOIN LATERAL (
   SELECT ARRAY (
      SELECT t.name
      FROM   article_tag a_t 
      JOIN   tag t ON t.id = a_t.tag_id
      WHERE  a_t.article_id = a.id
   -- ORDER  BY t.id  -- optionally sort array elements
      )
  ) AS tags(names) ON true
ORDER  BY a.score DESC
LIMIT  10;
_

LATERALサブクエリは、single_article_id_のタグを一度にまとめるため、_GROUP BY article_id_は冗長であり、結合条件_ON tags.article_id = article.id_、および基本的なARRAYコンストラクターは、残りの単純なケースではarray_agg(tag.name)よりも安価です。

または、低く相関サブクエリを使用しますが、通常はさらに高速ですが、

_SELECT a.id, a.title, a.score
     , ARRAY (
         SELECT t.name
         FROM   article_tag a_t 
         JOIN   tag t ON t.id = a_t.tag_id
         WHERE  a_t.article_id = a.id
      -- ORDER  BY t.id  -- optionally sort array elements
      ) AS names
FROM   article a
ORDER  BY a.score DESC
LIMIT  10;
_

db <> fiddle ここ
SQLフィドル

6