在 TIMESTAMP 列上创建索引以与范围运算符一起使用

Zac*_*ack 4 postgresql index timestamp range-types

TLDR:我可以创建一个由以下WHERE子句使用的索引吗:

WHERE foo_date <@ tsrange('2018-01-01', '2018-02-01')
Run Code Online (Sandbox Code Playgroud)


假设我有一张这样的表:

创建表 foo
(
    foo_id INTEGER 由默认身份生成,
    不带时区的 foo_date 时间戳 NOT NULL,
    约束 foo_pkey 主键 (foo_id)
);

此表包含 100,000 条记录,日期从2009-01-012018-12-29。我希望能够查询给定日期范围内的行(例如 2018 年 1 月的行)。

选项1

一种方法是使用BETWEEN运算符:

SELECT * FROM foo WHERE foo_date BETWEEN '2018-01-01' AND '2018-01-31';

这种方法的问题是,如果foo_date发生在2018-01-31午夜之后,它们将不会包含在此查询中。所以我可以将查询更改为BETWEEN '2018-01-01' AND '2018-02-01'. 那么问题来了,然而,上发生的记录2018-02-01 00:00:00。这些将被包括在内,这是我不想要的。

选项 2

Aaron Bertrand提出的另一种选择是使用这个结构:

foo_date >= '2018-01-01' AND foo_date < '2018-02-01'
Run Code Online (Sandbox Code Playgroud)

(是的,此博客适用于 SQL Server,但似乎适用于此处)。

虽然这种形式明确地为我提供了我想要的结果,但它很麻烦:我必须重复列名两次。

选项 3

由于 Postgres 为我们提供了范围数据类型,我认为更清晰的形式可能是:

foo_date <@ tsrange('2018-01-01', '2018-02-01') 
Run Code Online (Sandbox Code Playgroud)

所以我的下一个问题是,如果我使用这种形式,我可以使用索引来加快操作吗?


使用上面的选项 1 和 2,可以使用普通的 b 树索引:

CREATE INDEX idx_foo ON foo(foo_date);
Run Code Online (Sandbox Code Playgroud)

使用选项 1 或 2 的查询将使用索引:

EXPLAIN SELECT * FROM foo 
WHERE 
    foo_date >= '2018-01-01' 
    AND foo_date < '2018-02-01';
Run Code Online (Sandbox Code Playgroud)

给我这个查询计划:

Bitmap Heap Scan on foo  (cost=21.95..592.70 rows=942 width=12)
  Recheck Cond: ((foo_date >= '2018-01-01 00:00:00'::timestamp without time zone) AND (foo_date < '2018-02-01 00:00:00'::timestamp without time zone))
  ->  Bitmap Index Scan on idx_foo  (cost=0.00..21.71 rows=942 width=0)
        Index Cond: ((foo_date >= '2018-01-01 00:00:00'::timestamp without time zone) AND (foo_date < '2018-02-01 00:00:00'::timestamp without time zone))
Run Code Online (Sandbox Code Playgroud)

但是,如果我使用选项 3,则不使用索引:

EXPLAIN SELECT * FROM foo 
WHERE foo_date <@ tsrange('2018-01-01', '2018-02-01');
Run Code Online (Sandbox Code Playgroud)

给我:

Seq Scan on foo  (cost=0.00..1791.00 rows=500 width=12)
  Filter: (foo_date <@ '["2018-01-01 00:00:00","2018-02-01 00:00:00")'::tsrange)
Run Code Online (Sandbox Code Playgroud)

如果我尝试创建一个要点索引,我最初会收到一条错误消息。

ERROR:  data type timestamp without time zone has no default operator class for access method "gist"
HINT:  You must specify an operator class for the index or define a default operator class for the data type.
SQL state: 42704 
Run Code Online (Sandbox Code Playgroud)

添加btree_gist扩展后,我可以创建索引:

CREATE INDEX idx_foo ON foo USING gist (foo_date)
Run Code Online (Sandbox Code Playgroud)

但是,使用@><@仍然没有使用索引。

有什么我想念的吗?或者创建一个可以被这个构造使用的索引是不可行的?

Erw*_*ter 5

  1. 名称“foo_date”表示 adate并且对于timestamp列来说是一个错误的选择。选项 1适用于实际日期

  2. 一个简单的 btree 索引与选项 2 相结合明确的最佳解决方案。别再看了。除了具有物理排序数据的大型表的 BRIN 索引的特殊情况。看:

概念证明

也就是说,要使 GiST 或 SP-GiST 索引工作,您可以在假范围上创建表达式索引。您不需要此模块btree_gist。使用 SP-GiST 进行演示,因为这里通常会更快一些。看:

CREATE INDEX foo_date_spgist_idx ON foo USING spgist(tsrange(foo_date, foo_date, '[]'));

SELECT * FROM foo
WHERE  tsrange(foo_date, foo_date, '[]') <@ tsrange('2018-01-01', '2018-02-01')
Run Code Online (Sandbox Code Playgroud)

或使用范围文字:

...
WHERE  tsrange(foo_date, foo_date, '[]') <@ '[2018-01-01,2018-02-01)'
Run Code Online (Sandbox Code Playgroud)

但是:更大,维护成本更高,比 btree 索引慢。写起来也不那么麻烦。对你的情况毫无意义。

旁白:从技术上讲,您可以:

... WHERE foo_date BETWEEN '2018-01-01' AND '2018-01-31 23:59.999999';
Run Code Online (Sandbox Code Playgroud)

Postgres 时间戳类型(当前)以 µs 分辨率实现,即最大。6 位小数。因此,该表达式完全符合您的要求。但我强烈建议不要建立在这个实现细节上。选项2是要走的路。有关的: