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;
いいねページ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
これと他のすべてのクエリは、シークページネーションの目的全体を超える順次スキャンを生成します
改善の余地がもっとあるかもしれません。しかし、明らかな大きな問題は次のとおりです。
_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バイトに適合します。 (bigint
のlikes
を使用すると、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
を使用しないでください。見る: