提高 COUNT、JOIN 和 GROUP BY 查詢性能
我正在嘗試獲取給定使用者的每組計數並在下面編寫查詢。執行計劃看起來不錯。但是,在某些情況下,使用者有大約 10k 篇文章,並且在載入一點時,查詢開始變得非常慢並且 CPU 使用率上升。
如何提高以下查詢的性能?
我正在使用 PostgreSQL 11。
SELECT COUNT(*) AS occurences, seller_nickname AS value, seller_id AS id FROM data.saved_articles JOIN aggregated.offers USING (offer_id) WHERE user_id = 406943491 GROUP BY seller_nickname, seller_id;
範例結果集:
occurences value id 1 "nick1" id-1 8 "nick2" id-2
查詢計劃:
Finalize GroupAggregate (cost=50262.89..51401.41 rows=9427 width=23) (actual time=58.418..68.209 rows=3527 loops=1) Output: count(*), offers.seller_nickname, offers.seller_id Group Key: offers.seller_nickname, offers.seller_id Buffers: shared hit=17448 dirtied=75 -> Gather Merge (cost=50262.89..51248.22 rows=7856 width=23) (actual time=58.413..66.732 rows=4549 loops=1) Output: offers.seller_nickname, offers.seller_id, (PARTIAL count(*)) Workers Planned: 2 Workers Launched: 2 Buffers: shared hit=37116 dirtied=150 -> Partial GroupAggregate (cost=49262.86..49341.42 rows=3928 width=23) (actual time=45.467..46.616 rows=1516 loops=3) Output: offers.seller_nickname, offers.seller_id, PARTIAL count(*) Group Key: offers.seller_nickname, offers.seller_id Buffers: shared hit=37116 dirtied=150 Worker 0: actual time=31.676..32.206 rows=726 loops=1 Buffers: shared hit=4337 dirtied=16 Worker 1: actual time=46.740..48.138 rows=1841 loops=1 Buffers: shared hit=15331 dirtied=59 -> Sort (cost=49262.86..49272.68 rows=3928 width=15) (actual time=45.458..45.738 rows=3073 loops=3) Output: offers.seller_nickname, offers.seller_id Sort Key: offers.seller_nickname, offers.seller_id Sort Method: quicksort Memory: 436kB Worker 0: Sort Method: quicksort Memory: 110kB Worker 1: Sort Method: quicksort Memory: 314kB Buffers: shared hit=37116 dirtied=150 Worker 0: actual time=31.666..31.759 rows=1077 loops=1 Buffers: shared hit=4337 dirtied=16 Worker 1: actual time=46.732..47.088 rows=3811 loops=1 Buffers: shared hit=15331 dirtied=59 -> Nested Loop (cost=229.92..49028.37 rows=3928 width=15) (actual time=0.407..35.412 rows=3073 loops=3) Output: offers.seller_nickname, offers.seller_id Inner Unique: true Buffers: shared hit=37086 dirtied=150 Worker 0: actual time=0.096..28.787 rows=1077 loops=1 Buffers: shared hit=4322 dirtied=16 Worker 1: actual time=0.103..36.065 rows=3811 loops=1 Buffers: shared hit=15316 dirtied=59 -> Parallel Bitmap Heap Scan on data.saved_articles (cost=229.49..21594.86 rows=3928 width=4) (actual time=0.368..1.219 rows=3073 loops=3) Output: saved_articles.offer_id Recheck Cond: (saved_articles.user_id = 406943491) Heap Blocks: exact=67 Buffers: shared hit=178 Worker 0: actual time=0.056..0.384 rows=1077 loops=1 Buffers: shared hit=12 Worker 1: actual time=0.061..1.154 rows=3811 loops=1 Buffers: shared hit=59 -> Bitmap Index Scan on pk_saved_articles_user_id_offer_id (cost=0.00..227.13 rows=9427 width=0) (actual time=0.941..0.941 rows=9220 loops=1) Index Cond: (saved_articles.user_id = 406943491) Buffers: shared hit=40 -> Index Scan using pk_offers_offer_id on aggregated.offers (cost=0.43..6.98 rows=1 width=19) (actual time=0.011..0.011 rows=1 loops=9220) Output: offers.offer_id, offers.seller_id, offers.seller_nickname, offers.title, offers.condition, offers.ends_at, offers.current_price, offers.buynow_price, offers.category_id, offers.category_name_fr, offers.category_name_de, offers.category_name_it, offers.created_at, offers.updated_at, offers.bid_count, offers.quantity, offers.increment, offers.offer_type Index Cond: (offers.offer_id = saved_articles.offer_id) Buffers: shared hit=36908 dirtied=150 Worker 0: actual time=0.026..0.026 rows=1 loops=1077 Buffers: shared hit=4310 dirtied=16 Worker 1: actual time=0.009..0.009 rows=1 loops=3811 Buffers: shared hit=15257 dirtied=59 Planning Time: 0.274 ms Execution Time: 68.714 ms
為了提高性能,我可能會將結果集限制為每組 100 個。
表和索引:
CREATE TABLE aggregated.offers ( offer_id integer NOT NULL, seller_id integer NOT NULL, seller_nickname character varying(30) NOT NULL, title character varying(60) NOT NULL ); ALTER TABLE aggregated.offers ADD CONSTRAINT pk_offers_offer_id PRIMARY KEY (offer_id); CREATE INDEX idx_offers_seller_id ON aggregated.offers (seller_id); CREATE TABLE data.saved_articles ( user_id integer NOT NULL, offer_id integer NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone, CONSTRAINT pk_saved_articles_user_id_offer_id PRIMARY KEY (user_id, offer_id), CONSTRAINT fk_saved_articles_offer_id FOREIGN KEY (offer_id) REFERENCES aggregated.offers (offer_id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ) CREATE INDEX idx_saved_articles_offer_id ON data.saved_articles (offer_id);
編輯:
從最簡單的方法開始,我執行
VACUUM
並創建了一個索引:EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS) SELECT COUNT(*) AS occurences, seller_nickname AS value, seller_id AS id FROM data.saved_articles JOIN aggregated.offers USING (offer_id) WHERE user_id = 406943491 GROUP BY seller_nickname, seller_id
但是,它似乎沒有任何重大影響。
計劃:
Finalize GroupAggregate (cost=54772.66..56138.73 rows=11312 width=23) (actual time=54.118..61.137 rows=3527 loops=1) Output: count(*), offers.seller_nickname, offers.seller_id Group Key: offers.seller_nickname, offers.seller_id Buffers: shared hit=17925 -> Gather Merge (cost=54772.66..55954.92 rows=9426 width=23) (actual time=54.112..61.691 rows=4482 loops=1) Output: offers.seller_nickname, offers.seller_id, (PARTIAL count(*)) Workers Planned: 2 Workers Launched: 2 Buffers: shared hit=38070 -> Partial GroupAggregate (cost=53772.64..53866.90 rows=4713 width=23) (actual time=40.581..41.995 rows=1494 loops=3) Output: offers.seller_nickname, offers.seller_id, PARTIAL count(*) Group Key: offers.seller_nickname, offers.seller_id Buffers: shared hit=38070 Worker 0: actual time=43.304..45.268 rows=1882 loops=1 Buffers: shared hit=16389 Worker 1: actual time=24.626..25.086 rows=603 loops=1 Buffers: shared hit=3756 -> Sort (cost=53772.64..53784.42 rows=4713 width=15) (actual time=40.571..40.921 rows=3073 loops=3) Output: offers.seller_nickname, offers.seller_id Sort Key: offers.seller_nickname, offers.seller_id Sort Method: quicksort Memory: 437kB Worker 0: Sort Method: quicksort Memory: 323kB Worker 1: Sort Method: quicksort Memory: 76kB Buffers: shared hit=38070 Worker 0: actual time=43.293..43.759 rows=3969 loops=1 Buffers: shared hit=16389 Worker 1: actual time=24.616..24.709 rows=913 loops=1 Buffers: shared hit=3756 -> Nested Loop (cost=216.53..53485.09 rows=4713 width=15) (actual time=0.457..30.793 rows=3073 loops=3) Output: offers.seller_nickname, offers.seller_id Inner Unique: true Buffers: shared hit=38040 Worker 0: actual time=0.191..32.516 rows=3969 loops=1 Buffers: shared hit=16374 Worker 1: actual time=0.171..22.287 rows=913 loops=1 Buffers: shared hit=3741 -> Parallel Bitmap Heap Scan on data.saved_articles (cost=216.10..24104.73 rows=4713 width=4) (actual time=0.371..1.275 rows=3073 loops=3) Output: saved_articles.offer_id Recheck Cond: (saved_articles.user_id = 406943491) Heap Blocks: exact=60 Buffers: shared hit=166 Worker 0: actual time=0.075..1.420 rows=3969 loops=1 Buffers: shared hit=66 Worker 1: actual time=0.059..0.344 rows=913 loops=1 Buffers: shared hit=12 -> Bitmap Index Scan on pk_saved_articles_user_id_offer_id (cost=0.00..213.27 rows=11312 width=0) (actual time=0.929..0.929 rows=9220 loops=1) Index Cond: (saved_articles.user_id = 406943491) Buffers: shared hit=28 -> Index Only Scan using tmp_id on aggregated.offers (cost=0.43..6.23 rows=1 width=19) (actual time=0.009..0.009 rows=1 loops=9220) Output: offers.offer_id, offers.seller_id, offers.seller_nickname Index Cond: (offers.offer_id = saved_articles.offer_id) Heap Fetches: 9129 Buffers: shared hit=37874 Worker 0: actual time=0.007..0.007 rows=1 loops=3969 Buffers: shared hit=16308 Worker 1: actual time=0.023..0.023 rows=1 loops=913 Buffers: shared hit=3729 Planning Time: 0.275 ms Execution Time: 63.720 ms
由於我需要在另一列上執行相同的查詢,因此我不會觸及 PK,而是嘗試
seller_nickname
在不同的表上進行聚合,以便能夠在聚合後添加它。
您的查詢似乎已經執行正常。擠出更快時間的一些想法:
僅索引掃描
aggregated.offers
seller_nickname
似乎在功能上依賴於seller_id
.varchar(30)
額外讀取和分組 a 比僅基於a 更昂貴integer
。刪除seller_nickname
基本查詢應該會使其更快。然後,您可以使用子句添加seller_id
到 PKINCLUDE
以獲取覆蓋索引和非常快速的僅索引掃描aggregated.offers
:ALTER TABLE aggregated.offers DROP CONSTRAINT pk_offers_offer_id , ADD CONSTRAINT pk_offers_offer_id PRIMARY KEY (offer_id) INCLUDE (seller_id);
因為我們都
offer_id
達到了局部最優。看:seller_id``int4
更快的查詢:
SELECT count(*) AS occurrences, o.seller_id AS id -- note occurrences with "rr" FROM data.saved_articles a JOIN aggregated.offers o USING (offer_id) WHERE a.user_id = 406943491 GROUP BY o.seller_id;
如果您確實需要包含
seller_nickname
在結果中,請在查找表的聚合*之後添加它。*假設一個表seller
(你通常會擁有),理想情況下有一個索引(seller_id, seller_nickname)
用於另一個僅索引掃描:SELECT ao.*, s.seller_nickname FROM ( -- query from above SELECT count(*) AS occurrences, o.seller_id AS id FROM data.saved_articles a JOIN aggregated.offers o USING (offer_id) WHERE a.user_id = 406943491 GROUP BY o.seller_id ) ao LEFT JOIN data.seller s USING (seller_id);
應該仍然更快,尤其是結果中每個賣家的行數超過幾行。
或者,您可以在子句中添加
seller_nickname
PK 索引INCLUDE
以加快原始查詢。或者保持 PK 不變並在(offer_id, seller_id, seller_nickname)
. 但是,雖然seller_id
在 PK 索引中包含整數列是非常輕量級的,並且通常幾乎沒有任何缺點(請閱讀連結的答案!),但添加另一個列varchar(30)
的成本更高。並且您會希望保持 PK 索引快速用於其他目的。
autovacuum
為了data.saved_articles
您的 PK 指數
(user_id, offer_id)
似乎非常適合這項任務。但是,我看到一個Bitmap Index Scan on pk_saved_articles_user_id_offer_id
withrows=9220
,然後是一個Parallel Bitmap Heap Scan on data.saved_articles
withrows=3073
- 吞下約 40% 的查詢時間。沒有僅索引掃描。只有三分之一的索引元組產生一個堆元組。這表明 autovacuum 存在問題:過時的可見性映射和大量死索引元組。如果可能(在表上獲取排他鎖,並發工作負載是否允許?),嘗試:
VACUUM FULL ANALYZE data.saved_articles;
或用於在物理儲存中
CLUSTER
對具有相同的行進行分群。user_id
CLUSTER data.saved_articles USING pk_saved_articles_user_id_offer_id; ANALYZE data.saved_articles;
然後,即使寫入負載
saved_articles
阻止了僅索引掃描,也可以從堆中讀取更少的塊。但這是一種一次性的效果,隨著時間的推移會逐漸惡化。無論哪種方式,如果這樣可以提高性能,請
autovacuum
為 table 設置更積極的設置data.saved_articles
和/或考慮使用pg_repack
orpg_squeeze
- 它可以替換VACUUM FULL
/CLUSTER
並在並發負載下工作。有關的: