97

我正在使用美国/纽约时区。在秋季,我们“后退”一个小时——实际上是在凌晨 2 点“获得”一个小时。在过渡点发生以下情况:

现在是 01:59:00 -04:00
然后 1 分钟后变成:
01:00:00 -05:00

因此,如果您只是说“凌晨 1:30”,那么对于您是指 1:30 的第一次还是第二次来说,这是模棱两可的。我正在尝试将调度数据保存到 MySQL 数据库,但无法确定如何正确保存时间。

这是问题所在:
“2009-11-01 00:30:00”在内部存储为 2009-11-01 00:30:00 -04:00
“2009-11-01 01:30:00”在内部存储为2009-11-01 01:30:00 -05:00

这很好,也很符合预期。但是如何将任何内容保存到 01:30:00 -04:00?该文档没有显示对指定偏移量的任何支持,因此,当我尝试指定偏移量时,它被适当地忽略了。

我想到的唯一解决方案是将服务器设置为不使用夏令时的时区,并在我的脚本中进行必要的转换(为此我使用 PHP)。但这似乎没有必要。

非常感谢您的任何建议。

4

7 回答 7

88

为了我的目的,我已经弄清楚了。我会总结一下我学到的东西(抱歉,这些笔记很冗长;它们对于我未来的推荐和其他任何东西一样重要)。

与我在之前的评论中所说的相反,DATETIME 和 TIMESTAMP 字段的行为确实不同。TIMESTAMP 字段(如文档所示)采用“YYYY-MM-DD hh:mm:ss”格式发送的任何内容,并将其从当前时区转换为 UTC 时间。每当您检索数据时,相反的情况就会透明地发生。DATETIME 字段不进行此转换。他们拿走你寄给他们的任何东西,然后直接存储。

DATETIME 和 TIMESTAMP 字段类型都不能准确地将数据存储在遵守 DST 的时区中。如果您存储“2009-11-01 01:30:00”,则这些字段无法区分您想要的凌晨 1:30 的哪个版本——-04:00 或 -05:00 版本。

好的,所以我们必须将数据存储在非 DST 时区(例如 UTC)中。由于我将解释的原因,TIMESTAMP 字段无法准确处理此数据:如果您的系统设置为 DST 时区,那么您放入 TIMESTAMP 的内容可能不是您返回的内容。即使您向其发送已转换为 UTC 的数据,它仍会假定数据位于您的本地时区并再次转换为 UTC。当您的本地时区遵守 DST 时(因为“2009-11-01 01:30:00”映射到 2 个不同的可能时间),此 TIMESTAMP 强制执行的本地到 UTC-回到本地往返是有损的。

使用 DATETIME,您可以将数据存储在您想要的任何时区,并确信无论您发送什么数据,您都会取回(您不会被迫进行 TIMESTAMP 字段强加给您的有损往返转换)。因此,解决方案是使用 DATETIME 字段,然后在保存到字段之前将系统时区转换为您想要保存的任何非 DST 区域(我认为 UTC 可能是最好的选择)。这允许您将转换逻辑构建到脚本语言中,以便您可以显式保存“2009-11-01 01:30:00 -04:00”或“2009-11-01 01:30”的 UTC 等效值: 00 -05:00”。

另一个需要注意的重要事项是,如果您将日期存储在 DST TZ 中,MySQL 的日期/时间数学函数在 DST 边界附近无法正常工作。因此,更有理由保存 UTC。

简而言之,我现在这样做:

从数据库中检索数据时:

将数据库中的数据显式解释为 MySQL 之外的 UTC,以获得准确的 Unix 时间戳。为此,我使用 PHP 的 strtotime() 函数或其 DateTime 类。它不能在 MySQL 内部使用 MySQL 的 CONVERT_TZ() 或 UNIX_TIMESTAMP() 函数可靠地完成,因为 CONVERT_TZ 只会输出存在歧义问题的 'YYYY-MM-DD hh:mm:ss' 值,并且 UNIX_TIMESTAMP() 假定它的输入在系统时区中,而不是数据实际存储在 (UTC) 中的时区。

将数据存储到数据库时:

将您的日期转换为您希望在 MySQL 之外的精确 UTC 时间。例如:使用 PHP 的 DateTime 类,您可以指定“2009-11-01 1:30:00 EST”与“2009-11-01 1:30:00 EDT”不同,然后将其转换为 UTC 并保存正确的 UTC 时间到您的 DATETIME 字段。

呸。非常感谢大家的投入和帮助。希望这可以为其他人节省一些麻烦。

顺便说一句,我在 MySQL 5.0.22 和 5.0.27 上看到了这个

于 2009-10-30T16:30:58.237 回答
51

坦率地说,MySQL 的日期类型已损坏,并且无法正确存储所有时间,除非您的系统设置为恒定偏移时区,如 UTC 或 GMT-5。(我使用的是 MySQL 5.0.45)

这是因为您不能在夏令时结束前的一小时内存储任何时间。无论您如何输入日期,每个日期函数都会将这些时间视为在切换后的一小时内。

我系统的时区是America/New_York. 让我们尝试存储 1257051600(Sun, 01 Nov 2009 06:00:00 +0100)。

这里使用专有的 INTERVAL 语法:

SELECT UNIX_TIMESTAMP('2009-11-01 00:00:00' + INTERVAL 3599 SECOND); # 1257051599
SELECT UNIX_TIMESTAMP('2009-11-01 00:00:00' + INTERVAL 3600 SECOND); # 1257055200

SELECT UNIX_TIMESTAMP('2009-11-01 01:00:00' - INTERVAL 1 SECOND); # 1257051599
SELECT UNIX_TIMESTAMP('2009-11-01 01:00:00' - INTERVAL 0 SECOND); # 1257055200

甚至FROM_UNIXTIME()不会返回准确的时间。

SELECT UNIX_TIMESTAMP(FROM_UNIXTIME(1257051599)); # 1257051599
SELECT UNIX_TIMESTAMP(FROM_UNIXTIME(1257051600)); # 1257055200

奇怪的是,DATETIME仍然在 DST 开始的“丢失”小时内存储和返回(仅以字符串形式!)时间(例如2009-03-08 02:59:59)。但是在任何 MySQL 函数中使用这些日期是有风险的:

SELECT UNIX_TIMESTAMP('2009-03-08 01:59:59'); # 1236495599
SELECT UNIX_TIMESTAMP('2009-03-08 02:00:00'); # 1236495600
# ...
SELECT UNIX_TIMESTAMP('2009-03-08 02:59:59'); # 1236495600
SELECT UNIX_TIMESTAMP('2009-03-08 03:00:00'); # 1236495600

要点:如果您需要在一年中的每次存储和检索,您有一些不受欢迎的选择:

  1. 将系统时区设置为 GMT + 一些恒定偏移量。例如 UTC
  2. 将日期存储为 INT(正如 Aaron 发现的那样,TIMESTAMP 甚至不可靠)

  3. 假装 DATETIME 类型有一些恒定的偏移时区。例如,如果您在America/New_York,请将您的日期转换为 MySQL 之外的GMT-5 ,然后存储为 DATETIME (事实证明这是必不可少的:请参阅 Aaron 的答案)。然后你必须非常小心地使用 MySQL 的日期/时间函数,因为有些假设你的值是系统时区的,而其他的(尤其是时间算术函数)是“时区不可知论的”(它们可能表现得好像时间是 UTC)。

Aaron 和我怀疑自动生成的 TIMESTAMP 列也被破坏了。两者2009-11-01 01:30 -04002009-11-01 01:30 -0500都将被存储为模棱两可的2009-11-01 01:30

于 2009-10-30T15:13:53.260 回答
13

我认为 micahwittman 的链接对这些 MySQL 限制具有最佳实用的解决方案: 连接时将会话时区设置为 UTC:

SET SESSION time_zone = '+0:00'

然后你只需向它发送 Unix 时间戳,一切都应该没问题。

于 2010-03-29T22:21:39.183 回答
5

但是如何将任何内容保存到 01:30:00 -04:00?

您可以转换为 UTC,例如:

SELECT CONVERT_TZ('2009-11-29 01:30:00','-04:00','+00:00');


更好的是,将日期保存为TIMESTAMP字段。这始终存储在 UTC 中,而 UTC 不知道夏季/冬季时间。

您可以使用CONVERT_TZ从 UTC 转换为本地时间:

SELECT CONVERT_TZ(UTC_TIMESTAMP(),'+00:00','SYSTEM');

其中 '+00:00' 是 UTC,from timezone 和 'SYSTEM' 是运行 MySQL 的操作系统的本地时区。

于 2009-10-29T22:22:17.150 回答
4

这个线程让我很抓狂,因为我们使用TIMESTAMP带有On UPDATE CURRENT_TIMESTAMP(即:)的列recordTimestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP来跟踪更改的记录和 ETL 到数据仓库。

如果有人想知道,在这种情况下,行为是否正确,您可以通过将 转换为 unix 时间戳TIMESTAMP来区分两个相似的日期:TIMESTAMP

select TestFact.*, UNIX_TIMESTAMP(recordTimestamp) from TestFact;

id  recordTimestamp         UNIX_TIMESTAMP(recordTimestamp)
1   2012-11-04 01:00:10.0   1352005210
2   2012-11-04 01:00:10.0   1352008810
于 2013-02-15T19:05:19.130 回答
4

Mysql 使用 mysql db 中的 time_zone_name 表固有地解决了这个问题。在 CRUD 时使用 CONVERT_TZ 来更新日期时间,而不必担心夏令时。

SELECT
  CONVERT_TZ('2019-04-01 00:00:00','Europe/London','UTC') AS time1,
  CONVERT_TZ('2019-03-01 00:00:00','Europe/London','UTC') AS time2;
于 2019-04-01T11:51:45.953 回答
1

我正在记录页面访问次数并在图表中显示计数(使用 Flot jQuery 插件)。我用测试数据填满了表格,一切看起来都很好,但我注意到在图表的末尾,根据 x 轴上的标签,这些点是休息一天。经过检查,我注意到 2015 年 10 月 25 日的查看次数从数据库中检索了两次并传递给了 Flot,因此在此日期之后的每一天都向右移动了一天。
在我的代码中寻找了一段时间的错误后,我意识到这个日期是 DST 发生的时间。然后我来到了这个 SO 页面...
...但是建议的解决方案对于我需要的东西来说太过分了,或者它们还有其他缺点。我不是很担心无法区分不明确的时间戳。我只需要每天计算和显示记录。

首先,我检索日期范围:

SELECT 
    DATE(MIN(created_timestamp)) AS min_date, 
    DATE(MAX(created_timestamp)) AS max_date 
FROM page_display_log
WHERE item_id = :item_id

然后,在一个 for 循环中,从 开始min_date,以 结束max_date,一步一步(60*60*24),我正在检索计数:

for( $day = $min_date_timestamp; $day <= $max_date_timestamp; $day += 60 * 60 * 24 ) {
    $query = "
        SELECT COUNT(*) AS count_per_day
        FROM page_display_log
        WHERE 
            item_id = :item_id AND
            ( 
                created_timestamp BETWEEN 
                '" . date( "Y-m-d 00:00:00", $day ) . "' AND
                '" . date( "Y-m-d 23:59:59", $day ) . "'
            )
    ";
    //execute query and do stuff with the result
}

我对我的问题的最终快速解决方案是:

$min_date_timestamp += 60 * 60 * 2; // To avoid DST problems
for( $day = $min_date_timestamp; $day <= $max_da.....

所以我不是在一天开始时盯着循环,而是两个小时后。这一天仍然是一样的,我仍在检索正确的计数,因为我明确地向数据库询问当天 00:00:00 到 23:59:59 之间的记录,而不管时间戳的实际时间如何。而当时间跳跃一小时,我仍然在正确的一天。

注意:我知道这是 5 年的主题,我知道这不是对 OPs 问题的回答,但它可能会帮助像我这样遇到此页面的人寻找我所描述的问题的解决方案。

于 2015-07-08T21:58:53.043 回答