web-dev-qa-db-ja.com

シークページネーションがテーブル全体を順次スキャンしないようにする方法

3つのテーブルとマテリアライズドビューがあります。

resource_categoriesには、すべてのカテゴリ名とメタデータが含まれます

create table if not exists resource_categories (
    category_id INT,
    title VARCHAR(255),
    content TEXT,
    icon VARCHAR(50)
);

resourcesには、各カテゴリのすべてのリソースが含まれます

create table if not exists resources (
    resource_id INT,
    title VARCHAR(255),
    content TEXT,
    link VARCHAR(1000),
    category_id INT REFERENCES resource_categories(resource_id),
    icon VARCHAR(255),
    created_at DATE,
    updated_at DATE
);

resource_votesには、リソースが好きまたは嫌いなユーザーが含まれています

create table if not exists resource_votes (
    resource_id INT REFERENCES resources(resource_id),
    user_id INT,
    vote BOOLEAN
);

resource_votes_aggregateresource_idごとに高評価のあるマテリアライズドビュー

CREATE materialized view resource_votes_aggregate AS 
SELECT
   resource_id,
   COUNT(
   CASE
      WHEN
         vote = TRUE 
      THEN
         1 
   END
) AS likes 
FROM
   resource_votes 
GROUP BY
   resource_id;
  • 次のクエリを実行したい
  • 好きなものの降順で並べ替えられたすべてのリソースを検索します(最も好きなリソースを見つけます)
  • タイトルのアルファベット順に降順にソートされたすべてのリソースを検索します
  • SEEK/KEYSETのページネーションを効率的に使用したい

いいねページ1の降順でリソースを検索するクエリ

SELECT
   r.resource_id,
   title,
   COALESCE(likes, 0) AS likes 
FROM
   resources r 
   LEFT JOIN
      resource_votes_aggregate a 
      ON r.resource_id = a.resource_id 
ORDER BY
   likes DESC,
   resource_id DESC LIMIT 5;

実行計画

QUERY PLAN
Limit (cost=74.50..74.52 rows=5 width=157) (actual time=0.058..0.060 rows=5 loops=1)
-> Sort (cost=74.50..76.80 rows=918 width=157) (actual time=0.058..0.058 rows=5 loops=1)
Sort Key: (COALESCE(a.likes, '0'::bigint)) DESC, r.resource_id DESC
Sort Method: top-N heapsort Memory: 25kB
-> Hash Right Join (cost=12.03..59.25 rows=918 width=157) (actual time=0.032..0.046 rows=50 loops=1)
Hash Cond: (a.resource_id = r.resource_id)
-> Seq Scan on resource_votes_aggregate a (cost=0.00..30.40 rows=2040 width=12) (actual time=0.001..0.004 rows=38 loops=1)
-> Hash (cost=10.90..10.90 rows=90 width=149) (actual time=0.021..0.021 rows=50 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 11kB
-> Seq Scan on resources r (cost=0.00..10.90 rows=90 width=149) (actual time=0.003..0.010 rows=50 loops=1)
Planning Time: 0.050 ms
Execution Time: 0.075 ms

これと他のすべてのクエリは、シークページネーションの目的全体を超える順次スキャンを生成します

  • どうすれば修正できますか。助けて超感謝

DB FIDDLEへのリンク

1
PirateApp

改善の余地がもっとあるかもしれません。しかし、明らかな大きな問題は次のとおりです。

_SELECT
   r.resource_id,
   title,
   COALESCE(likes, 0) AS likes
FROM
   resources r 
   LEFT JOIN
      resource_votes_aggregate a 
      ON r.resource_id = a.resource_id 
ORDER BY
   likes DESC,               -- refers to output column!
   resource_id DESC
LIMIT 5;_

マニュアル:

_ORDER BY_式が出力列名と入力列名の両方に一致する単純な名前である場合、_ORDER BY_はそれを出力列名として解釈します。これは、同じ状況で_GROUP BY_が行う選択の逆です。この不整合は、SQL標準と互換性があるように作成されています。

したがって、_resource_votes_aggregate.likes_を含む適切なインデックスがある場合でも、列は式COALESCE(likes, 0) AS likesの背後に隠されているため使用できません。クエリプランには次のように反映されます。

ソートキー:(COALESCE(a.likes、 '0' :: bigint))DESC

修正と:

_SELECT
   r.resource_id,
   r.title,
   COALESCE(a.likes, 0) AS likes
FROM
   resources r 
   LEFT JOIN
      resource_votes_aggregate a 
      ON r.resource_id = a.resource_id 
ORDER BY
   a.likes DESC NULLS LAST,  -- refers to input column!
   r.resource_id DESC
LIMIT 5;_

これはinput列を参照しており、インデックスの使用に適しています。いずれの場合でも、インデックスがなくても評価する方が大幅に安くなるため、これで問題が発生することはありません。

likesは明らかにNULLとなる可能性があるため、必ず_NULLS LAST_を追加してください。見る:

(私が追加したような)all入力列のテーブル修飾は、当面の問題だけでなく、混乱を避けるための良い方法です。

_MATERIALIZED VIEW_

フィドルのMVは次のようにしてより効率的になります。

_CREATE MATERIALIZED VIEW resource_votes_aggregate AS
SELECT resource_id
     ,(count(*) FILTER (WHERE vote))::int AS likes  -- faster
FROM   resource_votes
GROUP  BY resource_id;
_

集計FILTERは通常、より高速です。
count()bigint(8バイト)を返します。 integer(4バイト)にキャストします。

これは最も重要なindexです(フィドルにありません):

_CREATE INDEX ON resource_votes_aggregate (likes DESC, resource_id DESC);
_

integer列が2つあるため、インデックスタプルは最小サイズの8バイトに適合します。 (bigintlikesを使用すると、16バイトになります(12 + 4パディング)。以下を参照してください:

もちろん、resources(resources_id)にもインデックスが必要です。それがPKであれば、そこにあります。

Ifこのクエリを頻繁に使用し、インデックスのみのスキャンの前提条件が設定されている場合、_(resources_id) INCLUDE (title)_の複数列インデックスが効果的です。見る:

最適化されたクエリ

Postgresが最適なクエリプランを提供するほど賢くない場合、この同等のクエリは次のようになります。

_(
SELECT r.resource_id, r.title, a.likes -- COALESCE not needed
FROM   resource_votes_aggregate a
JOIN   resources r USING (resource_id)
ORDER  BY a.likes DESC                 -- NULLS LAST not needed
        , a.resource_id DESC           -- perfect for above index
LIMIT  5  -- logically redundant, but may help with best plan
)
UNION ALL
(
SELECT resource_id, title, 0 AS likes 
FROM   resources r
WHERE  NOT EXISTS (  -- only rows without likes
   SELECT FROM resource_votes_aggregate a
   WHERE  a.resource_id = r.resource_id
   )
ORDER  BY r.resource_id DESC
LIMIT  5  -- logically redundant, but may help with best plan
)
LIMIT 5
_

2番目のSELECTは、いいねを含むリソースが5つ以上ある場合、通常はnever executeになります。見る:

さておき

あなたはページネーションについて述べました。大きなテーブルではLIMIT/OFFSETを使用しないでください。見る:

3