我在PostgreSQL中存储了一个相对合理(约300万)的非常小的行(整个数据库大约为300MB)。因此组织数据:
Table "public.tr_rating"
Column | Type | Modifiers
-----------+--------------------------+---------------------------------------------------------------
user_id | bigint | not null
place_id | bigint | not null
rating | smallint | not null
rated_at | timestamp with time zone | not null default now()
rating_id | bigint | not null default nextval('tr_rating_rating_id_seq'::regclass)
Indexes:
"tr_rating_rating_id_key" UNIQUE, btree (rating_id)
"tr_rating_user_idx" btree (user_id, place_id)
现在,我想检索您的朋友(一组用户)在一系列地方存放的评分
我写的自然查询是:
SELECT * FROM tr_rating WHERE user_id=ANY(?) AND place_id=ANY(?)
user_id数组的大小约为500,而place_id数组的大小约为10,000
这变成了:
Bitmap Heap Scan on tr_rating (cost=2453743.43..2492013.53 rows=3627 width=34) (actual time=10174.044..10174.234 rows=1111 loops=1)
Buffers: shared hit=27922214
-> Bitmap Index Scan on tr_rating_user_idx (cost=0.00..2453742.53 rows=3627 width=0) (actual time=10174.031..10174.031 rows=1111 loops=1)
Index Cond: ((user_id = ANY (...) ))
Buffers: shared hit=27922214
Total runtime: 10279.290 ms
我在这里看到的第一个可疑的事情是它估计扫描500个用户的索引将需要2.5M磁盘寻找
这里的其他所有内容看起来都很合理,除了这需要十秒钟才能完成!索引(通过\di
)看起来像:
public | tr_rating_user_idx | index | tr_rating | 67 MB |
在67 MB,我预计它会在很短的时间内撕掉索引,即使它必须按顺序执行。由于从EXPLAIN ANALYZE
显示的缓冲区显示,所有内容都已在内存中(因为除shared_hit之外的所有值都为零并因此被抑制)。
我尝试了REINDEX
,VACUUM
,ANALYZE
和CLUSTER
的各种组合,没有任何可衡量的改进。
有关我在这里做错了什么的想法,或者我如何进一步调试?我很神秘; 67MB的数据是花费这么多时间搜索的微不足道的数据......
作为参考,硬件是一个8路最近的Xeon,在RAID-10中有8个15K 300GB驱动器。应该足够了: - )
修改
Per btilly的建议,我尝试了临时表:
=> explain analyze select * from tr_rating NATURAL JOIN user_ids NATURAL JOIN place_ids;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------
Hash Join (cost=49133.46..49299.51 rows=3524 width=34) (actual time=13.801..15.676 rows=1111 loops=1)
Hash Cond: (place_ids.place_id = tr_rating.place_id)
-> Seq Scan on place_ids (cost=0.00..59.66 rows=4066 width=8) (actual time=0.009..0.619 rows=4251 loops=1)
-> Hash (cost=48208.02..48208.02 rows=74035 width=34) (actual time=13.767..13.767 rows=7486 loops=1)
Buckets: 8192 Batches: 1 Memory Usage: 527kB
-> Nested Loop (cost=0.00..48208.02 rows=74035 width=34) (actual time=0.047..11.055 rows=7486 loops=1)
-> Seq Scan on user_ids (cost=0.00..31.40 rows=2140 width=8) (actual time=0.006..0.399 rows=2189 loops=1)
-> Index Scan using tr_rating_user_idx on tr_rating (cost=0.00..22.07 rows=35 width=34) (actual time=0.002..0.003 rows=3 loops=2189)
Index Cond: (tr_rating.user_id = user_ids.user_id) JOIN place_ids;
Total runtime: 15.931 ms
为什么在面对临时表而不是数组时,查询计划会更好?数据完全相同,只是以不同的方式呈现。另外,我已经测量了在数十到数百毫秒运行时创建临时表的时间,这是一个相当陡峭的开销。我可以继续使用数组方法,但允许Postgres使用更快的散列连接吗?
编辑2
通过在user_id上创建哈希索引,运行时减少到250毫秒。向place_id添加另一个哈希索引可将运行时间进一步缩短为50毫秒。这仍然是使用临时表的两倍慢,但是制作表的开销否定了我看到的任何增益。我仍然不明白如何在btree索引中执行O(500)查找可能需要十秒钟,但哈希索引无疑要快得多。
答案 0 :(得分:1)
看起来它正在占据索引中的每一行,然后扫描你的user_id数组,然后如果它发现它通过你的place_id数组进行扫描。这意味着,对于300万行,它必须扫描100 user_id
秒,并且对于每个匹配,它扫描10,000 place_id
秒。这些匹配单独快速,但这是一个糟糕的算法,可能导致高达300亿次操作。
最好创建两个临时表,为它们提供索引,然后进行连接。如果它执行散列连接,那么您可能有600万次散列查找。 (user_id
为300万,place_id
为300万。)