为具有多个连接的大型 table 查询优化索引

Optimizing indexes for query on large table with multiple joins

我们有一个 images table 包含大约 2500 万条记录,当我根据来自多个连接的值查询 table 时,计划者的估计与实际情况大不相同行计数的结果。我们还有其他查询,这些查询在没有所有连接的情况下大致相同,而且速度要快得多。我想知道可以采取哪些步骤来调试和优化查询。另外,一个索引覆盖连接和 where 子句中包含的所有列还是多个索引一个用于每个连接列然后另一个索引包含 where 子句中的所有字段更好?

查询:

EXPLAIN ANALYZE
SELECT "images".* FROM "images" 
INNER JOIN "locations" ON "locations"."id" = "images"."location_id" 
INNER JOIN "users" ON "images"."creator_id" = "users"."id" 
INNER JOIN "user_groups" ON "users"."id" = "user_groups"."user_id" 
WHERE "images"."deleted_at" IS NULL 
AND "user_groups"."group_id" = 7 
AND "images"."creator_type" = 'User' 
AND "images"."status" = 2 
AND "locations"."active" = TRUE 
ORDER BY date_uploaded DESC 
LIMIT 50 
OFFSET 0;

说明:

Limit  (cost=25670.61..25670.74 rows=50 width=585) (actual time=1556.250..1556.278 rows=50 loops=1)
  ->  Sort  (cost=25670.61..25674.90 rows=1714 width=585) (actual time=1556.250..1556.264 rows=50 loops=1)
        Sort Key: images.date_uploaded
        Sort Method: top-N heapsort  Memory: 75kB
        ->  Nested Loop  (cost=1.28..25613.68 rows=1714 width=585) (actual time=0.097..1445.777 rows=160886 loops=1)
              ->  Nested Loop  (cost=0.85..13724.04 rows=1753 width=585) (actual time=0.069..976.326 rows=161036 loops=1)
                    ->  Nested Loop  (cost=0.29..214.87 rows=22 width=8) (actual time=0.023..0.786 rows=22 loops=1)
                          ->  Seq Scan on user_groups  (cost=0.00..95.83 rows=22 width=4) (actual time=0.008..0.570 rows=22 loops=1)
                                Filter: (group_id = 7)
                                Rows Removed by Filter: 5319
                          ->  Index Only Scan using users_pkey on users  (cost=0.29..5.40 rows=1 width=4) (actual time=0.006..0.008 rows=1 loops=22)
                                Index Cond: (id = user_groups.user_id)
                                Heap Fetches: 18
                    ->  Index Scan using creator_date_uploaded_Where_pub_not_del on images  (cost=0.56..612.08 rows=197 width=585) (actual time=0.062..40.992 rows=7320 loops=22)
                          Index Cond: ((creator_id = users.id) AND ((creator_type)::text = 'User'::text) AND (status = 2))
              ->  Index Scan using locations_pkey on locations  (cost=0.43..6.77 rows=1 width=4) (actual time=0.002..0.002 rows=1 loops=161036)
                    Index Cond: (id = images.location_id)
                    Filter: active
                    Rows Removed by Filter: 0
Planning time: 1.694 ms
Execution time: 1556.352 ms

我们在 RDS 数据库上 运行 Postgres 9.4。m4.large 实例。

至于查询本身,您唯一能做的就是跳过 users table。从 EXPLAIN 你可以看到它只做了一个 Index Only Scan 而没有真正触及 table。因此,从技术上讲,您的查询可能如下所示:

SELECT images.* FROM images
INNER JOIN locations ON locations.id = images.location_id
INNER JOIN user_groups ON images.creator_id = user_groups.user_id
WHERE images.deleted_at IS NULL 
AND user_groups.group_id = 7 
AND images.creator_type = 'User' 
AND images.status = 2 
AND locations.active = TRUE 
ORDER BY date_uploaded DESC 
OFFSET 0 LIMIT 50

剩下的就是索引了。 locations好像数据很少,这里优化对你没有任何好处。另一方面,user_groups 可以从索引 ON (user_id) WHERE group_id = 7ON (group_id, user_id) 中受益。这应该删除对 table 内容的一些额外过滤。

-- Option 1
CREATE INDEX ix_usergroups_userid_groupid7
ON user_groups (user_id)
WHERE group_id = 7;

-- Option 2
CREATE INDEX ix_usergroups_groupid_userid
ON user_groups (group_id, user_id);

当然,这里最大的就是images。目前,刨床将对 creator_date_uploaded_Where_pub_not_del 进行索引扫描,我怀疑这不完全符合要求。在这里,根据您的使用模式,会想到多个选项 - 从一个搜索参数相当常见的选项开始:

-- Option 1
CREATE INDEX ix_images_creatorid_typeuser_status2_notdel
ON images (creator_id)
WHERE creator_type = 'User' AND status = 2 AND deleted_at IS NULL;

到具有完全动态参数的一个:

-- Option 2
CREATE INDEX ix_images_status_creatortype_creatorid_notdel
ON images (status, creator_type, creator_id)
WHERE deleted_at IS NULL;

第一个索引更可取,因为它较小(值被过滤掉而不是索引)。

总而言之,除非您受到内存(或其他因素)的限制,否则我会在 user_groupsimages 上添加索引。指标的正确选择必须通过经验来确认,因为通常有多种选择,具体情况取决于数据的统计分布。

这里有一个不同的方法:

我认为问题之一是您进行了 1714 次连接,然后只返回了前 50 个结果。我们可能希望尽快避免额外的连接。

为此,我们将首先尝试通过 date_uploaded 建立索引。然后我们将过滤其余的列。此外,我们添加 creator_id 以获得仅索引扫描:

CREATE INDEX ix_images_sort_test
ON images (date_uploaded desc, creator_id)
WHERE creator_type = 'User' AND status = 2 AND deleted_at IS NULL;

您也可以使用通用版本(未过滤)。不过应该差一些。由于第一列将是 date_uploaded,我们需要读取整个索引以过滤其余列。

CREATE INDEX ix_images_sort_test
ON images (date_uploaded desc, status, creator_type, creator_id)
WHERE deleted_at IS NULL;

遗憾的是您还通过 group_id 进行过滤,后者在另一个 table 上。但即便如此,也值得尝试这种方法。

此外,验证所有加入的 table 都在外键上有一个索引。

因此,为 user_groups 添加索引为 (user_id, group_id)

此外,正如鲍里斯注意到的那样,您可以删除 "Users" 连接。