CategoryResourceRepost/极客时间专栏/MySQL 必知必会/实践篇/02 | 字段:这么多字段类型,该怎么定义?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

16 KiB
Raw Blame History

你好,我是朱晓峰。

MySQL中有很多字段类型比如整数、文本、浮点数等等。如果类型定义合理就能节省存储空间提升数据查询和处理的速度相反如果数据类型定义不合理就有可能会导致数据超出取值范围引发系统报错甚至可能会出现计算错误的情况进而影响到整个系统。

之前我们就遇到过这样一个问题在销售流水表中需要定义商品销售的数量。由于有称重商品不能用整数我们想当然地用了浮点数为了确保精度我们还用了DOUBLE类型。结果却造成了在没有找零的情况下客人无法结账的重大错误。经过排查我们才发现原来DOUBLE类型是不精准的不能使用。

你看,准确地定义字段类型,不但关系到数据存储的效率,而且会影响整个信息系统的可靠性。所以,我们必须要掌握不同字段的类型,包括它们的适用场景、定义方法,这节课,我们就聊一聊这个问题。

首先我要说的是MySQL中最简单的数据类型整数类型。

整数类型

整数类型一共有5种包括TINYINT、SMALLINT、MEDIUMINT、INTINTEGER和BIGINT它们的区别如下表所示

这么多整数类型,咱们该怎么选择呢?

其实,在评估用哪种整数类型的时候,你需要考虑存储空间和可靠性的平衡问题:一方面,用占用字节数少的整数类型可以节省存储空间;另一方面,要是为了节省存储空间,使用的整数类型取值范围太小,一旦遇到超出取值范围的情况,就可能引起系统错误,影响可靠性。

举个例子在我们的项目中商品编号采用的数据类型是INT。

我们之所以没有采用占用字节更少的SMALLINT类型整数原因就在于客户门店中流通的商品种类较多而且每天都有旧商品下架新商品上架这样不断迭代日积月累。如果使用SMALLINT类型虽然占用字节数比INT类型的整数少但是却不能保证数据不会超出范围65535。相反使用INT就能确保有足够大的取值范围不用担心数据超出范围影响可靠性的问题。

你要注意的是,在实际工作中,系统故障产生的成本远远超过增加几个字段存储空间所产生的成本。因此,我建议你首先确保数据不会超过取值范围,在这个前提之下,再去考虑如何节省存储空间。

接下来,我再给你介绍下浮点数类型和定点数类型。

浮点数类型和定点数类型

浮点数和定点数类型的特点是可以处理小数,你可以把整数看成小数的一个特例。因此,浮点数和定点数的使用场景,就比整数大多了。

我们先来了解下MySQL支持的浮点数类型分别是FLOAT、DOUBLE、REAL。

  • FLOAT表示单精度浮点数
  • DOUBLE表示双精度浮点数
  • REAL默认就是DOUBLE。如果你把SQL模式设定为启用“REAL_AS_FLOAT”那么MySQL就认为REAL是FLOAT。如果要启用“REAL_AS_FLOAT”就可以通过以下SQL语句实现
SET sql_mode = “REAL_AS_FLOAT”;

FLOAT和DOUBLE这两种数据类型的区别是啥呢其实就是FLOAT占用字节数少取值范围小DOUBLE占用字节数多取值范围也大。

看到这儿,你有没有发现一个问题:为什么浮点数类型的无符号数取值范围,只相当于有符号数取值范围的一半,也就是只相当于有符号数取值范围大于等于零的部分呢?

其实这里的原因是MySQL是按照这个格式存储浮点数的符号S、尾数M和阶码E。因此无论有没有符号MySQL的浮点数都会存储表示符号的部分。因此所谓的无符号数取值范围其实就是有符号数取值范围大于等于零的部分。

不过,我要提醒你的是,浮点数类型有个缺陷,就是不精准。因此在一些对精确度要求较高的项目中千万不要使用浮点数不然会导致结果错误甚至是造成不可挽回的损失。下面我来重点解释一下为什么MySQL的浮点数不够精准。

为了方便你理解,我来借助一个实际的例子演示下。

我们先创建一个表,如下所示:

CREATE TABLE demo.goodsmaster
(
  barcode TEXT,
  goodsname TEXT,
  price DOUBLE,
  itemnumber INT PRIMARY KEY AUTO_INCREMENT
);

运行这个语句我们就创建了一个表其中的字段“price”就是浮点数类型。我们再通过下面的SQL语句给这个表插入几条数据

-- 第一条
INSERT INTO demo.goodsmaster
(
  barcode,
  goodsname,
  price
)
VALUES 
(
  '0001',
  '书',
  0.47
);
-- 第二条
INSERT INTO demo.goodsmaster
(
  barcode,
  goodsname,
  price
)
VALUES 
(
  '0002',
  '笔',
  0.44
);
-- 第三条
INSERT INTO demo.goodsmaster
(
  barcode,
  goodsname,
  price
)
VALUES 
(
  '0002',
  '胶水',
  0.19
);

现在,我们运行一个查询语句,看看现在表里数据的情况:

SELECT * from demo.goodsmaster;

这个时候,我们可以得到下面的结果:

mysql> SELECT *
    -> FROM demo.goodsmaster;
+---------+-----------+-------+------------+
| barcode | goodsname | price | itemnumber |
+---------+-----------+-------+------------+
| 0001    | 书        |  0.47 |          1 |
| 0002    | 笔        |  0.44 |          2 |
| 0002    | 胶水      |  0.19 |          3 |
+---------+-----------+-------+------------+
3 rows in set (0.00 sec)

然后我们用下面的SQL语句把这3个价格加在一起看看得到了什么

SELECT SUM(price)
FROM demo.goodsmaster;

这里我们又用到一个关键字SUM这是MySQL中的求和函数是MySQL聚合函数的一种你只要知道这个函数表示计算字段值的和就可以了。

我们期待的运行结果是0.47 + 0.44 + 0.19 = 1.1,可是,我们得到的是:

mysql> SELECT SUM(price)
    -> FROM demo.goodsmaster;
+--------------------+
| SUM(price)         |
+--------------------+
| 1.0999999999999999 |
+--------------------+

查询结果是1.0999999999999999。看到了吗?虽然误差很小,但确实有误差。

你也可以尝试把数据类型改成FLOAT然后运行求和查询得到的是1.0999999940395355。显然,误差更大了。

虽然1.10和1.0999999999999999好像差不多,但是我们有时候需要以通过数值对比为条件进行查询,一旦出现误差,就查不出需要的结果了:

SELECT *
FROM demo.goodsmaster
WHERE SUM(price)=1.1

那么,为什么会存在这样的误差呢?问题还是出在MySQL对浮点类型数据的存储方式上

MySQL用4个字节存储FLOAT类型数据用8个字节来存储DOUBLE类型数据。无论哪个都是采用二进制的方式来进行存储的。比如9.625用二进制来表达就是1001.101或者表达成1.001101×2^3。看到了吗如果尾数不是0或5比如9.624),你就无法用一个二进制数来精确表达。怎么办呢?就只好在取值允许的范围内进行近似(四舍五入)。

现在你一定明白了为什么数据类型是DOUBLE的时候我们得到的结果误差更小一些而数据类型是FLOAT的时候误差会更大一下。原因就是DOUBLE有8位字节精度更高。

说到这里,我想你已经彻底理解了浮点数据类型不精准的原因了。

那么MySQL有没有精准的数据类型呢当然有这就是定点数类型DECIMAL

就像浮点数类型的存储方式决定了它不可能精准一样DECIMAL的存储方式决定了它一定是精准的。

浮点数类型是把十进制数转换成二进制数存储DECIMAL则不同它是把十进制数的整数部分和小数部分拆开分别转换成十六进制数进行存储。这样所有的数值就都可以精准表达了不会存在因为无法表达而损失精度的问题。

MySQL用DECIMALM,D的方式表示高精度小数。其中M表示整数部分加小数部分一共有多少位M<=65。D表示小数部分位数D<M。

下面我们就用刚才的表demo.goodsmaster验证一下。

首先我们运行下面的语句把字段“price”的数据类型修改为DECIMAL(5,2)

ALTER TABLE demo.goodsmaster
MODIFY COLUMN price DECIMAL(5,2);

然后,我们再一次运行求和语句:

SELECT SUM(price)
FROM demo.goodsmaster;

这次我们得到了完美的结果1.10。

由于DECIMAL数据类型的精准性在我们的项目中除了极少数比如商品编号用到整数类型外其他的数值都用的是DECIMAL原因就是这个项目所处的零售行业要求精准一分钱也不能差。

当然,在一些对精度要求不高的场景下,比起占用同样的字节长度的定点数,浮点数表达的数值范围可以更大一些。

简单小结下浮点数和定点数的特点:浮点类型取值范围大,但是不精准,适用于需要取值范围大,又可以容忍微小误差的科学计算场景(比如计算化学、分子建模、流体动力学等);定点数类型取值范围相对小,但是精准,没有误差,适合于对精度要求极高的场景(比如涉及金额计算的场景)。

文本类型

在实际的项目中我们还经常遇到一种数据就是字符串数据。比如刚刚那个简单的表demo.goodsmaster中有两个字段“barcode”和“goodsname”。它们当中存储的条码、商品名称都是字符串数据。这两个字段的数据类型我们都选择了TEXT类型。

TEXT类型是MySQL支持的文本类型的一种。此外MySQL还支持CHAR、VARCHAR、ENUM和SET等文本类型。我们来看看它们的区别。

  • CHAR(M)固定长度字符串。CHAR(M)类型必须预先定义字符串长度。如果太短,数据可能会超出范围;如果太长,又浪费存储空间。
  • VARCHAR(M) 可变长度字符串。VARCHAR(M)也需要预先知道字符串的最大长度,不过只要不超过这个最大长度,具体存储的时候,是按照实际字符串长度存储的。
  • TEXT字符串。系统自动按照实际长度存储不需要预先定义长度。
  • ENUM 枚举类型,取值必须是预先设定的一组字符串值范围之内的一个,必须要知道字符串所有可能的取值。
  • SET是一个字符串对象取值必须是在预先设定的字符串值范围之内的0个或多个也必须知道字符串所有可能的取值。

对于ENUM类型和SET类型来说你必须知道所有可能的取值所以只能用在某些特定场合比如某个参数设定的取值范围只有几个固定值的场景。

因为不需要预先知道字符串的长度系统会按照实际的数据长度进行存储所以TEXT类型最为灵活方便所以下面我们重点学习一下它。

TEXT类型也有4种它们的区别就是最大长度不同。

  • TINYTEXT255字符这里假设字符是ASCII码一个字符占用一个字节下同
  • TEXT 65535字符。
  • MEDIUMTEXT16777215字符。
  • LONGTEXT 4294967295字符相当于4GB

不过需要注意的是TEXT也有一个问题由于实际存储的长度不确定MySQL不允许TEXT类型的字段做主键。遇到这种情况你只能采用CHAR(M)或者VARCHAR(M)。

所以我建议你在你的项目中只要不是主键字段就可以按照数据可能的最大长度选择这几种TEXT类型中的的一种作为存储字符串的数据类型。

日期与时间类型

除了刚刚说的这3种类型还有一类也是经常用到的那就是日期与时间类型。

日期与时间是重要的信息,在我们的系统中,几乎所有的数据表都用得到。原因是客户需要知道数据的时间标签,从而进行数据查询、统计和处理。

用得最多的日期时间类型就是DATETIME。虽然MySQL也支持YEAR、TIME时间、DATE日期以及TIMESTAMP类型但是我建议你在实际项目中尽量用DATETIME类型。因为这个数据类型包括了完整的日期和时间信息使用起来比较方便。毕竟如果日期时间信息分散在好几个字段就会很不容易记而且查询的时候SQL语句也会更加复杂。

这里我也给你列出了MySQL支持的其他日期时间类型的一些参数

可以看到,不同数据类型表示的时间内容不同、取值范围不同,而且占用的字节数也不一样,你要根据实际需要灵活选取。

不过我也给你一条小建议为了确保数据的完整性和系统的稳定性优先考虑使用DATETIME类型。因为虽然DATETIME类型占用的存储空间最多但是它表达的时间最为完整取值范围也最大。

另外这里还有个问题为什么时间类型TIME的取值范围不是-23:59:5923:59:59呢原因是MySQL设计的TIME类型不光表示一天之内的时间而且可以用来表示一个时间间隔这个时间间隔可以超过24小时。

时间类型的应用场景还是比较广的后面我会单独用一节课来讲在数据库中处理时间的问题。这节课你一定要知道MySQL支持哪几种时间类型它们的区别是什么这样在学后面的内容时才能游刃有余。

总结

今天,我给你介绍了几个常用的字段数据类型,包括整数类型、浮点数类型、定点数类型、文本类型和日期时间类型。同时,我们还清楚了为什么整数类型用得少,浮点数为什么不精准,以及常用的日期时间类型。

另外我们还学习了几个新的SQL语句。尤其是第2条我们在项目中会经常用到你一定要重点牢记。

-- 修改字段类型语句
ALTER TABLE demo.goodsmaster
MODIFY COLUMN price DOUBLE;
-- 计算字段合计函数:
SELECT SUM(price)
FROM demo.goodsmaster;

最后我还想再给你分享1个小技巧。在定义数据类型时如果确定是整数就用INT如果是小数一定用定点数类型DECIMAL如果是字符串只要不是主键就用TEXT如果是日期与时间就用DATETIME。

  • 整数INT。
  • 小数DECIMAL。
  • 字符串TEXT。
  • 日期与时间DATETIME。

这样做的好处是,首先确保你的系统不会因为数据类型定义出错。

不过凡事都是有两面的可靠性好并不意味着高效。比如TEXT虽然使用方便但是效率不如CHAR(M)和VARCHAR(M)。如果你有进一步优化的需求,我再给你分享一份文档,你可以对照着看下。

思考题

假设用户需要一个表来记录会员信息,会员信息包括会员卡编号、会员名称、会员电话、积分值。如果要你为这些字段定义数据类型,你会如何选择呢?为什么?

欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享你的朋友或同事,我们下节课见。