众所周知,在进行数据库设计的时候,关于存储到秒级的时间类型字段,常用的有Timestamp和Datetime这2种。而这2种类型的字段,在存储格式以及存储空间上,又有着很大的不同,列举如下:
那么,在实际的数据库设计中,在Timestamp和Datetime这2种秒级别的时间列类型中,如何进行选择呢?让我们看下面示例。
为了方便对Timestamp和Datetime这2种列类型的数据存储行为进行研究,我们创建如下t_timestamp_demo测试表。
其中,my_date为DATETIME日期类型的列字段;create_time在新增数据时,自动将当前系统时区的时间戳,赋值给该列字段;update_time列字段,会在新增数据和当前行数据发生修改时,自动更新为当前系统时区的时间(戳)。
下面,我们插入一条数据:
如上图,通过只向my_data字段插入指定时间字符串'2099-12-24 18:18:18'后,该行数据的create_time和update_time字段,会自动插入为当前系统的时间(戳)(见上图右下角时间)。
接下来,我们在修改一下该条数据,如下:
如上图,经过对my_date字段进行修改后(修改其它字段也可),update_time字段就会自动更新为当前系统的最新时间(戳),这是因为在update_time字段定义的时候,给了ON UPDATE CURRENT_TIMESTAMP的列特性。
接下来我们看下,Timestamp类型的字段,所能够存储的时间范围的上限和下限分别是什么,如下:
如上图,我们修改了update_time的时间为'2038-01-19 11:14:07',这是阿K在本地环境中能够向Timestamp类型的列字段,插入的最大的值。
接下来,我们再多加1秒,即将update_time列字段的时间,修改为'2038-01-19 11:14:08',如下:
如上图,我们看到Mysql会报错,说'2038-01-19 11:14:08'这个时间值,在update_time列字段的存储是不正确的,也就是说'2038-01-19 11:14:07',是Timestamp类型的字段所能存储的最大值了,那么为什么会是'2038-01-19 11:14:07'呢?
让我们先看一下Mysql中,整数类型的存储和范围,如下:
其中,Int类型占用4个字节的空间,其有符号的存储范围为:-2147483648 ~ 2147483647。那么我们知道Timestamp类型的字段,也是占用4个字节的存储空间,那么,我们将'2038-01-19 11:14:07'换算成时间戳,如下:
如上图,将'2038-01-19 11:14:07'换算成时间戳后,就是2147483647。这正好是Mysql中Int类型字段所能存储的最大值了,多1秒就会超出Int类型的存储空间大小,溢出报错。
那么Timestamp类型的字段,所能存储的时间最小值,又是多少呢?让我们继续往下看:
如上图,我们将update_time字段修改为'1970-01-01 08:00:01',这是阿K在本地环境所能存储的Timestamp列类型字段的最小值了。
再减1秒,就会报错,如下:
如上图,我们看到'1970-01-01 08:00:00'这个时间值,在Mysql的Timestamp类型的列字段update_time中,存储是会报错的。也就是说,Timestamp类型的列字段,所能存储的最小时间是'1970-01-01 08:00:01',换算成时间戳,如下:
如上图,由此可见,Timestamp类型的列字段,能存储的时间戳范围,就是有符号Int类型字段的1 ~ 2147483647,因为Timestamp列类型字段,只能存储0以上的时间戳数值。如果是unsigned无符号整数,则可以存储的时间戳值为4294967295,也就是能存储到2106年,如下:
如上图,这可比有符号的Int类型整数,所表示的最大时间戳2147483647(即2038年),能多存68年呢。可惜目前Timestamp类型的列字段,只能存储到2147483647(2038年1月19日 11点14分07秒)。
上面我们分析了Timestamp和Datetime在数据库表中的存储行为和大小限制。那么,Timestamp和Datetime究竟是如何存储在数据库表里面的呢?下面通过一个示例,来看看Timestamp和Datetime类型的字段,在磁盘上面的表空间中,是如何存储的吧。
如上图,我们创建了2个表,t_one_datetime用来存储Datetime类型的数据,字段名为my_datetime。t_one_timestamp用来存储Timestamp类型的字段,字段名为my_timestamp。
接下来,我们在这2个表中,分别插入1条数据,如下:
接下来,我们找到磁盘上表t_one_datetime和表t_one_timestamp的表空间文件(后缀名是.ibd),打开进行对比,如下:
t_one_datetime表空间_元数据
t_one_timestamp表空间_元数据
从上图的元数据文件中,我们看到t_one_datetime表中,存储的是:a7 af 32 c8 ee,这5个字节的数据。而t_one_timestamp表中,存储的是:5f 90 fb 4f,这4个字节的数据。
那么这些数据到底代表着什么意思呢?让我们先将t_one_timestamp里面存储的数据,即:5f 90 fb 4f,转换为十进制数字,如下:
得到十进制数字为:1603337039,将其进行时间戳计算,如下:
这正好就是我们t_one_timestamp表中,my_timestamp字段存储的时间'2020-10-22 11:23:59'。由此可见,Timestamp类型的列字段,确实是以4字节的时间戳整数的方式存储在表空间里的。
那么t_one_datetime中,数据:a7 af 32 c8 ee,这5个字节存储的是什么东西呢?让我们先看下Mysql官网上对Datetime类型的解释,如下:
这里我们看到,从MySQL 5.6.4开始,Datetime的存储空间变成了5个字节了(准确的说应该是5字节+0~3个字节的FSP分秒精度),那么我们这里的5个字节数据:a7 af 32 c8 ee,是如何存储的呢?我们看官网的解释,如下:
如上图,按照官网的解释,Datetime的5个字节,总共是40个bit。将我们的数据:a7 af 32 c8 ee,转换为比特,如下:
得到40位bit,为:1010 0111 1010 1111 0011 0010 1100 1000 1110 1110,将其按照上面官网的Datetime解释,进行位对齐后,排列如下:
1 01001111010111100 11001 01100 100011 101110
转换为表格表示,如下:
上面表格中,我们的数据经过位拆分,分别计算后,得到的时间就是'3125年11月25日 12点35分46秒',也就是t_one_datetime表中,my_datetime字段所存储的Datetime类型的数据'3125-11-25 12:35:46'。
这里我要重点说明下,为什么年的存储,要采用"年*13 + 月"的方式进行存储呢?
假设我们本次要存储的时间为'9999年12月',如果按照"年月"的格式进行存储,如下:
我们看到,按照"年月"的方式进行'9999-12'的存储,共占用了20个bit的空间,也就是2.5个字节(1个字节8bit)。
那么如果按照"年*13+月"的方式进行存储,如下:
如上图,通过"年*13+月"的计算存储之后,只占用了17个bit的存储空间(前面000不计算在空间内),这样就节省了3个bit的空间,使得'9999-12-31 23:59:59'这样的时间范围,可以有效存储在5个字节中,Datetime这样的存储方式,只比Timestamp列类型的字段多占用了1个字节的存储空间,就能存储下足够大的时间范围了。
根据MySQL的官方文档,我们知道在5.6.4的版本开始,Timestamp和Datetime列类型的字段,在原有所占字节空间的基础上,都加入了1个叫做FSP(fractional-seconds precision)的分秒精度的扩展存储,如下:
这个分秒精度FSP的设计,主要是为保存毫秒值需求的时间字段而设计的,也就是说,自MySQL 5.6.4开始,我们开始存储时间,精确到秒后面的6位小数了。
看下面这个例子:
如上图,自MySQL 5.6.4开始,只要在Timestamp和Datetime列类型的字段后面,加上表示FSP(分秒精度)的小数点位数长度值,就可以存储时间秒后面为0~6位长度的毫秒时间了,上例中t_fsp_datetime表中,my_datetime字段的FSP长度值为6,所以就能插入,表示时间秒后6位长度的小数时间'2019-01-09 12:03:15.666999'。
那么这个FSP的位数的长短,分别会占用多少存储空间呢?官网的解释如下:
可见,指定不同FSP的位数长度,所占用的存储空间,分别从0-3个字节不等。
本篇主要针对MySQL数据库设计中,Timestamp和Datetime这2个字段类型,进行了对比和分析。
从存储限制上来说,虽然Datetime字段类型多占用了1些存储空间,但是却可以存储足够大的时间范围,适应性和可控性都Timestamp要强。
然而Timestamp字段类型的存储,几乎就等于是Int(有符号)类型的存储。这种情况下,如果有时间戳的存储需求,完全可以使用Unsigned Int(无符号Int)或者Bigint类型来进行时间戳的存储处理,这样它不但可以充分利用Int类型的存储空间来存储更长的时间,同时也能够进行插入赋于CURRENT_TIMESTAMP的能力,从而弥补Timestamp字段类型的不足。
从存储行为上来说,Timestamp的自动插入能力,Datetime也是支持的,所以Timestamp的额外亮点,就是在于具备自动更新的能力。但是这个能力,还要看设计者的评估,是否适合放在MySQL数据库这里进行更新。
因为在很多分布式的场景中,MySQL数据库往往与JAVA服务系统,不在同一个服务器节点上,那么可能存在MySQL的时间和JAVA服务系统的时间,没有进行NTP时间同步。而有的设计者认为,MySQL就应该只是负责单纯存储的地方,而时间戳的确认和计算,都应该交由服务系统来进行统一控制管理,这样就不会造成MySQL时间与服务系统时间不同步,造成的时间戳自动更新错乱的问题了。
在实际的数据库设计开发中,每位设计者的观点和想法都不尽相同,都有自己的设计考量。所谓萝卜白菜,各有所爱,关于Timestamp和Datetime的设计选择,作为开发人员的您,又是如何思考和决定的呢?
欢迎留言与阿K进行交流讨论,希望本篇文章对您有所帮助,谢谢!
本文中提到的MySQL的示例文件,在阿K的Gitee中都可以找到,如下:
https://gitee.com/Kival/mysql-work-demo
【后记】
关于Datetime在MySQL 5.6.4之前的版本中,占用8个字节的存储空间的计算方式,官方解释如下:
简单来说,就是用了2个Int类型的4字节空间,来存储时间数据。如:3018-12-20 15:17:55,存储的实际数据就是'30181220 151755',这样的2段4字节的Int类型空间。所以就有了大家普遍认知的MySQL中,Datetime比Timestamp的字段类型,多占用1倍(4字节)的存储空间的说法了。
【特别鸣谢】
jeremyCai
特别感谢以上各位朋友,对本文中的错误和用词不当,做出的指点批评。只有你们的批评指正,才能激发阿K的不断学习与进步,谢谢!