优化简单 SELECT 查询的缓慢性能

Has*_*aig 4 postgresql performance upgrade postgresql-9.6 query-performance

我有一个名为“链接”的应用程序,其中 1) 用户聚集在群组中并添加其他人,2) 在上述群组中为彼此发布内容。组由links_group我的 postgresql 9.6.5 DB 中的表定义,而他们在这些中发布的回复由links_reply表定义。总体而言,DB 的性能非常好。

然而SELECTlinks_reply表上的一个查询始终显示在slow_log 中。它花费的时间超过 500 毫秒,并且比我在大多数其他 postgresql 操作中遇到的速度慢约 10 倍。

我使用 Django ORM 来生成查询。这里的ORM电话:replies = Reply.objects.select_related('writer__userprofile').filter(which_group=group).order_by('-submitted_on')[:25]。本质上,这是为给定的组对象选择最新的 25 条回复。它还选择关联useruserprofile对象。

这是我的慢日志中相应 SQL 的示例:LOG: duration: 8476.309 ms 语句:

SELECT

    "links_reply"."id",             "links_reply"."text", 
    "links_reply"."which_group_id", "links_reply"."writer_id",
    "links_reply"."submitted_on",   "links_reply"."image",
    "links_reply"."device",         "links_reply"."category", 

    "auth_user"."id",               "auth_user"."username", 

    "links_userprofile"."id",       "links_userprofile"."user_id",
    "links_userprofile"."score",    "links_userprofile"."avatar" 

FROM 

    "links_reply" 
    INNER JOIN "auth_user" 
        ON ("links_reply"."writer_id" = "auth_user"."id") 
    LEFT OUTER JOIN "links_userprofile" 
        ON ("auth_user"."id" = "links_userprofile"."user_id") 
WHERE "links_reply"."which_group_id" = 124479 
ORDER BY "links_reply"."submitted_on" DESC 
LIMIT 25
Run Code Online (Sandbox Code Playgroud)

看看这里的解释分析结果:https : //explain.depesz.com/s/G4X索引扫描(向后)似乎一直在吃。

这是输出\d links_reply

Table "public.links_reply"
     Column     |           Type           |                        Modifiers                         
----------------+--------------------------+----------------------------------------------------------
 id             | integer                  | not null default nextval('links_reply_id_seq'::regclass)
 text           | text                     | not null
 which_group_id | integer                  | not null
 writer_id      | integer                  | not null
 submitted_on   | timestamp with time zone | not null
 image          | character varying(100)   | 
 category       | character varying(15)    | not null
 device         | character varying(10)    | default '1'::character varying
Indexes:
    "links_reply_pkey" PRIMARY KEY, btree (id)
    "category_index" btree (category)
    "links_reply_submitted_on" btree (submitted_on)
    "links_reply_which_group_id" btree (which_group_id)
    "links_reply_writer_id" btree (writer_id)
    "text_index" btree (text)
Foreign-key constraints:
    "links_reply_which_group_id_fkey" FOREIGN KEY (which_group_id) REFERENCES links_group(id) DEFERRABLE INITIALLY DEFERRED
    "links_reply_writer_id_fkey" FOREIGN KEY (writer_id) REFERENCES auth_user(id) DEFERRABLE INITIALLY DEFERRED
Referenced by:
    TABLE "links_groupseen" CONSTRAINT "links_groupseen_which_reply_id_fkey" FOREIGN KEY (which_reply_id) REFERENCES links_reply(id) DEFERRABLE INITIALLY DEFERRED
    TABLE "links_report" CONSTRAINT "links_report_which_reply_id_fkey" FOREIGN KEY (which_reply_id) REFERENCES links_reply(id) DEFERRABLE INITIALLY DEFERRED
Run Code Online (Sandbox Code Playgroud)

这是一个大表(约 2500 万行)。它运行的硬件有 16 个内核和 60 GB 内存。它与一个 python 应用程序共享这台机器。但我一直在监控服务器的性能,我没有看到那里的瓶颈。

有什么办法可以提高这个查询的性能吗?请就我在此处提供的所有选项(如果有)提出建议。


请注意,这个查询直到上周都表现得非常好。从那以后发生了什么变化?我进行了pg_dump,然后pg_restore将DB(在一个单独的VM),并且我升级和PostgreSQL 9.3.10至9.6.5。我还使用了一个pgbouncer之前调用的连接池,我还没有在我迁移到的新 VM 上配置它。就是这样。

最后,我还注意到(从用户体验中)对于直到上周创建的所有组对象,查询仍然执行得很快。但是现在创建的所有新对象都在生成慢日志。这可能是某种索引问题,特别是索引问题links_reply_submitted_on吗?


更新:规定的优化确实扭转了局面。看一看:

在此处输入图片说明

Erw*_*ter 8

疑似主要问题(概要)

  1. 您需要ANALYZE在使用pg_upgrade. 不复制表统计信息。也可以调整 autovacuum 设置。

  2. 在多列索引(which_group_id, submitted_on DESC)应该成为这个查询好。

询问

没有噪音的格式化查询并带有表别名以提高可读性:

SELECT lr.id, lr.text, lr.which_group_id, lr.writer_id
     , lr.submitted_on, lr.image, lr.device, lr.category
     , au.id, au.username
     , lu.id, lu.user_id, lu.score, lu.avatar
FROM   links_reply            lr
JOIN   auth_user              au ON au.id = lr.writer_id
LEFT   JOIN links_userprofile lu ON lu.user_id = au.id
WHERE  lr.which_group_id = 119287
ORDER  BY lr.submitted_on DESC
LIMIT  25;
Run Code Online (Sandbox Code Playgroud)

我认为查询本身没有任何问题。

索引损坏?

(我不这么认为。)

这可能是某种索引问题吗?

如果您怀疑腐败,请运行REINDEX手册建议:

如果您怀疑用户表上的索引损坏,您可以简单地重建该索引或表上的所有索引,使用REINDEX INDEXREINDEX TABLE

在并发访问的情况下:锁定在几个方面不同于从头开始删除和重新创建索引。手册:

REINDEX类似于索引的删除和重新创建,因为索引内容是从头开始重建的。然而,锁定的考虑是相当不同的。REINDEX锁定索引的父表的写入但不读取。它还对正在处理的特定索引进行排他锁,这将阻止尝试使用该索引的读取。相比之下,DROP INDEX暂时在父表上使用排他锁,阻止写入和读取。后续CREATE INDEX锁定写入但不锁定读取;由于索引不存在,没有读取会尝试使用它,这意味着不会有阻塞,但读取可能会被迫进行昂贵的顺序扫描。

如果并发操作仍然存在问题,请考虑CREATE INDEX CONCURRENTLY创建新的重复索引,然后在单独的事务中删除旧索引。

表统计

但是,看起来表统计信息才是真正的问题。引用您的查询计划:

使用 links_reply_submitted_on on links_reply 向后扫描索引
(成本=0.44..1,664,030.07行=2,001宽度=50)
(实际时间=522.811..716.414行=25循环=1)
    过滤器:(which_group_id = 119287)
    过滤器删除的行:1721320

大胆强调我的。看起来 Postgres 将这个查询计划建立在误导性统计数据的基础上。它期望更多的命中并且可能也低估了谓词的选择性which_group_id = 119287。最终过滤了 170 万行。这散发着不准确的表格统计信息。还有一个可能的解释:

升级主要版本时pg_upgrade不会将现有统计信息复制到新版本的数据库中。建议运行VACUUM ANALYZE或至少ANALYZEpg_upgrade. 该工具甚至会提示提醒您。手册:

由于优化器统计信息不会由 传输pg_upgrade,您将被指示运行命令以在升级结束时重新生成该信息。您可能需要设置连接参数以匹配您的新集群。

如果不这样做,表将没有当前的统计信息,直到自动清理被足够的表写入触发(或其他一些实用程序命令,例如CREATE INDEX或动态ALTER TABLE更新一些统计信息)。

对于任何转储/恢复周期(在您的情况下使用pg_dump& )pg_restore也是如此。转储中不包括表统计信息。

你的桌子很大(~25M rows)。autovacuum 的默认设置将阈值定义为 row_count 加上固定偏移量的百分比。有时这对大表不起作用,下一次自动分析需要相当长的时间。

ANALYZE在表或整个数据库上运行手册。

有关的:

更好的指数

...索引问题,特别是links_reply_submitted_on索引?

是的,那也是。索引"links_reply_submitted_on" btree (submitted_on)未针对查询中的模式进行优化:

SELECT ...
FROM   links_reply            lr
JOIN   ...
WHERE  lr.which_group_id = 119287
ORDER  BY lr.submitted_on DESC
LIMIT  25
Run Code Online (Sandbox Code Playgroud)

就像我们在上面的查询计划中看到的那样,Postgres 使用索引扫描,从底部读取索引并过滤不匹配的内容。如果which_group_id最近选择的所有(很少!)有 25 行,则此方法可能相当快。但是对于行的不均匀分布或 的许多不同值,它不会很好地工作which_group_id

这个多列索引更适合:

links_reply__which_group_id__submitted_on btree (which_group_id, submitted_on DESC)
Run Code Online (Sandbox Code Playgroud)

现在,which_group_id无论数据分布如何,Postgres 都可以为选定的 选择前 25 行。

有关的:

更多解释

关于你的观察:

最后,我还注意到(从用户体验中)对于直到上周创建的所有组对象,查询仍然执行得很快。但是现在创建的所有新对象都在生成缓慢的日志。

为什么?新对象可能还没有25 个条目,因此 Postgres 必须不断扫描整个大索引,希望能找到更多。虽然这对于您的旧索引和查询计划来说非常昂贵,但对于新索引(和更新的表统计信息)来说同样非常便宜。

此外,有了准确的表统计信息,Postgres 很可能会使用您的其他索引"links_reply_which_group_id" btree (which_group_id)来快速获取少数现有行(如果超过 25 行,则进行排序)。但无论如何,我的新索引提供了更可靠的查询计划。

小事

您还可以做其他各种(次要的)事情,例如优化表格布局或调整 autovacuum 设置,但是这个答案已经足够长了。有关的:

你后来评论说:

还删除了无关字段...

仅检索您实际需要的列当然有帮助。这样做,另外。但这不是这里的主要问题。