大表上的第一次查询调用速度非常慢

Posted

技术标签:

【中文标题】大表上的第一次查询调用速度非常慢【英文标题】:First call of query on big table is surprisingly slow 【发布时间】:2015-09-02 17:25:50 【问题描述】:

我有一个查询,感觉它需要的时间比它应该的要多。这仅适用于给定参数集的第一次查询,因此缓存时没有问题。

我不确定会发生什么,但是,考虑到设置和设置,我希望有人可以阐明一些问题,并深入了解可以采取哪些措施来加快查询速度。有问题的表相当大,Postgres 估计其中大约有 155963000 (14 GB)。

查询

    select ts, sum(amp) as total_amp, sum(230 * factor) as wh
    from data_cbm_aggregation_15_min
    where virtual_id in (1818) and ts between '2015-02-01 00:00:00' and '2015-03-31 23:59:59'
    and deleted is null
    group by ts
    order by ts

当我开始研究这个查询时,它花了大约 15 秒,经过一些更改后,我将它缩短到了大约 10 秒,对于像这样的简单查询来说,这似乎仍然很长。以下是来自explain analyze 的结果:http://explain.depesz.com/s/97V1。注意GroupAggregate 返回相同数量的行的原因是这个例子只使用了一个virtual_id,但可以有更多。

表格和索引

正在查询的表,每 15 分钟插入一次值

CREATE TABLE data_cbm_aggregation_15_min (
  virtual_id integer NOT NULL,
  ts timestamp without time zone NOT NULL,
  amp real,
  recs smallint,
  min_amp real,
  max_amp real,
  deleted boolean,
  factor real DEFAULT 0.25,
  min_amp_ts timestamp without time zone,
  max_amp_ts timestamp without time zone
)

ALTER TABLE data_cbm_aggregation_15_min ALTER COLUMN virtual_id SET STATISTICS 1000;
ALTER TABLE data_cbm_aggregation_15_min ALTER COLUMN ts SET STATISTICS 1000;

查询中使用的索引

CREATE UNIQUE INDEX idx_data_cbm_aggregation_15_min_virtual_id_ts
ON data_cbm_aggregation_15_min USING btree (virtual_id, ts DESC);

ALTER TABLE data_cbm_aggregation_15_min
CLUSTER ON idx_data_cbm_aggregation_15_min_virtual_id_ts;

Postgres 设置

其他设置为默认设置。

default_statistics_target = 100 
maintenance_work_mem = 2GB 
effective_cache_size = 11GB
work_mem = 256MB
shared_buffers = 3840MB
random_page_cost = 1

我尝试过的

我一直在关注你在https://wiki.postgresql.org/wiki/Slow_Query_Questions 发帖之前要尝试的事情,更详细的结果如下:

    摆弄 Postgres 设置,主要是在索引扫描后降低 random_page_cost,虽然它似乎并不太特别,但它比 random_page_cost 更高时尝试执行的位图堆扫描领先几英里。 向索引和WHERE 条件所基于的virtual_idts 列添加更多统计信息。更改后,查询规划器的估计行数更接近实际行数。 the idx_data_cbm_aggregation_15_min_virtual_id_ts 索引上的聚类似乎没有太大变化,我没有注意到。 手动运行 VACUUM 并没有太大变化,我已经在运行 autovacuum 所以这不足为奇。 在索引上运行 REINDEX 大大缩小了它(几乎 50%!),但速度并没有提高多少。

【问题讨论】:

数据是如何分布的?即,virtual_id 有多少不同的值,它们的行数是否大致相同?您通常是按整月查询,还是可以任意查询时间范围? 截至目前,表中有 15256 个不同的 virtual_id 值。与每个virtual_id 关联的总行数各不相同,但它们都以相同的速率获取数据,因此变化取决于它们的创建时间。有问题的查询将具有任意时间范围。 【参考方案1】:

几个小的改进

SELECT ts, sum(amp) AS total_amp, sum(factor) * 230  AS wh
FROM   data_cbm_aggregation_15_min
WHERE  virtual_id = 1818
AND    ts >= '2015-02-01 00:00'
AND    ts <  '2015-04-01 00:00'
AND    deleted IS NULL
GROUP  BY ts
ORDER  BY ts;

sum(230 * factor) - 将总和相乘一次 比将每个元素相乘更便宜:sum(factor) * 230 结果是一样的,即使是 NULL 值.

ts between '2015-02-01 00:00:00' and '2015-03-31 23:59:59' 可能不正确。要包括 2015 年 3 月的所有,请使用提供的替代方法。 BETWEEN 无论如何都会被翻译成 ts &gt;= lower AND ts &lt;= upper。拼出它总是稍微快一点。

virtual_id in (1818) 只是说virtual_id = 1818 的一种不必要的复杂方式。

更好的索引,可能更大的改进

CREATE INDEX data_cbm_aggregation_15_min_special_idx
ON data_cbm_aggregation_15_min (virtual_id, ts, amp, factor)
WHERE deleted IS NULL;

我在您的问题中看不到任何可以在您的原始索引中建议 DESC 的内容。虽然Index Scan Backward 几乎和普通的Index Scan 一样快,但最好还是去掉修饰符。

最重要的是,自 Postgres 9.2 以来就有 index-only scans。我附加的两个索引列(ampfactor)只有在您从中获得仅索引扫描时才有意义。

既然您显然对已删除的行不感兴趣,请将其设为部分索引。仅当表中有多个已删除行时才需要付费。 如果您还有其他大部分可以排除的表,请添加更多条件 - 并记住在查询中重复条件(即使它看起来是多余的),以便 Postgres 了解该索引是适用的。

表定义

像这样重新排序表列将每行节省 8 个字节:

CREATE TABLE data_cbm_aggregation_15_min (
   virtual_id integer NOT NULL,
   recs smallint,
   deleted boolean,
   ts timestamp NOT NULL,
   amp real,
   min_amp real,
   max_amp real,
   factor real DEFAULT 0.25,
   min_amp_ts timestamp,
   max_amp_ts timestamp
);

相关:

Configuring PostgreSQL for read performance

最后的最重要信息

对于非常大的表,第一次查询调用的开销可能会大得多,因为无法缓存整个表。随后的调用将从填充的缓存中受益。 Postgres 缓存块,不一定是整个表。

还有一件对 first 调用很重要的事情。由于 Postgres 的 MVCC 模型,它必须维护可见性信息。当自上次写入操作以来第一次读取表的页面时,Postgres 机会性地更新可见性信息,这可能会为第一次访问带来一些额外的成本(并且对后续调用有很大帮助)。 More in the manual here。 dba.SE 上的相关答案:

Why does a SELECT statement dirty cache buffers in Postgres?

关于你到目前为止所尝试的内容

SET STATISTICS 1000 for tsvirtual_id 是一个绝妙的主意,但是通过设置 random_page_cost = 1 基本上无效,这基本上是强制对该查询进行索引扫描。

random_page_cost = 1 告诉 Postgres 随机访问与顺序访问一样便宜。这对于(几乎)完全驻留在缓存中的数据库是有意义的。对于像您这样具有巨大表的数据库,此设置似乎太极端(即使它让 Postgres 支持所需的索引扫描)。将其设置为 random_page_cost = 1.1 或可能更高。

位图索引扫描通常是一个好的计划,用于您提供的查询的第一次调用 - 对于在表中随机分布的数据。由于您按照此查询的需要对表进行了聚类,因此索引扫描更有效。问题是:您的表会保持集群吗?

work_mem 和其他资源的设置取决于您拥有多少 RAM、磁盘速度、访问模式、您通常拥有多少并发连接、服务器上的其他程序竞争资源等。work_mem = 256MB 似乎太高了。对于呈现的查询,您几乎不需要那么多。将其设置得那么高实际上可能损害性能,因为它会减少可用于缓存的 RAM。

REINDEXCLUSTER 之后并不是多余的,因为无论如何都会重新创建所有索引。您必须在集群之前运行REINDEX,否则您对表的写入权限很重,已经再次变得如此臃肿。

各种

升级到 Postgres 9.4(或即将推出的 9.5,目前是 alpha)。 9.2 版本到现在已经 3 年了,最新版本已经有了很多改进。

query plan 表明没有实际上是聚合的rows=4,117 从索引中读取,rows=4,117 保留在 GroupAggregate 之后。看起来 ts 上的行已经是唯一的了?然后你就可以完全去掉聚合,把它变成一个简单的SELECT ...

如果这只是一个误导性的EXPLAIN 输出,并且您通常输出的行数比读取的行数少得多,那么在ts 上具有索引的MATERIALIZED VIEW 将是另一种选择。尤其是结合 Postgres 9.4,它引入了REFRESH MATERIALIZED VIEW CONCURRENTLY

【讨论】:

一个了不起的答案。但它不也是一个文件系统缓存,可以使后续查询更快吗? @zerkms:是的,文件系统缓存和 Postgres 缓存在一起。 这些建议看起来很有帮助,我会尝试并报告结果,非常感谢!在virtual_id 上使用IN 运算符是存在的,因为可以有多个。但是就像您提到的那样,当我只有一个 virtual_id 时,可能值得使用 = 并跳过 group by,因为它们在特定情况下什么都不做。

以上是关于大表上的第一次查询调用速度非常慢的主要内容,如果未能解决你的问题,请参考以下文章

中小型表上的左连接非常慢

大表 (EF) 上的插入性能非常慢

在 Django 中的大表上的内存效率(常量)和速度优化迭代

mysql 表很少,一个大表上的子查询执行缓慢

小表上的仅索引扫描非常慢

加入大表时,postgres 查询速度慢