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

88

Я использую часовой пояс Америки / Нью-Йорка. Осенью мы «отступаем» на час - фактически «набираем» один час в 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 или PHP, чтобы сформировать последовательный ответ, но держу пари, что это как-то связано с преобразованием в UTC и обратно.
Марк Рэнсом,
2
Внутри они все хранятся в формате UTC, не так ли?
Эли
4
Я нашел web.ivy.net/~carton/rant/MySQL-timezones.txt интересным чтением по этой теме.
micahwittman
Хорошая ссылка, micahwittman - очень полезно.
Аарон
отличные вопросы. общая проблема.
Vardumper

Ответы:

47

Типы дат 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.

Стив Клэй
источник
Спасибо за всю вашу помощь с этим mrclay. Вы очень точно обрисовали здесь ситуацию.
Аарон
Похоже, что вариант 3 на самом деле более безопасен для арифметики времени, потому что (кажется, что) функции были реализованы до добавления функциональности DST. Например, TIMEDIFF ('2009-11-01 02:30:00', '2009-11-01 00:30:00') возвращает 2:00, что является правильным для UTC, но в Америке / Нью-Йорке время составляет 3 часа. Кроме.
Стив Клей,
1
-1: Вы допустили ошибку, что функции даты и времени MySQL работают с типом DATETIME, который не зависит от часового пояса. Следовательно, аргумент, который вы передаете UNIX_TIMSTAMP, select '2009-11-01 00:00:00' + INTERVAL 3600 SECOND;имеет значение '2009-11-01 01:00:00'. Затем UNIX_TIMESTAMP просто пытается скрыть это до UTC в контексте часового пояса сеанса - он не пытается выполнить добавление в контексте правил DST этого часового пояса.
kbro
@kbro ОК, но проблема остается. Если сеанс tz есть America/New_York, я не вижу возможности сохранить 1257051600. А вы?
Стив Клэй,
77

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

Вопреки тому , что я сказал в одном из моих предыдущих комментариев, 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-обратный переход от локального к UTC-обратно к локальному происходит с потерями, когда в вашем часовом поясе наблюдается летнее время (поскольку «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 не работают должным образом в пределах границ летнего времени, если вы храните свои даты в 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

Аарон
источник
13

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

SET SESSION time_zone = '+0:00'

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

Стив Клэй
источник
Этот совет работает нормально. Проблема решена после того, как я инициализирую все соединения в моем пуле с помощью данного оператора.
Snowindy
4

Этот тред сделал меня уродом, поскольку мы используем 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
Мануэль Дарво
источник
3

Но как сохранить что-нибудь до 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, часовой пояс от, а "SYSTEM" - это местный часовой пояс ОС, в которой работает MySQL.

Андомар
источник
Спасибо за ответ. Насколько я могу судить, несмотря на то, что говорится в документах, поля TIMESTAMP и Datetime ведут себя одинаково: они хранят свои данные в UTC, но ожидают, что их ввод будет в локальном времени, и они автоматически конвертируют его в UTC - если я конвертирую в Сначала база данных не знает, что я сделал, и добавляет к этому времени еще 4 (или 5, в зависимости от того, летнее время мы или нет). Итак, проблема остается: как указать 2009-11-01 01:30:00 -04: 00 в качестве ввода?
Аарон
Что ж, я обнаружил, что источником большей части моей путаницы является тот факт, что функция UNIX_TIMESTAMP () всегда интерпретирует свой параметр даты относительно текущего часового пояса, независимо от того, извлекаете ли вы данные из поля TIMESTAMP или DATETIME. . Теперь, когда я думаю об этом, это имеет смысл. Я обновлю больше позже.
Аарон
2

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;
Арпан Джайн
источник
1

Я работал над регистрацией количества посещений страниц и отображением их на графике (используя плагин 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шагом one day ( 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-летняя ветка, и я знаю, что это не ответ на вопрос OP, но это может помочь людям вроде меня, которые столкнулись с этой страницей, в поисках решения описанной мной проблемы.

Лукас
источник
Вероятно, не имеет отношения к актуальному вопросу, но это ужасно неэффективно, и никто не должен его копировать! Вместо этого введите один запрос, например:
Doin
"SELECT CAST(created_timestamp AS date) day,COUNT(*) WHERE item_id=:item_id AND (created_timestamp BETWEEN '".date("Y-m-d 00:00:00", $min_date_timestamp)."' AND '".date("Y-m-d 23:59:59", $max_date_timestamp)."') GROUP BY day ORDER BY day";
Doin