如何使用时区信息在 MySQL 中存储日期时间

我有成千上万张在坦桑尼亚拍摄的照片,我想在 MySQL 数据库中存储每张照片拍摄的日期和时间。然而,服务器位于美国,当我试图存储一个坦桑尼亚日期时间时遇到了问题,这个日期时间恰好在春季夏令时(在美国)的“无效”小时之内。坦桑尼亚没有 DST,所以时间实际上是一个有效的时间。

额外的复杂性是,来自许多不同时区的协作者需要访问存储在数据库中的日期时间值。我希望他们出来的 一直都是作为坦桑尼亚的时间,而不是在当地时间,各种合作者在。

我不愿意设置会话时间,因为我知道如果有人忘记设置会话时间,并且把时间都弄错了,就会出现问题。我无权更改服务器的任何内容。

我读过: 夏时制和时区最佳做法 MySQL 日期时间字段和夏时制——如何引用“额外”小时 在 PHP/MySQL 中将日期时间存储为 UTC

但他们似乎都没有解决我的特殊问题。我不是 SQL 专家; 在设置 DATETIMEs 时有没有指定时区的方法?我没见过。否则,任何关于如何处理这个问题的建议都是非常值得赞赏的。

编辑: 下面是我遇到的问题的一个例子:

INSERT INTO Images (CaptureEvent, SequenceNum, PathFilename, TimestampJPG)
VALUES (122,1,"S2/B04/B04_R1/IMAG0148.JPG","2011-03-13 02:49:10")

我得到了一个错误:

Error 1292: Incorrect datetime value: '2011-03-13 02:49:10' for column 'TimestampJPG'

这个日期和时间存在于坦桑尼亚,但不存在于数据库所在的美国。

222938 次浏览

You said:

I want them to always come out as Tanzanian time and not in the local times that various collaborator are in.

If this is the case, then you should not use UTC. All you need to do is to use a DATETIME type in MySQL instead of a TIMESTAMP type.

From the MySQL documentation:

MySQL converts TIMESTAMP values from the current time zone to UTC for storage, and back from UTC to the current time zone for retrieval. (This does not occur for other types such as DATETIME.)

If you are already using a DATETIME type, then you must be not setting it by the local time to begin with. You'll need to focus less on the database, and more on your application code - which you didn't show here. The problem, and the solution, will vary drastically depending on language, so be sure to tag the question with the appropriate language of your application code.

All the symptoms you describe suggest that you never tell MySQL what time zone to use so it defaults to system's zone. Think about it: if all it has is '2011-03-13 02:49:10', how can it guess that it's a local Tanzanian date?

As far as I know, MySQL doesn't provide any syntax to specify time zone information in dates. You have to change it a per-connection basis; something like:

SET time_zone = 'EAT';

If this doesn't work (to use named zones you need that the server has been configured to do so and it's often not the case) you can use UTC offsets because Tanzania does not observe daylight saving time at the time of writing but of course it isn't the best option:

SET time_zone = '+03:00';

MySQL stores DATETIME without timezone information. Let's say you store '2019-01-01 20:00:00' into a DATETIME field, when you retrieve that value you're expected to know what timezone it belongs to.

So in your case, when you store a value into a DATETIME field, make sure it is Tanzania time. Then when you get it out, it will be Tanzania time. Yay!

Now, the hairy question is: When I do an INSERT/UPDATE, how do I make sure the value is Tanzania time? Two cases:

  1. You do INSERT INTO table (dateCreated) VALUES (CURRENT_TIMESTAMP or NOW()).

  2. You do INSERT INTO table (dateCreated) VALUES (?), and specify the current time from your application code.

CASE #1

MySQL will take the current time, let's say that is '2019-01-01 20:00:00' Tanzania time. Then MySQL will convert it to UTC, which comes out to '2019-01-01 17:00:00', and store that value into the field.

So how do you get the Tanzania time, which is '20:00:00', to store into the field? It's not possible. Your code will need to expect UTC time when reading from this field.

CASE #2

It depends on what type of value you pass as ?. If you pass the string '2019-01-01 20:00:00', then good for you, that's exactly what will be stored to the DB. If you pass a Date object of some kind, then it'll depend on how the db driver interprets that Date object, and ultimate what 'YYYY-MM-DD HH:mm:ss' string it provides to MySQL for storage. The db driver's documentation should tell you.

I once also faced such an issue where i needed to save data which was used by different collaborators and i ended up storing the time in unix timestamp form which represents the number of seconds since january 1970 which is an integer format. Example todays date and time in tanzania is Friday, September 13, 2019 9:44:01 PM which when store in unix timestamp would be 1568400241

Now when reading the data simply use something like php or any other language and extract the date from the unix timestamp. An example with php will be

echo date('m/d/Y', 1568400241);

This makes it easier even to store data with other collaborators in different locations. They can simply convert the date to unix timestamp with their own gmt offset and store it in a integer format and when outputting this simply convert with a

None of the answers here quite hit the nail on the head.

How to store a datetime in MySQL with timezone info

Use two columns: DATETIME, and a VARCHAR to hold the time zone information, which may be in several forms:

A timezone or location such as America/New_York is the highest data fidelity.

A timezone abbreviation such as PST is the next highest fidelity.

A time offset such as -2:00 is the smallest amount of data in this regard.

Some key points:

  • Avoid TIMESTAMP because it's limited to the year 2038, and MySQL relates it to the server timezone, which is probably undesired.
  • A time offset should not be stored naively in an INT field, because there are half-hour and quarter-hour offsets.

If it's important for your use case to have MySQL compare or sort these dates chronologically, DATETIME has a problem:

'2009-11-10 11:00:00 -0500' is before '2009-11-10 10:00:00 -0700' in terms of "instant in time", but they would sort the other way when inserted into a DATETIME.

You can do your own conversion to UTC. In the above example, you would then have
'2009-11-10 16:00:00' and '2009-11-10 17:00:00' respectively, which would sort correctly. When retrieving the data, you would then use the timezone info to revert it to its original form.

One recommendation which I quite like is to have three columns:

  • local_time DATETIME
  • utc_time DATETIME
  • time_zone VARCHAR(X) where X is appropriate for what kind of data you're storing there. (I would choose 64 characters for timezone/location.)

An advantage to the 3-column approach is that it's explicit: with a single DATETIME column, you can't tell at a glance if it's been converted to UTC before insertion.


Regarding the descent of accuracy through timezone/abbreviation/offset:

  • If you have the user's timezone/location such as America/Juneau, you can know accurately what the wall clock time is for them at any point in the past or future (barring changes to the way Daylight Savings is handled in that location). The start/end points of DST, and whether it's used at all, are dependent upon location, so this is the only reliable way.
  • If you have a timezone abbreviation such as MST, (Mountain Standard Time) or a plain offset such as -0700, you will be unable to predict a wall clock time in the past or future. For example, in the United States, Colorado and Arizona both use MST, but Arizona doesn't observe DST. So if the user uploads his cat photo at 14:00 -0700 during the winter months, was he in Arizona or California? If you added six months exactly to that date, would it be 14:00 or 13:00 for the user?

These things are important to consider when your application has time, dates, or scheduling as core function.


References:

Beginning with MySQL 8.0.19, you can specify a time zone offset when inserting TIMESTAMP and DATETIME values into a table.

The offset is appended to the time part of a datetime literal, with no intervening spaces, and uses the same format used for setting the time_zone system variable, with the following exceptions:

  • For hour values less than 10, a leading zero is required.
  • The value '-00:00' is rejected.
  • Time zone names such as 'EET' and 'Asia/Shanghai' cannot be used; 'SYSTEM' also cannot be used in this context.

The value inserted must not have a zero for the month part, the day part, or both parts. This is enforced beginning with MySQL 8.0.22, regardless of the server SQL mode setting.


EXAMPLE:

This example illustrates inserting datetime values with time zone offsets into TIMESTAMP and DATETIME columns using different time_zone settings, and then retrieving them:

mysql> CREATE TABLE ts (
->     id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
->     col TIMESTAMP NOT NULL
-> ) AUTO_INCREMENT = 1;


mysql> CREATE TABLE dt (
->     id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
->     col DATETIME NOT NULL
-> ) AUTO_INCREMENT = 1;


mysql> SET @@time_zone = 'SYSTEM';


mysql> INSERT INTO ts (col) VALUES ('2020-01-01 10:10:10'),
->     ('2020-01-01 10:10:10+05:30'), ('2020-01-01 10:10:10-08:00');


mysql> SET @@time_zone = '+00:00';


mysql> INSERT INTO ts (col) VALUES ('2020-01-01 10:10:10'),
->     ('2020-01-01 10:10:10+05:30'), ('2020-01-01 10:10:10-08:00');


mysql> SET @@time_zone = 'SYSTEM';


mysql> INSERT INTO dt (col) VALUES ('2020-01-01 10:10:10'),
->     ('2020-01-01 10:10:10+05:30'), ('2020-01-01 10:10:10-08:00');


mysql> SET @@time_zone = '+00:00';


mysql> INSERT INTO dt (col) VALUES ('2020-01-01 10:10:10'),
->     ('2020-01-01 10:10:10+05:30'), ('2020-01-01 10:10:10-08:00');


mysql> SET @@time_zone = 'SYSTEM';


mysql> SELECT @@system_time_zone;
+--------------------+
| @@system_time_zone |
+--------------------+
| EST                |
+--------------------+


mysql> SELECT col, UNIX_TIMESTAMP(col) FROM dt ORDER BY id;
+---------------------+---------------------+
| col                 | UNIX_TIMESTAMP(col) |
+---------------------+---------------------+
| 2020-01-01 10:10:10 |          1577891410 |
| 2019-12-31 23:40:10 |          1577853610 |
| 2020-01-01 13:10:10 |          1577902210 |
| 2020-01-01 10:10:10 |          1577891410 |
| 2020-01-01 04:40:10 |          1577871610 |
| 2020-01-01 18:10:10 |          1577920210 |
+---------------------+---------------------+


mysql> SELECT col, UNIX_TIMESTAMP(col) FROM ts ORDER BY id;
+---------------------+---------------------+
| col                 | UNIX_TIMESTAMP(col) |
+---------------------+---------------------+
| 2020-01-01 10:10:10 |          1577891410 |
| 2019-12-31 23:40:10 |          1577853610 |
| 2020-01-01 13:10:10 |          1577902210 |
| 2020-01-01 05:10:10 |          1577873410 |
| 2019-12-31 23:40:10 |          1577853610 |
| 2020-01-01 13:10:10 |          1577902210 |
+---------------------+---------------------+

Note:

  • Sadly, the offset is not displayed when selecting a datetime value, even if one was used when inserting it.
  • The range of supported offset values is -13:59 to +14:00, inclusive.
  • Datetime literals that include time zone offsets are accepted as parameter values by prepared statements.
  • MySQL converts TIMESTAMP values from the current time zone to UTC for storage, and back from UTC to the current time zone for retrieval. (This does not occur for other types such as DATETIME.)

You can't... you will find a lot of answers saying you "it is not necessary, store using UTC", but it is: you need to store datetimes with the timezone and MySQL can't...

I worked last 10 years in Postgres and all this kind of problems doesn't exist (date times and timezones are managed with no friction, you can store and compare datetimes expressed in different time zones transparently, the ISOString format is managed naturally,etc...).

I actually work in MariaDB and I can't understand why in 2022, in a globalized world, MySQL continues not supporting per value timezones.