mysql性能优化

mysql性能优化 

1.      mysql大表优化

MySQL单库数据量在5000万以内性能比较好,超过阈值后性能会随着数据量的增大而变弱。MySQL单表的数据量是500w-1000w之间性能比较好,超过1000w性能也会下降。

一般单表性能极点整形值为主的在千万上下,字符串为主的在五百万左右。

1.1.    字段优化

尽量使用TINYINT、SMALLINT、MEDIUM_INT作为整数类型而非INT,如果非负则加上UNSIGNED

VARCHAR的长度只分配真正需要的空间

使用枚举或整数代替字符串类型

尽量使用TIMESTAMP而非DATETIME,

单表不要有太多字段,建议在20以内

避免使用NULL字段,很难查询优化且占用额外索引空间

用整型来存IP

 

1.2.    索引

索引并不是越多越好,要根据查询有针对性的创建,考虑在WHERE和ORDER BY命令上涉及的列建立索引,可根据EXPLAIN来查看是否用了索引还是全表扫描

应尽量避免在WHERE子句中对字段进行NULL值判断,否则将导致引擎放弃使用索引而进行全表扫描

值分布很稀少的字段不适合建索引,例如"性别"这种只有两三个值的字段

字符字段只建前缀索引

字符字段最好不要做主键

不用外键,由程序保证约束

尽量不用UNIQUE,由程序保证约束

使用多列索引时主意顺序和查询条件保持一致,同时删除不必要的单列索引

 

1.3.    查询SQL优化

可通过开启慢查询日志来找出较慢的SQL

不做列运算:SELECT id WHERE age + 1 = 10,任何对列的操作都将导致表扫描,它包括数据库教程函数、计算表达式等等,查询时要尽可能将操作移至等号右边

sql语句尽可能简单:一条sql只能在一个cpu运算;大语句拆小语句,减少锁时间;一条大sql可以堵死整个库

不用SELECT *

OR改写成IN:OR的效率是n级别,IN的效率是log(n)级别,in的个数建议控制在200以内

不用函数和触发器,在应用程序实现

避免%xxx式查询

少用JOIN

使用同类型进行比较,比如用'123'和'123'比,123和123比

尽量避免在WHERE子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描

对于连续数值,使用BETWEEN不用IN:SELECT id FROM t WHERE num BETWEEN 1 AND 5

列表数据不要拿全表,要使用LIMIT来分页,每页数量也不要太大

 

2.      其它优化

升级硬件

Scale up,这个不多说了,根据MySQL是CPU密集型还是I/O密集型,通过提升CPU和内存、使用SSD,都能显著提升MySQL性能

读写分离

也是目前常用的优化,从库读主库写,一般不要采用双主或多主引入很多复杂性,尽量采用文中的其他方案来提高性能。同时目前很多拆分的解决方案同时也兼顾考虑了读写分离

 

缓存

缓存可以发生在这些层次:

MySQL内部:在系统调优参数介绍了相关设置

数据访问层:比如MyBatis针对SQL语句做缓存,而Hibernate可以精确到单个记录,这里缓存的对象主要是持久化对象Persistence Object

应用服务层:这里可以通过编程手段对缓存做到更精准的控制和更多的实现策略,这里缓存的对象是数据传输对象Data Transfer Object

Web层:针对web页面做缓存

浏览器客户端:用户端的缓存

 

3.      分库分表

3.1.    表分区

表分区其实就是将一张大数据量表中的数据按照不同的分区策略分配到不同的系统分区、硬盘或是不同的服务器设备上,实现数据的均衡分配,这样做的好处是均衡大数据量数据到不同的存储介子中,这样每个分区均摊了一部分数据,然后可以定位到指定的分区中,对数据表进行需求操作,

 

分区和分表的区别:

分区和分表针对的都是数据表,而分表是真正的生成数据表,是将一张大数据量的表分成多个小表实现数据均衡;

分区并不是生成新的数据表,而是将表的数据均衡分摊到不同的硬盘,系统或是不同服务器存储介子中,实际上还是一张表。

另外,分区和分表都可以做到将表的数据均衡到不同的地方,提高数据检索的效率,降低数据库的频繁IO压力值,

 

3.1.1.   分区表原理

分区表是由多个相关的底层表实现,这些底层表也是由句柄对象表示,所以我们也可以直接访问各个分区,存储引擎管理分区的各个底层表和管理普通表一样(所有的底层表都必须使用相同的存储引擎),分区表的索引只是在各个底层表上各自加上一个相同的索引,从存储引擎的角度来看,底层表和一个普通表没有任何不同,存储引擎也无须知道这是一个普通表还是一个分区表的一部分。

 

在分区表上的操作按照下面的操作逻辑进行:

select查询:

当查询一个分区表的时候,分区层先打开并锁住所有的底层表,优化器判断是否可以过滤部分分区,然后再调用对应的存储引擎接口访问各个分区的数据

insert操作:

当写入一条记录时,分区层打开并锁住所有的底层表,然后确定哪个分区接受这条记录,再将记录写入对应的底层表

delete操作:

当删除一条记录时,分区层先打开并锁住所有的底层表,然后确定数据对应的分区,最后对相应底层表进行删除操作

update操作:

当更新一条数据时,分区层先打开并锁住所有的底层表,mysql先确定需要更新的记录在哪个分区,然后取出数据并更新,再判断更新后的数据应该放在哪个分区,然后对底层表进行写入操作,并对原数据所在的底层表进行删除操作

 

虽然每个操作都会打开并锁住所有的底层表,但这并不是说分区表在处理过程中是锁住全表的,如果存储引擎能够自己实现行级锁,如:innodb,则会在分区层释放对应的表锁,这个加锁和解锁过程与普通Innodb上的查询类似。

 

3.1.2.   优点/应用场景

分区的优点如下:

1、相对于单个文件系统或是硬盘,分区可以存储更多的数据;

2、数据管理比较方便,比如要清理或废弃某年的数据,就可以直接删除该日期的分区数据即可;

3、精准定位分区查询数据,不需要全表扫描查询,大大提高数据检索效率;

4、可跨多个分区磁盘查询,来提高查询的吞吐量;

5、在涉及聚合函数查询时,可以很容易进行数据的合并;

 

应用场景:

A:表非常大以至于无法全部都放在内存中,或者只在表的最后部分有热点数据,其他都是历史数据

B:分区表的数据更容易维护,如:想批量删除大量数据可以使用清除整个分区的方式。另外,还可以对一个独立分区进行优化、检查、修复等操作

C:分区表的数据可以分布在不同的物理设备上,从而高效地利用多个硬件设备

D:可以使用分区表来避免某些特殊的瓶颈,如:innodb的单个索引的互斥访问,ext3文件系统的inode锁竞争等

E:如果需要,还可以备份和恢复独立的分区,这在非常大的数据集的场景下效果非常好

F:优化查询,在where字句中包含分区列时,可以只使用必要的分区来提高查询效率,同时在涉及sum()和count()这类聚合函数的查询时,可以在每个分区上面并行处理,最终只需要汇总所有分区得到的结果。

 

3.1.3.   不足/限制

A:一个表最多只能有1024个分区(mysql5.6之后支持8192个分区)

B:在mysql5.1中分区表达式必须是整数,或者是返回整数的表达式,在5.5之后,某些场景可以直接使用字符串列和日期类型列来进行分区(使用varchar字符串类型列时,一般还是字符串的日期作为分区)。

C:如果分区字段中有主键或者唯一索引列,那么所有主键列和唯一索引列都必须包含进来,如果表中有主键或唯一索引,那么分区键必须是主键或唯一索引

D:分区表中无法使用外键约束

E:mysql数据库支持的分区类型为水平分区,并不支持垂直分区,因此,mysql数据库的分区中索引是局部分区索引,一个分区中既存放了数据又存放了索引,而全局分区是指的数据库放在各个分区中,但是所有的数据的索引放在另外一个对象中

F:目前mysql不支持空间类型和临时表类型进行分区。不支持全文索引

 

子分区的建立需要注意以下几个问题:

A:每个子分区的数量必须相同

B:只要在一个分区表的任何分区上使用subpartition来明确定义任何子分区,就必须在所有分区上定义子分区,不能漏掉一些分区不进行子分区。

C:每个subpartition子句必须包括子分区的一个名字

D:子分区的名字必须是唯一的,不能在一张表中出现重名的子分区

E:mysql数据库的分区总是把null当作比任何非null更小的值,这和数据库中处理null值的order by操作是一样的,升序排序时null总是在最前面,因此对于不同的分区类型,mysql数据库对于null的处理也各不相同。对于range分区,如果向分区列插入了null,则mysql数据库会将该值放入最左边的分区,注意,如果删除分区,分区下的所有内容都从磁盘中删掉了,null所在分区被删除,null值也就跟着被删除了。在list分区下要使用null,则必须显式地定义在分区的散列值中,否则插入null时会报错。hash和key分区对于null的处理方式和range,list分区不一样,任何分区函数都会将null返回为0.

 

3.1.4.   分区方法

有四种分区方法:

RANGE分区:基于属于一个给定连续区间的列值,把多行分配给分区

LIST分区:类似于按RANGE分区,区别在于LIST分区是基于列值匹配一个离散值集合中的某个值来进行选择

HASH分区:基于用户定义的表达式的返回值来进行选择的分区,该表达式使用将要插入到表中的这些行的列值进行计算。这个函数可以包含MySQL中有效的、产生非负整数值的任何表达式

KEY分区:类似于按HASH分区,区别在于KEY分区只支持计算一列或多列,且MySQL服务器提供其自身的哈希函数。必须有一列或多列包含整数值

column分区,基本上是以上几种分区方式的组合。

 

分区适合的场景有:

最适合的场景数据的时间序列性比较强,则可以按时间来分区。

如果数据有明显的热点,而且除了这部分数据,其他数据很少被访问到,那么可以将热点数据单独放在一个分区,让这个分区的数据能够有机会都缓存在内存中,查询时只访问一个很小的分区表,能够有效使用索引和缓存

 

3.2.    分区实战

查看版本

mysql> select version(); # 8.0.13

 

3.2.1.    range分区

range分区:给定一个连续区间的范围值进行分区,某个字段的值满足这个范围就会被分配到该分区。适用于字段的值是连续的区间的字段,如 日期范围, 连续的数字

-- 语法

create table <table> (

    // 字段

) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1

partition by range (分区字段) (

  partition <分区名称> values less than (Value),

  partition <分区名称> values less than (Value),

  ...

  partition <分区名称> values less than maxvalue

);

 

也可以在创建表后使用alter命令分区,示例如下:

alter table tb partition by RANGE(id) (

    PARTITION p0 VALUES LESS THAN (1000000),

    PARTITION p1 VALUES LESS THAN (1500000),

    PARTITION p2 VALUES LESS THAN (2000000),

    PARTITION p3 VALUES LESS THAN (2500000),

    PARTITION p4 VALUES LESS THAN MAXVALUE

);

 

查看数据文件,由一个ibd文件变为5个文件。

 

使用range分区时表结构要么没有主键,要么分区字段必须是主键。

 

取消分区:alter table tb remove partitioning;

 

alter table t drop primary key;

alter table t add primary key (id,age);

# 500万条数据

alter table t partition by RANGE(age) (

    PARTITION p0 VALUES LESS THAN (20),

    PARTITION p1 VALUES LESS THAN (40),

    PARTITION p2 VALUES LESS THAN (60),

    PARTITION p3 VALUES LESS THAN (80),

    PARTITION p4 VALUES LESS THAN MAXVALUE

);

 

 

select count(*) from tbl_user_no_part where age > 25 and age<40; # 未分区前9.67sec

select count(*) from t where age > 25 and age<40; # 分区后1.32sec

从查询结果看,当查询条件中包括分区字段时,分区确实能提高查询效率。

 

3.2.2.    list分区

设置若干个固定值进行分区,如果某个字段的值在这个设置的值列表中就会被分配到该分区。适用于字段的值区分度不高的,或者值是有限的,特别是像枚举这样特点的列。list分区使用in表示一些固定的值的列表

 

-- 语法

create table <table> (

    // 字段

) ENGINE=数据库引擎  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1

partition by LIST (分区字段或者基于该字段的返回的整数值的表达式) (

  partition <分区名称> values IN (Value1,Value2, Value3),

  ...

  partition <分区名称> values IN (Value4, Value5),

);

 

3.2.3.    hash分区

Hash分区主要用来分散热点读,确保数据在预先确定个数的分区中可能的平均分布。对一个表执行Hash分区时,mysql会对分区键应用一个散列函数,以此确定数据应当放在N个分区中的哪个分区。

 

mysql支持两种hash分区,

 

常规hash分区和线性hash分区(linear hash分区),常规hash分区使用的是取模算法,对应一个表达式expr是可以计算出它被保存到哪个分区中,N = MOD(expr, num)

线性hash分区使用的是一个线性的2的幂运算法则。

对指定的字段(整型字段)进行哈希,将记录平均的分配到分区中,使得所有分区的数据比较平均。 hash分区只需要指定要分区的字段和要分成几个分区,

expr是一个字段值或者基于某列值云散返回的一个整数,expr可以是mysql中有效的任何函数或者其它表达式,只要它们返回一个即非常熟也非随机数的整数。

num 表示分区数量

 

-- HASH

create table <table> (

    // 字段

) ENGINE=数据库引擎  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1

PARTITION BY HASH(expr)

PARTITIONS <num>;

 

常规hash分区方式看上去挺不错的,通过取模的方式来数据尽可能平均分布在每个分区,让每个分区管理的数据都减少,提高查询效率,可是当我们要增加分区时或者合并分区,问题就来了,假设原来是5个常规hash分区,现在需要增加一个常规分区,原来的取模算法是MOD(expr, 5), 根据余数0~4分布在5个分区中,现在新增一个分区后,取模算法变成MOD(expr, 6),根据余数0~6分区在6个分区中,原来5个分区的数据大部分都需要通过重新计算进行重新分区。

 

常规hash分区在管理上带来了的代价太大,不适合需要灵活变动分区的需求。为了降低分区管理上的代价,mysql提供了线性hash分区,分区函数是一个线性的2的幂的运算法则。同样线性hash分区的记录被存在那个分区也是能被计算出来的。线性hash分区的优点是在分区维护(增加、删除、合并、拆分分区)时,mysql能够处理的更加迅速,缺点是:对比常规hash分区,线性hash各个分区之间数据的分布不太均衡。

 

-- LINEAR HASH

create table <table> (

    // 字段

) ENGINE=数据库引擎  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1

PARTITION BY LINEAR HASH(expr)

PARTITIONS <num>;

 

3.2.4.    key分区

按照key进行分区非常类似于按照hash进行分区,只不过hash分区允许使用用户自定义的表达式,而key分区不允许使用用于自定义的表达式,需要使用mysql服务器提供的hash函数,同时hash分区只支持整数分区,而key分区支持使用出blob or text类型外的其他类型的列作为分区键。

 

和hash功能一样,不同的是分区的字段可以是非int类型,如字符串、日期等类型。

 

可以使用partition by key(expr)子句来创建一个key分区表,expr是零个或者多个字段名的列表。key分区也支持线性分区linear key

 

partition by key(expr) partitions num;

-- 不指定默认首选主键作为分区键,在没有主键的情况下会选择非空唯一键作为分区键

partition by key() partitions num;

-- linear key

partition by linear key(expr)

 

 

create table <table> (

    // 字段

) ENGINE=数据库引擎  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1

PARTITION BY HASH(分区字段名)

PARTITIONS <count>;

 

3.2.5.    columns分区

在mysql5.5之前range分区和list分区只支持整数分区,可以通过额外的函数运算或者额外的转换从而得到一个整数。columns分区分为 range columns 和 list columns 两种,支持整数(tinyint到bigint, 不支持decimal 和float)、日期(date、datetime)、字符串(char、varchar、binary、varbinary)三大数据类型。

 

3.2.6.    管理分区

-- 删除list或者range分区(同时删除分区对应的数据)

alter table <table> drop partition <分区名称>;

 

-- 新增分区

-- range添加新分区

alter table <table> add partition(partition p4 values less than MAXVALUE);

 

-- list添加新分区

alter table <table> add partition(partition p4 values in (25,26,28));

 

-- hash重新分区

alter table <table> add partition partitions 4;

 

-- key重新分区

alter table <table> add partition partitions 4;

 

-- 子分区添加新分区,虽然我没有指定子分区,但是系统会给子分区命名的

alter table <table> add partition(partition p3 values less than MAXVALUE);

 

-- range重新分区

ALTER TABLE user REORGANIZE PARTITION p0,p1,p2,p3,p4 INTO (PARTITION p0 VALUES LESS THAN MAXVALUE);

 

-- list重新分区

ALTER TABLE <table> REORGANIZE PARTITION p0,p1,p2,p3,p4 INTO (PARTITION p0 VALUES in (1,2,3,4,5));

 

3.3.    分表

分表分为垂直分表、水平分表。

3.3.1.   垂直分表

垂直拆分的优点是:

可以使得行数据变小,一个数据块(Block)就能存放更多的数据,在查询时就会减少I/O次数(每次查询时读取的Block 就少)

可以达到最大化利用Cache的目的,具体在垂直拆分的时候可以将不常变的字段放一起,将经常改变的放一起

数据维护简单

缺点是:

主键出现冗余,需要管理冗余列

会引起表连接JOIN操作(增加CPU开销)可以通过在业务服务器上进行join来减少数据库压力

依然存在单表数据量过大的问题(需要水平拆分)

事务处理复杂

 

3.3.2.   水平分表

水平拆分是通过某种策略将数据分片来存储,分库内分表和分库两部分,每片数据会分散到不同的MySQL表或库,达到分布式的效果,能够支持非常大的数据量。前面的表分区本质上也是一种特殊的库内分表

库内分表,仅仅是单纯的解决了单一表数据过大的问题,由于没有把表的数据分布到不同的机器上,因此对于减轻MySQL服务器的压力来说,并没有太大的作用,大家还是竞争同一个物理机上的IO、CPU、网络,这个就要通过分库来解决

 

水平拆分的优点是:

不存在单库大数据和高并发的性能瓶颈

应用端改造较少

提高了系统的稳定性和负载能力

缺点是:

分片事务一致性难以解决

跨节点Join性能差,逻辑复杂

数据多次扩展难度跟维护量极大

分片原则

能不分就不分,参考单表优化

分片数量尽量少,分片尽量均匀分布在多个数据结点上,因为一个查询SQL跨分片越多,则总体性能越差,虽然要好于所有数据在一个分片的结果,只在必要的时候进行扩容,增加分片数量

分片规则需要慎重选择做好提前规划,分片规则的选择,需要考虑数据的增长模式,数据的访问模式,分片关联性问题,以及分片扩容问题,最近的分片策略为范围分片,枚举分片,一致性Hash分片,这几种分片都有利于扩容

尽量不要在一个事务中的SQL跨越多个分片,分布式事务一直是个不好处理的问题

查询条件尽量优化,尽量避免Select * 的方式,大量数据结果集下,会消耗大量带宽和CPU资源,查询尽量避免返回大量结果集,并且尽量为频繁使用的查询语句建立索引。

通过数据冗余和表分区赖降低跨库Join的可能

 

4.      数据切分及整合方案

数据库的数据切分能够极大的提高系统的扩展性。可是,数据库中的数据在经过垂直和(或)水平切分被存放在不同的数据库主机之后,应用系统面临的最大问题就是怎样来让这些数据源得到较好的整合。

数据的整合非常难依靠数据库本身来达到这个效果,尽管MySQL存在Federated存储引擎,能够解决部分相似的问题。可是在实际应用场景中却非常难较好的运用。那我们该怎样来整合这些分散在各个MySQL主机上面的数据源呢?

总的来说,存在两种解决思路:

1. 在每一个应用程序模块中配置管理自己须要的一个(或者多个)数据源。直接訪问各个数据库,在模块内完成数据的整合;

2. 通过中间代理层来统一管理全部的数据源。后端数据库集群对前端应用程序透明;

 

可能90%以上的人在面对上面这两种解决思路的时候都会倾向于选择另外一种,尤其是系统不断变得庞大复杂的时候。

确实。这是一个非常正确的选择,尽管短期内须要付出的成本可能会相对更大一些,可是对整个系统的扩展性来说,是非常有帮助的。

所以,对于第一种解决思路我这里就不准备过多的分析,以下我重点分析一下在另外一种解决思路中的一些解决方式。

 

自行开发中间代理层

在决定选择通过数据库的中间代理层来解决数据源整合的架构方向之后,有不少公司(或者企业)选择了通过自行开发符合自身应用特定场景的代理层应用程序。

通过自行开发中间代理层能够最大程度的应对自身应用的特定。最大化的定制非常多个性化需求,在面对变化的时候也能够灵活的应对。这应该说是自行开发代理层最大的优势了。

当然,选择自行开发,享受让个性化定制最大化的乐趣的同一时候,自然也须要投入很多其它的成本来进行前期研发以及后期的持续升级改进工作。并且本身的技术门槛可能也比简单的Web应用要更高一些。所以,在决定选择自行开发之前,还是须要进行比較全面的评估为好。

由于自行开发很多其它时候考虑的是怎样更好的适应自身应用系统,应对自身的业务场景,所以这里也不好分析太多。后面我们主要分析一下当前比較流行的几种数据源整合解决方式。

 

------附录------

5.      插入数据代码

通过存储过程插入500万条记录

 

use db_store;

 

# create table if table not exists

CREATE TABLE if not exists `tbl_user_no_part` (

   `id` int(11) NOT NULL AUTO_INCREMENT,

   `username` varchar(255) DEFAULT NULL,

   `email` varchar(20) DEFAULT NULL,

   `age` tinyint(4) DEFAULT NULL,

   `type` int(11) DEFAULT NULL,

   `create_time` datetime DEFAULT CURRENT_TIMESTAMP,

   PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

 

 

drop function if exists rand_string;

drop procedure if exists insert_user;

 

delimiter $$

 

create function rand_string(n int) returns varchar(255) DETERMINISTIC

begin

 # 定义三个变量

 declare chars_str varchar(100) default 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';

 declare return_str varchar(255) default '';

 declare i int default 0;

 

 while i < n do

   set return_str = concat(return_str, substring(chars_str, floor(1+rand()*52), 1));

   set i = i + 1;

 end while;

 return return_str;

end $$

 

-- 创建插入的存储过程

create procedure insert_user(in start int(10), in max_num int(10))

begin

    declare i int default 0;

    set autocommit = 0; 

    repeat

        set i = i + 1;

        insert into tbl_user_no_part values ((start+i) ,rand_string(8), concat(rand_string(6), '@random.com'), 1+FLOOR(RAND()*100), 3, now());

        until i = max_num

    end repeat;

   commit;

end $$

 

 

delimiter ;

 

# 调用insert_user插入490万条数据

# call insert_user(100001,5000000);

select count(*) from tbl_user_no_part;

 

# 清理store procedure

drop function if exists rand_string;

drop procedure if exists insert_user;

 

5.1.    其它代码

select count(*) from tb where id < 150000;

select count(*) from tbl_user_no_part where id < 150000;

 

select count(*) from t where age > 25 and age<40;

 

alter table t drop primary key;

alter table t modify id int,drop primary key;

alter table t add primary key('id','age');

 

alter table t partition by RANGE(age) (

    PARTITION p0 VALUES LESS THAN (20),

    PARTITION p1 VALUES LESS THAN (40),

    PARTITION p2 VALUES LESS THAN (60),

    PARTITION p3 VALUES LESS THAN (80),

    PARTITION p4 VALUES LESS THAN MAXVALUE

);

 

select count(*) from tbl_user_no_part where age > 25 and age<40;

 

posted @ 2019-04-09 20:27  木林森__𣛧  阅读(422)  评论(0)    收藏  举报