mysql 数据优化

在工作中我们经常要与数据库打交道,数据优化已经成为一个无法避免的问题,在这里先不考虑 mysql 服务器优化、连接数、主从同步等问题,只优化数据,以及更好的业务处理。

PS:这里我们假设数据是百万级,超过百万级可能就有一些纸上谈兵了,因为我还没有在 mysql 上处理过千万级以上的数据。

数据类型

选择最合适的类型,来存储数据,当然这是废话,就只说几个点吧。

1、小数类型为 decimal,禁止使用 float 和 double。float 和 double 在存储的时候,存在精度损失的问题,很可能在值的比较时,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数分开存储。mysql 在保存值时进行四舍五入,因此在 float(7,4) 列内插入 9999.00009 的近似结果是 999.0001。

2、存储的字符串长度几乎相等,使用 char 定长字符串类型。如 md5 加密过的值,或者类似微信 openid 的值。

3、varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长 度大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索 引效率。

4、表必备三字段:id, gmt_create, gmt_modified。其中 id 必为主键,类型为 unsigned bigint、单表时自增、步长为 1,如果考虑到以后数据量会增大,可以使用 mysql 里面的 uuid_short() 函数来生成 id 以方便表数据拆分。gmt_create, gmt_modified 的类型均为 date_time 类型。

5、字段允许适当冗余,以提高性能,但是必须考虑数据同步的情况。冗余字段应遵循:

  • 1)不是频繁修改的字段。
  • 2)不是 varchar 超长字段,更不能是 text 字段。

6、单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。

7、合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检 索速度。 正例:无符号值可以避免误存负数,且扩大了表示范围。

对象 年龄区间 类型 表示范围
150 岁之内 unsigned tinyint 无符号值:0 到 255
数百岁 unsigned tinyint 无符号值:0 到 65535
恐龙化石 数千万年 unsigned tinyint 无符号值:0 到约 42.9 亿
太阳 约 50 亿年 unsigned tinyint 无符号值:0 到约 10 的 19 次方

8、使用 ENUM 而不是 VARCHAR 。ENUM 类型是非常快和紧凑的。在实际上,其保存的是 TINYINT,但其外表上显示为字符串。这样一来,用这个字段来做一些选项列表变得相当的完美。

索引规则与优化

mysql 索引分为 B+tree 与 Hash 两种常用的类型, 默认索引为 B+tree。

  • Hash 索引仅仅能满足"=","IN"和"<=>"查询,不能使用范围查询。
  • B+tree 最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引(页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决,之前我们用 sphinx,后迁移到 solr)。

1、业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。

PS: 不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明 显的;另外,即使在应用层做了非常完善的校验和控制,只要没有唯一索引,根据墨菲定律("凡是可能出错的事必定会出错"), 必然有脏数据产生。

2、超过三个表禁止 join。需要 join 的字段,数据类型保持绝对一致;多表关联查询 时,保证被关联的字段需要有索引。

3、在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据 实际文本区分度决定索引长度。

PS: 索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90% 以上,可以使用 count(distinct left(列名, 索引长度)) / count(*) 的区分度 来确定。InnoDB 单列索引长度不能超过 767bytes,实际上联合索引还有一个限制是 3072bytes。

4、如果有 order by 的场景,请注意利用索引的有序性。order by 最后的字段是组合 索引的一部分,并且放在索引组合顺序的最后,避免出现 file_sort 的情况,影响查询性能。

5、建组合索引的时候,区分度最高的在最左边。

6、创建索引时避免有如下极端误解:

  • 1)误认为一个查询就需要建一个索引。
  • 2)误认为索引会消耗空间、严重拖慢更新和新增速度。
  • 3)误认为唯一索引一律需要在应用层通过“先查后插”方式解决。

7、尽量避免 null:应该指定列为 not null,除非你想存储 null。在 mysql 中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用 0、一个特殊的值或者一个空串代替空值。

8、where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。where 子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。

sql 规则与优化

1、不要使用 count(列名)或 count(常量)来替代 count(*),count(*) 就是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 null 和非 null 无关。count(*)会统计值为 null 的行,而 count(列名) 不会统计此列为 null 值的行。

2、count(distinct col) 计算该列除 null 之外的不重复数量。注意 count(di col1, col2) 如果其中一列全为 null,那么即使另一列有不同的值,也返回为 0。

3、当某一列的值全是 null 时,count(col) 的返回结果为 0,但 sum(col) 的返回结果为 null,因此使用 sum() 时需注意 NPE 问题。可以使用如下方式来避免 sum 的 NPE 问题:select IF(ISnull(SUM(g)),0,SUM(g)) FROM table;

4、在代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句。

5、不得使用外键与级联,一切外键概念必须在应用层解决。(外键影响数据库的插入速度)

6、禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。

7、in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控 制在 1000 个之内。

8、如果有全球化需要,所有的字符存储与表示,均以 utf-8mb4 编码。

PS: 从 mysql5.5 开始,可支持 4 个字节 UTF 编码,只要将编码标记成 utf8mb4 即可。并且 utf8mb4 是兼容 utf8 ,并支持 emoji 表情。

9、查看慢查询 sql 日志,使用 explain 命令调试索引命中信息,先 sql 运行看看是否真的很慢,注意设置 SQL_NO_CACHE。

查询结果解释如下:

  • id:select 标识符,表示 select 的查询序列号
  • select_type:表示 select 语句的类型。可取值有:SIMPLE 表示简单的查询,不包括连接查询和子查询;PRIMARY 表示主查询,或者最外层的查询语句;UNION 表示连接查询的第 2 个或者后面的查询语句;DEPENDENT UNIO 连接查询中的第 2 个或后面的 select 语句,取决于外面的查询;UNION RESULT 连接查询的结果;SUBQUERY 子查询中的第 1 个 select 语句;DEPENDENT SUBQUERY 子查询中的第 1 个select ,取决于外面的查询;DERIVED 表示导出表的 select
  • table:表示查询的表
  • type:表示表的连接类型,如下是从最佳类型到最差类型的介绍。
    1、system:该表是仅有一行的系统表,是 const 连接类型的一个特例
    2、const:数据表最多只有一个匹配行,将在查询开始时被读取,并在余下的查询优化中作为常量对待。const 用于使用常数值比较 PRIMARY KEY 或 UNIQUE 索引的所有部分的场合
    3、eq_ref:对于每个来自前面表的行组合,从该表中读取一行。当一个索引的所有部分都在查询中使用并且索引是 UNIQUE 或 PRIMARY KEY 时,即可使用该类型
    4、ref:对于来自前面的表的任意行组合,将从该表中读取所有匹配行。这种类型用于索引既不是 UNIQUE 也不是 PRIMARY KEY 的情况,或者查询中使用了索引列的子集。ref 可以用于使用 = 或者 <=> 操作符的带索引的列
    5、ref_or_null:该连接类型如同 ref,但是添加了 mysql 可以专门搜索包含 null 值得行。在解决子查询中常使用该连接类型的优化。
    6、index_merge:该连接类型使用了索引合并优化方法
    7、unique_subquery:一个索引查找函数,可以完全替换子查询,效率更高(见表2)
    8、index_subquery:该连接类型类似于 unique_subquery,可以替换 IN 子查询
    9、range:只检索给定范围的行,使用一个索引来选择行
    10、index:该连接类型与ALL相同除了只扫描索引树。由于索引文件通常比数据文件小,因此 index 比 ALL 快
    11、ALL:对于前面表的任意行组合,进行完整的表扫描,通常可以增加索引来避免使用ALL连接
  • possible_keys:表示 mysql 能使用哪个索引在该表中找到行,如果为 null 表示没有相关索引。在这种情况下,通过检查 where 子句看它是否引用某些列或适合索引的列来提高查询性能。如果有,则可通过创建索引提高查询性能
  • key:表示查询实际使用的索引,该值为 null 表示没有选择索引。
  • key_len:表示 mysql 选择的索引字段按子节计算的长度
  • ref:表示使用哪个列或常数与索引一起来查询记录
  • rows:显示 mysql 在表中查询时必须检查的行数
  • Extra:该列 mysql 在处理查询时的详细信息

10、可以使用连接(join)查询来代替子查询。连接查询不需要建立临时表,其速度比子查询更快,如果查询中使用索引的话,性能会更佳。连接之所以有更高的效率,是因为 mysql 不需要再内存中创建临时表来完成查询工作。

总结

上面的文章基本都是我从网上收集并整体修改的,大部分都是我工作中所遇到的问题,最后我抛出三个问题。

1、如何设计一张百万级的用户表结构。

2、百万级用户表后台分页。

3、通过昵称查询用户信息,昵称可以重复。

参考文献

1、阿里巴巴 Java 开发手册,MySql 规约

2、施瓦茨 (Baron Schwartz) 等 著,宁海元等 译;高性能MySQL(第3版);2013年5月1日

3、姜承尧 著;数据库技术丛书·MySQL技术内幕:InnoDB存储引擎(第2版);2013年6月5日

4、姜承尧 著;MySQL技术内幕:SQL编程。

你可能感兴趣的:(mysql 数据优化)