这是《高性能 MySQL(第三版)》第四章《Schema 与数据类型优化》的读书笔记。
数据类型的选择原则:
越小越好:选择满足需求的最小类型。注意,增大数据类型的范围是耗时操作,尽量避免。简单:整型比字符操作代价更低。尽量避免 NULL 列:列最好指定为 NOT NULL,除非确实可以是 NULL。如果查询中包含可为 NULL 的列,会使索引、索引统计和值比较都更复杂。可为 NULL 的列占用更多存储空间,在 MySQL 里也需要特殊处理。如果可为 NULL 的列被索引,每个索引记录需要一个额外的字节,在 MyISAM 里甚至可能导致固定大小的索引变成可变大小的索引。 把 NULL 列改为 NOT NULL 列带来的性能提升比较小,但是用于索引的列最好是 NOT NULL。另外,InnoDB 使用单独的位(bit)存储 NULL 值,包含很多 NULL 值的稀疏数据的空间效率比较高。首先选择大类型:数字、字符串、时间、布尔值等。然后选择具体类型。
可用的整型数据有:
TINYINT:8 bit。SMALLINT:16 bit。MEDIUMINT:24 bit。INT:32 bit。BIGINT:64 bit。所有的整数类型都支持 UNSIGNED 属性,表示无符号数据,例如 TINYINT UNSIGNED。有符号和无符号整型数据存储空间一样,性能一样。
MySQL 中可以为整数类型指定宽度,例如 INT(11),这个宽度只是规定了 MySQL 交互工具用来显示字符的个数,不影响存储和计算。
可用的实数数据有:
浮点类型: FLOAT:32 bit。不精确类型,支持浮点运算。DOUBLE:64 bit。不精确类型,支持浮点运算。DECIMAL:最多允许 65 个数字,例如 DECIMAL(33,32),DECIMAL(65,0)。可以存储精确的小数,也可以存储比 BIGINT 还大的整数,支持精确计算。因为 CPU 不支持对 DECIMAL 的直接计算,所以 MySQL 服务器自身实现了 DECIMAL 的高精度计算。MySQL 支持精确类型(DECIMAL),也支持不精确类型(浮点类型,即 FLOAT 和 DOUBLE)。
FLOAT、DOUBLE 和 DECIMAL 类型都可以指定精度。例如 DOUBLE(12,3) 表示最多 9 位整数、3 位小数。DECIMAL(18,9) 表示小数点两边各存储 9 个数字,一共使用 9 个字节(小数点占一个字节)。
DECIMAL 需要的存储空间大,计算开销高,如果数据量很大,可以考虑用 BIGINT 替代 DECIMAL。例如金额需要保证 0.001 的精度,则可以将所有金额乘以 1000 后取整存入 BIGINT 类型的字段,从而避免浮点数计算不准确和 DECIMAL 精确计算代价高的问题。
MySQL 中每个字符串列可以定义自己的字符集和排列规则(校对规则,collation)。
MySQL 中最重要的字符串类型,其在内存和磁盘中的存储方式跟存储引擎相关。这里假设用的是 InnoDB 或 MyISAM 引擎。
VARCHAR:变长字符串,节省空间(但是如果用 ROW_FORMAT = FIXED 创建表,每行都会定长存储,浪费空间)。需要 1 个或 2 个额外字节记录字符串长度(如果列的最大长度小于等于 255 字节则只需要 1 个字节,否则需要 2 个字节)。例如,对于 latin1 字符集,一个 VARCHAR(10) 的列需要 11 个字节的存储空间,VARCHAR(1000) 的列需要 1002 个字节的存储空间。 在执行 UPDATA 操作时,如果需要增大行的长度,需要额外工作。如果磁盘当前页的空间不够,不同存储引擎的处理方式不同。MyISAM 会将行拆成不同的片段存储,InnoDB 则需要分裂页来使行可以放进页内。InnoDB 会把过长的 VARCHAR 存储为 BLOB。CHAR:定长字符串,分配固定空间,如果字符串长度小于存储空间,则剩余空间填充空格。对于单字节字符集,CHAR(1) 只需要一个字节,而 VARCHAR(1) 则需要两个字节(包括记录长度的一个字节)。VARBINARY:二进制变长字符串,存储字节码而不是字符。BINARY:二进制定长字符串,存储字节码而不是字符,使用 \0(零字节)填充而不是空格,且检索时不会去掉填充值。适合使用 VARCHAR 的场景:
字符串列的最大长度比平均长度大得多列很少更新,所以碎片不是问题使用 UTF-8 字符集,每个字符用不同的字节数进行存储适合使用 CHAR 的场景:
短字符串定长字符串,例如密码的 HASH 值经常变更的字符串,不易产生碎片注意:CHAR 类型的字符串在存储时,会自动截断字符串末尾的空格:
MariaDB [foo]> CREATE TABLE char_test(char_col CHAR(10)); Query OK, 0 rows affected (0.04 sec) MariaDB [foo]> INSERT INTO char_test(char_col) VALUES -> ('string1'), (' string2'), ('string3 '); Query OK, 3 rows affected (0.02 sec) Records: 3 Duplicates: 0 Warnings: 0 MariaDB [foo]> SELECT CONCAT("'", char_col, "'") FROM char_test; +----------------------------+ | CONCAT("'", char_col, "'") | +----------------------------+ | 'string1' | | ' string2' | | 'string3' | +----------------------------+ 3 rows in set (0.00 sec)VARCHAR 类型的字符串在存储时,则不会截断字符串末尾的空格:
MariaDB [foo]> CREATE TABLE varchar_test(varchar_col VARCHAR(10)); Query OK, 0 rows affected (0.04 sec) MariaDB [foo]> INSERT INTO varchar_test VALUES -> ('string1'), (' string2'), ('string3 '); Query OK, 3 rows affected (0.01 sec) Records: 3 Duplicates: 0 Warnings: 0 MariaDB [foo]> SELECT CONCAT("'", char_col, "'") FROM varchar_test; +----------------------------+ | CONCAT("'", char_col, "'") | +----------------------------+ | 'string1' | | ' string2' | | 'string3 ' | +----------------------------+ 3 rows in set (0.00 sec)BLOB:二进制方式存储大数据的字符串类型,没有字符集和排序规则。具体的类型有:TINYBLOB, SMALLBLOB, BLOB, MIDIUMBLOB, LONGBLOB TEXT:字符方式存储大数据的字符串类型,有字符集和排序规则。具体的类型有:TINYTEXT, SMALLTEXT, TEXT, MIDIUMTEXT, LONGTEXT
在 MySQL 中,每个 BLOB 和 TEXT 类型的值都是独立对象。当值太大时,InnoDB 还会使用专门的外部存储区域来存储,此时行内只需 1-4 个字节存储指针。
使用枚举类型可以节省存储空间。MySQL 内部将每个枚举类型的字段存储为整数,并在表的 .frm 文件中保存“数字-字符串”映射关系的“查找表”。
MariaDB [foo]> CREATE TABLE enum_test( e ENUM('dog', 'fish', 'apple') NOT NULL); Query OK, 0 rows affected (0.17 sec) MariaDB [foo]> desc enum_test; +-------+----------------------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-------+----------------------------+------+-----+---------+-------+ | e | enum('dog','fish','apple') | NO | | NULL | | +-------+----------------------------+------+-----+---------+-------+ 1 row in set (0.01 sec) MariaDB [foo]> INSERT INTO enum_test(e) VALUES('fish'), ('dog'), ('apple'); Query OK, 3 rows affected (0.00 sec) Records: 3 Duplicates: 0 Warnings: 0MySQL 的枚举字段存储的实际上是数字而不是字符串:
MariaDB [foo]> SELECT e + 0 FROM enum_test; <-------实际存储整数值 +-------+ | e + 0 | +-------+ | 2 | | 1 | | 3 | +-------+ 3 rows in set (0.00 sec) MariaDB [foo]> SELECT e FROM enum_test; <-------取数据时自动转为字符串 +-------+ | e | +-------+ | fish | | dog | | apple | +-------+ 3 rows in set (0.00 sec)枚举字段排序时,默认按照内部存储的整数进行排序。可以使用 FIELD() 函数显式指定排序顺序,但会导致 MySQL 无法利用索引消除排序:
MariaDB [foo]> SELECT e FROM enum_test ORDER BY e; <-------排序时,根据实际存储的整数值排序 +-------+ | e | +-------+ | dog | | fish | | apple | +-------+ 6 rows in set (0.06 sec) MariaDB [foo]> SELECT e FROM enum_test ORDER BY FIELD(e, 'apple', 'dog', 'fish'); <-------自己制定排序方式 +-------+ | e | +-------+ | apple | | dog | | fish | +-------+ 6 rows in set (0.00 sec)枚举字段如果需要增删可用的枚举值,则需要使用 ALTER TABLE。如果枚举类型添加字符串,MySQL 会自动修改字段中保存的整数值以匹配字符串。为了不重建整个表,最好在列表末尾新增加字符串:
MariaDB [foo]> ALTER TABLE enum_test MODIFY e ENUM('dog', 'fish', 'apple', 'banana'); Query OK, 6 rows affected (0.08 sec) Records: 6 Duplicates: 0 Warnings: 0 MariaDB [foo]> DESC enum_test; +-------+-------------------------------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-------+-------------------------------------+------+-----+---------+-------+ | e | enum('dog','fish','apple','banana') | YES | | NULL | | +-------+-------------------------------------+------+-----+---------+-------+ 1 row in set (0.00 sec) MariaDB [foo]> SELECT e + 0 FROM enum_test; <-------存储的整数值不受影响 +-------+ | e + 0 | +-------+ | 2 | | 1 | | 3 | +-------+ 3 rows in set (0.00 sec) MariaDB [foo]> ALTER TABLE enum_test MODIFY e ENUM('test', 'dog', 'fish', 'apple', 'banana'); Query OK, 6 rows affected (0.04 sec) Records: 6 Duplicates: 0 Warnings: 0 MariaDB [foo]> SELECT e + 0 FROM enum_test; <-------存储的整数值受影响 +-------+ | e + 0 | +-------+ | 3 | | 2 | | 4 | +-------+ 6 rows in set (0.00 sec) MariaDB [foo]> SELECT e FROM enum_test; <-------实际读出来的字符串不会变化 +-------+ | e | +-------+ | fish | | dog | | apple | +-------+ 6 rows in set (0.00 sec)TIMESTAMP 的规则相当复杂,且在不同版本的 MySQL 中会发生变化。如果需要跨时区的人使用,最好使用 TIMESTAMP。常见的特殊属性有:
创建表时,所有的 TIMESTAMP 字段默认为 NOT NULL,且第一个 TIMESTAMP 字段有默认值 CURRENT_TIMESTAMP。INSERT 插入数据时,如果没有指定第一个 TIMESTAMP 列的值,MySQL 会设置这个列的值为当前时间。UPDATE 更新数据时,MySQL 会默认更新第一个 TIMESTAMP 列的值为当前时间(除非明确指定值)。MySQL 内置函数可以实现时间戳和日期的互相转换:
FROM_UNIXTIME():将 UNIX 时间戳转为日期。UNIX_TIMESTAMP():将日期转为 UNIX 时间戳。 MariaDB [foo]> CREATE TABLE t_test (id INT, t1 TIMESTAMP, t2 TIMESTAMP, d1 DATETIME, d2 DATETIME); Query OK, 0 rows affected (0.13 sec) MariaDB [foo]> DESC t_test; +-------+-----------+------+-----+---------------------+-----------------------------+ | Field | Type | Null | Key | Default | Extra | +-------+-----------+------+-----+---------------------+-----------------------------+ | id | int(11) | YES | | NULL | | | t1 | timestamp | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP | <------第一个 TIMESTAMP 字段会默认填充当前时间,且在更新数据时同步刷新 | t2 | timestamp | NO | | 0000-00-00 00:00:00 | | <------第二个 TIMESTAMP 字段会默认填充 0000-00-00 00:00:00 | d1 | datetime | YES | | NULL | | | d2 | datetime | YES | | NULL | | +-------+-----------+------+-----+---------------------+-----------------------------+ 5 rows in set (0.01 sec) MariaDB [foo]> CREATE TABLE time_test (id INT, stamp TIMESTAMP, time DATETIME); Query OK, 0 rows affected (0.18 sec) MariaDB [foo]> desc time_test; +-------+-----------+------+-----+-------------------+-----------------------------+ | Field | Type | Null | Key | Default | Extra | +-------+-----------+------+-----+-------------------+-----------------------------+ | id | int(11) | YES | | NULL | | | stamp | timestamp | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP | | time | datetime | YES | | NULL | | +-------+-----------+------+-----+-------------------+-----------------------------+ 3 rows in set (0.02 sec) MariaDB [foo]> INSERT INTO time_test VALUES(1, null, '2018-04-27 09:27:00'); Query OK, 1 row affected (0.00 sec) MariaDB [foo]> SELECT * FROM time_test; +------+---------------------+---------------------+ | id | stamp | time | +------+---------------------+---------------------+ | 1 | 2018-04-27 09:28:25 | 2018-04-27 09:27:00 | <------插入时,TIMESTAMP 会默认填充当前时间 +------+---------------------+---------------------+ 1 row in set (0.00 sec) MariaDB [foo]> SELECT stamp + 0 FROM time_test; +----------------+ | stamp + 0 | +----------------+ | 20180427092825 | +----------------+ 1 row in set (0.00 sec) MariaDB [foo]> SELECT time + 0 FROM time_test; +----------------+ | time + 0 | +----------------+ | 20180427092700 | +----------------+ 1 row in set (0.00 sec) MariaDB [foo]> UPDATE time_test SET id = 2 WHERE id = 1; Query OK, 1 row affected (0.03 sec) Rows matched: 1 Changed: 1 Warnings: 0 MariaDB [foo]> SELECT * FROM time_test; +------+---------------------+---------------------+ | id | stamp | time | +------+---------------------+---------------------+ | 2 | 2018-04-27 09:30:02 | 2018-04-27 09:27:00 | <------更新时,TIMESTAMP 会默认更新为当前时间 +------+---------------------+---------------------+ 1 row in set (0.00 sec)MySQL 中的位类型从技术上看都是字符串类型。
BIT 列中可以存储一个或多个 true/false 值。BIT(n) 定义了存储 n 个位的字段,BIT 列最大支持 64 个位。
BIT 的行为因存储引擎的不同而不同。MyISAM 会打包存储所有的 BIT 列,17 个单独的 BIT 列只需要 17 个位存储(假设没有可为 NULL 的列),只需要 3 个字节。假设存储引擎是 Memory 或 InnoDB,每个 BIT 列使用一个足够存储的最小整数来存放,无法节省存储空间。
MySQL 把 BIT 当做字符串类型,而不是数字类型。当检索 BIT(1) 的值时,结果是一个包含二进制 0 或 1 的字符串,而不是 ASCII 码的 0 或 1。但是在数字上下文的场景中,会自动将位字符转为对应的 ASCII 码。例如,对于存储了 b'00111001' 的 BIT(8) 列,正常检索时得到字符码为 57 的字符‘9’,但是在数字上下文场景中,得到数字 57:
MariaDB [foo]> CREATE TABLE bit_test(a BIT(8)); Query OK, 0 rows affected (0.01 sec) MariaDB [foo]> INSERT INTO bit_test VALUES(b'00111001'); Query OK, 1 row affected (0.02 sec) MariaDB [foo]> SELECT a, a + 0 FROM bit_test; +------+-------+ | a | a + 0 | +------+-------+ | 9 | 57 | +------+-------+ 1 row in set (0.00 sec)如果需要保存很多 true/false 值,可以合并这些列到一个 SET 数据类型,在 MySQL 内部以一系列打包的位的集合来表示。存储空间利用率高,使用方便(可以在查询中使用 FIELD() 和 FIELD() 函数)。缺点是改变列的定义时,代价较高(需要使用 ALTER TABLE),对于大表很麻烦。
标识列(identifier column)选择合适的数据类型很重要。标识列可以与其他值进行比较,或通过标识列寻找其他列。标识列也可以作为其他表中的外键。
选择标识列的类型时,既要考虑存储类型,也要考虑 MySQL 对这种类型怎么执行计算和比较。例如 MySQL 内部用整数存储 ENUM 和 SET 类型,在比较操作时转为字符串。
整数是标识列最佳选择,快,且可以使用 AUTO_INCREMENT。
标识列的糟糕选择。ENUM 和 SET 列适合存储固定信息,例如性别、产品类型。
尽量避免,慢,存储空间大。尤其是 MyISAM,会对字符串压缩索引。
例如低于秒级精度的时间戳。对于 IPv4 地址,可以使用无符号整数存储,MySQL 提供了 INET_ATON() 和 INET_NTOA() 函数进行转换。
MySQL 的存储引擎 API 工作时需要在服务器层和存储引擎层之间通过行缓冲格式拷贝数据,然后在服务器层将缓冲内容解码成各个列。MyISAM 的变长行和 InnoDB 的行结构都需要转换。
单个查询最多在 12 个表内做关联。
范式化数据库中,每个事实数据会出现且只出现一次。反范式化数据库中,信息是冗余的,可能存储在多处。
范式化的优点:
更新操作快较好的范式化可以减少数据冗余,修改简单范式化好的表更新冗余少的数据可以更少的使用 DISTINCT 或 GROUP BY 语句范式化设计的 schema 的缺点是通常需要关联查询。
反范式化的优点:所有数据都在一张表中,避免了关联。
最常见的混用范式化和反范式化的例子是复制或缓存,在不同的表中存储相同的特定列。可以通过触发器更新冗余列。
完全独立的缓存表和汇总表,可以存储少量冗余数据。
缓存表是实时维护的。
汇总表是定期重建的。
可以使用物化视图(MySQL 需要借助工具 Flexviews)和计数器表。
MySQL 执行 ALTER TABLE 时,通常是用新的结构创建一个空表,从旧表中查出所有数据插入新表,再删除旧表。
大部分 ALTER TABLE 操作会中断 MySQL 服务。有几种方式可以避免停机:
主备结构时,可以在不提供服务的备用库执行操作,完成后再主备切换。影子拷贝:用新的表结构创建新表,然后通过重命名和删表操作交换两张表。可以使用 Facebook 的“online schema change”工具。改变或删除列的默认值时,可以使用 ALTER COLUMN(不一定重建表)或 MODIFY COLUMN(需要重建表)。列的默认值存储在表的 .frm 文件中,
有风险。。。
ALTER TABLE 时,先禁用索引,载入数据后再启用索引,可以高效载入数据。
转载于:https://www.cnblogs.com/kika/p/10851631.html
