MYSQL中tinyint(1)的正确理解

  张一帆   2017年05月21日


mysql头图

  今天,想起了一个关于mysql中类型长度的问题。一切的起源就是两年前和同事聊天时讨论状态位的字段用什么字段类型来存储,我还依稀记得刚进新浪工作的时候开发项目时总是将数字类型设置为int并且在长度栏填写10。程序在应用的时候没有任何问题而且也从没有关注过mysql存储类型相关细节。

  可是,有的时候在和同事聊天的过程中会突然谈论起mysql的存储类型相关问题,而我也没觉得有什么可以拿出来侃侃而谈的知识。后来,有一天新浪基础架构部门发了一封关于调整所有部门数据库用户id字段类型的邮件,邮件中要求所有用到uid的地方需要改成bigint类型。

  当时我正在负责新浪游戏的微坛项目,项目中的uid当然就是用int(10)来设定的。为了响应公司的要求,我就简简单单的将uid摄为了bigint(11)。在做出这一改变的过程中,我也询问了部门技术主管关于为什么要更改这个字段类型的原因,主管跟我们说因为起初新浪微博的用户id设置的就是int(10),后来随着微博的业务发展壮大,用户量也急剧增长,原先10位的用户id已经不能够存储大于10位数了,所以让我们进行修改。比如以前用户id为10个9,后来有一个人注册了新浪微博账号,账号本应该加一位,可是因为只能够存十位所以数据库将这新进的人id覆盖了原先的id,这样就导致了问题的发生。

  当时的我一听觉得很有道理啊,10个9加1则进一位就成了1和10个0 。那么我改成bigint以后将长度写为11就能够满足正常的逻辑了,而且变成了11位,不论后面进再多少个人我也完全不怵啊。于是,这个问题就这样“完美”的解决了,而之后的运行也没有发生相关的问题。直到后来的某一天在翻看mysql数据类型所占字节长度的文章时,才发现当年的我真是兔羊兔森破。直到再后来又和同事聊状态位字段应该用什么字段类型及长度存储的时候,我说用tinyint(1)来存储,有符号tinyint可以存储数值最大是127,然而后边的长度1就是限定只能存储1位,也就是0-9 。而且,当年说这番话的时候我是很坚定的回答的。我都能想到当时我的样子是多么的胸有成竹啊,然而事实并不是这个样子的。。。我对问题的理解真是有够知其一不知其二的,如果那个同事能够反问我,如果1就只能存0-9,那么int(1)不和tinyint(1)一样了吗?我想我就不会认为自己很有道理了。

  好了,闲话就扯到这里。今天,我闲来有功夫就想将真正对的理解落于书面,这样的好处自然是如果接下来有理解错误的地方,我可以在未来的某个时间再接着此篇文章来进行知识的更新。那么,接下来我就以tinyint(1)这个概念进行阐述。

  在mysql中不同的字段类型有着不同的长度。tinyint就被设置为1字节。在计算机中规定8位为1字节。1字节为最小单位,字节的值就代表着高电平或者低电平,用数值来表示就是1和0 。那么,8位的话存储的大小就是00000000 - 11111111(无符号二进制),转化为十进制的话就是0-255 。所以,1字节在没有符号的时候能够存储的值可以用十进制的0-255来表示。也就是说tinyint类型可以存储256个十进制的数字。而如果有符号的话8位存储的二进制值就是10000000-01111111(-128~127),在计算机中涉及到负数的时候都是用补码的形式存储的,具体为什么可以参看《关于2的补码》

  话题扯远了,赶紧拽回来。。。

  我们知道了tinyint中在有无符号时分别可以存储什么范围的十进制数以后,那么后边的括号中的1又代表的是什么呢?其实我们可以理解为用多长的长度来读出这个数字。通过字面的意思我们可以理解为tinyint(1)就是用1位字符的长度读出tinyint值。我觉得tinyint(1)并不是很好的列子,我打算先用tinyint(2)作为例子来进行演示,之后再回头来说1的情况。那我们有两种情况:

  • 当我的值的长度超过2字符长度,结果会如何?
  • 当我的值的长度不超过2字节的长度,结果会如何?

  首先,让我们先创建一张表出来。

CREATE TABLE `a` (
  `id` int(11) NOT NULL,
  `c` tinyint(2) DEFAULT NULL,
  PRIMARY KEY (`id`),
) ENGINE=MyISAM DEFAULT CHARSET=utf8

  我往表中插入几个数据,

INSERT INTO `a` VALUES ('1','3');
INSERT INTO `a` VALUES ('2','33');
INSERT INTO `a` VALUES ('3','0');
INSERT INTO `a` VALUES ('4','111');

  我们分别执行select操作,看看会有什么结果。

mysql> select c from a where id =1;
+------+
| c    |
+------+
|    3 |
+------+
1 row in set (0.00 sec)

mysql> select c from a where id =2;
+------+
| c    |
+------+
|   33 |
+------+
1 row in set (0.00 sec)

mysql> select c from a where id =3;
+------+
| c    |
+------+
|    0 |
+------+

1 row in set (0.00 sec)

mysql> select c from a where id =4;
+------+
| c    |
+------+
|  111 |
+------+
1 row in set (0.00 sec)

  结果显示即使我设置了tinyint(2)以后查询出来的结果还是可以显示出所有位数的数据,这个(2)根本没起任何作用啊。即使我的数据的长度超过了2或者没有超过2结果都是可以正常被显示出来的。

  那么,我再重新创建一次这张表,不同的是我给c增加了0填充。

CREATE TABLE `a` (
  `id` int(11) NOT NULL,
  `c` tinyint(2) unsigned zerofill DEFAULT NULL,
  PRIMARY KEY (`id`),
) ENGINE=MyISAM DEFAULT CHARSET=utf8

  我同样插入一样的数据,然后再对他们执行查询操作。

mysql> select c from a where id =1;
+------+
| c    |
+------+
|   03 |
+------+
1 row in set (0.00 sec)

mysql> select c from a where id =2;
+------+
| c    |
+------+
|   33 |
+------+
1 row in set (0.00 sec)

mysql> select c from a where id =3;
+------+
| c    |
+------+
|   00 |
+------+
1 row in set (0.00 sec)

mysql> select c from a where id =4;
+------+
| c    |
+------+
|  111 |
+------+
1 row in set (0.00 sec)

  可以看到区别了吧,如果我设置了0填充的话。那么,数据会按照不足2位的前补0,多于2位的完全展示出来。这也就是我前面提到的“用多长的长度来读出这个数字”。

  好了,到这里我们发现,不论长度取什么值,光设置他是没有任何作用的,我们还需要指定当前数据长度小于这个值时用什么来填充它。于是,tinyint(4),tinyint(16)这样的设置我们就知道在读出的时候他们会长什么样子来了吧?

  这里有个mysql自己的设置细节,如果我要将c这个字段设置为0填充,那么也还需将此字段同时设置为unsigned。如果你没有作此操作,mysql会自动为你加上。mysql的官方文档《Numeric Type Overview》有提到。

if you specify ZEROFILL for a numeric column, MySQL automatically adds the UNSIGNED attribute to the column.
如果你指定了ZEROFILL,MYSQL会自动的将UNSIGNED属性一并添加上。

  我想这样设置有可能是跟补码有关的。我们知道正数存放的时候前面都是0,而负数的话高位那里必须是1,所以用零填充的话一定会出问题。