【问题标题】:Converting PostgreSQL interval to seconds produces wrong values将 PostgreSQL 间隔转换为秒会产生错误的值
【发布时间】:2019-12-19 19:12:06
【问题描述】:

我正在使用 Postgresql 中的一些间隔,但我不确定我是否理解间隔的工作原理。

我想要做的是将间隔转换为秒。 所以我有以下价值观:

select extract ('epoch' from '1 year'::INTERVAL);

产生数字 31557600。如果我们将这个数字除以 (60*60*24),即一天中的秒数,我们得到 365。
所以在 postgresql 间隔中的一年有 365天

select extract ('epoch' from '1 month'::INTERVAL);

产生数字 2592000。如果我们将此数字除以 (60*60*24),我们得到 30。
所以 postgresql 间隔中的一个月有 30 天。

所以按照这个逻辑,我试图计算以下间隔中的秒数:

select extract ('epoch' from '2 year 2 month 1 day 10 hour 5 minute'::INTERVAL);

我正在使用以下公式来计算上述查询的结果:

SELECT (years * 365 * 24 * 3600) + (months * 30 * 24 * 3600) + (days * 24 * 3600) + (hours * 3600) + (minutes * 60);

当我们用值替换变量时,表达式如下:

SELECT (2 * 365 * 24 * 3600) + (2 * 30 * 24 * 3600) + (1 * 24 * 3600) + (10 * 3600) + (5 * 60);

我遇到的问题是第一个查询的结果 (SELECT extract...) 产生结果 68421900 而来自上面公式的查询产生结果 68378700.
据我了解,结果应该是相同的,但结果之间存在 12 小时(43200 秒)的差异。为什么会这样?

重要的是要注意,如果我从间隔中删除年份,我会从两个查询中得到相同的结果,所以我想这与年份有关。

【问题讨论】:

    标签: postgresql postgresql-11


    【解决方案1】:

    你的问题是整数算术。错误就在这里:

    select extract ('epoch' from '1 year'::INTERVAL);
    

    产生数字 31557600。如果我们将此数字除以 (60*60*24),即一天中的秒数,我们得到 365。

    你可能做的是

    SELECT 31557600 / (60 * 60 * 24);
    
     ?column? 
    ----------
          365
    (1 row)
    

    但现实是:

    SELECT extract (epoch FROM INTERVAL '1 year') / (60 * 60 * 24);
    
     ?column? 
    ----------
       365.25
    (1 row)
    

    所以 PostgreSQL 中一年的长度实际上是 365 天 6 小时。这是一个近似值(真实值略小),应该考虑闰年。


    注意:这些值有点随意,因为对于“一个(日历)月有多长”或“一个(日历)年有多长”没有单一的正确答案 - 答案取决于单个月份或年份.

    如果将interval 添加到timestamp with time zone,结果将始终正确,因为在这种情况下,确切的长度是明确的。


    关于为什么假设一年有 365.25 天,而一个月有 30 天的问题,以下是消息来源在src/include/datatype/timestamp.h 中所说的:

    /*
     * Assorted constants for datetime-related calculations
     */
    
    #define DAYS_PER_YEAR   365.25  /* assumes leap year every four years */
    #define MONTHS_PER_YEAR 12
    /*
     *      DAYS_PER_MONTH is very imprecise.  The more accurate value is
     *      365.2425/12 = 30.436875, or '30 days 10:29:06'.  Right now we only
     *      return an integral number of days, but someday perhaps we should
     *      also return a 'time' value to be used as well.  ISO 8601 suggests
     *      30 days.
     */
    #define DAYS_PER_MONTH  30              /* assumes exactly 30 days per month */
    #define HOURS_PER_DAY   24              /* assume no daylight savings time changes */
    
    /*
     *      This doesn't adjust for uneven daylight savings time intervals or leap
     *      seconds, and it crudely estimates leap years.  A more accurate value
     *      for days per years is 365.2422.
     */
    #define SECS_PER_YEAR   (36525 * 864)   /* avoid floating-point computation */
    #define SECS_PER_DAY    86400
    #define SECS_PER_HOUR   3600
    #define SECS_PER_MINUTE 60
    #define MINS_PER_HOUR   60
    

    所以我猜原因是 ISO 8601 规定一个月有 30 天。

    【讨论】:

    • 感谢您的回答,在一百万年后我不会注意到这一点:) 无论如何,为什么时间只添加到一年中?如果将月、小时、分钟甚至秒的长度调整为 12 个月加起来等于 1 年零 6 小时,那不是最好吗?
    • 这是一种方法。我同意当前的行为有些不一致:如果一年的长度受到tropical year 的启发,为什么不也使用lunar month?还是 365.25 / 12?我已阅读源代码并将相关部分添加到答案中。
    【解决方案2】:

    我遇到了类似的问题,最终找到了可行的方法。

    SELECT AVG( EXTRACT(EPOCH FROM (end_ts - start_ts)) )
    

    产生了不正确的值。

    SELECT AVG( EXTRACT(EPOCH FROM INTERVAL (end_ts - start_ts)) )
    

    产生了语法错误。

    从开始和结束时间戳中分别提取纪元,然后对它们进行数学运算。

    SELECT AVG( ( EXTRACT(EPOCH FROM end_ts) - EXTRACT(EPOCH FROM start_ts) ) )
    

    我添加了一些额外的数学来转换为小时数。

    SELECT AVG( ( EXTRACT(EPOCH FROM end_ts) - EXTRACT(EPOCH FROM start_ts) )::FLOAT/86400 )
    

    【讨论】:

      【解决方案3】:

      首先,你So a month in postgresql interval has 30 days.的说法是错误的:

      janbet=> select '2019-01-01'::date + '1 month'::interval;
            ?column?
      ---------------------
       2019-02-01 00:00:00
      (1 row)
      
      janbet=> select '2019-02-01'::date + '1 month'::interval;
            ?column?
      ---------------------
       2019-03-01 00:00:00
      (1 row)
      

      如您在上面看到的,月份可能是 31 天或 28 天,具体取决于上下文。事实上,它是一个,而月的长度不同。

      同样适用于'2 year 2 month 1 day 10 hour 5 minute'::interval:

      janbet=> select '2019-01-01'::date + '2 year 2 month 1 day 10 hour 5 minute'::interval - '2019-01-01'::date;
           ?column?
      -------------------
       791 days 10:05:00
      (1 row)
      
      janbet=> select '2021-01-01'::date + '2 year 2 month 1 day 10 hour 5 minute'::interval - '2021-01-01'::date;
           ?column?
      -------------------
       790 days 10:05:00
      (1 row)
      

      根据2 years 是否包含闰年,我们会得到不同的结果。

      我对你的 12 小时的最佳猜测是 2 年在 50% 的情况下包括闰年,所以这是 one day * 1/2。事实上,如果你用“3 年”重复计算,你会得到 18 小时的差异,所以我敢肯定它是这样工作的。

      我知道这可能令人惊讶,但我想不出与可变年/月长度一致的其他结果,这是所需的interval 行为。

      【讨论】:

      • 但如果是这样,为什么select extract ('epoch' from '1 month'::INTERVAL); 返回的秒数等于 30 天?为什么它不返回 28 或 29 或 31?当您将其添加到日期时,我理解并看到了您示例中的逻辑,但这意味着当它不带日期使用时,结果是不可预测的?
      • 为什么是 30 而不是其他东西(365 / 12 也许?) - 我不知道。没有一致的算术是可能的,所以可能有人认为1 month is 30 days 在大多数情况下是最好的答案,仅此而已。结果并非不可预测,但我不知道确切的规则。尽可能多地产生这个最佳答案可能非常复杂。
      • 是的,但是我需要这些规则才能始终如一地使用 interval 类型。我很感谢您的回答,这绝对是正确的方向,但这不是我正在寻找的答案。
      • 恕我直言interval 并不是要以您想要的方式使用它。例如,引用 wiki.postgresql.org/wiki/… Many larger INTERVAL values, like the calendar values they reflect, are not constant in length when expressed in smaller INTERVAL values - 你正在寻找的是这个较小间隔(秒)中的确切长度。无论如何,我也对其他答案很感兴趣。
      • 只是一个想法:您可能会考虑一个恒定的参考点(例如,纪元开始,1970-01-01),并按照我之前描述的方式计算秒数,方法是在日期和减去日期。这不是一个漂亮的解决方案,但至少您将拥有一个一致且可预测的基于 interval 的算法。
      猜你喜欢
      • 2020-01-17
      • 1970-01-01
      • 1970-01-01
      • 2011-02-18
      • 2014-10-06
      • 1970-01-01
      • 2015-09-14
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多