数据库mysql优化介绍

mysql是关系型数据库的一种,深入了解mysql内部的机制有助于我们写出高质量的sql语句。虽然知道基础的sql语法也能帮助我们完成平常的工作,但是作为对自己有要求的后端,必须去学习一些内部原理,这样如果在工作过程中碰到数据库的问题,我们也就有排查的思路,然后给出恰当的解决方案。

1. 数据类型

在建表之初,我们就该考虑各个字段应该使用何种数据类型。从业务角度来看,选择哪种数据类型是无关紧要的,只要能实现需求。从技术角度来看,在还没遇到数据库性能瓶颈之前,选择哪种数据类型也确实看不到什么优势。但是随着业务数据激增,以前一点一滴的思考最终会带来收益。

1.1 整数类型

如果字段能用整型存储就尽量用整型。相比于字符类型,整型的计算更快。
尽量使用可以正确存储数据的最小数据类型。更小的数据类型,占用更少的磁盘、内存和CPU缓存,比如tinyint相比于int占用更少的空间。但是在ddl语句中int(10)和int(11)并不会实际改变存储所需的空间,只会影响客户端显示的数据长度。
如果是非负整数使用unsingned属性。unsingned属性可以使正数上限提高一倍。

1.2 varchar和char

长度一定的字符串考虑使用char类型,比如32个字符的hash串。varchar是变长类型,会使用1到2个字节额外记录数据长度。从磁盘角度来说varchar是按需分配空间,并不会存在浪费,但是char类型可能会存在浪费,不过从数据更新角度来说,varchar字段长度可能会改变,所以可能会造成页分裂等问题,导致更多的磁盘io。从内存分配角度来说,varchar字段总是会被分配最大长度的空间(ddl语句varchar(n)中的n),相比于char,可能会造成更多的内存浪费。

2.索引

mysql中索引的实现与具体的存储引擎相关,本篇文章默认说的是innodb引擎。关于索引的一些基础知识这里不再赘述,不了解的朋友可以参看《高性能mysql一书》。

2.1索引创建

创建索引之前可以看下整张表包含哪些索引,是否能满足需求,如果不能,再考虑是不是可以在原先的索引做改造,比如把单列索引改造成多列索引。索引本身也会占用内存磁盘,并且插入数据时需要更新索引,所以尽量减少重复的或者无用的索引。
请注意索引列的选择性,如果你的语句中使用了索引但是该索引列的选择性不高,数据库引擎可能还是会采用全表扫描的方式进行查询。可以使用一下式子计算列的选择性,唯一索引的选择是1,是性能最好的索引

mysql> select count(distinct(column)) / count(*) from table;

如果是在varchar列创建索引,如果不考虑排序和分组的情景,可以使用列前缀创建索引,并且应该选择足够长的前缀以确保较高的选择性,同时又不能太长。下面例子中的n就是前缀的长度,这个值需要根据实际情况计算得到。

mysql> alter table my_table add key idx_column (column(n));

如果要对一列创建索引,但是这一列的字符串太长,而且c中的前缀索引的方案并不能很好的工作,那么可以使用一些特别的技巧。列是不定长的,可以使用一定的算法得到定长字符串然后作为该列的一个字段存储,并在计算列创建索引,查询的时候可以对计算列进行索引。可以使用crc32等算法,考虑到冲突问题,查询的时候除了比较计算列还要对原列进行比较。这是一个平衡内存使用和查询速度的解决方案。

mysql>alter table my_table add key idx_crc_column (crc_column);
mysql>insert into my_table column='xxx', crc_column=crc32('xxx');
mysql>select id from my_table where crc_column=crc32('yyy') and column='yyy';

3.查询优化

3.1结果集获取

不要在所有查询语句中使用select *,也就是不要获取你不需要的列。额外的字段会带来额外的内存、cpu、磁盘io的消耗。另外,select *会让优化器无法完成覆盖索引扫描这类优化,关于覆盖索引扫描的概念一会会提到。
关注是否返回了多余的结果集,对并不需要的结果集,不要去获取。多余的结果集会占用带宽、内存。比如当你确定只获取一列数据的时候应该使用limit 1限制返回的结果集,当然如果是唯一索引的查询就不需要该限制,对于非唯一索引条件实际上还是可能会返回多余的数据的,如果是没有索引的查询,还会进行全表扫描,所以请注意查询语句是否返回了多余的结果集。

3.2 sql优化

优化count语句。关于count的优化,网上有很多的版本,到底是使用count(column)还是count(*)或者是count(0)。count有两个作用,一个是统计某个列的数量,另一个是统计行数。如果是count某一列的话会判定该列的值是否为NULL。实际上如果优化器判定被count的是一个常量如count(*)、count(1)、count(0)等,那么就不会就行NULL判定,只是简单的获取行数。
优化回表查询。innodb主键是聚簇索引,其它索引的叶子节点保存的是主键值,而不是行指针,所以利用辅助索引查询的时候会存在回表的问题。比如某个表有三列,id列(主键)、time列(辅助索引)、column列(筛选列),并且id和time都是单调递增的列。现在需要查询某个时间段的数据,有两种方案:

mysql> select count(*) from my_table where time between a and b and column='xxx'
mysql> select id from my_table where time>= a limit 1;  // id=x
mysql> select id from my_table where time<= b limit 1; // id=y
mysql> select count(*) from my_table where id between x and y and column='xxx';

在数据量不大的时候可能两种查询差不多,但是当a与b时间段中的数据达十万级百万级时,这两者的性能差别很大。一方面使用辅助索引存在大量的回表查询,另外一方面,使用id主键进行范围查询可以一次性顺序读取磁盘数据。
使用覆盖索引。覆盖索引的概念是如果一个索引包含(或者覆盖)所有需要查询的字段的值,我们就称之为"覆盖索引"。所以这是一个相对的概念,是相对查询语句来说的,对于sql1可能是覆盖索引,对于sql2就可能不是覆盖索引了。这个概念跟其它的索引概念有所不一样。我们并没有利用这个所谓的"覆盖索引"去"索引"数据,而是利用索引其它的特性,比如数据量小,能够顺序磁盘io,以及减少内存使用量,最终达到快速获取结果的目的。

mysql> alter table my_table add key idx_a_b (a, b);
mysql> select a from my_table where b="xxx";

上面这个例子中,select语句并不能使用前缀索引去"索引"数据,但是并不妨碍select语句使用idx_a_b索引优化查询,它可以顺序扫描这个索引,并对每一行进行筛选。所以可以针对覆盖查询这种场景建立复合索引,那么查询也就变成了覆盖索引查询。
优化关联查询。关联查询join,mysql使用的是嵌套循环关联,所以应该尽量用小表驱动大表,尽量减少被驱动表扫描的次数(也就是小表的行数),并且被驱动的表在关联的字段上建立索引,这样可以加快关联速度。
in和exists的选择。

mysql> select a.id from a where a.id not in (select a_id from b) limit 100;
mysql> select a.id from a where not exists(select b.a_id from from b where b.a_id=a.id limit 1) limit 100;

上面两个语句中都有子查询,但是in是独立的子查询,而exist是关联子查询(跟a表相关)。假设b表中的a_id字段有索引,考虑a表小(100行)b表大(100W行)的情况。使用in查询会读取所有b表所有的数据,占用大量内存,并且做条件筛选的时候,not in相当于 a.id != n1 and a.id !=n2 and a.id !=n3,此时是不能使用索引优化查询的,所以性能不是很好。使用exists查询会对a进行全表扫描(100行),并且会对每一行数据进行一次exists子查询,由于b表有索引,所以查询很快。所以这种情况下明显是not exists语句优于not in语句的。

mysql> select a.id from a where a.id in (select a_id from b) limit 100;
mysql> select a.id from a where exists(select b.a_id from from b where b.a_id=a.id limit 1) limit 100;

下面一种情况类似,区别在于in语句可以利用索引进行查询了,如果a表大b表小,应该使用in语句,如果a表小b表大,应该使用exists语句。可能实际的情况更加复杂,也可以考虑把子查询转换成关联查询,具体情况具体分析。

上面有的只是给出了一部分例子,其实还有更多可以优化的点,弄清楚数据库索引的原理会非常有帮助。

4.数据库配置

4.1 内存分配

设置恰当的innodb_buffer_pool_size。如果大部分表是innodb表,那么innodb缓冲池或许比其它任何东西更需要内存。缓冲池缓冲了索引、行的数据、插入缓冲、锁以及其他内部数据结构。innodb还借助缓冲池帮助延迟写入,这样就可以合并多个写入操作。总之,innodb严重依赖缓冲池,必须确保得到了足够的内存。
确定是否需要开启查询缓存query_cache_type,开启查询缓存有一定的好处,但是也会带来一些额外的复杂的实现,比如需要确保缓存数据和磁盘数据一致,这就带来了很多额外的消耗。如果有专门的缓存服务器(实际上如果要构建高性能网站,那么缓存服务是必须的),这一配置可以关闭。
设置合适的innodb_log_buffer_size。innodb使用日志来减少提交事务时的开销。核心原理是将随机的io转换成顺序io,事务修改的数据和索引通常会映射到表空间的随机位置,所以刷新这些变更到磁盘需要很多随机io,利用日志记录事务并顺序写入硬盘,一旦日志安全写到硬盘,事务就持久化了,即使变更还没写到数据文件,innodb可以重放日志并且恢复已经提交的事务。如果是大内存服务器,分配32MB~128MB的日志缓冲,可以帮助避免压力瓶颈。

4.2 IO优化

关于innodb事务日志的配置,除了缓冲区的大小以外,还有一个决定何时将日志写入磁盘的配置innodb_flush_log_at_trx_commit,0表示每秒刷新一次,但是事务提交时不做任何事,1表示每次事务提交都刷新到持久化存储(默认配置),2表示每次提交时把日志缓冲写到日志文件,但是并不刷新。可能不了解linux系统的同学会有点疑惑2是个什么意思,linux系统存在文件缓存,所以当你写入日志时内核只是把这部分数据写入文件缓存当中,并把该页设置成脏页,后续何时刷到磁盘由操作系统决定,所以这里要区分清楚"把日志缓冲写到日志文件"和"把日志持久化到存储"是不同的概念。所以如果是设置2,mysql崩溃了并不会丢失任何事务,但是如果是服务器挂了,则还是可能会丢失一部分事务。1是默认的设置,可以保证不会丢失任何已经提交的事物,除非相关的系统调用是一个伪刷新。如果不需要严格的持久化,可以将值设为2或0。

5.回顾

优化的方式有很多,但大致都脱离不了cpu、磁盘io、缓存、网络、算法的范畴,当然这里指的是单机服务的优化。从顶向下的思考方式可以让你非常轻松的理解优化的点。innodb使用b+tree作为索引加快查询速度,这与算法相关,覆盖索引、事务日志、缓冲区等优化了磁盘io。优化的过程中也要记得二八原则,我们不需要做到十全十美,实际上也做不到,优化重要的点可能可以解决大部分问题。另外如果一个优化的方案让你感到很复杂,那么最好不要使用该方案,出了问题解决起来也将非常的棘手。当然还有其它的没提到的一些点,比如读写分离、表分区等技术都可以帮助解决一些问题,这一部分请自行了解。

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