1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-08-01 16:28:03 +08:00

[docs update]MySQL 时间类型完善

This commit is contained in:
Guide 2025-04-02 15:46:20 +08:00
parent ae269485b7
commit f63f39f068
7 changed files with 139 additions and 42 deletions

View File

@ -158,10 +158,10 @@ DATETIME 类型没有时区信息TIMESTAMP 和时区有关。
TIMESTAMP 只需要使用 4 个字节的存储空间,但是 DATETIME 需要耗费 8 个字节的存储空间。但是这样同样造成了一个问题Timestamp 表示的时间范围更小。
- DATETIME1000-01-01 00:00:00 ~ 9999-12-31 23:59:59
- Timestamp1970-01-01 00:00:01 ~ 2037-12-31 23:59:59
- DATETIME'1000-01-01 00:00:00.000000' 到 '9999-12-31 23:59:59.999999'
- Timestamp'1970-01-01 00:00:01.000000' UTC 到 '2038-01-19 03:14:07.999999' UTC
关于两者的详细对比,请参考我写的[MySQL 时间类型数据存储建议](./some-thoughts-on-database-storage-time.md)。
关于两者的详细对比,请参考我写的 [MySQL 时间类型数据存储建议](./some-thoughts-on-database-storage-time.md)。
### NULL 和 '' 的区别是什么?

View File

@ -3,30 +3,43 @@ title: MySQL日期类型选择建议
category: 数据库
tag:
- MySQL
head:
- - meta
- name: keywords
content: MySQL 日期类型选择, MySQL 时间存储最佳实践, MySQL 时间存储效率, MySQL DATETIME 和 TIMESTAMP 区别, MySQL 时间戳存储, MySQL 数据库时间存储类型, MySQL 开发日期推荐, MySQL 字符串存储日期的缺点, MySQL 时区设置方法, MySQL 日期范围对比, 高性能 MySQL 日期存储, MySQL UNIX_TIMESTAMP 用法, 数值型时间戳优缺点, MySQL 时间存储性能优化, MySQL TIMESTAMP 时区转换, MySQL 时间格式转换, MySQL 时间存储空间对比, MySQL 时间类型选择建议, MySQL 日期类型性能分析, 数据库时间存储优化
---
我们平时开发中不可避免的就是要存储时间,比如我们要记录操作表中这条记录的时间、记录转账的交易时间、记录出发时间、用户下单时间等等。你会发现时间这个东西与我们开发的联系还是非常紧密的,用的好与不好会给我们的业务甚至功能带来很大的影响。所以,我们有必要重新出发,好好认识一下这个东西。
在日常的软件开发工作中,存储时间是一项基础且常见的需求。无论是记录数据的操作时间、金融交易的发生时间,还是行程的出发时间、用户的下单时间等等,时间信息与我们的业务逻辑和系统功能紧密相关。因此,正确选择和使用 MySQL 的日期时间类型至关重要,其恰当与否甚至可能对业务的准确性和系统的稳定性产生显著影响。
本文旨在帮助开发者重新审视并深入理解 MySQL 中不同的时间存储方式,以便做出更合适项目业务场景的选择。
## 不要用字符串存储日期
绝大部分对数据库不太了解的新手一样,我在大学的时候就这样干过,甚至认为这样是一个不错的表示日期的方法。毕竟简单直白,容易上手
许多数据库初学者一样,笔者在早期学习阶段也曾尝试使用字符串(如 VARCHAR类型来存储日期和时间甚至一度认为这是一种简单直观的方法。毕竟'YYYY-MM-DD HH:MM:SS' 这样的格式看起来清晰易懂
但是,这是不正确的做法,主要会有下面两个问题:
1. 字符串占用的空间更大!
2. 字符串存储的日期效率比较低(逐个字符进行比对),无法用日期相关的 API 进行计算和比较。
1. **空间效率**:与 MySQL 内建的日期时间类型相比,字符串通常需要占用更多的存储空间来表示相同的时间信息。
2. **查询与计算效率低下**
- **比较操作复杂且低效**:基于字符串的日期比较需要按照字典序逐字符进行,这不仅不直观(例如,'2024-05-01' 会小于 '2024-1-10'),而且效率远低于使用原生日期时间类型进行的数值或时间点比较。
- **计算功能受限**:无法直接利用数据库提供的丰富日期时间函数进行运算(例如,计算两个日期之间的间隔、对日期进行加减操作等),需要先转换格式,增加了复杂性。
- **索引性能不佳**:基于字符串的索引在处理范围查询(如查找特定时间段内的数据)时,其效率和灵活性通常不如原生日期时间类型的索引。
## Datetime 和 Timestamp 之间的抉择
## DATETIME 和 TIMESTAMP 选
Datetime 和 Timestamp 是 MySQL 提供的两种比较相似的保存时间的数据类型,可以精确到秒。他们两者究竟该如何选择呢?
`DATETIME``TIMESTAMP` 是 MySQL 中两种非常常用的、用于存储包含日期和时间信息的数据类型。它们都可以存储精确到秒MySQL 5.6.4+ 支持更高精度的小数秒)的时间值。那么,在实际应用中,我们应该如何在这两者之间做出选择呢?
下面我们来简单对比一下二者。
下面我们从几个关键维度对它们进行对比:
### 时区信息
**DateTime 类型是没有时区信息的(时区无关)** DateTime 类型保存的时间都是当前会话所设置的时区对应的时间。这样就会有什么问题呢?当你的时区更换之后,比如你的服务器更换地址或者更换客户端连接时区设置的话,就会导致你从数据库中读出的时间错误
`DATETIME` 类型存储的是**字面量的日期和时间值**,它本身**不包含任何时区信息**。当你插入一个 `DATETIME` 值时MySQL 存储的就是你提供的那个确切的时间,不会进行任何时区转换
**Timestamp 和时区有关**。Timestamp 类型字段的值会随着服务器时区的变化而变化,自动换算成相应的时间,说简单点就是在不同时区,查询到同一个条记录此字段的值会不一样。
**这样就会有什么问题呢?** 如果你的应用需要支持多个时区,或者服务器、客户端的时区可能发生变化,那么使用 `DATETIME` 时,应用程序需要自行处理时区的转换和解释。如果处理不当(例如,假设所有存储的时间都属于同一个时区,但实际环境变化了),可能会导致时间显示或计算上的混乱。
**`TIMESTAMP` 和时区有关**。存储时MySQL 会将当前会话时区下的时间值转换成 UTC协调世界时进行内部存储。当查询 `TIMESTAMP` 字段时MySQL 又会将存储的 UTC 时间转换回当前会话所设置的时区来显示。
这意味着,对于同一条记录的 `TIMESTAMP` 字段在不同的会话时区设置下查询可能会看到不同的本地时间表示但它们都对应着同一个绝对时间点UTC 时间)。这对于需要全球化、多时区支持的应用来说非常有用。
下面实际演示一下!
@ -41,16 +54,16 @@ CREATE TABLE `time_zone_test` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```
插入数据:
插入一条数据(假设当前会话时区为系统默认,例如 UTC+0:
```sql
INSERT INTO time_zone_test(date_time,time_stamp) VALUES(NOW(),NOW());
```
看数据
询数据(在同一时区会话下)
```sql
select date_time,time_stamp from time_zone_test;
SELECT date_time, time_stamp FROM time_zone_test;
```
结果:
@ -63,17 +76,16 @@ select date_time,time_stamp from time_zone_test;
+---------------------+---------------------+
```
现在我们运行
修改当前会话的时区:
现在,修改当前会话的时区为东八区 (UTC+8):
```sql
set time_zone='+8:00';
SET time_zone = '+8:00';
```
再次查数据:
再次查数据:
```plain
```bash
# TIMESTAMP 的值自动转换为 UTC+8 时间
+---------------------+---------------------+
| date_time | time_stamp |
+---------------------+---------------------+
@ -81,7 +93,7 @@ set time_zone='+8:00';
+---------------------+---------------------+
```
**扩展:一些关于 MySQL 时区设置的一个常用 sql 命令**
**扩展:MySQL 时区设置常用 SQL 命令**
```sql
# 查看当前会话时区
@ -102,28 +114,26 @@ SET GLOBAL time_zone = 'Europe/Helsinki';
![](https://oss.javaguide.cn/github/javaguide/FhRGUVHFK0ujRPNA75f6CuOXQHTE.jpeg)
在 MySQL 5.6.4 之前DateTime 和 Timestamp 的存储空间是固定的,分别为 8 字节和 4 字节。但是从 MySQL 5.6.4 开始它们的存储空间会根据毫秒精度的不同而变化DateTime 的范围是 5~8 字节Timestamp 的范围是 4~7 字节。
在 MySQL 5.6.4 之前DateTime 和 TIMESTAMP 的存储空间是固定的,分别为 8 字节和 4 字节。但是从 MySQL 5.6.4 开始它们的存储空间会根据毫秒精度的不同而变化DateTime 的范围是 5~8 字节TIMESTAMP 的范围是 4~7 字节。
### 表示范围
Timestamp 表示的时间范围更小,只能到 2038 年:
`TIMESTAMP` 表示的时间范围更小,只能到 2038 年:
- DateTime1000-01-01 00:00:00.000000 ~ 9999-12-31 23:59:59.499999
- Timestamp1970-01-01 00:00:01.000000 ~ 2038-01-19 03:14:07.499999
- `DATETIME`'1000-01-01 00:00:00.000000' 到 '9999-12-31 23:59:59.999999'
- `TIMESTAMP`'1970-01-01 00:00:01.000000' UTC 到 '2038-01-19 03:14:07.999999' UTC
### 性能
由于 TIMESTAMP 需要根据时区进行转换,所以从毫秒数转换到 TIMESTAMP 时,不仅要调用一个简单的函数,还要调用操作系统底层的系统函数。这个系统函数为了保证操作系统时区的一致性,需要进行加锁操作,这就降低了效率
由于 `TIMESTAMP` 在存储和检索时需要进行 UTC 与当前会话时区的转换,这个过程可能涉及到额外的计算开销,尤其是在需要调用操作系统底层接口获取或处理时区信息时。虽然现代数据库和操作系统对此进行了优化,但在某些极端高并发或对延迟极其敏感的场景下,`DATETIME` 因其不涉及时区转换,处理逻辑相对更简单直接,可能会表现出微弱的性能优势
DATETIME 不涉及时区转换,所以不会有这个问题。
为了避免 TIMESTAMP 的时区转换问题,建议使用指定的时区,而不是依赖于操作系统时区。
为了获得可预测的行为并可能减少 `TIMESTAMP` 的转换开销,推荐的做法是在应用程序层面统一管理时区,或者在数据库连接/会话级别显式设置 `time_zone` 参数,而不是依赖服务器的默认或操作系统时区。
## 数值时间戳是更好的选择吗?
很多时候,我们也会使用 int 或者 bigint 类型的数值也就是数值时间戳来表示时间
除了上述两种类型,实践中也常用整数类型(`INT``BIGINT`来存储所谓的“Unix 时间戳”(即从 1970 年 1 月 1 日 00:00:00 UTC 起至目标时间的总秒数,或毫秒数)
这种存储方式的具有 Timestamp 类型的所具有一些优点,并且使用它的进行日期排序以及对比等操作的效率会更高,跨系统也很方便,毕竟只是存放的数值。缺点也很明显,就是数据的可读性太差了,你无法直观的看到具体时间。
这种存储方式的具有 `TIMESTAMP` 类型的所具有一些优点,并且使用它的进行日期排序以及对比等操作的效率会更高,跨系统也很方便,毕竟只是存放的数值。缺点也很明显,就是数据的可读性太差了,你无法直观的看到具体时间。
时间戳的定义如下:
@ -132,7 +142,8 @@ DATETIME 不涉及时区转换,所以不会有这个问题。
数据库中实际操作:
```sql
mysql> select UNIX_TIMESTAMP('2020-01-11 09:53:32');
-- 将日期时间字符串转换为 Unix 时间戳 (秒)
mysql> SELECT UNIX_TIMESTAMP('2020-01-11 09:53:32');
+---------------------------------------+
| UNIX_TIMESTAMP('2020-01-11 09:53:32') |
+---------------------------------------+
@ -140,7 +151,8 @@ mysql> select UNIX_TIMESTAMP('2020-01-11 09:53:32');
+---------------------------------------+
1 row in set (0.00 sec)
mysql> select FROM_UNIXTIME(1578707612);
-- 将 Unix 时间戳 (秒) 转换为日期时间格式
mysql> SELECT FROM_UNIXTIME(1578707612);
+---------------------------+
| FROM_UNIXTIME(1578707612) |
+---------------------------+
@ -149,13 +161,26 @@ mysql> select FROM_UNIXTIME(1578707612);
1 row in set (0.01 sec)
```
## PostgreSQL 中没有 DATETIME
由于有读者提到 PostgreSQLPG 的时间类型因此这里拓展补充一下。PG 官方文档对时间类型的描述地址:<https://www.postgresql.org/docs/current/datatype-datetime.html>
![PostgreSQL 时间类型总结](https://oss.javaguide.cn/github/javaguide/mysql/pg-datetime-types.png)
可以看到PG 没有名为 `DATETIME` 的类型:
- PG 的 `TIMESTAMP WITHOUT TIME ZONE`在功能上最接近 MySQL 的 `DATETIME`。它存储日期和时间,但不包含任何时区信息,存储的是字面值。
- PG 的`TIMESTAMP WITH TIME ZONE` (或 `TIMESTAMPTZ`) 相当于 MySQL 的 `TIMESTAMP`。它在存储时会将输入值转换为 UTC并在检索时根据当前会话的时区进行转换显示。
对于绝大多数需要记录精确发生时间点的应用场景,`TIMESTAMPTZ`是 PostgreSQL 中最推荐、最健壮的选择,因为它能最好地处理时区复杂性。
## 总结
MySQL 中时间到底怎么存储才好Datetime?Timestamp?还是数值时间戳?
MySQL 中时间到底怎么存储才好?`DATETIME`?`TIMESTAMP`?还是数值时间戳?
并没有一个银弹,很多程序员会觉得数值型时间戳是真的好,效率又高还各种兼容,但是很多人又觉得它表现的不够直观。
《高性能 MySQL 》这本神书的作者就是推荐 Timestamp原因是数值表示时间不够直观。下面是原文
《高性能 MySQL 》这本神书的作者就是推荐 TIMESTAMP,原因是数值表示时间不够直观。下面是原文:
<img src="https://oss.javaguide.cn/github/javaguide/%E9%AB%98%E6%80%A7%E8%83%BDmysql-%E4%B8%8D%E6%8E%A8%E8%8D%90%E7%94%A8%E6%95%B0%E5%80%BC%E6%97%B6%E9%97%B4%E6%88%B3.jpg" style="zoom:50%;" />
@ -167,4 +192,10 @@ MySQL 中时间到底怎么存储才好Datetime?Timestamp?还是数值时间
| TIMESTAMP | 4~7 字节 | YYYY-MM-DD hh:mm:ss[.fraction] | 1970-01-01 00:00:01[.000000] 2038-01-19 03:14:07[.999999] | 是 |
| 数值型时间戳 | 4 字节 | 全数字如 1578707612 | 1970-01-01 00:00:01 之后的时间 | 否 |
**选择建议小结:**
- `TIMESTAMP` 的核心优势在于其内建的时区处理能力。数据库负责 UTC 存储和基于会话时区的自动转换,简化了需要处理多时区应用的开发。如果应用需要处理多时区,或者希望数据库能自动管理时区转换,`TIMESTAMP` 是自然的选择(注意其时间范围限制,也就是 2038 年问题)。
- 如果应用场景不涉及时区转换,或者希望应用程序完全控制时区逻辑,并且需要表示 2038 年之后的时间,`DATETIME` 是更稳妥的选择。
- 如果极度关注比较性能,或者需要频繁跨系统传递时间数据,并且可以接受可读性的牺牲(或总是在应用层转换),数值时间戳是一个强大的选项。
<!-- @include: @article-footer.snippet.md -->

View File

@ -6,7 +6,7 @@ tag:
head:
- - meta
- name: keywords
content: JVM,JDK,JRE,字节码详解,Java 基本数据类型,装箱和拆箱
content: Java特点,Java SE,Java EE,Java ME,Java虚拟机,JVM,JDK,JRE,字节码,Java编译与解释,AOT编译,云原生,AOT与JIT对比,GraalVM,Oracle JDK与OpenJDK区别,OpenJDK,LTS支持,多线程支持,静态变量,成员变量与局部变量区别,包装类型缓存机制,自动装箱与拆箱,浮点数精度丢失,BigDecimal,Java基本数据类型,Java标识符与关键字,移位运算符,Java注释,静态方法与实例方法,方法重载与重写,可变长参数,Java性能优化
- - meta
- name: description
content: 全网质量最高的Java基础常见知识点和面试题总结希望对你有帮助

View File

@ -6,7 +6,7 @@ tag:
head:
- - meta
- name: keywords
content: 面向对象,构造方法,接口,抽象类,String,Object
content: 面向对象, 面向过程, OOP, POP, Java对象, 构造方法, 封装, 继承, 多态, 接口, 抽象类, 默认方法, 静态方法, 私有方法, 深拷贝, 浅拷贝, 引用拷贝, Object类, equals, hashCode, ==, 字符串, String, StringBuffer, StringBuilder, 不可变性, 字符串常量池, intern, 字符串拼接, Java基础, 面试题
- - meta
- name: description
content: 全网质量最高的Java基础常见知识点和面试题总结希望对你有帮助

View File

@ -6,7 +6,7 @@ tag:
head:
- - meta
- name: keywords
content: Java异常,泛型,反射,IO,注解
content: Java异常处理, Java泛型, Java反射, Java注解, Java SPI机制, Java序列化, Java反序列化, Java IO流, Java语法糖, Java基础面试题, Checked Exception, Unchecked Exception, try-with-resources, 反射应用场景, 序列化协议, BIO, NIO, AIO, IO模型
- - meta
- name: description
content: 全网质量最高的Java基础常见知识点和面试题总结希望对你有帮助

View File

@ -210,14 +210,80 @@ public CommonResponse<Object> method1() {
AOP 的常见实现方式有动态代理、字节码操作等方式。
Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 **JDK Proxy**,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 **Cglib** 生成一个被代理对象的子类来作为代理,如下图所示:
Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 **JDK Proxy**,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 CGLIB 生成一个被代理对象的子类来作为代理,如下图所示:
![SpringAOPProcess](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/230ae587a322d6e4d09510161987d346.jpeg)
**Spring Boot 和 Spring 的动态代理的策略是不是也是一样的呢?**其实不一样,很多人都理解错了。
Spring Boot 2.0 之前,默认使用 **JDK 动态代理**。如果目标类没有实现接口,会抛出异常,开发者必须显式配置(`spring.aop.proxy-target-class=true`)使用 **CGLIB 动态代理** 或者注入接口来解决。Spring Boot 1.5.x 自动配置 AOP 代码如下:
```java
@Configuration
@ConditionalOnClass({ EnableAspectJAutoProxy.class, Aspect.class, Advice.class })
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = false)
// 该配置类只有在 spring.aop.proxy-target-class=false 或未显式配置时才会生效。
// 也就是说如果开发者未明确选择代理方式Spring 会默认加载 JDK 动态代理。
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", matchIfMissing = true)
public static class JdkDynamicAutoProxyConfiguration {
}
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
// 该配置类只有在 spring.aop.proxy-target-class=true 时才会生效。
// 即开发者通过属性配置明确指定使用 CGLIB 动态代理时Spring 会加载这个配置类。
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = false)
public static class CglibAutoProxyConfiguration {
}
}
```
Spring Boot 2.0 开始,如果用户什么都不配置的话,默认使用 **CGLIB 动态代理**。如果需要强制使用 JDK 动态代理,可以在配置文件中添加:`spring.aop.proxy-target-class=false`。Spring Boot 2.0 自动配置 AOP 代码如下:
```java
@Configuration
@ConditionalOnClass({ EnableAspectJAutoProxy.class, Aspect.class, Advice.class,
AnnotatedElement.class })
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = false)
// 该配置类只有在 spring.aop.proxy-target-class=false 时才会生效。
// 即开发者通过属性配置明确指定使用 JDK 动态代理时Spring 会加载这个配置类。
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", matchIfMissing = false)
public static class JdkDynamicAutoProxyConfiguration {
}
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
// 该配置类只有在 spring.aop.proxy-target-class=true 或未显式配置时才会生效。
// 也就是说如果开发者未明确选择代理方式Spring 会默认加载 CGLIB 代理。
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true)
public static class CglibAutoProxyConfiguration {
}
}
```
当然你也可以使用 **AspectJ** Spring AOP 已经集成了 AspectJ AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。
**Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。** Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。
Spring AOP 已经集成了 AspectJ AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,
Spring AOP 已经集成了 AspectJ AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单
如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。
## 参考
- AOP in Spring Boot, is it a JDK dynamic proxy or a Cglib dynamic proxy?<https://www.springcloud.io/post/2022-01/springboot-aop/>
- Spring Proxying Mechanisms<https://docs.spring.io/spring-framework/reference/core/aop/proxying.html>

View File

@ -142,7 +142,7 @@ public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
**Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。** Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。
Spring AOP 已经集成了 AspectJ AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单
Spring AOP 已经集成了 AspectJ AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单
如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。