我正在尝试优化以下场景:
文字格式:我有2个表,alerts
和user_devices
;在user_devices
中,我们跟踪耦合到user_id
的设备是否想要获得通知,在alerts
表中,我们跟踪用户到通知器的关系。基本上,任务是选择每个有任何警报的user_id
,并允许通知到任何注册到它的设备。
表'alerts',大约900k条记录:
Table "public.alerts"
Column | Type | Modifiers
-------------+--------------------------+-----------
id | uuid | not null
user_id | uuid |
target_id | uuid |
target_type | text |
added_on | timestamp with time zone |
old_id | text |
Indexes:
"alerts_pkey" PRIMARY KEY, btree (id)
"one_alert_per_business_per_user" UNIQUE CONSTRAINT, btree (user_id, target_id)
"addedon" btree (added_on)
"targetid" btree (target_id)
"userid" btree (user_id)
"userid_targetid" btree (user_id, target_id)
Foreign-key constraints:
"alerts_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id)
表'user_devices',大约12k的记录:
Table "public.user_devices"
Column | Type | Modifiers
---------------------+--------------------------+-----------
id | uuid | not null
user_id | uuid |
device_id | text |
device_token | text |
push_notify_enabled | boolean |
device_type | integer |
device_name | text |
badge_count | integer |
added_on | timestamp with time zone |
Indexes:
"user_devices_pkey" PRIMARY KEY, btree (id)
"push_notification" btree (push_notify_enabled)
"user_id" btree (user_id)
"user_id_push_notification" btree (user_id, push_notify_enabled)
Foreign-key constraints:
"user_devices_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id)
下面的查询:
select COUNT(DISTINCT a.user_id)
from alerts a
inner join user_devices ud on a.user_id = ud.user_id
WHERE ud.push_notify_enabled = true;
耗时约3秒,生成如下计划:
explain select COUNT(DISTINCT a.user_id) from alerts a inner join user_devices ud on a.user_id = ud.user_id WHERE ud.push_notify_enabled = true;
QUERY PLAN
------------------------------------------------------------------------------------
Aggregate (cost=49777.32..49777.33 rows=1 width=16)
-> Hash Join (cost=34508.97..48239.63 rows=615074 width=16)
Hash Cond: (ud.user_id = a.user_id)
-> Seq Scan on user_devices ud (cost=0.00..480.75 rows=9202 width=16)
Filter: push_notify_enabled
-> Hash (cost=20572.32..20572.32 rows=801732 width=16)
-> Seq Scan on alerts a (cost=0.00..20572.32 rows=801732 width=16)
我错过了什么,有没有办法加快速度?
谢谢。
== edit ==
按照建议,尝试将条件移动到join中,没有区别:
=> explain select COUNT(DISTINCT a.user_id) from alerts a inner join user_devices ud on a.user_id = ud.user_id and ud.push_notify_enabled;
QUERY PLAN
------------------------------------------------------------------------------------
Aggregate (cost=49777.32..49777.33 rows=1 width=16)
-> Hash Join (cost=34508.97..48239.63 rows=615074 width=16)
Hash Cond: (ud.user_id = a.user_id)
-> Seq Scan on user_devices ud (cost=0.00..480.75 rows=9202 width=16)
Filter: push_notify_enabled
-> Hash (cost=20572.32..20572.32 rows=801732 width=16)
-> Seq Scan on alerts a (cost=0.00..20572.32 rows=801732 width=16)
所以,没有办法摆脱2个FTS?如果我至少能让它以某种方式使用"警报"表上的索引,那就太好了。
== edit ==
添加' EXPLAIN ANALYZE.
=> explain ANALYZE select COUNT(DISTINCT a.user_id) from alerts a inner join user_devices ud on a.user_id = ud.user_id and ud.push_notify_enabled;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=49777.32..49777.33 rows=1 width=16) (actual time=5254.355..5254.356 rows=1 loops=1)
-> Hash Join (cost=34508.97..48239.63 rows=615074 width=16) (actual time=1824.607..2863.635 rows=614768 loops=1)
Hash Cond: (ud.user_id = a.user_id)
-> Seq Scan on user_devices ud (cost=0.00..480.75 rows=9202 width=16) (actual time=0.048..16.784 rows=9186 loops=1)
Filter: push_notify_enabled
-> Hash (cost=20572.32..20572.32 rows=801732 width=16) (actual time=1824.229..1824.229 rows=801765 loops=1)
Buckets: 4096 Batches: 32 Memory Usage: 990kB
-> Seq Scan on alerts a (cost=0.00..20572.32 rows=801732 width=16) (actual time=0.047..878.429 rows=801765 loops=1)
Total runtime: 5255.427 ms
(9 rows)
===编辑===
添加请求的配置。大部分是Ubuntu PG9.1默认值:
/etc/postgresql/9.1/main# cat postgresql.conf | grep -e "work_mem" -e "effective_cache" -e "shared_buff" -e "random_page_c"
shared_buffers = 24MB # min 128kB
#work_mem = 1MB # min 64kB
#maintenance_work_mem = 16MB # min 1MB
#wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers
#random_page_cost = 4.0 # same scale as above
#effective_cache_size = 128MB
用部分索引替换索引:
DROP INDEX user_id_push_notification ;
CREATE INDEX user_id_push_notification ON user_devices (user_id)
WHERE push_notify_enabled =True
;
,并将random_page_cost设置为较低的值:
SET random_page_cost = 1.1;
给我造成了一个Index Scan using push_notification on user_devices ud
(<300 ms)。YMMV。
警报上的序列扫描似乎或多或少是不可避免的,因为您期望800K/900K:= 88%)行。只有当行大小非常大时,索引扫描才有效。
UPDATE:将users表添加到查询中似乎会强制执行三重索引扫描。(但几乎同时)
explain ANALYZE
select COUNT(DISTINCT a.user_id)
from alerts a
join user_devices ud on a.user_id = ud.user_id
join users us on a.user_id = us.id
WHERE ud.push_notify_enabled = true;
正如评论中所说,真正的问题是对alerts
表的完整扫描。逻辑上,对于给定的用户ID, alerts
中的任何和所有记录都可以匹配该用户ID。
有一个条件可能会限制扫描:push_notify_enabled
;你不需要false
的行。但是在这列上没有索引,所以在alerts
上完全扫描仍然是连接两个表的最快方法。
在
push_notify_enabled
上尝试使用位图索引,如果你的Postgres版本支持的话。
为了加快查询速度,您必须限制alerts
中扫描的行数,也就是说,在alerts
的某些索引列上添加条件。如果索引具有足够的选择性,则可以使用索引扫描而不是完整扫描。
例如,如果有意义的话,您可以按目标ID或某些与日期相关的列进行过滤。
如果你有900k的警报,这些警报都是活跃的,并且可以在用户之间任意共享,你几乎没有选择;可能增加RAM以保持alerts
表始终缓存可能会有所帮助。(添加硬件通常是最简单和最经济的解决方案。)
AFAICT您只对与推送通知相关的警报感兴趣。如果有推送通知的用户从不与没有推送通知的用户共享警报,则可以根据此条件有效地拆分alerts
。
如果您有位图索引,您可以将push_notify_enabled
列移动到alerts
。否则,您可以尝试在该列上使用分区对其进行物理分割。如果带有推送通知的警报数量明显低于警报总数,则将扫描alerts
的一小部分以进行连接。