Демо: как ломается время при использовании TIMESTAMP в MySQL
Происходящее ниже — иллюстрация к этой статье.
Лабораторная работа
Нам понадобится Docker (по моему мнению самый простой способ провести эксперимент с системным часовым поясом без влияния на реальный часовой пояс используемой системы) и bash. Если с ними проблемы, то результат (полученный, правда немного другим путем: менялся не системный часовой пояс, а session.time_zone
), можно увидеть на
SQL Fiddle.
Запустим MySQL-сервер (и подождем, пока он запустится):
docker run --name time_test -e MYSQL_ROOT_PASSWORD=helloworld -d mysql:5.7 && \
docker exec -i -e MYSQL_PWD=helloworld time_test bash <<< 'while ! mysql -u root -e exit 2>/dev/null; do sleep 1; echo -n .; done; echo " MySQL seems to be ready"'
Убедимся, что на сервере UTC:
docker exec -it time_test date; \
docker exec -it time_test cat /etc/timezone; \
docker exec -i -e MYSQL_PWD=helloworld time_test mysql -u root <<< "SELECT NOW();"; \
docker exec -i -e MYSQL_PWD=helloworld time_test mysql -u root <<< "SHOW VARIABLES LIKE 'system_time_zone';"
Sat Sep 30 16:51:27 UTC 2017
Etc/UTC
NOW()
2017-09-30 16:51:27
Variable_name Value
system_time_zone UTC
Создадим таблицу следующей структуры:
+-------+------------------+------+-----+-------------------+-----------------------------+
| Field | Type | Null | Key | Default | Extra |
+-------+------------------+------+-----+-------------------+-----------------------------+
| id | int(10) unsigned | NO | | NULL | |
| ts | timestamp | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+-------+------------------+------+-----+-------------------+-----------------------------+
docker exec -i -e MYSQL_PWD=helloworld time_test mysql -u root <<< "
CREATE DATABASE test;
USE test;
CREATE TABLE timestamps (id int NOT NULL, ts timestamp NOT NULL);
"
Сделаем вставку с текущим временем:
docker exec -i -e MYSQL_PWD=helloworld time_test mysql -u root test <<< "INSERT INTO timestamps VALUES (1, '$(date +%Y-%m-%d\ %H:%M:%S)');"
Подстановку $(date +%Y-%m-%d\ %H:%M:%S)
осуществит bash, это текущее системное время в формате, требуемом MySQL.
Так, теперь поменяем системный часовой пояс и убедимся, что всё применилось:
docker exec -i time_test tee /etc/timezone <<< 'Europe/Minsk'; \
docker exec -i time_test dpkg-reconfigure -f noninteractive tzdata; \
docker exec -i time_test date
Europe/Minsk
Current default time zone: 'Europe/Minsk'
Local time is now: Sat Sep 30 19:51:51 +03 2017.
Universal Time is now: Sat Sep 30 16:51:51 UTC 2017.
Sat Sep 30 19:51:51 +03 2017
В результате выполнения date
(последняя строка) UTC изменился на +03 – верный признак успеха.
Перезапустим MySQL, чтобы применилось новое значение системного часового пояса:
docker restart time_test && \
docker exec -i -e MYSQL_PWD=helloworld time_test bash <<< 'while ! mysql -u root -e exit 2>/dev/null; do sleep 1; echo -n .; done; echo " MySQL seems to be ready"' && \
docker exec -i -e MYSQL_PWD=helloworld time_test mysql -u root test <<< "SELECT NOW();" && \
docker exec -i -e MYSQL_PWD=helloworld time_test mysql -u root test <<< "SHOW VARIABLES LIKE 'system_time_zone';"
. MySQL seems to be ready
NOW()
2017-09-30 19:52:13
Variable_name Value
system_time_zone +03
Сделаем еще одну вставку и посмотрим на результат:
docker exec -i -e MYSQL_PWD=helloworld time_test mysql -u root test <<< "INSERT INTO timestamps VALUES (2, '$(date +%Y-%m-%d\ %H:%M:%S)');" && \
docker exec -i -e MYSQL_PWD=helloworld time_test mysql -u root test <<< "SELECT * FROM timestamps;"
id ts
1 2017-09-30 22:51:45
2 2017-09-30 19:52:24
Несмотря на то, что в моем случае между вставками прошло всего 39 секунд, по результатам выборки этого никогда не скажешь. Давайте еще раз разберемся в причинах. При первой вставке MySQL считал переданное значение временем с нулевым смещением относительно UTC и посчитал timestamp для этого времени. На PHP это бы выглядело примерно следующим образом:
<?php
$timeZone = new DateTimeZone('UTC');
$timeToInsert = new DateTime('2017-09-30T19:51:45', $timeZone);
$unixTimestamp = $timeToInsert->getTimestamp();
var_dump($unixTimestamp);
int(1506801105)
Во второй раз было передано время 2017-09-30 19:52:24:
<?php
$timeZone = new DateTimeZone('+03:00');
$timeToInsert = new DateTime('2017-09-30T19:52:24', $timeZone);
$unixTimestamp = $timeToInsert->getTimestamp();
var_dump($unixTimestamp);
int(1506790344)
После этого мы сделали запрос на выборку и MySQL вывел эти значения Unix Timestamp в текущем часовом поясе, т.е. +03:00:
<?php
$records = [
['id' => 1, 'ts' => 1506801105],
['id' => 2, 'ts' => 1506790344],
];
echo 'id', "\t", 'ts', "\n";
foreach ($records as $record) {
$dateTime = new DateTime("@{$record['ts']}");
$dateTime->setTimeZone(new DateTimeZone('+03:00'));
echo $record['id'], "\t", $dateTime->format('Y-m-d H:i:s'), "\n";
}
id ts
1 2017-09-30 22:51:45
2 2017-09-30 19:52:24
Таким образом, значения в базе начинают идти вразнос, если кто-то меняет значение часового пояса в MySQL любым из приведенных выше способов.