Поля 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

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