优化对一系列时间戳的查询(两列)

Ste*_*and 129 postgresql index optimization explain postgresql-9.1

我在 Ubuntu 12.04 上使用 PostgreSQL 9.1。

我需要在一个时间范围内选择记录:我的表time_limits有两个timestamp字段和一个integer属性。我的实际表中还有其他列与此查询无关。

create table (
   start_date_time timestamp,
   end_date_time timestamp, 
   id_phi integer, 
   primary key(start_date_time, end_date_time,id_phi);
Run Code Online (Sandbox Code Playgroud)

该表包含大约 200 万条记录。

像下面这样的查询花费了大量的时间:

select * from time_limits as t 
where t.id_phi=0 
and t.start_date_time <= timestamp'2010-08-08 00:00:00'
and t.end_date_time   >= timestamp'2010-08-08 00:05:00';
Run Code Online (Sandbox Code Playgroud)

所以我尝试添加另一个索引 - PK的倒数:

create index idx_inversed on time_limits(id_phi, start_date_time, end_date_time);
Run Code Online (Sandbox Code Playgroud)

我的印象是性能有所提高:访问表中间记录的时间似乎更合理:介于 40 到 90 秒之间。

但是对于时间范围中间的值,它仍然是几十秒。在针对表格末尾时(按时间顺序),还有两次。

explain analyze第一次尝试得到这个查询计划:

 Bitmap Heap Scan on time_limits  (cost=4730.38..22465.32 rows=62682 width=36) (actual time=44.446..44.446 rows=0 loops=1)
   Recheck Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
   ->  Bitmap Index Scan on idx_time_limits_phi_start_end  (cost=0.00..4714.71 rows=62682 width=0) (actual time=44.437..44.437 rows=0 loops=1)
         Index Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
 Total runtime: 44.507 ms
Run Code Online (Sandbox Code Playgroud)

在 depesz.com 上查看结果。

我可以做些什么来优化搜索?您可以看到所有时间都花在扫描两个时间戳列一次id_phi设置为0. 而且我不明白时间戳上的大扫描(60K 行!)。它们不是由主键索引而idx_inversed我添加的吗?

我应该从时间戳类型更改为其他类型吗?

我已经阅读了一些关于 GIST 和 GIN 索引的内容。我认为它们在自定义类型的某些条件下可以更有效。它对我的用例来说是一个可行的选择吗?

Erw*_*ter 224

对于 Postgres 9.1 或更高版本:

CREATE INDEX idx_time_limits_ts_inverse
ON time_limits (id_phi, start_date_time, end_date_time DESC);
Run Code Online (Sandbox Code Playgroud)

在大多数情况下,索引的排序顺序几乎不相关。Postgres 可以几乎一样快地向后扫描。但是对于多列的范围查询,它可以产生巨大的差异。密切相关:

考虑您的查询:

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    start_date_time <= '2010-08-08 00:00'
AND    end_date_time   >= '2010-08-08 00:05';
Run Code Online (Sandbox Code Playgroud)

id_phi索引中第一列的排序顺序无关紧要。因为它检查了相等性( =),所以它应该放在第一位。你说对了。更多在这个相关的答案:

Postgres 可以立即跳入id_phi = 0并考虑匹配索引的以下两列。这些查询使用倒排顺序( <=, >=) 的范围条件。在我的索引中,符合条件的行排在第一位。应该是 B 树索引1的最快方法:

  • 您想要start_date_time <= something:索引首先具有最早的时间戳。
  • 如果符合条件,还要检查第 3 列。
    递归直到第一行不符合条件(超快)。
  • 您想要end_date_time >= something:索引首先具有最新的时间戳。
  • 如果符合条件,则继续获取行,直到第一个不获取行(超快)。
    继续第 2 列的下一个值 ..

Postgres 可以向前向后扫描。您拥有索引的方式,它必须读取前两列匹配的所有行,然后过滤第三列。请务必阅读索引ORDER BY一章和手册中的内容。它非常适合你的问题。

前两列有多少行匹配?
只有少数具有start_date_time接近开始时间范围的表。但几乎所有行都id_phi = 0在表的时间顺序末尾!因此,随着启动时间的延长,性能会下降。

规划师估算

规划器估计rows=62682您的示例查询。其中,没有一个符合 ( rows=0)。如果增加表的统计目标,您可能会得到更好的估计。对于 2.000.000 行...

ALTER TABLE time_limits ALTER start_date_time SET STATISTICS 1000;
ALTER TABLE time_limits ALTER end_date_time   SET STATISTICS 1000;
Run Code Online (Sandbox Code Playgroud)

......可能会付出代价。甚至更高。更多在这个相关的答案:

我想你不需要它id_phi(只有几个不同的值,均匀分布),但对于时间戳(很多不同的值,分布不均)。
我也认为改进的索引并不重要。

CLUSTER / pg_repack / pg_squeeze

但是,如果您希望它更快,您可以简化表中行的物理顺序。如果您有能力以独占方式锁定您的表(例如在非工作时间),请根据索引重写您的表并使用以下命令对行进行排序CLUSTER

CLUSTER time_limits USING idx_time_limits_inversed;
Run Code Online (Sandbox Code Playgroud)

或者考虑pg_repack或后来的pg_squeeze,它们可以在没有表上排他锁的情况下做同样的事情。

无论哪种方式,结果都是需要从表中读取更少的块,并且所有内容都已预先排序。这是一种一次性效果,随着时间的推移而恶化,表上的写入会破坏物理排序顺序。

Postgres 9.2+ 中的 GiST 索引

1在 pg 9.2+ 中,还有另一个可能更快的选项:范围列GiST 索引。

  • timestamptimestamp with time zone: tsrange,tstzrange有内置的范围类型。btree 索引对于integerid_phi. 更小,维护成本也更低。但是使用组合索引,查询总体上可能仍会更快。

  • 更改表定义或使用表达式索引

  • 对于手头的多列 GiST 索引,您还需要btree_gist安装附加模块(每个数据库一次),该模块提供运算符类以包含integer.

三连冠!阿多列功能的GiST指数

CREATE EXTENSION IF NOT EXISTS btree_gist;  -- if not installed, yet

CREATE INDEX idx_time_limits_funky ON time_limits USING gist
(id_phi, tsrange(start_date_time, end_date_time, '[]'));
Run Code Online (Sandbox Code Playgroud)

现在在您的查询中使用“包含范围”运算符@>

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    tsrange(start_date_time, end_date_time, '[]')
    @> tsrange('2010-08-08 00:00', '2010-08-08 00:05', '[]')
Run Code Online (Sandbox Code Playgroud)

Postgres 9.3+ 中的 SP-GiST 索引

一个SP-GiST的指数可能是更快了这种查询-除了那个,报价手册

目前只有 B-tree、GiST、GIN 和 BRIN 索引类型支持多列索引。

在 Postgres 12 中仍然如此。
您必须将spgiston的索引(tsrange(...))与 的第二个btree索引相结合(id_phi)。由于增加了开销,我不确定这是否可以竞争。
相关答案与仅tsrange列的基准:

  • 我应该至少说一次,你对 SO 和 DBA 的每一个答案都是**非常高的附加值/专业**,而且大部分时间都是最完整的。只说一次:尊重! (104认同)
  • @StephaneRolland:当您看到查询花费超过 40 秒时,为什么解释分析输出显示 45 毫秒仍然很有趣。 (2认同)
  • @John:Postgres 可以向前或向后遍历索引,但不能在同一次扫描中改变方向。理想情况下,每个节点首先(或最后)有所有符合条件的行,但所有列必须具有相同的对齐方式(匹配查询谓词)才能获得最佳结果。 (2认同)

小智 5

然而,欧文的回答已经很全面了:

时间戳的范围类型在 PostgreSQL 9.1 中可用,带有 Jeff Davis 的 Temporal 扩展:https : //github.com/jeff-davis/PostgreSQL-Temporal

注意:功能有限(使用 Timestamptz,并且您只能让 '[)' 样式重叠 afaik)。此外,升级到 PostgreSQL 9.2 还有很多其他重要的理由。