postgres:时间戳字段的索引

问题描述 投票:2回答:1

我是Postgres的新手,并且对时间戳类型有疑问。

[设置场景,我有一张像下面的桌子:

CREATE TABLE IF NOT EXISTS tbl_example (
    example_id bigint not null,
    example_name text,
    example_timestamp timestamp,
    primary key (example_id)
);

现在,我想运行查询以使用时间戳基于特定日期为我找到示例列表。

例如,将始终运行的常见查询是:

select example_id, example_name, example_timestamp where example_timestamp = date_trunc('datepart', example_timestamp) order by example_timestamp desc;

但是,为了加快搜索过程,我正在考虑向example_timestamp字段添加索引:

CREATE INDEX idx_example_timestamp on tbl_example(example_timestamp);

我的问题是,postgres是如何在时间戳上执行索引的?换句话说,它将基于日期/时间来为时间戳编制索引,还是以秒/毫秒为单位,等等?

或者,我正在考虑创建一个带有'example_date'的新列,并在该列上建立索引以简化操作。我并不希望同时拥有日期和时间戳字段,因为我可以从时间戳字段中获取日期,但是出于索引目的,我认为最好创建一个单独的字段。

如果有人对此有任何想法,将不胜感激?

谢谢。

postgresql jpa-2.0 postgresql-9.4
1个回答
2
投票

不用担心,要开心

postgres如何在时间戳上执行索引-换句话说,它将基于日期/时间为时间戳编制索引,还是以秒/毫秒为单位,等等?

Postgres使用的索引方案的内部结构通常应该对您透明,无论如何。请记住,您今天学习的实现可能会在Postgres的未来版本中发生变化。

您可能会陷入premature optimization的陷阱。信任Postgres及其默认行为,直到您知道存在明显的性能问题。

片刻

日期处理比您可能理解的要复杂。

首先,您正在使用TIMESTAMP,它实际上是TIMESTAMP的缩写名称。此类型无法代表时刻。此类型仅存储日期和时间。例如,2020年1月23日中午12:00。但这是否意味着在日本东京中午?还是几个小时后在法国巴黎中午?还是几个小时后的美国俄亥俄州托莱多中午?

我建议始终将类型名称完全扩展出来,以使SQL中的名称非常清楚。使用TIMESTAMP WITHOUT TIME ZONE而不是TIMESTAMP WITHOUT TIME ZONE

但是如果您实际上是要表示时刻,即时间轴上的特定点,则必须使用TIMESTAMP。此名称来自SQL标准。但是在Postgres和其他一些数据库中,这有点用词不当。 Postgres实际上并不存储时区。相反,Postgres使用随输入一起提交的任何时区或UTC偏移量信息来调整UTC。写入存储的值始终以UTC为单位。如果您关心原始的区域名称或偏移号(小时-分钟-秒),则需要将其存储在第二列中。

从数据库中检索时,该值也以UTC形式出现。但是请注意,某些中间件工具坚持在检索后将默认时区应用于该值。尽管有很好的意图,但这种反功能可能会引起很多混乱。如下所示,使用java.time对象时,您不会有这种困惑。

时间间隔查询

Postgres在UTC中存储时刻,可能是从TIMESTAMP WITH TIME ZONE日期时间开始的计数,因为数据类型被证明是64位(8个八位字节)的整数。根据Wikipedia的说法,Postgres使用的纪元参考为2000-01-01,大概是该日期在UTC的第一时刻2000-01-01T00:00:00.0Z。我们没有任何理由在乎使用什么纪元引用,但是您就可以了。

真正的重点是Postgres中的日期时间值仅存储为数字,即TIMESTAMP WITH TIME ZONE的计数。时间戳类型不是您可能正在考虑的特定日期和时间。您的查询当然可以从timestamp列上的索引中受益,但面向日期(无时间)的查询将不会特别受益。该索引不是面向日期的,也不是我将在下面解释的。

从某个时刻确定日期需要一个时区。在任何给定时刻,日期都会随时区在全球范围内变化。在巴黎午夜过后几分钟,法国是新的一天,而在魁北克蒙特利尔仍然是“昨天”。

要按日期查询时刻,您需要确定当天的第一时刻和第二天的第一时刻。然后,我们使用“半开式”方法来定义时间范围,其中开始是包含在内的,而结尾是排斥的。我们搜索等于或晚于开始时间但也早于结束时间的时刻。提示:“等于或晚于开始”的另一种说法是“不早于”。

您正在使用Java,因此可以在其中使用业界领先的java.time

类。

java.time

类使用的分辨率为epoch-reference,比Postgres中使用的微秒更好。因此,将Postgres值加载到Java中将没有问题。但是,当以纳秒为单位进行转换时,要当心数据丢失,将被静默地截断以仅存储微秒。

确定一天的第一时刻时,请勿假设一天从00:00:00.0开始。某些区域中的某些日期从其他时间开始,例如01:00:00.0。始终让java.time

确定一天的第一时刻。
microseconds

编写您的Half-Open SQL语句。请not

使用SQL命令nanoseconds,因为它不是半开的。
ZoneId z = ZoneId.of( "Asia/Tokyo" ) ;                          // Or `Africa/Tunis`, `America/Montreal`, etc.
LocalDate today = LocalDate.now( z ) ;
ZonedDateTime zdtStart = today.atStartOfDay( z ) ;              // First moment of the day.
ZonedDateTime zdtStop = today.plusDays( 1 ).atStartOfDay( z ) ; // First moment of the following day.

将您的开始和结束值传递给准备好的语句。

您的支持J BETWEENString sql = "SELECT * FROM tbl WHERE event !< ? && event < ? ;" ; // Half-Open query in SQL. 和更高版本可以通过使用JDBC driverDBC 4.2与大多数java.time

一起工作。奇怪的是,JDBC规范not要求支持两种最常用的类型:PreparedStatement::setObject(始终在UTC中)和ResultSet::getObject。这些可能无法使您的特定驱动程序正常工作。该标准确实需要对Instant的支持,因此让我们转换为它。
Instant

传递给ZonedDateTime的结果ZonedDateTime对象将带有该时区在该日期时间使用的偏移量。对于调试或好奇心,您可能希望在UTC中查看这些值。因此,让我们通过提取OffsetDateTime来适应UTC,然后应用零小时-分钟-秒的偏移量来获取带有CTC本身的偏移量的preparedStatement.setObject( 1 , zdtStart.toOffsetDateTime() ) ; preparedStatement.setObject( 2 , zdtStop.toOffsetDateTime() ) ;

OffsetDateTime

传递到准备好的语句。

PreparedStatement

一旦这些InstantOffsetDateTime值到达数据库服务器,它们将被转换为代表从纪元开始计数的数字,即一个简单的整数。然后Postgres执行简单的数字比较。如果这些整数上存在索引,则Postgres查询计划人员认为合适时可以使用该索引,也可以不使用该索引。

如果行的数量相对较少,并且有很多RAM来缓存它们,则可能不需要索引。执行测试,并使用EXPLAIN / ANALYZE查看实际性能。

通过Java的日期列

如果您已完成工作以证明面向日期查询的性能问题,则可以添加第二个类型为OffsetDateTime start = zdtStart.toInstant().atOffset( ZoneOffset.UTC ) ; OffsetDateTime stop = zdtStop.toInstant().atOffset( ZoneOffset.UTC ) ; 的列。然后索引该列,并在面向日期的查询中显式引用它。

[插入您的时刻时,还应包括在您的应用有意义的任何时区中可以看到的日期的计算值。只需确保清楚记录您的意图以及确定日期时所用的时区细节即可。提示:Postgres提供了一项功能,即在列名称及其数据类型旁边将模糊文本作为列定义的一部分。

由于第二个preparedStatement.setObject( 1 , start ) ; preparedStatement.setObject( 2 , stop ) ; 列是从另一列派生的,因此根据定义,它是多余的,并且已进行了规范化。通常,您应该只考虑将非规范化作为最后的手段。

插入值时的Java代码。

start

确定当前时刻,以及在时区stop中感知到的当前时刻的日期。

DATE

传递到准备好的语句。

DATE

现在您可以在DATE列上进行面向日期的查询。如果需要,可以索引。

通过SQL的日期列

或者,您可以在Postgres中自动填充String sql = "INSERT INTO tbl ( event , date_tokyo ) VALUES ( ? , ? ) ;" ; 列。

触发器

您可以编写一个触发器,该触发器使用Postgres中内置的日期时间函数来确定该时点的日期,如在时区Asia/Tokyo中所示。然后,触发器可以将结果日期值写入第二列。

生成值列

或者,对于Postgres 12,您可以更简单地使用新生成的列功能。这项新功能执行相同的工作,但无需定义和附加触发器。有关此新功能的讨论,请参见:

  • Instant now = Instant.now() ; // Always in UTC, no need to specify a time zone here. OffsetDateTime odt = now.atOffset( ZoneOffset.UTC ) ; // Convert from `Instant` to `OffsetDateTime` if your JDBC driver does not support `Instant`. ZoneId z = ZoneId.of( "Asia/Tokyo" ) ; ZonedDateTime zdt = now.atZone( z ) ; LocalDate localDate = zdt.toLocalDate() ; // Extract the date as seen at this moment by people in the Tokyo time zone.
  • [preparedStatement.setObject( 1 , odt ) ; preparedStatement.setObject( 2 , localDate ) ; by Kirk Roybal] >>
  • [date_tokyo作者Daniel Westermann
  • 在Postgres 12中,具有date_tokyo的列已物理存储其值,并且可以建立索引。

    注意

对于此类日期时间工作而言,至关重要的是有关当前时区定义的正确信息。通常,此信息来自Asia/Tokyo / IANA维护的New In PostgreSQL 12: Generated Columns

Java和Postgres都包含自己的tz数据副本。

世界各地的政界人士对重新定义时区表现出浓厚的兴趣,通常很少或根本没有警告。因此,请务必跟踪您关心的时区的变化。当您更新Java或Postgres时,您可能会获得tz数据的新副本。但是在某些情况下,您可能需要手动更新这两个环境中的一个或两个(Java和Postgres)。您的主机Generated columns in PostgreSQL 12也有一个tz数据副本,fyi。


推荐问答