3

我正在尝试优化以下场景:

以文字格式:我有 2 个表格,alerts并且user_devices; 在user_devices我们跟踪与 a 耦合的设备是否user_id想要获得通知,在alerts表中我们跟踪用户到通知者的关系。基本上,任务是选择每个user_id具有任何警报并允许向其注册的任何设备发出通知。

表“警报”,大约 90 万条记录:

               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)

我错过了什么,有没有办法加快速度?

谢谢你。

== 编辑 ==

根据建议,尝试在连接内移动条件,没有区别:

=> 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 吗?如果我至少可以让它以某种方式使用“警报”表上的索引,那就太好了..

== 编辑 ==

添加`解释分析。

=> 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
4

2 回答 2

1

正如评论中所说,真正的猪是alerts表的完整扫描。从逻辑上讲,对于给定的用户 ID,任何和所有记录都alerts可能与该用户 ID 匹配。

您有一种情况可能会限制扫描push_notify_enabled:你不需要它所在的行false。但是您缺少此列的索引,因此全扫描alerts仍然是连接两个表的最快方法。

push_notify_enabled如果您的 Postgres 版本支持,请尝试在 上使用位图索引。(显然,2 值列上的 btree 索引不好。)

为了加快查询速度,您必须限制要扫描的行数alerts,即在 的某些索引列上添加条件alerts。然后,如果索引足够有选择性,则可以进行索引扫描而不是完整扫描。

例如,如果有意义的话,您可以按目标 ID 或某些与日期相关的列进行过滤。

如果您有 900k 的警报全部处于活动状态,并且可以在用户之间任意共享,那么您别无选择;可能添加 RAM 以保持alerts表始终缓存可能会有所帮助。(添加硬件通常是最简单且最具成本效益的解决方案。)

AFAICT,您只对与推送通知相关的警报感兴趣。如果有推送通知的用户从不与没有推送通知的用户共享警报,则您可能会alerts被此条件有效拆分。

如果您有位图索引,则可以将push_notify_enabled列移动到alerts. 否则,您可能会尝试使用partitioning在该列上物理拆分它。如果带有推送通知的警报数量明显低于警报总数,alerts则将扫描一小部分以进行加入。

于 2013-01-19T13:57:58.287 回答
1

用部分索引替换索引:

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影响(< 300ms)。YMMV。

警报上的 seqscan 似乎或多或少是不可避免的,因为您期望 800K/900K := 88%) 行。仅当行大小非常​​大时,索引扫描才会有效,恕我直言。

更新:将用户表添加到查询中似乎会强制进行三重索引扫描。(但大约在同一时间)

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;
于 2013-01-19T15:41:07.490 回答