缓慢的 Postgres 9.3 查询,再次

Slow Postgres 9.3 Queries, again

这是 问题的后续。

新索引绝对有帮助。但我们看到的是,有时查询在实践中比我们 运行 EXPLAIN ANALYZE 慢得多。示例如下,生产数据库上的 运行:

explain analyze SELECT * FROM messages WHERE groupid=957 ORDER BY id DESC LIMIT 20 OFFSET 31980;
                                                                       QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=127361.90..127441.55 rows=20 width=747) (actual time=152.036..152.143 rows=20 loops=1)
   ->  Index Scan Backward using idx_groupid_id on messages  (cost=0.43..158780.12 rows=39869 width=747) (actual time=0.080..150.484 rows=32000 loops=1)
         Index Cond: (groupid = 957)
 Total runtime: 152.186 ms
(4 rows)

在启用慢速查询日志记录的情况下,我们看到此查询的实例占用了 2 秒以上的时间。我们也有 log_lock_waits=true,大约在同一时间没有报告慢锁。什么可以解释执行时间的巨大差异?

LIMIT x OFFSET y 通常不会比 LIMIT x + y 快多少。大的 OFFSET 总是 比较贵。建议的索引 有帮助,但是虽然您无法从中获得 仅索引扫描 ,但 Postgres 仍然必须至少检查堆(主要关系)中的可见性x + y 行以确定正确的结果。

SELECT *
FROM   messages
WHERE  groupid = 957
ORDER  BY id DESC
LIMIT  20
OFFSET 31980;

CLUSTER 上的索引 (groupid,id) 将有助于增加堆中数据的位置并减少每个查询要读取的数据页数。绝对是一场胜利。但是,如果所有 groupid 被查询的可能性相同,那将不会消除用于缓存的 RAM 太少的瓶颈。如果您有并发访问,请考虑 pg_repack 而不是 CLUSTER:

  • Optimize Postgres timestamp query range

您真的需要返回所有列吗? (SELECT *) 如果您只需要返回几个小列,则启用 仅索引扫描 的覆盖索引可能会有所帮助。 (不过,autovacuum 必须足够强大以应对对 table 的写入。只读 table 将是理想的。)

此外,根据您的链接问题,您的 table 磁盘空间为 32 GB。 (通常在 RAM 中多一点)。 (groupid,id) 上的索引至少增加了 308 MB (没有任何膨胀):

SELECT pg_size_pretty(7337880.0 * 44);  -- row count * tuple size
  • Making sense of Postgres row sizes

您有 8 GB RAM,您预计其中约 4.5 GB 用于缓存 (effective_cache_size = 4608MB)。这足以缓存索引以供重复使用,但还不足以缓存整个 table.

如果您的查询恰好在缓存中找到数据页,则速度很快。否则,不是那么多。差别很大,即使使用 SSD 存储(使用 HDD 时差异更大)。

与此查询没有直接关系,但 8 MB 的 work_mem (work_mem = 7864kB) 对于您的设置来说似乎太小了。根据各种其他因素,我会将其设置为至少 64MB(除非您有许多带有排序/哈希操作的并发查询)。就像@Craig 评论的那样,EXPLAIN (BUFFERS, ANALYZE) 可能会告诉我们更多信息。

最佳查询计划还取决于值频率。如果只有几行通过过滤器,结果可能groupid为空,查询速度比较快。如果必须获取 table 的大部分内容,则可以使用普通的顺序扫描。您需要有效的 table 统计数据(再次 autovacuum)。 groupid:

的更大统计目标
  • Keep PostgreSQL from sometimes choosing a bad query plan

由于 OFFSET 很慢,替代方法是使用另一列和一些索引准备来模拟 OFFSET。我们需要 table 上的 UNIQUE 列(如 PRIMARY KEY)。如果有none,可以加一个:

CREATE SEQUENCE messages_pkey_seq ;
ALTER TABLE messages 
  ADD COLUMN message_id integer DEFAULT nextval('messages_pkey_seq');

接下来我们为 OFFSET 模拟创建 position 列:

ALTER TABLE messages ADD COLUMN position INTEGER;
UPDATE messages SET position = q.position FROM (SELECT message_id,
  row_number() OVER (PARTITION BY group_id ORDER BY id DESC) AS position
  FROM messages ) AS q WHERE q.message_id=messages.message_id ;
CREATE INDEX ON messages ( group_id, position ) ;

现在我们已准备好在 OP 中使用新版本的查询:

SELECT * FROM messages WHERE group_id = 957 AND
  position BETWEEN 31980 AND (31980+20-1) ;