本系列为MySQl基础函数与命令。
MySQL性能分析可参考《MySQL性能分析与优化》
发现有朋友是找计算数据大小的函数
- 如果是找
length
、char_length
、bit_length
等函数,参考《MySQL length/bit_length/char_length计算字段长度/大小》即可- 如果是想计算
VARCHAR
的最大长度,函数帮不了你,但是这篇文章可以帮你:《MySQL中VARCHAR最大长度是多少?》本文主要介绍字段长度、存储原理的,建议作为进一步阅读资料。
本文介绍了各类型的定义语法、存储原理、取值范围。
建议您收藏,日常评估存储开销时可以速查。
本文默认约定:
- 字符集:UTF8。
GBK等字符集的多字节字符长度略有不同,但原理类似。- MySQL版本:5.6/5.7/8.x。
部分新特性如时间小数部分的额外长度,是5.6.4后的,之前版本不考虑即可。- InnoDB引擎。
MySQL中字段声明的长度,大部分指的是字符数,比如varchar(15)
、bigint(18)
、decimal(8,4)
等。
而计算存储开销时,需要换算成字节(Byte)和位(Bit)1。
以字符为单位,定义字段长度,适用于CHAR
,VARCHAR
和TEXT
。2
如果列使用
CHARACTER SET binary
修饰,会变为二进制类型,如CHAR
变为BINARY
,VARCHAR
变为VARBINARY
和TEXT
变为BLOB
。
两个类型类似,存储多个字符、可设置最大存储的字符数,存储开销与长度、字符集有关3。
先用两个小例子,快速了解下两个类型:
CHAR(4)
,最多存储4个字符,不足4个尾部用空格填满。存储字节数 = 数据值的字节和 + 补位空格数VARCHAR(4)
,最多存储4个字符,有几个字符存储几个。存储字节数 = 数据值的字节和 + 1字节(长度标识)下面是具体的特性介绍和对比4:
特性 | CHAR |
VARCHAR |
---|---|---|
长度 | 定长,固定字符数 最大255个字符 数据长度不足声明值时,在尾部自动填充空格 |
长度可变,可设置最大存储字符数 最大不超过行大小(默认65535字节,注意是字节,下面会讲原因) |
前缀 | 无 | 1~2字节,看列长度是否可能超过255字节 比如 VARCHAR(100) ,字符集为UTF8 ,则字节最大可能为300字节,所以会使用2个字节标识长度 |
有否尾部空格 | 长度不足默认用空格填满 检索和获取时会自动去除 |
不会自动填充空格输入值就包含空格,则会存储,检索和获取数据都会体现 |
超长处理 | 超长部分如果是空格自动截断 如果是字符,严格模式下会报错 |
超长部分如果是空格自动截断,并生成警告 如果是字符,严格模式下会报错 |
存储开销 | 数据值的字节和 + 补位空格数 | 数据值的字节和 + 长度标识字节数 |
PAD_CHAR_TO_FULL_LENGTH
模式,检索时尾部空格不会去除CHAR
超过255字符会报错,提示使用TEXT
或BLOB
:ERROR 1074 (42000): Column length too big for column ''long_char'' (max = 255); use BLOB or TEXT instead
VARCHAR
的最大值受此限制
在
COMPACT
、DYNAMIC
行格式下,行大小除了数据列长度,还包括可空列标识,即NULL
标识位。
如果有一个列允许为空,则需要1 bit
来标识,每8 bits
的标识会组成一个字段,该字段会存放在每行最开始的位置。
假设一张表中存在N个可空字段,NULL
标识位需要 ⌈ N / 8 ⌉ \lceil{N/8}\rceil ⌈N/8⌉ (向上取整)个字节。此时整行可用于数据存储的空间只有 65535 − ⌈ N / 8 ⌉ 65535 - \lceil{N/8}\rceil 65535−⌈N/8⌉个字节。
需要了解更多行大小内容,可参考官方文档5。
VARCHAR
的长度计算比较复杂,这里列出其中的要点,详细的计算过程和案例,请参考《MySQL中VARCHAR最大长度是多少?》:
VARCHAR(100)
,字符集为UTF8
,可能的字节数为300,长度标识则为2字节。
长度标志位只是存储开销,不影响长度约束。长度约束的是数据的字符数,允许的最大字符数与字符集有关。
UTF8
、每个字符最大可占用3个字节,行最大长度为65535字节,那么该列可设置的最大列大小为65535 ÷ 3 ≈ 21844(向下取整)
同理如果是
UTF8MB4
、单字符最大占4个字节,可设置最大列大小为65535 ÷ 4 ≈ 16383
TEXT
包括四种类型:TINYTEXT
,TEXT
, MEDIUMTEXT
和LONGTEXT
。
这里只简单介绍TEXT
,对其他三个有兴趣,可以参考官方文档2。
TEXT
最大长度为65535(216 − 1)个字符。如果是多字节字符,则有效最大长度会更少。存储时会增加2字节的前缀、标识长度。
TEXT
可以声明为TEXT(M)
,M
指该列最多可存储多少个字符;此时TEXT
基本可看做VARCHAR
,存储特征基本类似,但也有一点区别:
TEXT
列仅占用9 ~ 12字节的行大小,因为其内容是存储在数据页外的5。TEXT
列不允许设默认值TEXT
列的索引必须设置索引前缀长度,而VARCHAR
可以不设。TEXT
无法使用临时表。因为TEXT
列可能很大,临时表空间会膨胀的非常快,所以MYSQL的MEMORY引擎不支持这类大的数据类型。TEXT
类型,MySQL会直接用磁盘上的表、而不是内存中的表。TEXT
字段;TEXT
单独拆出一个表,这样读写时减少与该列发生关系的可能,性能也会提升。TEXT
的大小,除了上面说的类型限制,实际上还有物理因素限制:在客户端和服务器之间传输的最大值,由可用内存量和通信缓冲区的大小确定。
可通过更改max_allowed_packet变量,来更改消息缓冲区的大小,但是服务器和客户端都要修改。
整数是定长类型,长度为字节数
常见的INT(4)
只是设置了显示宽度,不影响存储长度。要了解显示宽度细节,可参考《MySQL显示宽度与字段长度》
类型 | 容量(Bytes) | 最小(signed) | 最大(signed) | unsigned范围 |
---|---|---|---|---|
TINYINT |
1 | -128 | 127 | 0~255 |
SMALLINT |
2 | -32768 | 32767 | 0~65,535 |
MEDIUMINT |
3 | -8388608 | 8388607 | 0~16777215 |
INT |
4 | -2147483648 | 2147483647 | 0~4294967295 |
BIGINT |
8 | -263 = 9223372036854775808 |
263-1 = 9223372036854775807 |
0~18446744073709551615 |
数值类型默认是有符号的:
再次提醒:常见的
INT(4)
只是设置了显示宽度,不影响存储长度。要了解显示宽度细节,可参考《MySQL显示宽度与字段长度》
从精确度考虑,建议使用定点类型。且MySQL 8.0.17开始,将逐步去除浮点型的支持。
下面先介绍定点类型。
DECIMAL(M,D)
如果D省略,则默认值为0。如果M省略,则默认值为10。
DECIMAL
以二进制存储,因此实际存储长度、与表达的数据范围是需要换算的:
4个字节恰好是
INT
的长度,而上面讲过,有符号的INT
最大表达2*10^10
左右的十进制数字,所以每9个打包成4个字节就可以理解了。
参考官方文档:《DECIMAL Data Type Characteristics》
如果遇到该类型的字段超长错误,可参考实例分析:https://learn.blog.csdn.net/article/details/100988201
浮点类型表示近似数据值,包括单精度FLOAT
和双精度DOUBLE
。
类型 | 精度 | 取值范围 | 存储开销 |
---|---|---|---|
FLOAT |
0 - 23 | -3.402823466E+38 ~ -1.175494351E-38 0 1.175494351E-38 ~ 3.402823466E+38 |
4 |
DOUBLE |
24 - 53 | -1.7976931348623157E+308 ~ -2.2250738585072014E-308 0 2.2250738585072014E-308 ~ 1.7976931348623157E+308 |
8 |
上面的取值范围是基于IEEE标准的理论限制。实际范围可能会略小,具体取决于硬件或操作系统
关于IEEE标准的分析,可参考《浮点型数据(float, double)存储IEEE标准解析和应用》
支持两种语法:
FLOAT(P)
,P表示精度,但实际上该值决定了存储开销。P在023之间占4个字节(单精度),在2453之间则占8个字节(双精度)。也就意味着虽然用FLOAT
声明,但实际上精度如果大于23,则实际变成了DOUBLE
。FLOAT(M,D)
或REAL(M,D)
或DOUBLE PRECISION(M,D)
。M表示总位数,D表示小数位。
FLOAT(7,4)
效果为999.9999
999.00009
保存到FLOAT(7,4)
列中,MySQL会自动四舍五入,结果是999.0001
考虑到用准确精度定义近似数据,可能会出现一些问题。并且他们的实现还受硬件或操作系统的影响。从8.0.17开始,MySQL不支持非标准语法。
MySQL 8.0.17是对浮点影响比较大的一个版本。从该版本开始,MySQL将逐步去除浮点型的支持。除了上面提到的语法变化,还建议去除UNSIGNED
支持,MySQL考虑使用简单的检查约束来替代它。
所以建议使用定点类型(DECIMAL
等)替代浮点类型(FLOAT
和DOUBLE
)。
如果你的业务一定要近似数据值,那么也建议使用DOUBLE
来声明浮点,不建议使用FLOAT
或DOUBLE(P)
。
FLOAT
可能还有一些意向不到的问题(如https://dev.mysql.com/doc/refman/8.0/en/no-matching-rows.html)。浮点型的常见问题,可参考https://dev.mysql.com/doc/refman/8.0/en/problems-with-float.html
MySQL字段类型声明时,可以使用BOOLEAN
类型,对应Java POJO字段可以是boolean
。
但MySQL实际上没有内置的布尔类型,声明为布尔类型,会使用TINYINT(1)
存储。
show create table xxx;
,会发现BOOLEAN
字段的类型是TINYINT(1)
TRUE=1
、FALSE=0
,查询结果集时可以看到显示的是0、1尽管如此,建表SQL中仍强烈建议写为BOOLEAN
,这样语义清晰、便于维护。
BOOLEAN的介绍可参考:https://www.yiibai.com/mysql/boolean.html
- 建议读完下面的类型介绍后,进一步阅读后面的《容易忽略的存储细节》章节,对时间类型的理解会很有帮助。
- 时间类型要用好,需配合各类函数,可参考《MySQL日期与时间函数(日期/时间格式化、增减、对比、时区等)》
描述 | 显示格式 | 取值范围 | |
---|---|---|---|
YEAR |
年 | YYYY |
0000 、1901 ~2155 |
DATE |
有日期、没有时间 | YYYY-MM-DD |
1000-01-01 ~ 9999-12-31 |
TIME |
时分秒 | hh:mm:ss |
-838:59:59 ~ 838:59:59 7 |
DATETIME |
包含日期和时间 | YYYY-MM-DD hh:mm:ss |
1000-01-01 00:00:00 ~ 9999-12-31 23:59:59 |
TIMESTAMP |
包含日期和时间 | YYYY-MM-DD hh:mm:ss |
1970-01-01T00:00:01Z ~ 2038-01-19T03:14:07Z |
从5.6.4开始,含TIME的类型开始支持小数秒,在此之前所有类型都不支持(使用MICROSECOND
可以获取,但存储时都会丢弃)。多了数据,存储开销也有变化:
类型 | 小数秒 5.6.4开始 |
存储字节数 5.6.4之前 |
存储字节数 5.6.4开始 |
---|---|---|---|
YEAR |
不支持 | 1 | 1 |
DATE |
不支持 | 3 | 3 |
TIME |
-838:59:59.000000 ~ 838:59:59.000000 |
3 | 3+小数秒存储 |
DATETIME |
1000-01-01 00:00:00.000000 ~ 9999-12-31 23:59:59.999999 |
8 | 5+小数秒存储 |
TIMESTAMP |
1970-01-01T00:00:01.000000Z ~ 2038-01-19T03:14:07.999999Z |
4 | 4+小数秒存储 |
小数秒的存储需要0到3个字节,取决于精度:
精度(小数秒位数) |
0 | 1~2 | 3~4 | 5~6 |
---|---|---|---|---|
存储字节数(Byte) |
0 | 1 | 2 | 3 |
注意:小数秒会四舍五入。
如果向一个未声明小数秒精度的列存入小数秒,当小数秒≥.5
时,会发生四舍五入。
比如极端边界值2019-11-19 23:59:59.999
,如果存放到datetime
字段中,实际存放值会变成2019-11-12 00:00:00
,日期实际上多了一天。
YEAR
有2位和4位两种类型,取值范围不同。日常中主要使用的是4位,上面也只介绍了这种。MySQL 8.0开始已经不再支持2位,如果想要了解,可参考《MySQL官方文档-YEAR类型》。TIME
类型小时部分最大值可以达到838
而不是24
,是因为TIME
类型还支持表示时间差值,比如TIMEDIFF
等函数运算结果。TIMESTAMP
是将当前时间转换为 UTC时间 进行存储,查询或检索时再转换为当前时区。
- 如果存储时是一个时区,查询时更换了时区,则显示值与存储时的值是不同的。
DATETIME
等类型存储绝对值,而不是UTC值,所以没有该问题。TIMESTAMP
同样存在 UNIX 2038问题、即无法表示大于2038-01-19T03:14:07.999999
的值,大部分系统和语言已解决该问题,MySQL目前没有解决该问题。(测试时注意时区问题,东八区需插入2038-01-19T11:14:08
来测试)- MySQL有一个
UNIX_TIMESTAMP()
函数,是获取UNIX时间戳的整数值,不会像TIMESTAMP
转换成当前时区显示,且入参支持大于TIMESTAMP
最大值的时间。
DATETIME
支持ON UPDATE CURRENT_TIMESTAMP
,该项不再是TIMESTAMP
专有。但每个表仍然只能有一个自动更新的时间列。在阅读前面的章节时,不知道你有否下面的疑问:
DATE
是3字节、TIME
是3字节,而DATETIME
不是6字节呢?DATETIME
可以从8字节降到5字节,而数据取值范围不变?DATETIME
和TIMESTAMP
有什么区别,哪些区别导致了两者存储差了1个字节?要解决这几个问题,要从日期和时间的存储结构入手8。下面我们看看字段的取值范围与存储结构。
YEAR
和DATE
的存储结构YEAR
和DATE
因为不支持小数秒,所以在5.6.4前后,取值范围和存储结构不变,且字节序仍为LITTLE-ENDIAN
(《什么是Little Endian和Big Endian》):
YEAR
:1字节整数。存储从1901
开始的年数,1字节最大可表示255,因此可以表示到2155
;显示时转换为YYYY
;
00000000 = 0000
00000001 = 1901
11111111 = 2155
DATE
:3字节整数。取值范围1000-01-01 00:00:00
~ 9999-12-31 23:59:59
,以YYYY×16×32 + MM×32 + DD
计算出实际值存放,这样算最大位数就是 log 2 9999 × 16 × 32 ≈ 23 \log_2^{9999\times16\times32}≈23 log29999×16×32≈23,近似3字节。
尽管存储上支持
2019-11-31
,但MySQL服务器会判断日期是否真实有效。对于2019-11-31
,严格模式下会报错,非严格模式下会自动转成0000-00-00
。
如果要启用这些非法的日期,可以配置系统变量ALLOW_INVALID_DATES
。
所有时间类型都如此。
TIME
、DATETIME
、TIMESTAMP
的存储结构从5.6.4开始,三个类型的非小数部分,字节序由LITTLE-ENDIAN
变更为BIG-ENDIAN
([《什么是Little Endian和Big Endian》]。
同时TIME
和DATETIME
的存储结构发生了较大变化。
TIME
TIME
类型从5.6.4开始,除了增加了小数秒支持,存储结构也发生了较大变化:
DD×24×3600 + HH×3600 + MM×60 + SS
计算出实际值,存储到3字节的整数上。取值范围为-838:59:59 ~ 838:59:59
。
log 2 31 × 24 × 3600 ≈ 22 \log_2^{31\times24\times3600}≈22 log231×24×3600≈22
0
0
00000000 00
000000
000000
符号位
预留扩展位
HH(0~838)
MM(0~59)
SS(0~59)
TIMESTAMP
TIMESTAMP
类型在5.6.4前后,只是增加了小数秒支持,存储结构基本无变化,仍是以4字节整数(可看做SIGNED INT
)存储从1970-01-01T00:00:00Z
(UNIX纪元)经过的秒数。
UNIX纪元看做0值。小于UNIX纪元的时间插入会报错,官方文档虽未说明,怀疑TIMESTAMP
与DATETIME
类似,符号位永远是1,0值是保留值、仅用于以后扩展。
DATETIME
DATETIME
类型从5.6.4开始,除了增加了小数秒支持,存储结构也发生了较大变化,存储开销从8字节降到了5字节:
YYYY×10000 + MM×100 + DD
),4字节整数表示时间(HH×10000 + MM×100 + SS
),计算出实际值,存储到8字节的整数上。取值范围为1000-01-01 00:00:00
~ 9999-12-31 23:59:59
。
10000已经大于MM*100+DD/SS了,所以下面只用这个来估算位数:
( log 2 9999 × 10000 ≈ 30 \log_2^{9999\times10000}≈30 log29999×10000≈30) + ( log 2 60 × 10000 ≈ 18 \log_2^{60\times10000}≈18 log260×10000≈18)
疑问:按算法来讲,时间部分存储到3个字节就够了,那5.6之前为什么要4个字节呢? 官方文档没有说明,即使时间类型底层是整数值,但DATE
类型也可以使用3字节的整数、而不是4字节的INT
。有了解的朋友还请留言指导下。
符号位 年+月 YYYY(0~9999)*13 + MM(0~12)
天 DD(0~31)
小时 HH(0~23)
分 MM(0~59)
秒 SS(0~59)
0
0 0000 0000 0000 0000
0 0000
0 0000
00 0000
00 0000
1bit 17bit 5bit 5bit 6bit 6bit 共计
1+17+5+5+6+6=40bits
,占用5个字节。其中符号位永远是1,0只仅是保留值、用于以后扩展。
特别感谢August大橙子的帮助,对该问题深入源码层面的分析。推荐阅读他的《MySQL 5.6.4 及以上,DATETIME 存储空间如何从 8 字节 减小到 5 字节 ,却能存储同样的时间范围的?》,理解MySQL在存储上的巧妙设计
日常使用中,时间类型的选择是一个比较头疼的问题,有提倡DATETIME
或TIMESTAMP
,有受TIMESTAMP
启发、建议直接存储INT
或BIGINT
的。
我们从下面几个维度比较:
DATETIME
> TIMESTAMP
= INT
DATETIME
= TIMESTAMP
> INT
DATETIME
> TIMESTAMP
= INT
DATETIME
和TIMESTAMP
支持ON UPDATE,INT
不支持如果不考虑存储、对细微的性能感受不大、对时间范围要求较高,建议选择DATETIME
;
如果考虑存储、且对时间范围要求不高,建议选择TIMESTAMP
。
INT
的代码处理成本较高,不建议;BIGINT
虽然可以解决UNIX 2038问题,但代码处理成本高,而且相信MySQL未来会提供标准解决方案。
关于字段的取值范围、存储开销,以上就是全部内容。
- 如需计算字段长度,可以使用函数length、char_length、bit_length,具体可参考《MySQL length/bit_length/char_length计算字段长度/大小》
- 更多基础命令可参考《https://learn.blog.csdn.net/article/category/9232935》
文中有几个设计疑问,未找到资料验证,待更新补充:
DATETIME
以前为什么用4字节而不是3字节存储时分秒部分;10000
这个位移量是怎么设计得来的?DATETIME
在5.6.4开始,年份用13位存储,理论上存不到9999才对,为什么官方说可以支持?
以上。感谢您的阅读。
MySQL字段存储开销:https://dev.mysql.com/doc/refman/8.0/en/storage-requirements.html ↩︎
MySQL字符串类型概述:https://dev.mysql.com/doc/refman/8.0/en/string-type-overview.html ↩︎ ↩︎
MySQL Char类型:https://dev.mysql.com/doc/refman/5.6/en/char.html ↩︎
MySQL官方文档-存储要求:https://dev.mysql.com/doc/refman/8.0/en/storage-requirements.html ↩︎
MySQL官方文档-列数与行大小限制:https://dev.mysql.com/doc/refman/8.0/en/column-count-limit.html#column-count-limits ↩︎ ↩︎
MySQL官方文档-数值类型:https://dev.mysql.com/doc/refman/5.6/en/integer-types.html ↩︎
MySQL官方文档 - 时间与日期的各部分取值:https://dev.mysql.com/doc/refman/8.0/en/date-and-time-literals.html ↩︎
MySQL官方文档 - 时间与日期的存储表现:https://dev.mysql.com/doc/internals/en/date-and-time-data-type-representation.html ↩︎