关于时区:我应该在PostgreSQL数据库中选择哪种时间戳类型?

Which timestamp type should I choose in a PostgreSQL database?

我想定义一个在多时区项目上下文中在Postgres数据库中存储时间戳的最佳实践。

我可以

  • 选择TIMESTAMP WITHOUT TIME ZONE并记住该字段在插入时使用的时区。
  • 选择TIMESTAMP WITHOUT TIME ZONE并添加另一个字段,该字段将包含插入时使用的时区名称。
  • 选择TIMESTAMP WITH TIME ZONE并相应插入时间戳
  • 我对选项3(带有时区的时间戳)有一点偏好,但我想对这个问题有一个有根据的意见。


    首先,PostgreSQL的时间处理和算法非常好,在一般情况下,选项3也很好。然而,这是一个不完整的时间和时区视图,可以补充:好的。

  • 将用户时区的名称存储为用户首选项(例如America/Los_Angeles,而不是-0700)。
  • 将用户事件/时间数据本地提交到其引用框架(很可能是与UTC的偏移量,如-0700)。
  • 在应用程序中,将时间转换为UTC并使用TIMESTAMP WITH TIME ZONE列存储。
  • 返回时间请求本地到用户时区(即从UTC转换为America/Los_Angeles)。
  • 将数据库的timezone设置为UTC
  • 这个选项并不总是有效的,因为很难获得用户的时区,因此对于轻量级应用程序,使用TIMESTAMP WITH TIME ZONE的对冲建议是可行的。也就是说,让我更详细地解释这个选项4的一些背景方面。好的。

    与选项3一样,WITH TIME ZONE的原因是发生某件事的时间是绝对时间。WITHOUT TIME ZONE产生一个相对时区。不要,永远,永远,永远混合绝对和相对的时间戳。好的。

    从程序和一致性的角度来看,确保所有计算都使用UTC作为时区。这不是PostgreSQL的要求,但在与其他编程语言或环境集成时,它会有所帮助。在列上设置一个CHECK,以确保对时间戳列的写入具有0的时区偏移量,这是一个防御位置,可以防止一些类型的错误(例如,脚本将数据转储到一个文件,而其他一些则使用词汇排序对时间数据进行排序)。同样,PostgreSQL不需要这样做来正确地进行日期计算或在时区之间转换(即,PostgreSQL非常擅长在任意两个时区之间转换时间)。为确保进入数据库的数据以零的偏移量存储:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    CREATE TABLE my_tbl (
      my_timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
      CHECK(EXTRACT(TIMEZONE FROM my_timestamp) = '0')
    );
    test=> SET timezone = 'America/Los_Angeles';
    SET
    test=> INSERT INTO my_tbl (my_timestamp) VALUES (NOW());
    ERROR:  NEW ROW FOR relation"my_tbl" violates CHECK CONSTRAINT"my_tbl_my_timestamp_check"
    test=> SET timezone = 'UTC';
    SET
    test=> INSERT INTO my_tbl (my_timestamp) VALUES (NOW());
    INSERT 0 1

    它不是100%完美的,但它提供了足够强大的反足迹措施,确保数据已经转换为UTC。关于如何做到这一点,有很多意见,但从我的经验来看,这似乎是最好的做法。好的。

    对数据库时区处理的批评在很大程度上是合理的(有很多数据库处理的能力非常差),但是PostgreSQL对时间戳和时区的处理非常出色(尽管这里和那里有一些"特性")。例如,一个这样的特性:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    -- Make sure we're all working off of the same local time zone
    test=> SET timezone = 'America/Los_Angeles';
    SET
    test=> SELECT NOW();
                  now              
    -------------------------------
     2011-05-27 15:47:58.138995-07
    (1 ROW)

    test=> SELECT NOW() AT TIME ZONE 'UTC';
              timezone          
    ----------------------------
     2011-05-27 22:48:02.235541
    (1 ROW)

    请注意,AT TIME ZONE 'UTC'使用目标的参照系(UTC创建一个相对的TIMESTAMP WITHOUT TIME ZONE。好的。

    当从不完整的TIMESTAMP WITHOUT TIME ZONE转换为TIMESTAMP WITH TIME ZONE时,丢失的时区继承自您的连接:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    test=> SET timezone = 'America/Los_Angeles';
    SET
    test=> SELECT EXTRACT(TIMEZONE_HOUR FROM NOW());
     date_part
    -----------
            -7
    (1 ROW)
    test=> SELECT EXTRACT(TIMEZONE_HOUR FROM TIMESTAMP WITH TIME ZONE '2011-05-27 22:48:02.235541');
     date_part
    -----------
            -7
    (1 ROW)

    -- Now change to UTC    
    test=> SET timezone = 'UTC';
    SET
    -- Create an absolute time with timezone offset:
    test=> SELECT NOW();
                  now              
    -------------------------------
     2011-05-27 22:48:40.540119+00
    (1 ROW)

    -- Creates a relative time in a given frame of reference (i.e. no offset)
    test=> SELECT NOW() AT TIME ZONE 'UTC';
              timezone          
    ----------------------------
     2011-05-27 22:48:49.444446
    (1 ROW)

    test=> SELECT EXTRACT(TIMEZONE_HOUR FROM NOW());
     date_part
    -----------
             0
    (1 ROW)

    test=> SELECT EXTRACT(TIMEZONE_HOUR FROM TIMESTAMP WITH TIME ZONE '2011-05-27 22:48:02.235541');
     date_part
    -----------
             0
    (1 ROW)

    底线是:好的。

    • 将用户的时区存储为命名标签(如America/Los_Angeles,而不是与UTC的偏移量(如-0700)
    • 除非有令人信服的理由存储非零偏移量,否则对所有内容都使用UTC
    • 将所有非零UTC时间视为输入错误
    • 从不混合和匹配相对和绝对时间戳
    • 如果可能的话,也使用UTC作为数据库中的timezone

    随机编程语言注释:python的datetime数据类型非常擅长保持绝对时间和相对时间之间的区别(尽管起初令人沮丧,直到您用类似pytz的库对其进行补充)。好的。

    编辑好的。

    我再解释一下相对和绝对的区别。好的。

    绝对时间用于记录事件。示例:"用户123登录"或"毕业典礼从2011-05-28太平洋标准时间下午2点开始"。无论您所在的时区是什么,如果您可以传送到事件发生的位置,您都可以见证事件的发生。数据库中的大多数时间数据都是绝对的(因此应该是TIMESTAMP WITH TIME ZONE,理想情况下,偏移量为+0,文本标签表示控制特定时区的规则,而不是偏移量)。好的。

    一个相对的事件是从一个尚未确定的时区的角度记录或安排某个事物的时间。例如:"我们公司的大门早上8点开门,晚上9点关门","让我们每周一早上7点开会,每周开早餐会",或"每个万圣节晚上8点",通常情况下,模板或工厂中的活动使用相对时间,而绝对时间几乎用于其他所有活动。有一个罕见的例外值得指出,它应该说明相对时间的价值。对于未来足够远的事件,如果不确定某些事情发生的绝对时间,则使用相对时间戳。下面是一个现实世界的例子:好的。

    假设是2004年,你需要在2008年10月31日美国西海岸下午1点交货(即America/Los_Angeles/PST8PDT)。如果您使用’2008-10-31 21:00:00.000000+00’::TIMESTAMP WITH TIME ZONE存储的是绝对时间,那么交付将在下午2点出现,因为美国政府通过了2005年《能源政策法》,该法改变了夏令时的管理规则。2004年计划交付时,10-31-2008的日期本应为太平洋标准时间(+8000),但从2005年开始+时区数据库承认10-31-2008本应为太平洋夏令时(+0700)。将相对时间戳与时区一起存储将导致正确的交付时间表,因为相对时间戳不受国会不知情篡改的影响。使用相对时间与绝对时间之间的界限是一条模糊的线,但我的经验法则是,对于未来超过3-6个月的任何事情,调度都应该使用相对时间戳(调度=绝对vs计划=相对)???).好的。

    另一种/最后一种相对时间是INTERVAL。示例:"用户登录后会话将超时20分钟"。INTERVAL可以与绝对时间戳(TIMESTAMP WITH TIME ZONE或相对时间戳(TIMESTAMP WITHOUT TIME ZONE一起正确使用。同样正确的说法是,"用户会话在成功登录后20分钟到期(登录时间为u utc+会话u duration)"或"我们的早餐会只能持续60分钟(重复的u start u time+会议u length)"。好的。

    最后一点混淆:DATETIMETIME WITHOUT TIME ZONETIME WITH TIME ZONE都是相对的数据类型。例如:'2011-05-28'::DATE表示相对日期,因为您没有可以用来标识午夜的时区信息。同样,'23:23:59'::TIME是相对的,因为您不知道时间所代表的时区或DATE。即使有了'23:59:59-07'::TIME WITH TIME ZONE,你也不知道DATE会是什么。最后,带时区的DATE实际上不是DATE,而是TIMESTAMP WITH TIME ZONE:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    test=> SET timezone = 'America/Los_Angeles';
    SET
    test=> SELECT '2011-05-11'::DATE AT TIME ZONE 'UTC';
          timezone      
    ---------------------
     2011-05-11 07:00:00
    (1 ROW)

    test=> SET timezone = 'UTC';
    SET
    test=> SELECT '2011-05-11'::DATE AT TIME ZONE 'UTC';
          timezone      
    ---------------------
     2011-05-11 00:00:00
    (1 ROW)

    在数据库中放置日期和时区是一件好事,但很容易得到微妙的错误结果。正确和完整地存储时间信息需要最少的额外工作,但这并不意味着总是需要额外的工作。好的。好啊。


    肖恩的回答过于复杂和误导。

    事实上,"有时区"和"没有时区"都将值存储为类似于Unix的绝对UTC时间戳。区别在于时间戳的显示方式。当"有时区"时,显示的值是转换到用户区域的UTC存储值。当"无时区"时,不管用户设置了哪个时区,都会扭曲UTC存储值以显示相同的时钟面。

    "无时区"唯一可用的情况是,无论实际时区如何,时钟面值都适用。例如,当时间戳指示投票站可能关闭的时间(即,无论某人的时区如何,投票站都会在20:00关闭)。

    使用选择3。除非有非常具体的理由不这样做,否则请始终使用"带时区"。


    我倾向于选择3,因为Postgres可以为你重新计算与时区相关的时间戳,而其他两个你必须自己计算。用时区存储时间戳的额外存储开销实际上可以忽略不计,除非您谈论的是数百万条记录,在这种情况下,您可能已经有了相当多的存储需求。