Home MySQL优化小结
Post
Cancel

MySQL优化小结

最近在迁移历史数据,调试前端效果时,发现了不少慢查询。现在将过程记录下来。

为什么有慢查询

慢查询的主要原因无非就那么几个:表设计的烂;索引加的烂;SQL写的烂

表设计的烂

下面举两个常见的例子

字段膨胀

比如一个金融理财产品表,产品有很多种属性,比如名称,系列,期数,编号,外部编号,风险等级,募集方式,周期类型,个人认购起点金额,企业认购起点金额,挂牌机构,挂牌价格,是否预约,预约额度,存续天数,兑付日,付息周期,付息特点等等。到最后可能膨胀到有上百种金融属性。

一个简单而暴力的方式就是需求新增什么字段,就往表里新增一个字段。到最后一张产品表上百个字段,没人清楚某个字段还有用没用。 甚至把那种两个日期减一减就能得出的比如存续天数等也整到表里。到期日更新了存续天数还得跟着更新。

如果一张表里字段很多,而且一些字段使用频率不高的话,可以将这张表拆一下,比如拆一个产品详情表,专门放那些查询频率低的字段。

另外,对于数据量比较大的表,新增一个字段代价也是比较高昂的,可以在表设计的时候预留一下扩展字段,在应用中将一些信息转为JSON存储到扩展字段。对于MySQL5.6及以下的版本,存储类型可以用VARCHAR。在MySQL5.7中新增了JSON类型字段,对于检索与解析有不错的支持。

类型优化

下面谈几个常见的类型

日期/时间

MySQL中有五种和日期时间相关的类型,分别是:

  • DATETIME 8字节 范围:1000-01-01 00:00:00到9999-12-31 23:59:59
  • DATE 3字节 范围:1000-01-01到9999-12-31
  • TIMESTAMP 4字节 范围:1970-01-01 00:00:00 UTC到2038-01-19 03:14:07 UTC
  • YEAR 1字节
  • TIME 3字节

TIMESTAMP列常用来表示更新时间,只需要将其属性设置为ON UPDATE CURRENT_TIMESTAMP

对于开发来说,如果确定一个业务字段只需要表示具体的某一天,用DATE即可,好处不但在于节省存储空间,更避免TIMESTAMP/DATETIME可以存储时间引发潜在BUG(如由于我们在东八区,所以对于一些web应用来说,前端传过来的日期可能会隐式被+8小时最终存储在表中,这样就可能会由于差了8小时,筛选不到表中的某个日期,当然这个问题也可以通过业务应用中配置或者写定制化的转换来规避)。但从根本上来说,表设计的时候一个不带时间的业务字段在有合适类型的情况下本来就不应该给它更广泛的类型。

数字类型

MySQL中有五种整数类型

  • TINYINT 1字节
  • SMALLINT 2字节
  • MEDIUMINT 3字节
  • INT 4字节
  • BIGINT 8字节

在应用开发中常用TINYINT来表示是否删除或者一些枚举类型,而INT/BIGINT常用作表的自增主键,BIGINT也可以用于一些精度到分的金额扩大一百倍后存储。

在金融应用中,有时候需要考虑舍弃效率而用DECIMAL来存储金额,并不是说123.45元以12345分存储到库表中不可行,而是在需要存储一些金额运算中间结果的情况下,如果乘以100以分为单位存储,会扩大最终的精度误差。例如计算一段时间内的结转利息,如果每天在代码中作了高精度运算,但是入表精度到分,最终一天天下去必然会扩大误差。

字符类型

在实际应用中比较常见的字符类型有两种CHAR和VARCHAR。 CHAR(N)用于存储定长字符串,N范围为0-255 VARCHAR(N)用于存储变长字符串,N范围为0-65535 N的含义在MySQL4.1之前表示字符串的字节,而在之后表示长度。

VARCHAR在存储中需要额外增加1-2个字节用于存储字符串的长度,当长度<=255时用1个字节,>255时需要2个字节

BINARY/VARBINARY/BLOB/TEXT在生产环境中较少使用,这里略去分析介绍

通用表设计

目前使用MySQL开发OLTP应用一般使用InnoDB存储引擎。对于InnoDB存储引擎,整理一些最佳表设计实践

  1. 用BIGTINT作自增主键 InnoDB存储引擎以索引组织表,这里索引本质上是一颗B+树。如果对于一张表我们设计时以name为主键,由于在新增时,数据会插入到B+树中某个不可预期的位置,而下一条数据很小概率会插入到相同的块里,这样很容易导致数据页的SPLIT操作,而对于自增主键,由于每次插入的位置一定是索引树的最后,数据分页次数会显著小于前者。毕竟顺序对磁盘进行I/O必然是比随机访问性能要高的。而之所以建议BIGINT而非INT是因为对于一张数据量大的表,完全有可能在一段时期增/删之后,自增主键数值真的超过INT上限了,这时改数据类型代价有些大。千万级数据量的表可能需要几分钟才能完成类型转换,在此期间表无法进行写入,很容易引发事故。

  2. 尽量避免使用外键 外键:在学校的教学中一般一个良好的数据库表设计都讲究范式,讲究数据完整性约束。但是外键对于互联网应用来说,是显得有些累赘的,首先是其导致的性能问题,每次对数据操作都会涉及外键检查。另外,对于复杂的业务不能保证绝对不会有脏数据进入,一旦需要修改线上表,如果存在多表外键依赖,会比较困难。 但是这不代表我们就完全放弃了外键,我们只是放弃了在数据库层作外键。外键约束完全可以在应用层实现。

  3. 使用删除标志位 之所以使用删除标志位来进行逻辑删除而不是直接物理删除最主要的考量还是数据是宝贵的,一旦发生误删,逻辑删除数据比较容易恢复。

索引加的烂

MySQL的辅助索引本质上也都是B+树,与聚集索引不同的是它的叶子节点不存储行中的所有列,而是存储主键的值。所以查询在辅助索引上找到值后,会再去聚集索引中查询该条记录。而如果筛选字段本身已经被索引列包含,则MySQL不会再去聚集索引中查询。

避免冗余索引

前文说过,每一个索引都对应于一颗B+树,所以每一次数据的更新都会涉及到所有索引的更新。冗余的索引是不该出现的。何为冗余索引,例如对一张表中的x, y, z增加联合索引(x, y, z)就相当于对(x)和(x,y)添加了索引。

This post is licensed under CC BY 4.0 by the author.
Contents

一处隐蔽的空指针异常

MySQL执行计划总结