对于 MySQL 数据库来说,好的逻辑表和物理表的规划至关重要,我们需要根据查询语句来针对性地设计 Schema ,没有万能好用的 Schema。一个 denormalized 的 schema 可以在某些场景下加速语句查询,但是放在其他应用场景下就会适得其反。增加辅助表可以优化查询,但是表的维护成本就很高。MySQL 的实现原理决定了这一事实。
《最全的MySQL性能指南》系列文章重点讨论数据库索引和 MySQL 的 schema 设计。在读之前,读者应该熟悉数据库设计基本知识。在系列文章中,我们会讲到逻辑设计,物理级别的设计,和语句执行。
MySQL 支持多种数据类型,存储数据类型的选择对性能至关重要,下面的一些基本原则可以帮助你在不同场景下选择合适的数据类型:
字段越小越好
一般情况下,选择可以存储你的数据的最小数据类型,越小越快,因为占用更少的磁盘空间,更少的内存,和 CPU 缓存。小的数据类型也只需要较少的 CPU 周期来处理。
简单即更优
处理简单的数据类型通常耗费更少的 CPU 周期。举个例子,整数比较比字母比较更快,将字母用数字表示更好。而且我们不应该用字符串,而是应该用 MySQL 原生的日期和时间类型来存储时间数据,用整数来存储 IP 地址。
尽可能避免 NULL
很多表都支持带 null 值的字段,即便是应用软件没有 null 这么个概念,也还是会在表里加入这个一个值做为默认值。最好的方式是每一个字段都标注 NOT NULL,除非应用确实有 NULL 的概念。MySQL 对涉及 null 字段的查询无法做到最佳优化,因为 null 值本身让索引和数值比较更加复杂。带 null 值的字段消耗更多的存储空间,而且需要一些特殊的处理。当带 null 值的字段被加了索引的时候,每一个字段值都会多出一个字节,在 MyISAM 存储引擎中还有可能将固定大小的索引变成随机大小的索引。
把一个带 null 的字段修改成 NOT NULL 的字段带来的性能提升不大,所以如果不是必须的话,请不要在建表之后去修改,但是如果要在字段上加索隐,最好还是用 NOT NULL。
也有例外的情况。比如 InnoDB 用一个比特来存储 NULL,所以对于稀疏表,是很节省空间的。但是 MyISAM 就不是这样。
确定字段类型的第一步,就是了解数据的基本特征:数值型,字符串型,时间型,等等。这个很好确定,但是也有上文提到的特殊情况。
下一步就是确定具体的数据类型。很多 MySQL 的数据类型可以存储相同的数据,但是对数据的范围要求、精度、存储格式却不一样。某些数据格式有特殊的性质。
比如说,DATETIME 和 TIMESTAMP 字段可以存储相同的数据 - 日期和时间,精确到秒。但是 TIMESTAMP 只需要 DATETIME 一般的存储空间,能够存储时区信息,并且还支持自动数据生成。但是,TIMESTAMP 能表示的时间范围就小很多,而且其自动数值生成功能有时候会产生不要的效果
整数
数字有两种类型:整数和实数(带小数点位的数字)。如果是存储整数,我们可以可以使用TINYINT (8 bits,能存储
到
范围的整数)
SMALLINT (16 bits,能存储
到
范围的整数)
MEDIUMINT (24 bits,能存储
到
范围的整数)
INT (32 bits,能存储
到
范围的整数)
BITINT (64 bits,能存储
到
范围的整数)
整数类型的可以自带 UNSIGNED 属性,该属性去除了所有负值,将能表示的最大值翻两倍。例如,TINYINT UNSIGNED 可以存储 0 到 255 的数值,而 TINYINT 只能存储 -128 到 127。
Signed 和 unsigned 数据类型占用相同的存储空间,性能效果相同,所以根据你自己的需求随意设置就好。
我们对数据类型的选择决定了 MySQL 存储数据的方式。但是整数相关的计算通常用的是 64位 BITINT 的方式进行,就算在32位处理器中也是如此。
MySQL 可以指定整数的上限位数,比如 INT(11),这其实是一个没啥卵用的功能:这并不会改变整数的范围,仅仅只是制定 MySQL 在 command line 下显示整数所用的占位符的个数。从存储和计算角度上来说,INT(1) 和 INT(20) 是一样的
实数
实数是带有小数点位的数字,但它们不仅仅存储分数性质的数字,你也可以用 DECIMAL 来存储达到连 BIGINT 都存不下的数值。MySQL 支持完全准确和近似值类型的实数
FLOAT 和 DOUBLE 支持用浮点方式的近似计算,计算方式因处理器的不同而稍有差异。
DECIMAL 是用来存储完全准确的实数的。在 MySQL 5.0 以及以后的版本中,DECIMAL 支持完全精确计算。而 MySQL 4.1 和更早的版本则是使用浮点方式来进行 DECIMAL 的运算,这在某些情况下会出现很诡异的计算结果,所以在老版本中,DECIMAL 只是一个 “存储类型”。
在 MySQL 5.0 以及以后的版本中,MySQL server 负责 DECIMAL 的所有计算,因为 CPU 本身不支持 DECIMAL 运算。浮点运算(Floating-point )会快很多,因为 CPU 支持浮点运算。
浮点(floating-point)和 DECIMAL 类型都支持指定精度。对于 DECIMAL 字段,可以指定小数点之前和之后的最大位数,注意,这会影响字段的存储。
MySQL 5.0 以及以后的版本中的 DECIMAL 支持最高带有 65 个数字的数值。
相比于 DECIMAL 浮点类型通常要求较少的存储空间来存储相同的数据。一个 FLOAT 字段占用 4 个字节,DOUBLE 占用 8 个字节并且精度更高。和整数一样,浮点和 DECIMAL 只是存储方式上的区别,计算都是通过 DOUBLE 的计算方式进行的。
因为占用更多的存储空间和计算资源,应该只有在需要精确小数计算的时候你才应该使用 DECIMAL。一个可用的场景是存储金融相关的数据。但是对于大体量数据,更好的方法是用 BIGINT,将货币数据转化成可复原的整数形式。比如你需要存储小数点后两位的货币数据,你可以将原数值乘以一百万,存成 BITINT,这样可以防止因为浮点数据存储和 DECIMAL 运算带来的精度误差。
字符串类型
MySQL 支持相当多的字符串类型,这些类型在 4.1 版本和 5.0 版本中有了很大的变化,也因此变得更加复杂。
MySQL 4.1 开始,每个字符串字段能有不同的字符集和对应的字符集排序规则。
VARCHAR 和 CHAR 类型
两个主要的字符串类型是 VARCHAR 和 CHAR,每个存储引擎对这两者的存储方式都很不一样。我们这里仅讨论 InnoDB 和 MyISAM 两种存储,如果你关注的是其它存储引擎,请参阅对应的文档。
我们来看一下 VARCHAR 和 CHAR 是如何在磁盘上存储的。存储引擎在内存和磁盘上存储 VARCHAR 和 CHAR 的方式可能不一样,不仅如此,当 MySQL 把字符串从存储引擎中读出来的时候还会进行二次转化。以下是两种类型的一般比较:
VARCHAR
VARCHAR 存储的是变量长度的字符串,也是最常见的数据类型。VARCHAR 比定长的字符串可以占用更少的存储空间,因为 VARCHAR 存储短的字符串就只需要更少的空间。但是在 MyISAM 中用 ROW_FORMAT=FIXED 创建出来的表,用的就是占用固定的磁盘空间,这可能会带来存储资源的浪费。
VARCHAR 借助 1 或者 2 个额外的字节来记录字符串的长度。如果字段的最大长度小于等于 255 个字节,就用一个字节,如果大于 255 就用 两个字节。在拉丁语字符集情况下,VARCHAR(10) 最高占用 11 个字节的存储空间,VARCHAR(1000) 占用 1002 个字节,因为额外需要两个字节来存储。
VARCHAR 对性能提升有帮助,因为占用更少的空间。但是,因为是长度可变的,这可能会带来额外的开销。如果长度超过的了最大值,不同的存储引擎会有不同的策略。例如,MyISAM 会将一行数据切分,而 InnoDB 可能会将表分成多个 page,其他引擎可能根本就不会在原数据上做修改。
当最长字符串长度远大于平均字符串长度,字符串更新很少见(因为上文提到的切分就不会出现),并且使用 UTF-8 这样的复杂字符集(每一个字都占用不同字节数量的存储空间)的时候,VARCHAR 是一个不错的选择,。
MySQL 5.0 以及以后的版本中,会保留字符串后面的空格,而在 4.1 以及以前的版本中,MySQL 会将尾部空格剔除。
CHAR
CHAR 是定长的:MySQL 总是按照一定数量划分出足够的空间,存储 CHAR 的时候,MySQL 总是删除尾随空格。CHAR 在相互比较的时候为了长度相同,会在尾部添加空位。
如果存储很短的字符串,或者所有字符串都基本一样长,CHAR 是一个更好的选择。比如说,CHAR 特别适合存储密码的 MD5 值,MD5 值的长度都是相同固定的。对于经常变化的数据,CHAR 也是一个比 VARCHAR 更好的选择,因为定长字符串不会出现分片现象。对于很短的字符串字段,CHAR 有更好的性能;CHAR(1) 用来存储 Y 和 N 在单字节字符集中只需要一个字节,而 VARCHAR(1) 则需要两个字节,其中一个用来记录长度。
CHAR 和 VARCHAR 的姊妹类型是 BINARY 和 VARBINARY,用来存储二进制字符串。二进制字符串和普通字符串类似,只是存的是字节,不是字母。在尾部添加空位也不一样:MySQL 给 BINARY 尾部添位用的是 \0 ,而不是空格,而且不会剔除尾部 \0
当数据是二进制的,而且需要比较的情况下,BINARY 和 VARBINARY 是比较好的选择。二进制比较的优点除了不区分大小写,MySQL 是一个一个字节地比较 BINARY 字符串,比较每个字节的数值,这样做的结果就是二进制比较会更加简单快速。
一般化的处理方式可能会适得其反
存储 “hello” 这个单词对于 VARCHAR(5) 和 VARCHAR(200) 需要的存储空间是一样的,那用 VARCHAR(5) 好在哪里呢?
确实会好很多,VARCHAR(200) 会消耗更多的内存,因为 MySQL 会分配固定的内存空间,这对排序和内存操作带来很大压力。
BLOB 和 TEXT 类型
BLOB 和 TEXT 是用来存储占体很大的数据,分别以二进制和字符的形式存储。
实际上,不止是 BLOB 和 TEXT 这两个,还有 TINYTEXT,SMALL TEXT,TEXT,MEDIUMTEXT,和 LONGTEXT,TINYBLOB,SMALLBLOB,BLOB,MEDIUMBLOB,和 LONGBLOB。 BLOB 等同于 SMALLBLOB, TEXT 等同于 SMALLTEXT。
和其它数据类型不同的是,MySQL 存储 BLOB 和 TEXT的方式有些特殊,InnoDB 会用一个单独的“外部”存储区域,表里实际存的是这个外部区域的位置信息(1 到 4 个字节)。
BLOB 和 TEXT 的唯一区别是 BLOB 类型存储的是二进制数据,没有 collation 和字符集,而 TEXT 则有字符集和 collation。
MySQL 用了和其它类型不同的方法给 BLOB and TEXT 排序:不会给整个字符串排序,而是给前 max_sort_length 数量的字节排序。如果你只想根据前面几个字符排序,要么降低 max_sort_length 要么使用 ORDER BY SUBSTRING(column, length)
MySQL 无法按照这些数据类型的原长建立索引,也不能在排序的时候用到索引。
BLOB 和 TEXT 类型 - 磁盘临时表 & 排序
因为内存存储引擎(memory storage engine)不支持 BLOB 和 TEXT 类型,查询 BLOB 和 TEXT 字段并且需要创建临时表的话的查询语句需要使用 MyISAM 的磁盘临时表(Percona 的内存存储引擎也支持),即使是访问小部分数据也是如此。
这会对性能带来很严重的影响,即便你将 MySQL 的配置设置成在内存存放临时表,也会导致很多的大开销的系统调用。
最好的方法是不到万不得已的情况下不要使用 BLOB 和 TEXT 类型。如果一定需要的话,就在需要使用 BLOB 字段的语句(包括 ORDER BY 语句)中使用 SUBSTRING(column, length),将其转换成字符形式,因为这样就可以使用内存临时表了。这里要确保使用比较短的 substring,保证临时表不会比 max_heap_table_size 或者 tmp_table_size 更大,否则 MySQL 会将表变成磁盘形式的 MyISAM 表。该方法同样适用于排序。
举个例子,假设有一个一千万行的表,占用了几个G的磁盘空间,表中包含一个 VARCHAR(1000),utf8字符集的字段,每个字母最多占用 3 个字节,所以最长的字符串值有 3000 个字节。如果在 ORDER BY 语句中涉及该字段,那么整个表的查询会产生 30 多个 GB 的临时表用来排序!
如果 EXPLAIN 中的 “Extra” 字段包含 “Using temporary”,那么代表着查询语句用到了临时表。
ENUM
有些时候可以用 ENUM 替代字符串类型,ENUM 字段存储某些预设好的字符串值。MySQL 用非常节省空间的方式存储 ENUM,根据预设值的个数,只用一个或者两个字节存储,存储方式是用一个整数当作字段值,这个整数等于所有预设值列表里某个具体字符串的下表,通过一个查找表来维护这个映射。举个例子:
mysql> CREATE TABLE enum_test(
-> e ENUM('fish', 'apple', 'dog') NOT NULL
-> );
mysql> INSERT INTO enum_test(e) VALUES('fish'), ('dog'), ('apple');
三行数据实际上存储的是数字,不是字符串:
mysql> SELECT e + 0 FROM enum_test;
+-------+
| e + 0 |
+-------+
| 1 |
| 3 |
| 2 |
+-------+
如果 ENUM 的真实值是数字,那么这会导致很大的歧义,所以Jack建议不要这么做。
另外,ENUM 是根据对应的整数值排序的,不是按照真实数据:
mysql> SELECT e FROM enum_test ORDER BY e;
+-------+
| e |
+-------+
| fish |
| apple |
| dog |
+-------+
解决办法是,可以在定义 ENUM 成员值的时候,有意按照顺序指定真实值,你也可以使用 FIELD() 来明确指出一个查询语句的相关顺序,但是这样 MySQL 就无法在排序的时候使用索引了:
mysql> SELECT e FROM enum_test ORDER BY FIELD(e, 'apple', 'dog', 'fish');
+-------+
| e |
+-------+
| apple |
| dog |
| fish |
+-------+
像上面这种情况,如果我们定义 ENUM 值的时候就按照字母排好序,我们之后就不需要用 FIELD() 了。
ENUM 一个最大的缺点就是预设值不能随意再添加,如果要添加或者删除需要用到 ALTER TABLE,所以如果预设值在将来可能会变动,ENUM 可能不是一个最佳选择,除非 ALTER TABLE 不会对应用产生大的影响,这在 MySQL 5.1 有比较好的支持。
因为 MySQL 存储的是整数,而且需要将整数映射到具体的预设值,ENUM 字段会有一些额外的开销。这可以通过 ENUM 的小范围预设值来抵消,但不是所有情况都是如此,比如说把 CHAR 或者 VARCHAR 字段和一个 ENUM 字段 join 起来,会比和另一个 CHAR 或者 VARCHAR 字段 join 起来要慢。
为了展示说明,这里通过一个举例来简单看一下 MySQL 执行 join 的速度。我们有一个 primary key 比较“宽”的表:
CREATE TABLE webservicecalls (
day date NOT NULL,
account smallint NOT NULL,
service varchar(10) NOT NULL,
method varchar(50) NOT NULL,
calls int NOT NULL,
items int NOT NULL,
time float NOT NULL,
cost decimal(9,5) NOT NULL,
updated datetime,
PRIMARY KEY (day, account, service, method)
) ENGINE=InnoDB;
这个表有大概11万行数据,大小只有 10 MB,所以整个表可以放入内存。service 字段包含 5 个预设值,平均长度是 4 个字符,method 字段包含 71 个预设值,平均长度是 20 个字符。
我们复制了一份这个表,并且将 service 和 method 字段转成了 ENUM:
CREATE TABLE webservicecalls_enum (
... 省略 ...
service ENUM(...预设值省略...) NOT NULL,
method ENUM(...预设值省略...) NOT NULL,
... 省略 ...
) ENGINE=InnoDB;
然后我们测量了一下根据 primary key 来 join 表的性能:
mysql> SELECT SQL_NO_CACHE COUNT(*)
-> FROM webservicecalls
-> JOIN webservicecalls USING(day, account, service, method);
我们将上述查询语句稍作修改,来相互 join VARCHAR 和 ENUM 字段,一下是测量结果:VARCHAR join VARCHAR:2.6 秒
VARCHAR join ENUM: 1.7 秒
ENUM join VARCHAR: 1.8 秒
ENUM join ENUM: 3.5 秒
将被 join 的字段改成 ENUM 之后,速度提升,但是 ENUM join VARCHAR 慢了一些,所以我们发现如果 VARCHAR 字段不是被 join 的情况,可以使用 ENUM。通常情况下,用 ENUM 这样的整数对照表,比 join 字符要来的高效。
然而,还有一个将 service 和 method 字段转成 ENUM 的好处:根据 SHOW TABLE STATUS 中的 Data_length 字段,转换之后的表缩小了三分之一。在某些时候,存储方面带来的提升可能大过 ENUM join VARCHAR 带来的性能耗损。而且,primary key 也小了一半。在 InnoDB 引擎中,缩小 primary key 也能让索引大大缩小
Date 和 Time 类型
MySQL 针对日期和时间有多种不同的类型。比如 YEAR 和 DATE。MySQL 能存储的最小时间单位是秒(MariaDB 是微秒),但却能在微秒级别上进行计算。
大多数时间相关的数据类型没有替代类型,所以也就不存在哪个更好的说法。唯一的问题就是,如果需要同时存储日期和时间,该怎么办。对此 MySQL 提供两个类似的数据类型:DATETIME 和 TIMESTAMP。对于大多数应用来说,两者都可以,但某些情况下,就有优劣之分了,我们具体来看一下:
DATETIME:
这个类型可以存储大范围的数据,从 1001 年到 9999 年,精度到达秒级,存储日期和时间用一个整数形式表示:YYYYMMDDHHMMSS,无时区,使用 8 个字节的存储空间。
默认情况下,MySQL 显示 DATETIME 的格式是 “2008-01-16 22:37:08”,这是 ANSI 标准下规范的日期时间表示方法
TIMESTAMP:
顾名思义,TIMESTAMP 存储的是从 格林尼治时间(GMT)1970 年 1 月 1 日凌晨过去了多少秒,和 Unix 时间戳是一样的。TIMESTAMP 只需要 4 个字节存储,所以可能表示的时间范围比 DATETIME 小很多:从 1970 年到 2039 年。MySQL 提供 FROM_UNIXTIME() 和 UNIX_TIMESTAMP() 进行 Unix 时间和日期之间的转换。
MySQL 4.1 和之后的版本用和展示 DATETIME 相同的方式展示 TIMESTAMP 的值,但是 MySQL 4.0 和之前的版本在展示的时候没有标点符号,但这仅仅只是展示形式的不同,TIMESTAMP 的存储格式在所有 MySQL 的版本中都是一样的
TIMESTAMP 的展示数值也取决于时区,MySQL,操作系统,和访问客户端都有自己的时区设置。所以,当 TIMESTAMP 存储的是 0 的时候,展示出来的是 1969-12-31 19:00:00 EST,和 GMT 有 5 个小时的时差。这个区别需要注意:如果是在不同时区存储和访问时间数据,TIMESTAMP 和 DATETIME 的应对方式是不同的。前者是带了时区,后者强调日期和时间的展示形式。
TIMESTAMP 也有 DATETIME 不具备的特殊性质。默认情况下,如果没有制定具体的值,MySQL 会把第一个 TIMESTAMP 字段的值设成当前时间
一般情况下,如果可以用 TIMESTAMP 尽量用,因为 TIMESTAMP 比 DATETIME 更节省存储空间。一些人会将 UNIX 时间戳存成整数形式,但是这样并没有什么优势,整数形式比较不和规范,所Jack我建议不要这么做。
那如果需要存储秒级一下的数据呢?比如毫秒。MySQL 目前没有特定的数据格式支持,但是你可以用现有的类型做一些设计:你可以用 BIGINT 存储微秒级的时间戳,或者你可以用 DOUBLE 来存储带小数点的秒级数据。哪种方法都合适,或者可以直接用 MariaDB
Bit-存储数据类型
MySQL 里面的少数存储类型支持用数据中的某些 bit ,用压缩的方式存储数据。所有这些格式是用字符串形式存储
BIT
在 MySQL 5.0 之前,BIT 就是 TINYINT,在是在 MySQL 5.0 和以后的版本中,就是一个完全不同的数据类型
你能用 BIT 字段存储一个或者多个 true/false 值,BIT(1) 定义了一个包含了单个 bit 的值,BIT(2) 包含两个 bit,以此类推,BIT 字段的最大程度是 64 个 bits。
BIT 的性质在不同存储引擎中是不一样的。MyISAM 将所有 bit 字段放在一起,所以 17 个 BIT 字段只需要 17 个 bit 就行(字段值不能出现 NULL), MyISAM 将其转换成 3 个字节存储。其他存储引擎中,例如内存的 InnoDB,将 bit 用最小整数表示,这样会比较消耗存储空间。
MySQL 将 BIT 当作字符串对待,而不是数值。当你读取一个 BIT(1) 字段的时候,显示出来的实际上是一个字符串,值是 0 或者 1,而不是 ASCII ”0“ 或者 ”1“。但是,如果用数值的方法读取,那结果就是数值。例如,如果存储的是 b'00111001' (相当于数字 57),将其存储在 BIT(8) 字段中,读取之后,你会得到字母编码是 57 的字符串,这也是 ”9“ 的ASCII 字符码:
mysql> CREATE TABLE bittest(a bit(8));
mysql> INSERT INTO bittest VALUES(b'00111001');
mysql> SELECT a, a + 0 FROM bittest;
+------+-------+
| a | a + 0 |
+------+-------+
| 9 | 57 |
+------+-------+
这会产生很多歧义,Jack建议慎用 BIT,在大多数场景下,最好不要使用 BIT 类型
如果你需要将 true/false 值存在一个单个 bit 的字段中,另一个方法就是用 nullable 的 CHAR(0) 字段,该字段可以存储无数据(NULL)和长度是0的数据(空字符串)
SET
如果你需要存储多个 true/false 值,可以考虑用 MySQL 自带的 SET 数据类型将其合并成一个字段,MySQL 对此的存储方式是用一个 bit set。存储起来非常高效,MySQL 还可以利用 FIND_IN_SET() 和 FIELD() 优化查询语句。最大的缺点就是如果需要修改字段的定义,那么代价就很高:需要 ALTER TABLE 语句,这在大的表中代价是很大的,而且,你不能在 SET 字段上用索引
整数字段上的位运算
另外一种和 SET 有相同效果的方案是始终整数表示一个 bit 集合,例如,你可以将 8 个 bits 放入一个 TINYINT,用位运算符来处理它们。在应用中,还可以利用代名称的常数来表示每一个 bit,简化复杂度。
这种方法的优点在于我们可以不需要 ALTER TABLE 就能改变字段含义。缺点是你的查询语句会变的比较复杂和难懂,有些人喜欢位运算,觉得逼格高,但是有些人则不喜欢,所以这主要还是看个人和团队喜好。
一个合并 bits 的应用就是 ACL,控制访问权限。每个 bit 或者 SET 元素代表了一个访问权限开关,比如 CAN_READ,CAN_READ,CAN_DELETE。如果你使用的是 SET 字段类型,那么你会依赖 MySQL 的 bit 映射表;如果使用整数字段,映射表会放在应用当时。一个 SET 字段查询语句举例:
mysql> CREATE TABLE acl (
-> perms SET('CAN_READ', 'CAN_WRITE', 'CAN_DELETE') NOT NULL
-> );
mysql> INSERT INTO acl(perms) VALUES ('CAN_READ,CAN_DELETE');
mysql> SELECT perms FROM acl WHERE FIND_IN_SET('AN_READ', perms);
+---------------------+
| perms |
+---------------------+
| CAN_READ,CAN_DELETE |
+---------------------+
如果你用的是一个整数,那么上述举例就是:
mysql> SET @CAN_READ := 1 << 0,
-> @CAN_WRITE := 1 << 1,
-> @CAN_DELETE := 1 << 2;
mysql> CREATE TABLE acl (
-> perms TINYINT UNSIGNED NOT NULL DEFAULT 0
-> );
mysql> INSERT INTO acl(perms) VALUES(@CAN_READ + @CAN_DELETE);
mysql> SELECT perms FROM acl WHERE perms & @CAN_READ;
+-------+
| perms |
+-------+
| 5 |
+-------+
我们这里使用的变量来定义,但你也可以在应用中使用自己的常数。
ID 字段的类型选择
给 ID 类型的字段选择一个合适的字段类型至关重要。这些字段经常会被拿去和其它字段比较(比如 join),或者用这点字段去查找其它字段。你也会用它当多 foreign keys,所以如果是给这些字段选择类型,还需要考虑其他相关表的相关字段类型。
在选择类型的时候,你不仅要考虑存储方面的问题,还需要结合 MySQL 是如何对这个类型进行计算和比较的。比如,MySQL 存储 ENUM 和 SET 类型的方式是用整数,在比较字符串的时候会将其转换成字符串。
当选定了一个 ID 字段的类型之后,在其它表的相关字段中也需要使用相同的类型,需要完全相同,比如是否都是 UNSIGNED
选择占用空间最小的类型来存储,适当留出额外空间来应对将来的增长需求。比如,如果你要 province_id 字段存储国内所有的省份,你不需要存储几百万个数值,所以不要用 INT,TINYINT 就足够了而且还省了 3 个字节的存储空间,如果你将这个字段当作 oreign key 和其它表相连,3 个字节能带来巨大的性能差异。这里Jack给几点建议:整数类型:整数类型是 ID 类型的最佳选择,因为处理起来很快,并且可以使用 AUTO_INCREMENT
ENUM 和 SET:ENUM 和 SET 非常不适合用作 ID 类型的字段,只是在小的静态定义类型的表中可以用作 ID 类型的字段。ENUM 和 SET 字段适合存储比如订单状态,产品类型,或者性别等数据。举个例子,如果你用 ENUM 定义产品类型,那么你需要另一个 ID 是 ENUM 字段的表,但这样的情况应该避免。
字符串类型:尽量避免使用字符串类型的 ID 字段,因为占用太多的空间而且比整数型要慢很多。而且要慎用
如果需要存储 UUID 数据,你应该将 UUID 中的中划线剔除,或者用 UNHEX() 将其转换成 16 字节的数字,然后存储在 BINARY(16) 的字段中,之后你就能用 HEX() 将它们读成16进制的数字。
特殊类型的数据
某些数据没有直接对应的存储类型,毫秒级别的时间戳就是一个例子。我们在上文当中已经阐述了如何存储这类型的数据。
另一个例子就是 IPv4 地址。人们通常使用 VARCHAR(15) 来存储,但是,IPv4 实际上是 unsigned 的 32 位整数,不是字符串。里面的点符号只是为了方便大家阅读罢了。我们应该将 IP 地址存成 unsigned 整数。MySQL 支持用 INET_ATON() 和 INET_NTOA() 将两者相互转换。
总结费脑,码字不易,如若有用,还请点赞,分享更赞~
参考^某些第三方存储引擎,例如 Infobright,在存储格式和压缩方式上和大多数 MySQL 原生支持的存储引擎很不一样
^读取 BINARY 的时候要格外注意,MySQL 会在尾部添加 \0,所以如果要比较 BINARY,一定要记住这一点!
^在不同 MySQL 版本之间,TIMESTAMP 有不同的复杂规则,所以你需要亲自验证
^如果你使用 InnoDB,如果字段类型不完全一样,是不能创建 foreign key 的