【问题标题】:Why do I get a 'Hash Join' and FTS on this PostgreSQL query?为什么我在这个 PostgreSQL 查询上得到一个“哈希连接”和 FTS?
【发布时间】:2013-01-18 17:20:25
【问题描述】:

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

在文字格式中:我有 2 个表,alertsuser_devices;在user_devices 中,我们跟踪与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

【问题讨论】:

  • 那么 PostgreSQL 几乎需要访问警报表中的每一行。所以seq扫描将是最快的事情。如果您使用的是 9.2,它实际上可能只对 userid 索引进行索引扫描。
  • 如果将where 条件移动到join 条件如inner join user_devices ud on a.user_id = ud.user_id and ud.push_notify_enabled 有什么不同吗?不需要= true 部分顺便说一句。
  • 将尝试移动,看看是否有帮助。 Afk now,将在几个小时内检查。谢谢你们的cmets。
  • @Clodoaldo:尝试过,没有帮助,请参阅编辑:(
  • 请添加EXPLAIN ANALYZE 的输出,它将显示两者预期和观察到的行数。 (也许您的统计信息已关闭或不存在)

标签: performance postgresql join hash indexing


【解决方案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 (

警报上的 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;

【讨论】:

  • 9.1 还是 9.2?对我来说没有什么不同,但正如我们所说的,我正在升级到 9.2.2。
  • 9.1.2.我也需要升级 ;-)
  • 我做错了什么吗?您提出的查询强制我进行三次 Seq 扫描 :)
  • 奇怪。不过,我确实将 uuid 更改为整数和串行。 (为了更容易通过 generate_series() 插入,它似乎不支持 uuids )
  • 我很乐意将使用 uuids 用于此数据库的人开枪打死,但他在 6000 英里之外。 :(
【解决方案2】:

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

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

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

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

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

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

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

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

【讨论】:

  • 不幸的是,这里似乎没有可用的位图索引。另一个问题(可能完全是废话,但只是大声思考) - 如果我将 push_notify_enabled 转移到警报表怎么办?
  • 我的错 — 我出于某种原因将 push_notify_enabled 误认为是来自 alerts 的专栏。我将编辑我的回复。
  • 尝试将东西升级到 9.2 以获得机会。啊,我需要了解更多关于 postgres 的信息,这太令人沮丧了 :)
  • @favoretti:“当你没有得到你想要的东西时,经验就是你得到的东西”:)
  • 另外:好的判断源于经验;经验源于错误的判断。
猜你喜欢
  • 2011-03-05
  • 2018-08-20
  • 1970-01-01
  • 1970-01-01
  • 2011-02-28
  • 2012-05-19
  • 2022-01-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多