Поля часу та часу 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 та з 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 все одно збереже і поверне (лише у формі рядка!) Разів протягом "загубленої" години, коли починається літній час (наприклад 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 насправді безпечніший для арифметики часу, оскільки (здається, що) функції були реалізовані до того, як була додана функція літнього часу. Наприклад, TIMEDIFF ('2009-11-01 02:30:00', '2009-11-01 00:30:00') повертає 2:00, що правильно для UTC, але в Америці / New_York час становить 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 у контексті часового поясу сеансу - він не намагається виконати додавання в контексті правил літнього часу цього часового поясу.
kbro

@kbro Гаразд, але проблема залишається. Якщо сеанс tz є America/New_York, я не бачу способу зберігати 1257051600. Ви?
Стів Клей

76

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

Попри те , що я сказав в одному з моїх попередніх коментарів, DATETIME і TIMESTAMP поля дійсно поводяться по- різному. Поля TIMESTAMP (як зазначено в документах) беруть все, що ви надсилаєте у форматі "РРРР-ММ-ДД ч: мм: сс", і перетворюють його з поточного часового поясу на час UTC. Зворотне відбувається прозоро кожного разу, коли ви отримуєте дані. Поля DATETIME не роблять цього перетворення. Вони беруть все, що ви їм надсилаєте, і просто зберігають це безпосередньо.

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

Гаразд, тому ми повинні зберігати наші дані у часовому поясі, що не за літнім часом (наприклад, UTC). Поля TIMESTAMP не можуть точно обробляти ці дані з причин, які я пояснимо: якщо для вашої системи встановлений часовий пояс літнього часу, то те, що ви вводите в TIMESTAMP, може бути не тим, що ви отримаєте назад. Навіть якщо ви надішлете йому дані, які ви вже перетворили на UTC, він все одно буде приймати дані у вашому місцевому часовому поясі та здійснить ще одне перетворення в UTC. Цей примусовий TIMESTAMP туди-назад до локального зворотного туру збитковий, коли ваш місцевий часовий пояс дотримується літнього часу (з "карт 2011-01-01 01:30:00" до 2 різних можливих часів).

Завдяки 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. Для цього я використовую функцію strtotime () або клас DateTime PHP. Це неможливо надійно зробити всередині MySQL, використовуючи функції CONVERT_TZ () або UNIX_TIMESTAMP () MySQL, оскільки 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


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 - якщо я конвертую в Спочатку 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). Я заповнив таблицю тестовими даними, і все виглядало нормально, але я помітив, що в кінці графіка точки виходили на один день відповідно до міток на осі х. Після огляду я помітив, що кількість переглядів за день 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-річна нитка, і я знаю, що це не відповідь на питання ОП, але це може допомогти таким людям, як я, які зіткнулися на цій сторінці, шукаючи рішення проблеми, яку я описав.


Можливо, це не стосується власне питання, але це жахливо неефективно, і ніхто не повинен його копіювати! Натомість видайте один запит, наприклад:
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";
Робити
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.