Поля MySQL datetime и летнее время - как я могу ссылаться на "дополнительный" час?
Я использую часовой пояс Америка/Нью-Йорк. Осенью мы "отступаем" на час - эффективно "набираем" один час в 2 часа ночи. В точке перехода происходит следующее:
it 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). Но это не похоже, что это необходимо.
Большое спасибо за любые предложения.
Ответы
Ответ 1
Типы дат 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
Вынос: если вам нужно хранить и извлекать каждый раз в году, у вас есть несколько нежелательных вариантов:
- Установите системный часовой пояс на GMT + некоторое постоянное смещение. Например. UTC
-
Сохранять даты как INT (как обнаружил Аарон, TIMESTAMP не является даже надежным)
-
Притвориться, что тип DATETIME имеет некоторый часовой пояс с постоянным смещением. Например. Если вы находитесь в America/New_York
, конвертируйте свою дату в GMT-5 за пределами MySQL, а затем сохраните ее как DATETIME (это оказывается существенным: см. Ответ Aaron). Затем вы должны проявлять большую осторожность, используя функции даты и времени MySQL, потому что некоторые предполагают, что ваши значения относятся к системному часовому поясу, другие (например, арифметические функции времени) являются "агностиками временной зоны" (они могут вести себя так, как если бы время было UTC).
Мы с Аароном подозреваем, что также генерируются автогенерирующие столбцы TIMESTAMP. Оба 2009-11-01 01:30 -0400
и 2009-11-01 01:30 -0500
будут сохранены как неоднозначные 2009-11-01 01:30
.
Ответ 2
Я понял это для своих целей. Я обобщу то, что узнал (извините, эти заметки многословны, они так же важны для моего будущего направления, как и все остальное).
В отличие от того, что я сказал в одном из моих предыдущих комментариев, поля DATETIME и TIMESTAMP do ведут себя по-другому. Поля TIMESTAMP (как указано в документах) принимают все, что вы отправляете в формате "ГГГГ-ММ-ДД чч: мм: 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. Этот TIMESTAMP-принудительный переход между локальными и UTC-обратными к местным является потерями, когда ваш локальный часовой пояс наблюдает за DST (поскольку "2009-11-01 01:30:00" отображает до двух разных возможных времен).
С 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
Ответ 3
Я думаю, что micahwittman имеет лучшее практическое решение для этих ограничений MySQL: установите часовой пояс сеанса на UTC при подключении:
SET SESSION time_zone = '+0:00'
Затем вы просто отправляете отметки времени Unix, и все должно быть хорошо.
Ответ 4
Но как мне сэкономить до 01:30:00 -04: 00
Вы можете конвертировать в UTC, например:
SELECT CONVERT_TZ('2009-11-29 01:30:00','-04:00','+00:00');
Еще лучше сохранить даты как TIMESTAMP. Это всегда хранится в UTC, и UTC не знает о летнем/зимнем времени.
Вы можете конвертировать из UTC в localtime, используя CONVERT_TZ:
SELECT CONVERT_TZ(UTC_TIMESTAMP(),'+00:00','SYSTEM');
Где "+00: 00" - это UTC, часовой пояс и "SYSTEM" - локальный часовой пояс для ОС, где работает MySQL.
Ответ 5
Этот поток сделал меня freak, поскольку мы используем столбцы 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
Ответ 6
Я работал над протоколированием посещений страниц и отображением графов в графе (с использованием плагина Flot jQuery). Я заполнил таблицу тестовыми данными, и все выглядело отлично, но я заметил, что в конце графика точки были в один выходной день в соответствии с метками на оси х. После осмотра я заметил, что количество просмотров на день 2015-10-25 было дважды извлечено из базы данных и передано Флоту, поэтому каждый день после этой даты был перемещен на один день вправо.
После поиска ошибки в моем коде какое-то время я понял, что эта дата, когда 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, но он может помочь таким людям, как я, столкнувшимся с этой страницей, найти решение проблемы, которую я описал.