Поля даты и времени MySQL и летнее время - как я могу сослаться на "дополнительный" час?

Я использую часовой пояс Америки / Нью-Йорка. Осенью мы "отступаем" на час - фактически "набираем" один час в 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). Но это не кажется необходимым.

Большое спасибо за любые предложения.

8 ответов

Решение

Честно говоря, типы дат в MySQL не работают и не могут правильно храниться все время, если в вашей системе не установлен часовой пояс с постоянным смещением, например, UTC или GMT-5. (Я использую MySQL 5.0.45)

Это потому, что вы не можете хранить какое-либо время в течение часа до окончания летнего времени. Независимо от того, как вы вводите даты, каждая функция даты будет обрабатывать это время так, как если бы оно было в течение часа после переключения.

Часовой пояс моей системы America/New_York, Попробуем сохранить 1257051600 (вс, 01 ноября 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 (как обнаружил Аарон, TIMESTAMP даже не надежен)

  3. Представьте, что тип DATETIME имеет некоторый постоянный часовой пояс смещения. Например, если вы находитесь в America/New_York конвертируйте свою дату в GMT-5 за пределами MySQL, затем сохраняйте как DATETIME (это оказывается необходимым: см. ответ Аарона). Тогда вы должны быть очень осторожны, используя функции даты / времени MySQL, потому что некоторые предполагают, что ваши значения относятся к системному часовому поясу, другие (особенно арифметические функции времени) являются "независимыми от часового пояса" (они могут вести себя так, как если бы время было UTC).

Аарон и я подозреваю, что автоматически генерирующиеся столбцы TIMESTAMP также не работают. И то и другое 2009-11-01 01:30 -0400 а также 2009-11-01 01:30 -0500 будет храниться как неоднозначный 2009-11-01 01:30,

Я понял это для моих целей. Я суммирую то, что я узнал (извините, эти заметки многословны; они так же важны для моего будущего направления, как и все остальное).

Вопреки тому, что я сказал в одном из моих предыдущих комментариев, поля DATETIME и TIMESTAMP ведут себя по-разному. Поля TIMESTAMP (как указывают документы) берут все, что вы отправляете, в формате "ГГГГ-ММ-ДД чч: мм: сс" и преобразует его из текущего часового пояса в время UTC. Обратное происходит прозрачно всякий раз, когда вы получаете данные. Поля DATETIME не делают это преобразование. Они берут все, что вы им отправляете, и просто храните это напрямую.

Ни типы полей DATETIME, ни TIMESTAMP не могут точно хранить данные в часовом поясе, который соблюдает DST. Если вы храните "2009-11-01 01:30:00", в полях нет никакой возможности определить, какую версию 1:30 вы хотели - версию -04:00 или -05:00.

Итак, мы должны хранить наши данные в часовом поясе не в летнее время (например, UTC). Поля TIMESTAMP не могут обрабатывать эти данные точно по причинам, которые я объясню: если ваша система настроена на часовой пояс DST, то то, что вы поместили в TIMESTAMP, может быть не тем, что вы получите обратно. Даже если вы отправите данные, которые вы уже преобразовали в UTC, они все равно будут предполагать, что данные находятся в вашем местном часовом поясе, и выполнят еще одно преобразование в UTC. Эта вынужденная временная поездка туда и обратно, обеспечиваемая TIMESTAMP, вызывает потери, когда ваш местный часовой пояс соблюдает летнее время (так как "2009-11-01 01:30:00" отображается в 2 различных возможных времени).

С DATETIME вы можете хранить свои данные в любом часовом поясе и быть уверенным, что получите все, что отправите (вы не будете вынуждены переходить с потерями туда и обратно, на что навязывают вам поля TIMESTAMP). Таким образом, решение состоит в том, чтобы использовать поле DATETIME и перед сохранением в поле преобразовать из часового пояса вашей системы в любой не-DST-пояс, в котором вы хотите его сохранить (я думаю, что UTC, вероятно, лучший вариант). Это позволяет вам встроить логику преобразования в свой язык сценариев, чтобы можно было явно сохранить UTC-эквивалент "2009-11-01 01:30:00 -04:00" или ""2009-11-01 01:30:00 -05:00".

Еще одна важная вещь, которую стоит отметить, - то, что математические функции даты / времени MySQL не работают должным образом вокруг границ DST, если вы храните свои даты в DST TZ. Так что все больше причин экономить на UTC.

В двух словах, я сейчас делаю это:

При получении данных из базы данных:

Явно интерпретировать данные из базы данных как UTC за пределами MySQL, чтобы получить точную метку времени Unix. Для этого я использую функцию PHP strtotime() или ее класс DateTime. Это не может быть надежно выполнено внутри MySQL с использованием функций MySQL CONVERT_TZ() или UNIX_TIMESTAMP(), поскольку CONVERT_TZ будет выводить только значение "YYYY-MM-DD hh:mm:ss", которое страдает от проблем неоднозначности, а UNIX_TIMESTAMP() предполагает его вход находится в системном часовом поясе, а не в часовом поясе, в котором данные были фактически сохранены (UTC).

При хранении данных в базе данных:

Преобразуйте вашу дату в точное время UTC, которое вы хотите за пределами MySQL. Например: с помощью класса 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

Я думаю, что ссылка micahwittman имеет лучшее практическое решение этих ограничений MySQL: установите часовой пояс сеанса в UTC при подключении:

SET SESSION time_zone = '+0:00'

Тогда вы просто отправляете метки времени Unix, и все должно быть в порядке.

Но как мне сохранить что-нибудь до 01:30:00 -04:00?

Вы можете конвертировать в UTC как:

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


Более того, сохраните даты как поле TIMESTAMP. Это всегда хранится в UTC, а UTC не знает о летнем / зимнем времени.

Вы можете конвертировать из UTC в местное время, используя CONVERT_TZ:

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

Где "+00:00" - это UTC, часовой пояс "from", а "SYSTEM" - местный часовой пояс ОС, в которой работает MySQL.

Mysql по своей природе решает эту проблему, используя таблицу time_zone_name из mysql db. Используйте CONVERT_TZ во время CRUD для обновления даты и времени, не беспокоясь о переходе на летнее время.

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;

Эта тема заставила меня взбеситься, так как мы используем TIMESTAMP столбцы с On UPDATE CURRENT_TIMESTAMP (То есть: recordTimestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) отслеживать измененные записи и ETL в хранилище данных.

В случае, если кто-то задается вопросом, в этом случае, TIMESTAMP вести себя правильно, и вы можете различить две одинаковые даты, преобразовав TIMESTAMP Unix метка времени:

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

Я работал над регистрацией количества посещений страниц и отображением их на графике (используя плагин Flot jQuery). Я заполнил таблицу тестовыми данными, и все выглядело хорошо, но я заметил, что в конце графика точки были на один день согласно меткам на оси x. После проверки я заметил, что счетчик просмотров за день 2015-10-25 дважды извлекался из базы данных и передавался во Flot, поэтому каждый день после этой даты перемещался на один день вправо.
После того, как я некоторое время искал ошибку в своем коде, я понял, что именно тогда наступает время перехода на летнее время. Тогда я пришел на эту страницу 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

Затем в цикле, начиная с 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-летняя ветка, и я знаю, что это не ответ на вопрос ОП, но он может помочь таким людям, как я, которые сталкивались с этой страницей, в поисках решения описанной мной проблемы.

Если вам интересно, как обрабатывать EST / EDT во время преобразования. Это может помочь:

      SELECT convert_tz('2021-04-06 02:06:00','US/Eastern','UTC');

Или наоборот,

      SELECT convert_tz('2021-04-06 06:06:00','UTC','US/Eastern');
Другие вопросы по тегам