加入聯結表以進行有效排序/分頁的推薦方法是什麼?
摘要:我有一個簡單的數據庫模式,但即使只有幾十萬條記錄,基本查詢的性能已經成為一個問題。
數據庫: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);
生產數據資訊:
所有表都是讀/寫的。低寫入量,每幾分鐘左右只有一個新記錄。
大約記錄數:
- ~66K 項目
- ~63K 標籤
- ~147K 文章標籤
每篇文章平均有 5 個標籤。
問題:我想創建一個視圖
article_tags
,其中包含每個文章記錄的標籤數組,可以通過article.score
或不使用附加過濾來排序和分頁。在我的第一次嘗試中,我驚訝地發現查詢需要大約 350 毫秒才能執行並且沒有使用索引。在隨後的嘗試中,我能夠將其降低到約 5 毫秒,但我不明白髮生了什麼。我希望所有這些查詢都花費相同的時間。我在這裡缺少什麼關鍵概念?
嘗試(SQL 小提琴):
分頁
對於pagination,
LIMIT
(andOFFSET
) 是簡單的,但對於較大的表格來說通常是低效的工具。你的測試**LIMIT 10
只顯示了冰山一角**。無論您選擇哪種查詢,性能都會隨著 的增長而下降。OFFSET
如果您沒有或只有很少的並發寫訪問權限,則更好的解決方案是
MATERIALIZED VIEW
添加行號,並在其上加上索引。並且您的所有查詢都按行號選擇行。在並發寫入負載下,這樣的 MV 很快就會過時(但
CONCURRENTLY
可以接受每 N 分鐘刷新一次 MV 之類的折衷方案)。
LIMIT
/OFFSET
根本無法正常工作,因為“下一頁”是那裡的移動目標,並且LIMIT
/OFFSET
無法應對。最好的技術取決於未公開的資訊。有關的:
指數
您的索引通常看起來不錯。但是您的評論表明該表
tag
有很多行。通常,像這樣的表上的寫入負載非常少tag
,這非常適合僅索引支持。所以添加一個多列(“覆蓋”)索引:CREATE INDEX ON tag(id, name);
有關的:
僅前 N 行
如果您實際上並不需要更多頁面(嚴格來說不是“分頁”),那麼任何查詢樣式都可以減少從相關表中檢索詳細資訊
article
之前的合格行(昂貴)。您的“有限子查詢”(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
子查詢一次組裝一個標籤*,*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 here