Поля даты и времени 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).Но, похоже, в этом нет необходимости.
Большое спасибо за любые предложения.
Решение
Типы дат в MySQL, честно говоря, не работают и не могут правильно хранить все раз, если в вашей системе не установлен часовой пояс с постоянным смещением, например UTC или GMT-5. (Я использую MySQL 5.0.45)
Это потому, что вы не можете хранить любое время в течение часа до окончания летнего времени . Независимо от того, как вы вводите даты, каждая функция даты будет обрабатывать это время так, как если бы оно было в течение часа после переключения.
Часовой пояс моей системы - Америка / 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 будет по-прежнему сохранять и возвращать (только в строковой форме!) времена в течение " потерянного " час, когда начинается летнее время (например, 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
Вывод. Если вам нужно хранить и получать каждый раз в году, у вас есть несколько нежелательных вариантов:
<Ол>Сохраняйте даты как INT (как обнаружил Аарон, TIMESTAMP даже ненадежен)
Представьте, что тип 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 1:30 код>.
Другие советы
Я это придумал для своих целей.Я подытожу то, что узнал (извините, эти заметки слишком многословны;они нужны для моего будущего направления, как и все остальное).
Вопреки тому, что я сказал в одном из моих предыдущих комментариев, поля DATETIME и TIMESTAMP делать вести себя по-другому.Поля TIMESTAMP (как указано в документации) принимают все, что вы им отправляете, в формате «ГГГГ-ММ-ДД чч: мм: сс» и преобразуют его из вашего текущего часового пояса во время UTC.Обратное происходит прозрачно всякий раз, когда вы извлекаете данные.Поля DATETIME не выполняют это преобразование.Они берут все, что вы им отправляете, и просто сохраняют это напрямую.
Ни типы полей DATETIME, ни TIMESTAMP не могут точно хранить данные в часовом поясе, в котором соблюдается летнее время..Если вы сохраните «2009-11-01 01:30:00», в полях не будет возможности отличить, какую версию 1:30 утра вы хотите — версию -04:00 или -05:00.
Итак, мы должны хранить наши данные в часовом поясе, отличном от летнего времени (например, UTC).Поля TIMESTAMP не могут точно обрабатывать эти данные по причинам, которые я объясню:если в вашей системе установлен часовой пояс летнего времени, то то, что вы вводите в TIMESTAMP, может не соответствовать тому, что вы получите обратно.Даже если вы отправите ему данные, которые вы уже конвертировали в UTC, он все равно будет считать, что данные относятся к вашему местному часовому поясу, и выполнит еще одно преобразование в UTC.Этот двусторонний обмен между локальным и UTC-обратно-локальным, обеспечиваемый TIMESTAMP, осуществляется с потерями, когда в вашем местном часовом поясе наблюдается летнее время (поскольку «2009-11-01 01:30:00» соответствует двум различным возможным временам).
С помощью DATETIME вы можете хранить свои данные в любом часовом поясе и быть уверенными, что получите обратно все, что вы их отправите (вас не заставляют выполнять двусторонние преобразования с потерями, которые вам навязывают поля TIMESTAMP).Итак, решение состоит в том, чтобы использовать поле DATETIME и перед сохранением в поле конвертируйте часовой пояс вашей системы в любую зону, отличную от летнего времени, в которой вы хотите его сохранить (я думаю, что UTC, вероятно, лучший вариант).Это позволяет вам встроить логику преобразования в ваш язык сценариев, чтобы вы могли явно сохранять эквивалент UTC «2009-11-01 01:30:00 -04:00» или «2009-11-01 01:30»: 00-05:00».
Еще одна важная вещь, которую следует отметить, это то, что математические функции даты и времени MySQL не работают должным образом в пределах границ летнего времени, если вы храните свои даты в TZ летнего времени.Так что тем больше причин экономить в UTC.
В двух словах я сейчас делаю так:
При получении данных из базы данных:
Явно интерпретируйте данные из базы данных как UTC вне MySQL, чтобы получить точную временную метку Unix.Для этого я использую функцию PHP strtotime() или ее класс DateTime.Это невозможно надежно сделать внутри MySQL с использованием функций MySQL CONVERT_TZ() или UNIX_TIMESTAMP(), поскольку CONVERT_TZ будет выводить только значение 'ГГГГ-ММ-ДД чч:мм:сс', которое страдает от проблем неоднозначности, а UNIX_TIMESTAMP() предполагает его ввод находится в системном часовом поясе, а не в часовом поясе, в котором данные ФАКТИЧЕСКИ хранились (UTC).
При сохранении данных в базе данных:
Преобразуйте дату в точное время UTC, которое вам нужно, за пределами MySQL.Например:с помощью класса DateTime PHP вы можете указать «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, и все должно быть в порядке.
Этот поток меня разозлил, поскольку мы используем столбцы TIMESTAMP
с в UPDATE CURRENT_TIMESTAMP
(т.е.: отметка времени recordTimestamp NOT NULL DEFAULT CURRENT_TIMESTAMP в 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
Но как мне сохранить что -либо до 01:30:00 -04: 00?
Вы можете конвертировать в UTC, например:
SELECT CONVERT_TZ('2009-11-29 01:30:00','-04:00','+00:00');
Еще лучше, сохраните даты как ТАЙМ-МЕТКА поле.Оно всегда хранится в формате 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;
Я работал над регистрацией количества посещений страниц и отображением их на графике (используя плагин 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
Затем в цикле 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-летняя ветка, и я знаю, что это не ответ на вопрос ОП, но он может помочь таким людям, как я, которые столкнулись с этой страницей, найти решение описанной мной проблемы.