美文网首页
《高性能MySQL》笔记(1)——Schema与数据类型优化

《高性能MySQL》笔记(1)——Schema与数据类型优化

作者: esrever | 来源:发表于2018-03-14 10:43 被阅读50次

    Schema与数据类型优化

    最常用的数据类型分类

    • 数字(int、decimal)
    • 字符串(varchar、char、text)
    • 时间(date、datetime、timestamp)

    在PHPmyadmin工具中,还可以看到两种大类型:spatial(空间类型)、JSON,不过使用很少

    如何选择数据类型?

    • 简单的区分大类型:数字、字符串、时间等
    • 尽量使用可以正确存储数据的最小数据类型(比如:数字范围小的考虑 tinyint 之类)
    • 尽量使用简单的数据类型(整型 > 字符串,内建类型[date, timestamp, datetime] > 字符串)
    • 日期。TIMESTAMP占用的存储空间是DATETIME的一半,但是TIMESTAMP的范围只能是1970~2038,需要具体分析

    整数类型的范围

    类型 存储位数 有符号的max(2^(n-1)-1) 显示最大宽度
    TINYINT 8 127 4
    SMALLINT 16 32767 6
    MEDIUMINT 24 8388607 8
    INT 32 2147483647 11
    BIGINT 64 9223372036854775807 20

    INT(11)中的11表示为整数类型指定显示的宽度。所以,对于存储与计算来说,INT(1)INT(20)是相同的

    实数(带有小数部分的数字)类型

    1、 FLOAT、DOUBLE类型

    精度不足,仅支持近似计算

    但是由于CPU可以直接支持原生浮点型计算:

    • 运算速度更快
    • 空间消耗比较小

    2、 DECIMAL类型

    MySQL 5.0及以上的版本,将数字打包保存到一个二进制字符串中(每4个字节存9个数字),且小数点占1个字节。最多允许65个数字

    建议:因为需要额外的空间和计算开销,所以应该尽量只在对小数进行精确计算时才使用DECIMAL——例如财务数据。在数据量比较大时,可以考虑使用BIGINT代替DECIMAL,乘以相应的倍数得到最终的结果。这样可以同时避免浮点存储计算不精确和DECIMAL精确计算代价高的问题

    字符串类型

    1、 VARCHAR、CHAR类型

    最主要的两种字符串类型,一般存储的数据量较小

    VARCHAR的特点:

    • 用于存储可变长的字符串
    • 需要1或2个额外的字节记录字符串的长度,len <= 255需要1个字节,否则需要2个
    • 可以节省存储空间,但是update可能会造成碎片产生

    使用VARCHAR的场景:

    • 字符串列的最大长度比平均长度大很多
    • 列更新较少
    • 使用复杂的字符集(如UTF-8),每个字符使用不同的字节数进行存储

    另外需要注意的: 虽然VARCHAR(5)VARCHAR(200)来存储hello字符串,空间开销是一样的,但是后者需要分配更多的内存去保存内部值和排序。所以最好的策略是只分配真正需要的空间

    InnoDB可以把过长的VARCHAR存储为BLOB

    CHAR的特点:

    • 根据定义的字符串长度分配足够的空间
    • 不需要记录长度的额外字节

    使用CHAR的场景:

    • 存储很短的字符串,或者所有值都接近同一个长度(如MD5密码值等)
    • 经常变更的数据,CHAR优于VARCHAR,因为CHAR不容易产生碎片

    2、 BLOB、TEXT类型

    为存储很大的数据而设计的字符串数据类型,BLOB采用二进制,TEXT采用字符方式存储

    特点:

    • 当作独立的对象处理,存储也会特殊处理(太大的数据量可能存储为指针,然后在外部存储区域存储实际的值)
    • 排序只对最前max_sort_length字节排序,如果只需要排序前面一小部分字符,可以减小max_sort_length的配置,或者使用ORDER BY SUBSTRING(column, length)
    mysql> select @@max_sort_length;
    +-------------------+
    | @@max_sort_length |
    +-------------------+
    |              1024 |
    +-------------------+
    1 row in set (0.00 sec)
    

    问题: 如果查询使用了BLOB、TEXT类型并且需要使用隐式临时表,将不得不使用MyISAM磁盘临时表(即EXPLAIN的Extra列包含Using temporary),造成严重的性能开销!

    • 最好的解决方案是尽量避免使用BLOB和TEXT类型
    • 如果无法避免则可以在用到这些类型的地方使用SUBSTRING(column, length),将列值转换为字符串,这样就可以使用内存临时表了。但是必须确保截取的子字符串足够短,否则如果临时表大小超过max_heap_table_sizetmp_table_size,仍然会转换为磁盘临时表
    mysql> select @@max_heap_table_size;
    +-----------------------+
    | @@max_heap_table_size |
    +-----------------------+
    |              16777216 |
    +-----------------------+
    1 row in set (0.00 sec)
    
    mysql> select @@tmp_table_size;
    +------------------+
    | @@tmp_table_size |
    +------------------+
    |         16777216 |
    +------------------+
    1 row in set (0.00 sec)
    

    3、 使用枚举(ENUM)代替字符串类型

    相对固定且少量的字符串,可以用枚举类型代替,在MySQL中存储为整数,并用一个“数字 - 字符串”的映射表保存

    mysql> create table enum_test(e ENUM('fish', 'apple', 'dog') NOT NULL);
    Query OK, 0 rows affected (0.03 sec)
    
    mysql> INSERT INTO enum_test(e) VALUES ('fish'), ('dog'), ('apple');
    Query OK, 3 rows affected (0.00 sec)
    Records: 3  Duplicates: 0  Warnings: 0
    
    mysql> select e from enum_test;
    +-------+
    | e     |
    +-------+
    | fish  |
    | dog   |
    | apple |
    +-------+
    3 rows in set (0.00 sec)
    
    mysql> select e+0 from enum_test;
    +-----+
    | e+0 |
    +-----+
    |   1 |
    |   3 |
    |   2 |
    +-----+
    3 rows in set (0.01 sec)
    
    mysql> desc enum_test;
    +-------+----------------------------+------+-----+---------+-------+
    | Field | Type                       | Null | Key | Default | Extra |
    +-------+----------------------------+------+-----+---------+-------+
    | e     | enum('fish','apple','dog') | NO   |     | NULL    |       |
    +-------+----------------------------+------+-----+---------+-------+
    1 row in set (0.00 sec)
    

    有些问题需要注意:

    • 枚举类型在ORDER的时候,是根据ENUM的顺序排序,如果需要对字符串特定排序,可以使用FIELD函数显式指定。 ORDER BY FIELD(e, 'apple', 'dog', 'fish')
    • 需要增加或删除字符串的时候,除非只在列表末尾添加元素,否则需要重建整个表
    • 在关联(JOIN)多表的情况下,ENUM之间关联,速度比VARCHAR之间关联会快很多,所以在优化中很值得使用。另外有时候,VARCHAR与ENUM之间关联,会比VARCHAR之间关联更慢,一定要注意保证ENUM与ENUM关联

    4、 日期和时间类型

    MySQL能存储的最小时间粒度为秒,最常用的类型:DATETIME、TIMESTAMP

    DATETIME

    从1001年到9999年,把日期封装到格式为YYYYMMDDHHMMSS的整数中,与时区无关,使用8个字节存储

    TIMESTAMP(无特殊要求的情况下,建议使用)

    从1970年到2038年,对应UNIX时间戳,依赖于时区,只需要4个字节存储

    存储比秒更小粒度的时间:

    • BIGINT
    • DOUBLE
    • 使用MariaDB替代MySQL

    范式与反范式

    在范式化的数据库中,每个事实数据会出现且只出现一次。相反,在反范式化的数据库中,信息是冗余的,可能会存储在多个地方

    范式的优缺点:

    • 范式化更新操作通常比反范式化要快

    • 当数据较好地规范化时,就只有很少或者没有重复数据,所以只需要修改更少的数据

    • 范式化的表通常更小,可以更好地放到内存中,执行操作会更快

    • 检索时更少需要DISTINCT或者GROUP BY语句

    • 通常需要关联一次或多次,代价昂贵且使一些索引策略失效

    反范式的优缺点:

    • 如果不需要关联,即使表没有使用索引,最差的情况也只是全表扫描,避免了随机I/O
    • 单独的表可以使用更有效的索引策略

    缓存表和汇总表

    有时提高性能最好的方法是在同一张表中保存衍生的冗余数据,有时也需要创建一张完全独立的汇总表或缓存表

    缓存表示例: 可以把复杂查询的结果放在一个索引合理的表中,便于多次查询

    汇总表示例: 假设需要计算之前24小时内发送的消息数,可以每小时生成一张汇总表,或者在汇总表的基础上,把之前23个完整的小时的统计表中的计数全部加起来,最后加上当前小时内的计数即可

    在重建缓存表和汇总表的时候,通常需要保证数据在操作时依然可用,需要通过“影子表”来实现

    DROP TABLE IF EXISTS my_s_new, my_s_old;
    CREATE TABLE my_s_new LIKE my_s;
    -- 按照需要去填充my_s_new
    RENAME TABLE my_s TO my_s_old, my_s_new TO my_s;
    

    计数器表的例子

    如果应用在表中保存计数器,则在更新计数器时可能碰到并发问题,出现一个全局的互斥锁(mutex),这会使得这些事务只能串行执行

    可以预先在一张表中增加100行数据,随机选择一个槽(slot)进行更新,使用聚合查询来统计总数

    UPDATE hit_counter SET cnt = cnt + 1 WHERE slot = RAND() * 100;
    SELECT SUM(cnt) FROM hit_counter;
    

    可以用ON DUPLICATE KEY UPDATE来代替预先生成行,统计每日的数据

    INSERT INTO daily_hit_counter(day, slot, cnt) VALUES (CURRENT_DATE, RAND() * 100, 1) ON DUPLICATE KEY UPDATE cnt = cnt + 1;
    

    上面的方法都是“更快地读,更慢地写”,通过建立一些额外索引、增加冗余列、创建缓存表和汇总表,虽然增加写查询的负担,但是会提升读查询的速度

    相关文章

      网友评论

          本文标题:《高性能MySQL》笔记(1)——Schema与数据类型优化

          本文链接:https://www.haomeiwen.com/subject/jvgxqftx.html