在使用MySQL的过程中,在存储字符串时,大家或许都有过这样或那样的困惑,譬如:

1.  对于固定长度的字符串,为什么推荐使用 CHAR 来存储?

2.  VARCHAR 可设置的最大长度是多少?

3.  给定一个字符串,怎么知道它的空间使用情况?

4.  创建索引时,提示“Index column size too large. The maximum column size is 767 bytes”,该如何解决?

5.  VARCHAR 为何要按需设置?VARCHAR(50) 和 VARCHAR(500) 有什么区别?

下面就这些问题做一个系统的分析。

1. CHAR与VARCHAR的区别

两者都可用来存储字符串。只不过 CHAR 常用来存储固定长度的字符串,VARCHAR 常用来存储可变长度的字符串。为什么要这样区分呢?

首先看下面这个表格。CHAR(4) 和 VARCHAR(4) 的存储对比。

CHAR(4) 存储需求(字节) VARCHAR(4) 存储需求(字节)
'' '    ' 4 '' 1
'ab' 'ab   ' 4 'ab' 3
'abcd' 'abcd' 4 'abcd' 5
'abcdefgh' 'abcd' 4 'abcd' 5

基于表格的内容,我们可以得出以下结论:

对于 CHAR(4) ,

1.  无论插入什么值,存储需求都是不变的,固定4个字节。

2.  在实际存储的时候,对于不足4字节的值,右边会以空格填充。

对于 VARCHAR(4) ,

1.  存储的需求与插入的值有关。

2.  存储的需求 = 字符串所占的字节数 + 1。为什么要加1呢?这个与 VARCHAR 的实现有关,为了实现“按需分配”的目的,它需要额外的字节来表示字符串的长度。

所以,对于固定长度的字符串推荐使用 CHAR 来存储,相对于 VARCHAR ,前者会少用一个字节。

另外,'abcdefgh'被截断为'abcd'进行存储,是在 SQL_MODE 非严格模式下。具体什么是 SQL_MODE 的非严格模式,可参考:使用MySQL,SQL_MODE有哪些坑,你知道么?

2. VARCHAR(M) 能设置的最大长度

M 限制了 VARCHAR 能存储的字符串的最大长度,注意,是字符,不是字节,其有效值范围为 0 ~ 65535。虽然可设置的范围是 0 ~ 65535,但 M 真的就能设置为65535 吗?

看下面这个测试。

mysql> create table t (c1 varchar(65535)) charset latin1;
ERROR 1118 (42000): Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs mysql> create table t (c1 varchar(65534)) charset latin1;
ERROR 1118 (42000): Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs mysql> create table t (c1 varchar(65533)) charset latin1;
ERROR 1118 (42000): Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs mysql> create table t (c1 varchar(65532)) charset latin1;
Query OK, 0 rows affected (0.06 sec)

由此来看,在 latin1 字符集下,M 最大就只能设置为 65532。

其实不然,再看下面这个示例。

mysql> create table t (c1 varchar(65533) not null) charset latin1;
Query OK, 0 rows affected (0.06 sec) mysql> create table t (c1 varchar(65534) not null) charset latin1;
Query OK, 0 rows affected (0.06 sec)

如果将列定义为NOT NULL,M 最大可设置为 65533。

上面演示的是latin1下的使用场景,如果是其它字符集呢?

mysql> create table t (c1 varchar(65533) not null) charset utf8mb4;
ERROR 1074 (42000): Column length too big for column 'c1' (max = 16383); use BLOB or TEXT instead mysql> create table t (c1 varchar(65533) not null) charset utf8;
ERROR 1074 (42000): Column length too big for column 'c1' (max = 21845); use BLOB or TEXT instead

基于报错信息,可以看出,对于utf8mb4字符集,M最大只能设置为16383。对于utf8字符集,M最大只能设置为21845。这两个数值是怎么计算出来的呢?

在utf8mb4字符集中,最多需要4个字节来表示一个字符,所以 65535 / 4 = 16383 。而在utf8字符集中,最多需要3个字节来表示一个字符,所以 65535 / 3 = 21845。

由此来看,在设置 M 的大小时,起决定作用的并不是 M 的有效值范围(0 ~ 65535),而是 M * 字符集的最大字节数不能超过65535个字节。

为什么不能超过 65535 字节呢?因为MySQL限制了一条记录的最大长度就是 65535 字节。

除此之外,对于VARCHAR,在实际设置时,还需考虑以下两个因素:

1.  MySQL需要1 ~  2个字节来表示字符串的长度。具体来说,如果字符串占用的字节数在 0 ~255 之间,需1个字节来表示,如果大于 255 个字节,则需2个字节来表示。

2.  如果列定义为NULL,额外还需要1个字节。

既然 65535 是记录的最大长度,则这个限制不仅仅是针对一列,而是所有列。即所有列的长度加起来不能超过65535。

看下面这个示例,指定了2个列,分别定义为VARCHAR和INT。

mysql> create table t (c1 varchar(65530) not null,c2 int not null) charset latin1;
ERROR 1118 (42000): Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs mysql> create table t (c1 varchar(65529) not null,c2 int not null) charset latin1;
Query OK, 0 rows affected (0.10 sec)

因为INT会占4个字节,所以 c1 最大就只能设置为65535 - 4 - 2 = 65529。这里之所以要减去2,是因为varchar(65529)超过了255个字节,需要2个字节来表示其长度。

报错信息中,提到“The maximum row size for the used table type, not counting BLOBs, is 65535”,即记录的最大长度限制不包括BLOB等字段。

之所以将BLOB和TEXT排除在外,是因为它的内容会单独存储在其它页中。但即便如此,存储BLOB和TEXT的指针信息也需要9 ~ 12个字节,具体来说:

  • TINYTEXT(TINYBLOB): 9 字节
  • TEXT(BLOB): 10 字节
  • MEDIUMTEXT(MEDIUMBLOB): 11字节
  • LONGTEXT(LONGBLOB): 12字节。

看下面这个示例,指定了2个列,分别定义为VARCHAR和TEXT。

mysql> create table t (c1 varchar(65524) not null,c2 text not null) charset latin1;
ERROR 1118 (42000): Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change somecolumns to TEXT or BLOBs mysql> create table t (c1 varchar(65523) not null,c2 text not null) charset latin1;
Query OK, 0 rows affected (0.13 sec)

因为TEXT占了10个字节,所以 c1 最大可设置为65535 - 10 - 2 = 65523。

注意,上面提到的一条记录的最大长度不能超过65535字节是MySQL的限制,与存储引擎无关。实际上,存储引擎对行长也有限制。在InnoDB存储引擎中,就规定一条记录的最大长度不能超过数据页大小的1/2。

InnoDB中数据页的大小由innodb_page_size参数决定,默认为16K。所以,在页长为16K的情况下,InnoDB中一条记录的大小不能超过8K。如果超过了8K,InnoDB会将部分变长字段存储在外部页中。

看下面这个示例,因为定义的都是CHAR,定长字段,所以不会存储在外部页中。

mysql> create table t (
c1 char(255),c2 char(255),c3 char(255),
c4 char(255),c5 char(255),c6 char(255),
c7 char(255),c8 char(255),c9 char(255),
c10 char(255),c11 char(255),c12 char(255),
c13 char(255),c14 char(255),c15 char(255),
c16 char(255),c17 char(255),c18 char(255),
c19 char(255),c20 char(255),c21 char(255),
c22 char(255),c23 char(255),c24 char(255),
c25 char(255),c26 char(255),c27 char(255),
c28 char(255),c29 char(255),c30 char(255),
c31 char(255),c32 char(255)
) charset latin1;
ERROR 1118 (42000): Row size too large (> 8126). Changing some columns to TEXT or BLOB may help. In current row format, BLOB prefix of 0 bytes is stored inline.

3. VARCHAR的空间使用情况

给定一个VARCHAR类型的字符串,怎么知道它的空间使用情况呢?

首先,看看官方文档的说法。

类型 存储需求
VARCHAR(M) L + 1 bytes if column values require 0 − 255 bytes, L + 2 bytes if values may require more than 255 bytes

这里的 L 指的是字符串所占的字节数,与字符集有关。

以“中国”为例。

在utf8mb4中,一个中文汉字需要 3 个字节来表示,所以,“中国”通过utf8mb4来存储,会占用6个字节。

而在GBK中,一个中文汉字只需2个字节来表示,所以,“中国”通过GBK来存储,会占用4个字节。

除此之外,还需要额外的1~2个字节来表示字符串的长度。

4. VARCHAR(50) 和 VARCHAR(500) 的区别

很多人可能会好奇,VARCHAR 不是按需分配的么?在满足业务的需求情况下,设置 VARCHAR(50) 和 VARCHAR(500) 有什么区别呢?最后的空间占用还不是以实际写入的字符串为主,从这个角度来看,确实没错。但考虑以下两点:

1. 索引有最大长度的限制

对于行格式为 REDUNDANT 或 COMPACT 的InnoDB表,索引的最大长度被限制为767字节。所以,在MySQL 5.6 中,在创建索引时,我们通常会碰到“Index column size too large. The maximum column size is 767 bytes”错误:

mysql> create table t(c1 varchar(200)) charset=utf8mb4;
Query OK, 0 rows affected (0.02 sec) mysql> alter table t add index(c1);
ERROR 1709 (HY000): Index column size too large. The maximum column size is 767 bytes.

上面这个限制不仅仅适用于单个索引,同样也适用于复合索引。

要解决这个问题,可选的方案有:

1.  减少字段的长度,确保字段的长度 * 字符集的最大字节数不超过767。

2.  设置前缀索引,确保前缀索引的长度 * 字符集的最大字节数不超过767。

mysql> alter table t add index(c1(191));
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0

3.  将表的行格式设置为 DYNAMIC 或 COMPRESSED,同时开启innodb_large_prefix,索引的最大长度可支持3072字节。

在 MySQL 5.6 中,如果要将表的行格式设置为DYNAMIC 或 COMPRESSED,必须将参数innodb_file_format设置为Barracuda。

innodb_file_format用来设置InnoDB文件的格式,支持的文件格式有Antelope和Barracuda。Antelope是最早的文件格式,支持REDUNDANT和COMPACT这两种行格式,而Barracuda则是最新的文件格式,支持DYNAMIC和COMPRESSED这两种行格式。

在 MySQL 5.6 中,innodb_file_format默认为Antelope,创建表时,如果没有显式指定row_format,则默认为Compact。

在 MySQL 5.7 中,innodb_file_format默认为Barracuda,创建表时,如果没有显式指定row_format,则默认为Dynamic。

在 MySQL 8.0 中,innodb_file_format被移除了,取而代之的是innodb_default_row_format。该参数用来设置默认的row_format,默认值为dynamic。

而innodb_large_prefix呢?在 MySQL 5.6 中,默认为OFF。在 MySQL 5.7 中,默认为ON,在 MySQL 8.0中,也移除了。

这也是为什么在 MySQL 5.6 中,更容易出现“Index column size too large. The maximum column size is 767 bytes”错误。

2. MEMORY引擎的限制

在 MySQL 8.0 之前,内存临时表只支持 MEMORY 引擎,而 MEMORY 引擎会将 VARCHAR 等变长类型作为定长来分配内存。

When in-memory internal temporary tables are managed by the MEMORY storage engine, fixed-length row format is used. VARCHAR and VARBINARY column values are padded to the maximum column length, in effect storing them as CHAR and BINARY columns.

可喜的是,从 MySQL 8.0 开始,内存临时表支持 TempTable 引擎。TempTable 引擎,从 MySQL 8.0.13 开始,优化了VARCHAR等变长类型的存储。

5. 总结

1.  对于固定长度的字符串推荐使用 CHAR 来存储。

2.  VARCHAR能设置的最大长度与使用的字符集、自身占用的字节数、是否定义为NULL有关。

3.  MySQL限制了一条记录的最大长度是 65535 字节。

4.  InnoDB存储引擎限制了一条记录的最大长度不能超过数据页大小的1/2。但在实际处理的时候,InnoDB会将部分变长字段存储在外部页中,所以我们实际能存储的比限制的要大。

5.  字符串所占的字节数,与字符集有关。除此之外,还需要额外的1~2个字节来表示字符串的长度。

6.  在满足业务需求的情况下,VARCHAR应越短越好。

7.  对于行格式为 REDUNDANT 或 COMPACT 的InnoDB表,索引的最大长度为767字节。

8.  对于行格式为 DYNAMIC 或 COMPRESSED 的InnoDB表,索引的最大长度为3072字节。

9.  MEMORY 引擎会将 VARCHAR 等变长类型作为定长来分配内存。

说说 VARCHAR 背后的那些事的更多相关文章

  1. spring @EnableAspectJAutoProxy背后的那些事(spring AOP源码赏析)

    在这个注解比较流行的年代里,当我们想要使用spring 的某些功能时只需要加上一行代码就可以了,比如: @EnableAspectJAutoProxy开启AOP, @EnableTransaction ...

  2. 关于《Swift开发指南》背后的那些事

    时间轴(倒叙)2014年8月底在图灵出版社的大力支持下,全球第一本全面.系统.科学的,包含本人多年经验的呕心沥血之作<Swift开发指南>(配有同步视频课程和同步练习)全线重磅推出2014 ...

  3. Now直播应用的后台服务器性能测试实践

    版权声明:本文由Oliver原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/208 来源:腾云阁 https://www.q ...

  4. [读书系列] 深度探索C++对象模型 初读

    2012年底-2014年初这段时间主要用C++做手游开发,时隔3年,重新拿起<深度探索C++对象模型>这本书,感觉生疏了很多,如果按前阵子的生疏度来说,现在不借助Visual Studio ...

  5. [C语言]贪吃蛇_结构数组实现

    一.设计思路 蛇身本质上就是个结构数组,数组里存储了坐标x.y的值,再通过一个循环把它打印出来,蛇的移动则是不断地刷新重新打印.所以撞墙.咬到自己只是数组x.y值的简单比较. 二.用上的知识点 结构数 ...

  6. Android性能优化之Splash页应该这样设计

    目前SplashActivity的设计 目前市场上的应用在启动时基本上都会先启动一个SplashActivity,作为一个欢迎界面,为什么这样设计呢? 个人总结有三个优点: 1.可以给用户更好的体验 ...

  7. Android 开发:由模块化到组件化(一)

    在Android SDK一文中,我们谈到模块化和组件化,现在我们来聊聊组件化开发背后的哪些事.最早是在广告SDK中应用组件化,但是同样适用于普通应用开发 以下高能,请做好心理准备,看不懂请发私信来交流 ...

  8. [Android Pro] 由模块化到组件化(一)

    cp from : https://blog.csdn.net/dd864140130/article/details/53645290 在Android SDK一文中,我们谈到模块化和组件化,现在我 ...

  9. Java 原生日志 java.util.logging

    简介 Java 中的 Logging API 让 Java 应用可以记录不同级别的信息,它在debug过程中非常有用,如果系统因为各种各样的原因而崩溃,崩溃原因可以在日志中清晰地追溯,下面让我们来看看 ...

随机推荐

  1. Jenkins自动化CI&CD流水线

    1 环境说明 主机名称 IP cpu核数/内存/硬盘 安装软件 用途 controlnode 172.16.1.120 2/2/60 git 代码仓库 slavenode1 172.16.1.121 ...

  2. 由ASP.NET Core WebApi添加Swagger报错引发的探究

    缘起 在使用ASP.NET Core进行WebApi项目开发的时候,相信很多人都会使用Swagger作为接口文档呈现工具.相信大家也用过或者了解过Swagger,这里咱们就不过多的介绍了.本篇文章记录 ...

  3. Java:java -jar命令讲解

    1. 当前ssh窗口被锁定,可按CTRL + C打断程序运行,或直接关闭窗口,程序退出 #正常启动jar包 java -jar XXX.jar#当前ssh窗口被锁定,可按CTRL + C打断程序运行, ...

  4. shiro框架基础

    一.shiro框架简介 Apache Shiro是Java的一个安全框架.其内部架构如下: 下面来介绍下里面的几个重要类: Subject:主体,应用代码直接交互的对象就是Subject.代表了当前用 ...

  5. B站挂了之后出现的tengine是个啥?

    一.描述 晚上刚洗漱完之后听同学说:B站挂了?woc?真挂了? 嗯!确实挂了,404的状态码,懂的都懂. 不过,最下面的tengine字眼吸引了我的注意,一时兴起,打算看看它是个什么东西,起码搞一个h ...

  6. NIO 输入输出

    NIO 是java14 API 提供的一种新输入输出流,一套用于标准IO的文件读写,一套用于网络编程. 1. NIO 与IO 的区别 IO流以字节流输入输出,一次以一个字节进行数据操作,效率慢: NI ...

  7. 传统.NET 4.x应用容器化体验(2)

    上一篇我们基于Windwos Server 2019 with Container初步跑了一个ASP.NET WebForm应用程序.本篇我们来自己编译部署一个ASP.NET MVC应用程序到Wind ...

  8. 理解并掌握Promise的用法

    前沿:  Promise在处理异步操作非常有用.项目中,与后端进行数据请求的时候经常要用到Promise.我们可以用promise + xhr进行ajax的封装.也可以使用基于promise封装的请求 ...

  9. .net core番外第2篇:Autofac的3种依赖注入方式(构造函数注入、属性注入和方法注入),以及在过滤器里面实现依赖注入

    本篇文章接前一篇,建议可以先看前篇文章,再看本文,会有更好的效果. 前一篇跳转链接:https://www.cnblogs.com/weskynet/p/15046999.html 正文: Autof ...

  10. springMVC-11-验证码

    springMVC-11-验证码 导入依赖 <!--Kaptcha 验证码依赖 前面已导过servlet-api需排除--> <dependency> <groupId& ...