PostgreSQL的学习心得和知识总结(八)|PostgreSQL时间戳之TIMESTAMP类型(对比MySQL)

最近在学习中(使用数据库迁移工具:从MySQL迁移数据到Postgresql)遇到了这么一个问题,使用工具MySQL_fdw连接外部表,其源码链接MySQL_fdw的git仓库。其编译安装可以参考这位老哥的博客:PostgreSQL插件:MySQL_fdw源码安装使用

背景如下:

1、MySQL的TIMESTAMP类型 和 PG的TIMESTAMP类型(名字相同,然相差巨大)
2、当使用一些数据迁移工具的过程中,会发现有时差的存在
3、数据的存储原理不同,造成“数据的不一致”现象

PostgreSQL 提供两种存储时间戳的数据类型: 不带时区的 TIMESTAMP 和带时区的 TIMESTAMPTZ。

  1. TIMESTAMP 数据类型可以同时存储日期和时间,但它不存储时区。这意味着,当修改了数据库服务器所在的时区时,它里面存储的值也是不会改变。
  2. TIMESTAMPTZ 数据类型在存储日期和时间的同时还能正确处理时区。PostgreSQL 使用 UTC 值(世界标准时间)来存储 TIMESTAMPTZ 数据。在向 TIMESTAMPTZ 字段插入值的时候,PostgreSQL 会自动将值转换成 UTC 值,并保存到表里。当从一个 TIMESTAMPTZ 字段查询数据的时候,PostgreSQL 会把存储在其中的 UTC 值转换成数据库服务器、用户或当前连接所在的时区。
  3. 注: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上读取是一致的。

最后的小结:

  1. datetime类型的日期,输入的数据不会变动

  2. timestamp的日期类型随着不同的服务器的时区而进行时间的变动;并注意避免使用timestamp类型相关函数,如:make_timestamp;这里着重强调一下:不要用timestamp without time zone存储timestamp!

  3. MySQL的timestamp类型在存储时间戳数据时:首先将本地时区时间转换为世界标准时间,再将世界标准时间转换为INT格式的毫秒值(其中是使用UNIX_TIMESTAMP函数),然后存放到数据库中。在读取 该时间戳数据时,先将INT格式的毫秒值转换为世界标准时间(其中是使用FROM_UNIXTIME函数),然后再根据当前设置的本地时区进行时间转换,最后返回给客户端。考虑到时区,对应的UTC时间是保持一致的。

  4. MySQL的DateTime类型保存的是没有时区的时间(可以理解为字符串的时间格式)。数据保存时,会原封不动地将传入的时间保存下来,不进行任何转换。该类型适合一些本地化系统使用。在数据库服务器时区变化的时候,它对应的UTC时间已经偏离1小时。

  5. 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和迁移工具,然后再进行数据迁移就没有问题了!
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_43949535/article/details/106092871