日期类型
日期类型 | 占用空间(字节数) | 表示范围 |
date | 4 | 1000-01-01 ~ 9999-12-31 |
datetime | 8 | 1000-01-01 00:00:00.000000 ~ 9999-12-31 23:59:59.999999 |
timestamp | 4 | 1970-01-01 00:00:00.000000UTC ~ 2038-01-19 03:14:07.000000UTC |
year | 1 | 1901-2155 |
time | 3 | -838:59:59.000000 ~ 838:59:59.000000 |
datetime
从MySQL5.6版本开始,DATETIME类型支持毫秒,DATETIME(N)中的N表示毫秒的精度。例如,DATETIME(6)表示可以存储6位的毫秒值。同时,一些日期函数也支持精确到毫秒,例如常见的函数 NOW、SYSDATE:
mysql> select now();
+---------------------+
| now() |
+---------------------+
| 2021-05-15 17:33:09 |
+---------------------+
1 row in set (0.01 sec)
mysql> select now(6);
+----------------------------+
| now(6) |
+----------------------------+
| 2021-05-15 17:33:19.068396 |
+----------------------------+
1 row in set (0.00 sec)
用户可以将DATETIME初始化值设置为当前时间,并设置自动更新当前时间的属性。例如数据库的每个表都会有一个字段create_time来都会记录创建时间,使用update_time来记录这条记录每次修改的时间。
mysql> show create table user;
+-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| user | CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL,
`create_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`update_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 |
+-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.01 sec)
mysql> insert into user(name) values('morris');
Query OK, 1 row affected (0.01 sec)
mysql> select * from user;
+----+--------+----------------------------+----------------------------+
| id | name | create_time | update_time |
+----+--------+----------------------------+----------------------------+
| 1 | morris | 2021-05-15 17:40:00.332704 | 2021-05-15 17:40:00.332704 |
+----+--------+----------------------------+----------------------------+
1 row in set (0.00 sec)
mysql> update user set name='marry' where id = 1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from user;
+----+-------+----------------------------+----------------------------+
| id | name | create_time | update_time |
+----+-------+----------------------------+----------------------------+
| 1 | marry | 2021-05-15 17:40:00.332704 | 2021-05-15 17:40:19.785901 |
+----+-------+----------------------------+----------------------------+
1 row in set (0.00 sec)
在上面的表User中,列create_time表示创建时间,DEFAULT CURRENT_TIMESTAMP表示记录插入时,若没有指定时间,默认就是当前时间。列update_time表示当前记录最后的修改时间,DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) 表示每次修改都会修改为当前时间。
在做表结构设计规范时,强烈建议你每张业务核心表都增加一个DATETIME类型的update_time字段,并设置修改自动更新机制,方便标识每条记录最后修改的时间,这样在数据库的同步时也可以利用到这个字段。
TIMESTAMP
除了DATETIME,日期类型中还有一种TIMESTAMP的时间戳类型,其实际存储的内容为‘1970-01-01 00:00:00’到现在的毫秒数。在MySQL中,由于类型TIMESTAMP占用4个字节,因此其存储的时间上限只能到‘2038-01-19 03:14:07’。
同类型DATETIME一样,从MySQL 5.6版本开始,类型TIMESTAMP也能支持毫秒。与DATETIME不同的是,若带有毫秒时,类型TIMESTAMP占用7个字节,而DATETIME无论是否存储毫秒信息,都占用8个字节。
类型TIMESTAMP最大的优点是可以带有时区属性,因为它本质上是从毫秒转化而来。如果你的业务需要对应不同的国家时区,那么类型TIMESTAMP是一种不错的选择。比如新闻类的业务,通常用户想知道这篇新闻发布时对应的自己国家时间,那么TIMESTAMP是一种选择。
另外,有些国家会执行夏令时。根据不同的季节,人为地调快或调慢1个小时,带有时区属性的TIMESTAMP类型本身就能解决这个问题。
参数time_zone指定了当前使用的时区,默认为SYSTEM使用操作系统时区,用户可以通过该参数指定所需要的时区。
如果想使用TIMESTAMP的时区功能,你可以通过下面的语句将之前的用户表User的创建时间字段类型从DATETIME(6)修改为TIMESTAMP(6):
mysql> alter table user change create_time create_time timestamp(6) not null default current_timestamp(6);
Query OK, 1 row affected (3.07 sec)
Records: 1 Duplicates: 0 Warnings: 0
这时通过设定不同的 time_zone,可以观察到不同时区下的注册时间:
mysql> select @@time_zone;
+-------------+
| @@time_zone |
+-------------+
| SYSTEM |
+-------------+
1 row in set (0.00 sec)
mysql> select * from user;
+----+-------+----------------------------+----------------------------+
| id | name | create_time | update_time |
+----+-------+----------------------------+----------------------------+
| 1 | marry | 2021-05-15 17:40:00.332704 | 2021-05-15 17:40:19.785901 |
+----+-------+----------------------------+----------------------------+
1 row in set (0.00 sec)
mysql> set time_zone='-08:00';
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user;
+----+-------+----------------------------+----------------------------+
| id | name | create_time | update_time |
+----+-------+----------------------------+----------------------------+
| 1 | marry | 2021-05-15 01:40:00.332704 | 2021-05-15 17:40:19.785901 |
+----+-------+----------------------------+----------------------------+
1 row in set (0.00 sec)
从上述例子中,你可以看到,中国的时区是+08:00,美国的时区是-08:00,因此改为美国时区后,可以看到用户注册时间比之前延迟了16个小时。当然了,直接加减时区并不直观,需要非常熟悉各国的时区表。在MySQL中可以直接设置时区的名字,如:
mysql> set time_zone='America/Los_Angeles';
Query OK, 0 rows affected (0.01 sec)
mysql> select now();
+---------------------+
| now() |
+---------------------+
| 2021-05-15 03:09:53 |
+---------------------+
1 row in set (0.00 sec)
mysql> set time_zone='Asia/Shanghai';
Query OK, 0 rows affected (0.00 sec)
mysql> select now();
+---------------------+
| now() |
+---------------------+
| 2021-05-15 18:10:39 |
+---------------------+
1 row in set (0.00 sec)
在上面设置时区的名字时可能会报如下错误:
mysql> set time_zone='America/Los_Angeles';
ERROR 1298 (HY000): Unknown or incorrect time zone: 'America/Los_Angeles'
这时需要安装timezone的表,可以到http://dev.mysql.com/downloads/timezones.html下载对应版本与系统的sql语句,一般是下载posix标准的那张表,下载完后然后导入到mysql的mysql库中:
mysql> use mysql
Database changed
mysql> source /root/timezone_posix.sql
... ...
Query OK, 3214 rows affected, 1 warning (0.14 sec)
Records: 3214 Duplicates: 0 Warnings: 1
DATETIME vs TIMESTAMP vs INT,怎么选?
在做表结构设计时,对日期字段的存储,开发人员通常会有3种选择:DATETIME、TIMESTAMP、INT。
INT类型就是直接存储’1970-01-01 00:00:00’到现在的毫秒数,本质和TIMESTAMP一样,因此用INT不如直接使用TIMESTAMP。
当然,有些同学会认为INT比TIMESTAMP性能更好。但是,由于当前每个CPU每秒可执行上亿次的计算,所以无须为这种转换的性能担心。更重要的是,在后期运维和数据分析时,使用INT存储日期,是会让DBA和数据分析人员发疯的,INT的可运维性太差。
也有的同学会热衷用类型 TIMESTEMP 存储日期,因为类型 TIMESTAMP 占用 4 个字节,比 DATETIME 小一半的存储空间。
但若要将时间精确到毫秒,TIMESTAMP要7个字节,和DATETIME 8字节差不太多。另一方面,现在距离TIMESTAMP的最大值‘2038-01-19 03:14:07’已经很近,这是需要开发同学好好思考的问题。
总的来说,我建议你使用类型DATETIME。对于时区问题,可以由前端或者服务这里做一次转化,不一定非要在数据库中解决。
不要忽视TIMESTAMP的性能问题
虽然从毫秒数转换到类型TIMESTAMP本身需要的CPU指令并不多,这并不会带来直接的性能问题。但是如果使用默认的操作系统时区,则每次通过时区计算时间时,要调用操作系统底层系统函数 __tz_convert(),而这个函数需要额外的加锁操作,以确保这时操作系统时区没有修改。所以,当大规模并发访问时,由于热点资源竞争,会产生两个问题。
- 性能不如 DATETIME:DATETIME不存在时区转化问题。
- 性能抖动: 海量并发时,存在性能抖动问题。
为了优化TIMESTAMP的使用,强烈建议你使用显式的时区,而不是操作系统时区。比如在配置文件中显示地设置时区,而不要使用系统时区:
[mysqld]
default-time-zone = '+8:00'
最后,通过命令mysqlslap来测试TIMESTAMP设置时区与不设置的性能,命令如下:
不设置时区,采用默认时区SYSTEM:
# mysqlslap -uroot --number-of-queries=1000000 --concurrency=100 --query='SELECT NOW()' -p
Enter password:
Benchmark
Average number of seconds to run all queries: 29.898 seconds
Minimum number of seconds to run all queries: 29.898 seconds
Maximum number of seconds to run all queries: 29.898 seconds
Number of clients running queries: 100
Average number of queries per client: 10000
在my.inf文件中指定时区:
# mysqlslap -uroot --number-of-queries=1000000 --concurrency=100 --query='SELECT NOW()' -p
Enter password:
Benchmark
Average number of seconds to run all queries: 27.597 seconds
Minimum number of seconds to run all queries: 27.597 seconds
Maximum number of seconds to run all queries: 27.597 seconds
Number of clients running queries: 100
Average number of queries per client: 10000
从运行结果中可以发现,显式指定时区的性能要远远好于直接使用操作系统时区。所以,日期字段推荐使用DATETIME,没有时区转化。即便使用 TIMESTAMP,也需要在数据库中显式地配置时区,而不是用系统时区。