最近在学习中(使用数据库迁移工具:从MySQL迁移数据到Postgresql)遇到了这么一个问题,使用工具MySQL_fdw连接外部表,其源码链接MySQL_fdw的git仓库。其编译安装可以参考这位老哥的博客:PostgreSQL插件:MySQL_fdw源码安装使用
背景如下:
1、MySQL的TIMESTAMP类型 和 PG的TIMESTAMP类型(名字相同,然相差巨大)
2、当使用一些数据迁移工具的过程中,会发现有时差的存在
3、数据的存储原理不同,造成“数据的不一致”现象
PostgreSQL 提供两种存储时间戳的数据类型: 不带时区的 TIMESTAMP 和带时区的 TIMESTAMPTZ。
- TIMESTAMP 数据类型可以同时存储日期和时间,但它不存储时区。这意味着,当修改了数据库服务器所在的时区时,它里面存储的值也是不会改变。
- TIMESTAMPTZ 数据类型在存储日期和时间的同时还能正确处理时区。PostgreSQL 使用 UTC 值(世界标准时间)来存储 TIMESTAMPTZ 数据。在向 TIMESTAMPTZ 字段插入值的时候,PostgreSQL 会自动将值转换成 UTC 值,并保存到表里。当从一个 TIMESTAMPTZ 字段查询数据的时候,PostgreSQL 会把存储在其中的 UTC 值转换成数据库服务器、用户或当前连接所在的时区。
- 注:TIMESTAMPTZ 并不会存储时区,它只是一个 UTC 值,之后会根当前数据库server时区进行转换。
我们先做一个小实验:
一、我们先看一下Pg端的时区:
db=# SHOW TIMEZONE;
TimeZone
----------
PRC
(1 row)
二、查看一下两种类型的存储字节大小:
db=# select typname,typlen from pg_type where typname ~ '^timestamp';
typname | typlen
-------------+--------
timestamp | 8
timestamptz | 8
(2 rows)
三、创建一个含有 TIMESTAMP 和 TIMESTAMPTZ 的表mytime,并插入当前时间数据:
db=# CREATE TABLE mytime (ts TIMESTAMP, tstz TIMESTAMPTZ);
CREATE TABLE
db=# insert into mytime (ts,tstz) values ('2020-5-13 11:33:47','2020-5-13 11:34:00');
INSERT 0 1
db=# select * from mytime;
ts | tstz
---------------------+------------------------
2020-05-13 11:33:47 | 2020-05-13 11:34:00+08
(1 row)
四、设置一下,邻国越南的胡志明时区:(东7区)
# 查询时区的定义
select * from pg_timezone_names;
# 例如 Asia/Ho_Chi_Minh
五、再次查询时间,发现TIMESTAMPTZ 类型的值已经改变:
db=# SET timezone = 'Asia/Ho_Chi_Minh';
SET
db=# SHOW TIMEZONE;
TimeZone
------------------
Asia/Ho_Chi_Minh
(1 row)
db=# select * from mytime;
ts | tstz
---------------------+------------------------
2020-05-13 11:33:47 | 2020-05-13 10:34:00+07
(1 row)
六、然后(在东7区)插入一些数据:
db=# insert into mytime (ts,tstz) values ('2020-5-13 11:55:26','2020-5-13 11:55:18');
INSERT 0 1
db=# select * from mytime;
ts | tstz
---------------------+------------------------
2020-05-13 11:33:47 | 2020-05-13 10:34:00+07
2020-05-13 11:55:26 | 2020-05-13 11:55:18+07
(2 rows)
七、然后我们更换回中国时区(Asia/Beijing):
db=# SET timezone = 'Asia/Beijing';
SET
db=# SHOW TIMEZONE;
TimeZone
--------------
Asia/Beijing
(1 row)
db=# select * from mytime;
ts | tstz
---------------------+------------------------
2020-05-13 11:33:47 | 2020-05-13 11:34:00+08
2020-05-13 11:55:26 | 2020-05-13 12:55:18+08
(2 rows)
注:对比就会发现 TIMESTAMP 类型字段的值不变,而 TIMESTAMPTZ 类型字段的值变成了当前时区下的时间。因此在使用过程中,为保证数据的准确性在保存\使用\计算过程中应尽量使用timestamptz和timetz,尽量避免使用timestamp和time。
注:需要下面安装的软件,大家自行操作;下面的操作,MySQL端时区为系统时区,Pg端为PRC。都是东八区!
OK,我们先复现一下这个问题场景,首先布置我们的Pg端:
一、初始化并启动集群:
./initdb -W -D testpg
./pg_ctl start -D testpg/
二、登录进去
三、创建插件、创建SERVER和创建用户映射关系
db=# create extension mysql_fdw;
CREATE EXTENSION
db=# CREATE SERVER mysql_server FOREIGN DATA WRAPPER mysql_fdw OPTIONS (host '192.129.10.183', port '3306');
CREATE SERVER
db=# CREATE USER MAPPING FOR uxdb
db-# SERVER mysql_server
db-# OPTIONS (username 'root', password '123456');
CREATE USER MAPPING
四、下面在Pg端创建一个外部表,使用TIMESTAMP类型:
db=# CREATE FOREIGN TABLE warehouse(warehouse_id int,warehouse_name text,warehouse_created TIMESTAMP) SERVER mysql_server OPTIONS (dbname 'testdb', table_name 'warehouse');
CREATE FOREIGN TABLE
MySQL端如下:
一、启动MySQL:sudo /etc/init.d/mysql start
二、登录MySQL:mysql -h localhost -u root -p
三、连接数据库:use testdb;
四、创建上面的同名的表,并向里面插入数据:
mysql> CREATE TABLE warehouse(warehouse_id int primary key not null,warehouse_name text,warehouse_created timestamp);
Query OK, 0 rows affected (0.06 sec)
mysql> INSERT INTO warehouse values (1, 'hellp', now());
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO warehouse values (2, 'world', now());
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO warehouse values (3, 'mysql', now());
Query OK, 1 row affected (0.01 sec)
mysql> select * from warehouse;
+--------------+----------------+---------------------+
| warehouse_id | warehouse_name | warehouse_created |
+--------------+----------------+---------------------+
| 1 | hellp | 2020-05-13 13:17:43 |
| 2 | world | 2020-05-13 13:17:55 |
| 3 | mysql | 2020-05-13 13:18:25 |
+--------------+----------------+---------------------+
3 rows in set (0.00 sec)
OK,现在问题的背景已经布置完成了,下面我们在Pg端来查看一下:
db=# select * from warehouse;
ERROR: failed to connect to MySQL: Can't connect to MySQL server on '192.129.10.183' (113)
怎么有问题了呢?
首先检查MySQL的端口号还是不是3306:
mysql> show global variables like 'port';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| port | 3306 |
+---------------+-------+
1 row in set (0.01 sec)
然后检查MySQL端的防火墙状态:
[db@localhost ~]$ sudo firewall-cmd --state
[sudo] password for db:
running
[db@localhost ~]$ sudo systemctl stop firewalld.service
[db@localhost ~]$
果然是,那就先关掉它!
然后我们的时间差 现象就出现了:
db=# select * from warehouse;
warehouse_id | warehouse_name | warehouse_created
--------------+----------------+---------------------
1 | hellp | 2020-05-13 05:17:43
2 | world | 2020-05-13 05:17:55
3 | mysql | 2020-05-13 05:18:25
(3 rows)
时间相差了8小时!!!
第一种设想:倘若我们Pg端的这个外部表所使用的时间类型是TIMESTAMPTZ ,结果是不是就正常了呢? 结果显示:NO
db=# drop foreign table warehouse;
DROP FOREIGN TABLE
db=#
db=# CREATE FOREIGN TABLE warehouse(warehouse_id int,warehouse_name text,warehouse_created TIMESTAMPTZ) SERVER mysql_server OPTIONS (dbname 'testdb', table_name 'warehouse');
CREATE FOREIGN TABLE
db=# select * from warehouse;
warehouse_id | warehouse_name | warehouse_created
--------------+----------------+------------------------
1 | hellp | 2020-05-13 05:17:43+08
2 | world | 2020-05-13 05:17:55+08
3 | mysql | 2020-05-13 05:18:25+08
(3 rows)
db=# INSERT INTO warehouse values (4, 'postgres', now());
INSERT 0 1
db=# select * from warehouse;
warehouse_id | warehouse_name | warehouse_created
--------------+----------------+------------------------
1 | hellp | 2020-05-13 05:17:43+08
2 | world | 2020-05-13 05:17:55+08
3 | mysql | 2020-05-13 05:18:25+08
4 | postgres | 2020-05-13 05:49:14+08
(4 rows)
db=# SHOW TIMEZONE;
TimeZone
----------
PRC
(1 row)
db=# select now();
now
-------------------------------
2020-05-13 13:49:53.928455+08
(1 row)
如上面所示:即使Pg端使用TIMESTAMPTZ 类型来读取/插入一个时间(这个值在MySQL端是由MySQL数据库的TIMESTAMP类型定义的),它总是少上8小时。(这个8小时:北京时区为东八区,与世界标准时间就相差8小时)。
第二种设想:在MySQL端使用datetime类型,Pg端使用TIMESTAMPTZ 类型。这样的结果显示是一致的吗? 结果显示:YES
MySQL端如下:(使用datetime类型)
mysql> CREATE TABLE warehouse(warehouse_id int primary key not null,warehouse_name text, warehouse_created datetime);
Query OK, 0 rows affected (0.02 sec)
mysql> INSERT INTO warehouse values (1, 'hellp', now());
Query OK, 1 row affected (0.01 sec)
mysql> INSERT INTO warehouse values (2, 'world', now());
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO warehouse values (3, 'mysql', now());
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO warehouse values (4, 'postgres', now());
Query OK, 1 row affected (0.00 sec)
mysql> select * from warehouse;
+--------------+----------------+---------------------+
| warehouse_id | warehouse_name | warehouse_created |
+--------------+----------------+---------------------+
| 1 | hellp | 2020-05-13 14:06:39 |
| 2 | world | 2020-05-13 14:06:43 |
| 3 | mysql | 2020-05-13 14:06:46 |
| 4 | postgres | 2020-05-13 14:06:51 |
+--------------+----------------+---------------------+
4 rows in set (0.00 sec)
mysql>
Pg端读取:(使用TIMESTAMPTZ类型)
db=# select * from warehouse;
warehouse_id | warehouse_name | warehouse_created
--------------+----------------+------------------------
1 | hellp | 2020-05-13 14:06:39+08
2 | world | 2020-05-13 14:06:43+08
3 | mysql | 2020-05-13 14:06:46+08
4 | postgres | 2020-05-13 14:06:51+08
(4 rows)
上面从Pg上读取从MySQL上面插入的数据,结果正常了。但是若是在Pg端插入呢?
db=# INSERT INTO warehouse values (5, 'python', now());
INSERT 0 1
uxdb=# select * from warehouse;
warehouse_id | warehouse_name | warehouse_created
--------------+----------------+------------------------
1 | hellp | 2020-05-13 14:06:39+08
2 | world | 2020-05-13 14:06:43+08
3 | mysql | 2020-05-13 14:06:46+08
4 | postgres | 2020-05-13 14:06:51+08
5 | python | 2020-05-13 06:08:39+08
(5 rows)
此时的两端的结果(最后一条):在MySQL和Pg上读取的最后一条时间都少8小时。
第三种设想:在MySQL端使用datetime类型,Pg端使用TIMESTAMP类型。这样的结果显示是一致的吗? 结果显示:YES
MySQL端如上;
Pg端:(使用TIMESTAMP类型)
db=# CREATE FOREIGN TABLE warehouse(warehouse_id int,warehouse_name text,warehouse_created TIMESTAMP) SERVER mysql_server OPTIONS (dbname 'testdb', table_name 'warehouse');
CREATE FOREIGN TABLE
db=#
db=# select * from warehouse;
warehouse_id | warehouse_name | warehouse_created
--------------+----------------+---------------------
1 | hellp | 2020-05-13 14:06:39
2 | world | 2020-05-13 14:06:43
3 | mysql | 2020-05-13 14:06:46
4 | postgres | 2020-05-13 14:06:51
5 | python | 2020-05-13 06:08:39
(5 rows)
db=# INSERT INTO warehouse values (6, 'shell', now());
INSERT 0 1
db=# select * from warehouse;
warehouse_id | warehouse_name | warehouse_created
--------------+----------------+---------------------
1 | hellp | 2020-05-13 14:06:39
2 | world | 2020-05-13 14:06:43
3 | mysql | 2020-05-13 14:06:46
4 | postgres | 2020-05-13 14:06:51
5 | python | 2020-05-13 06:08:39
6 | shell | 2020-05-13 14:13:13
(6 rows)
mysql> select * from warehouse;
+--------------+----------------+---------------------+
| warehouse_id | warehouse_name | warehouse_created |
+--------------+----------------+---------------------+
| 1 | hellp | 2020-05-13 14:06:39 |
| 2 | world | 2020-05-13 14:06:43 |
| 3 | mysql | 2020-05-13 14:06:46 |
| 4 | postgres | 2020-05-13 14:06:51 |
| 5 | python | 2020-05-13 06:08:39 |
| 6 | shell | 2020-05-13 14:13:13 |
+--------------+----------------+---------------------+
6 rows in set (0.00 sec)
此时的两端的结果(最后一条):在MySQL和Pg上读取是一致的。
最后的小结:
-
datetime类型的日期,输入的数据不会变动
-
timestamp的日期类型随着不同的服务器的时区而进行时间的变动;并注意避免使用timestamp类型相关函数,如:make_timestamp;这里着重强调一下:不要用timestamp without time zone存储timestamp!
-
MySQL的timestamp类型在存储时间戳数据时:首先将本地时区时间转换为世界标准时间,再将世界标准时间转换为INT格式的毫秒值(其中是使用UNIX_TIMESTAMP函数),然后存放到数据库中。在读取 该时间戳数据时,先将INT格式的毫秒值转换为世界标准时间(其中是使用FROM_UNIXTIME函数),然后再根据当前设置的本地时区进行时间转换,最后返回给客户端。考虑到时区,对应的UTC时间是保持一致的。
-
MySQL的DateTime类型保存的是没有时区的时间(可以理解为字符串的时间格式)。数据保存时,会原封不动地将传入的时间保存下来,不进行任何转换。该类型适合一些本地化系统使用。在数据库服务器时区变化的时候,它对应的UTC时间已经偏离1小时。
-
PostgreSQL的timestamptz类型,它呈现出来是带时区的。数据库设置为哪个时区,就转化为该时区对应的时间。操作结果基本与MySQL一致。这句话,我现在的测试表明,不太准确!
现在我已经将问题查明:
第一个问题:
在我使用的旧版本的MySQL_fdw里面,迁移过来的数据 在外部表里面和pg的新表里面的数据差8小时现象。原因如下:这个地方设置了一个世界标准时间,而忽略了本地时区的影响。
这个问题,已经在新版的MySQL_fdw里面修正了,如下:
第二个问题:
在上面的问题修正之后,迁移数据是没有问题了。但是从MySQL以及我们pg这边的表 看他们的表结构说是都是带时区的。可是此时我若是在pg端 往外部表里面插入或者更新数据,会发现少8小时。MySQL查出来也是少 这个时候再在往pg迁移 迁过来的数据就有问题了!
原因如上图所示:
我们在Pg端 insert或者update 数据,传入的值now()虽然我们这里是使用了有时区的TimeStampTZ类型,但是经过MySQL_fdw的处理,被全部转化成了无时区的TimeStamp类型。因此这个在MySQL底层存储的时间是无时区(少8小时的)。
下面是我的解决方案:
此时重新编译MySQL_fdw和迁移工具,然后再进行数据迁移就没有问题了!