在一系列时间戳上优化查询(两列)


96

我在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);

该表包含大约2M条记录。

进行以下查询需要花费大量时间:

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';

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

create index idx_inversed on time_limits(id_phi, start_date_time, end_date_time);

我感觉性能有所提高:访问表中间的记录的时间似乎更合理:介于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

在depesz.com上查看结果。

我该怎么做才能优化搜索?你可以看到所有的时间都花在扫描两个时间戳列一旦id_phi设置为0。而且我不了解时间戳上的大扫描(60K行!)。他们不是通过主键索引的idx_inversed吗?

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

我已经阅读了一些有关GIST和GIN索引的信息。我收集到,在某些情况下,对于自定义类型它们可以更有效。我的用例是否可行?


1
好吧,那是45岁。我不知道为什么说45毫秒。我什至不会开始抱怨它是否快到45毫秒... :-)也许解释分析输出中的错误。或者也许是进行分析的时候了。不知道。但是我测量的是40/50秒。
Stephane Rolland

2
explain analyze输出中报告的时间是服务器上查询所需的时间。如果您的查询花了45秒,那么将花费额外的时间将数据从数据库传输到运行查询的程序中。毕竟它是62682行,并且如果每行很大(例如长varchar或多text列),这可能会影响传输时间剧烈地。
a_horse_with_no_name 2013年

@a_horse_with_no_name:rows=62682 rows是计划者的估计。查询返回0行。(actual time=44.446..44.446 rows=0 loops=1)
Erwin Brandstetter

@ErwinBrandstetter:嗯,对。我忽略了这一点。但是我还是从未见过解释分析的输出是关于执行时间的。
a_horse_with_no_name

Answers:


162

对于Postgres 9.1或更高版本:

CREATE INDEX idx_time_limits_ts_inverse
ON time_limits (id_phi, start_date_time, end_date_time DESC);

在大多数情况下,索引的排序顺序几乎不相关。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';

id_phi索引中第一列的排序顺序无关。由于已检查其是否相等=),因此应该排在第一位。你说对了。此相关答案中的更多内容:

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

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

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;

...可能会付钱。甚至更高。此相关答案中的更多内容:

我猜你不需要id_phi(只有几个不同的值,均匀分布),但不需要时间戳(很多不同的值,不均匀分布)。
我也认为改进索引并没有太大关系。

CLUSTER / pg_repack

如果您希望更快,则可以简化表中行的物理顺序。如果您有能力在短时间内专门锁定表(例如在下班时间)来重写表并根据索引对行进行排序:

ALTER TABLE time_limits CLUSTER ON idx_time_limits_inversed;

对于并发访问,请考虑使用pg_repack,而无需排他锁即可做到这一点。

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

Postgres 9.2+中的GiST索引

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

  • 有内置的范围类型timestamptimestamp with time zonetsrangetstzrange。对于b之类的附加integer列,btree索引通常更快id_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, '[]'));

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

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', '[]')

Postgres 9.3+中的SP-GiST索引

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

当前,仅B树,GiST,GIN和BRIN索引类型支持多列索引。

在Postgres 12中仍然适用。
您必须spgist仅将上的索引(tsrange(...))与上的第二个btree索引合并(id_phi)。随着额外的开销,我不确定这是否可以竞争。
仅针对某tsrange列的基准的相关答案:


78
我至少应该告诉一次,您在SO和DBA上的每个答案都具有很高的附加值/专业知识,并且大多数时候是最完整的。只需说一次:尊重!。
Stephane Rolland

1
谢谢!:)那您得到更快的结果了吗?
Erwin Brandstetter

我必须完成从我的笨拙的查询中生成的大批量副本,因此使该过程真的很慢,在问这个问题之前已经花了好几个小时。但是我已经计算过了,我决定让它转动到明天早上,它将完成,新桌子也准备好明天装满。我试图在工作期间同时创建索引,但是由于访问过多(我认为),索引的创建应被锁定。我将再次重复相同的测试时间,以完善您的解决方案。我还研究了如何将Debian / ubuntu升级到9.2 ;-)。
Stephane Rolland

2
@StephaneRolland:为什么解释分析输出显示45毫秒,而您却看到查询花费了40秒,这仍然很有趣。
a_horse_with_no_name

1
@John:Postgres可以向前或向后遍历索引,但不能在同一扫描中改变方向。理想情况下,每个节点的第一个(或最后一个)都有所有符合条件的行,但是对于所有列,它必须具有相同的对齐方式(匹配查询谓词)才能获得最佳结果。
Erwin Brandstetter

5

但是,欧文的答案已经很全面了:

时间戳的范围类型可从Jeff Davis的Temporal扩展中的PostgreSQL 9.1中获得:https : //github.com/jeff-davis/PostgreSQL-Temporal

注意:功能有限(使用Timestamptz,并且您只能使'[)'样式重叠afaik)。另外,还有很多其他重要原因可以升级到PostgreSQL 9.2。


3

您可以尝试以不同的顺序创建多列索引:

primary key(id_phi, start_date_time,end_date_time);

我曾经发布过一个类似的问题,该问题也与多列索引上的索引排序有关。关键是首先尝试使用限制性最强的条件来减少搜索空间。

编辑:我的错误。现在,我看到您已经定义了该索引。


我已经有了两个索引。除了主键之外,主键是另一个,但您建议的索引已经存在,并且如果您查看以下说明,它就是使用的索引:Bitmap Index Scan on idx_time_limits_phi_start_end
Stephane Rolland 2013年

1

我设法迅速增加(从1秒到70ms)

我有一个表,其中包含许多度量值和许多级别(l列)(30s,1m,1h等)的聚合,其中有两个范围绑定列:$s用于开始和$e结束。

我创建了两个多列索引:一个用于开始,一个用于结束。

我调整了选择查询:选择范围,其起始界限在给定范围内。此外,还应选择其末端范围在给定范围内的范围。

说明显示了有效使用索引的两行数据流。

索引:

drop index if exists agg_search_a;
CREATE INDEX agg_search_a
ON agg (measurement_id, l, "$s");

drop index if exists agg_search_b;
CREATE INDEX agg_search_b
ON agg (measurement_id, l, "$e");

选择查询:

select "$s", "$e", a, t, b, c from agg
where 
    measurement_id=0 
    and l =  '30s'
    and (
        (
            "$s" > '2013-05-01 02:05:05'
            and "$s" < '2013-05-01 02:18:15'
        )
        or 
        (
             "$e" > '2013-05-01 02:00:05'
            and "$e" < '2013-05-01 02:18:05'
        )
    )

;

说明:

[
  {
    "Execution Time": 0.058,
    "Planning Time": 0.112,
    "Plan": {
      "Startup Cost": 10.18,
      "Rows Removed by Index Recheck": 0,
      "Actual Rows": 37,
      "Plans": [
    {
      "Startup Cost": 10.18,
      "Actual Rows": 0,
      "Plans": [
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 26,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone))",
          "Plan Rows": 29,
          "Parallel Aware": false,
          "Actual Total Time": 0.016,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.016,
          "Total Cost": 5,
          "Actual Loops": 1,
          "Index Name": "agg_search_a"
        },
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 36,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone))",
          "Plan Rows": 39,
          "Parallel Aware": false,
          "Actual Total Time": 0.011,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.011,
          "Total Cost": 5.15,
          "Actual Loops": 1,
          "Index Name": "agg_search_b"
        }
      ],
      "Node Type": "BitmapOr",
      "Plan Rows": 68,
      "Parallel Aware": false,
      "Actual Total Time": 0.027,
      "Parent Relationship": "Outer",
      "Actual Startup Time": 0.027,
      "Plan Width": 0,
      "Actual Loops": 1,
      "Total Cost": 10.18
    }
      ],
      "Exact Heap Blocks": 1,
      "Node Type": "Bitmap Heap Scan",
      "Plan Rows": 68,
      "Relation Name": "agg",
      "Alias": "agg",
      "Parallel Aware": false,
      "Actual Total Time": 0.037,
      "Recheck Cond": "(((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone)) OR ((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone)))",
      "Lossy Heap Blocks": 0,
      "Actual Startup Time": 0.033,
      "Plan Width": 44,
      "Actual Loops": 1,
      "Total Cost": 280.95
    },
    "Triggers": []
  }
]

诀窍是您的计划节点仅包含所需的行。以前我们在计划节点中获得了数千行,因为它选择all points from some point in time to the very end,然后下一个节点删除了不必要的行。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.