MySQL优化
MySQL优化概述
为什么要优化
- 数据的处理,成为主要的性能瓶颈
- 随着时间的延长,数据量会变得越来越大
- 而且数据是存在磁盘上的,硬盘的读写速度跟内存是没法比较的
如何优化
- 设计数据库时:数据表、字段的设计,存储引擎的选择
- 利用好MySQL服务器提供的功能
- 横向扩展,由多台MySQL提供服务。负载均衡,读写分离
- SQL语句优化
字段设计
字段类型设计,设计规范、范式
设计原则:尽量用整型表示字符串
- 整型的优势:
- 存储空间固定,往往占用的空间比较小
- 运算速度快
- 举例:存储IP
例如:IP:38.71.123.333
- 常规做法:用varchar类型来存储,要占用13个字节
- 优化选择:int unsigned,用整型存储只需要占用4个字节
- MySQL和其它的应用语言都提供了,IP和整型的转换函数:
inet_aton():地址转整型数组
inet_ntoa():整型数字转IP
MySQL内部的枚举类型和集合类型
很少去用,因为维护成本很高。使用关联表的方式来代替。
金额的存储
对精度要求高,方案有两个:定点数(decimal)、小单位,大数额(int)。整数的运算是没有精度问题的,只有小数才存在精度问题,因为在计算机中不能将小数完全转换为二进制。
如37.85元——>3785分
- int和decimal的选取
decimal(8,2)有两位小数的定点数,能表示的数很大,超过了int,bigint所能表示的范围。
定长数据类型和变长数据类型的选取
- 例如:定点数和浮点数的选择
- 定点数(decimal):不会损失精度。占用的存储空间随着数字的增加而增加
- 浮点数(float、double):会损失精度。占用固定的存储空间,无论多大的数据,存储空间都是固定的
- 定长类型(运算效率相对较高):存储空间固定,如int,float,double,char,date
变长数据类型:存储空间可变,如varchar,decimal,text - 选取原则:
若更追求精度,则选择变长数据类型,若追求空间,则选择定长数据类型
字符串类型选择
- varchar:占用字段的总空间,字段的总空间是有限的。默认是65535字节。
text:text类型独立存储,不占用字段总空间。 - 选取:若要存储大量的text则选用text,否则选择varchar,varchar的效率更高
原则:尽可能选择小的数据类型
原则:尽可能的使用not null
- 非空字段要比null字段的处理效率要高些
- 不需要判断是否为null
- null在MySQL中,不好处理,需要占用额外的存储空间,运算也需要特殊的运算符!
- null参与常规运算,结果就是null
解决方案:
使用一个特殊的数据,进行占位
int not null default-1
string not null default ‘ ’
注意:但是会产生歧义,因为假如一个字段的本身值就是为‘ ’,那么就不知道这个数据表示的是null还是‘ ’。所以应该尽量使用不会出现在业务逻辑中的数据作为默认,表示null数据。
原则:字段注释完整
原则:字段名具有逻辑含义
原则:单张表的字段数量不易过多
如果过多就可能导致某个业务逻辑只用到其中少部分的字段
原则:可以预留字段
预留字段当前没有作用,为了在后期业务逻辑设计改变时,快速修改表结构
关联表的设计
一对一
一个记录的字段比较多,分布到多个表中进行存储
一对多,多对一
在多端,使用关联字段,关联一端的主键
多对多
一个多对多的关系由三张表实现
由两个一对多的关系来实现
范式
第一范式
字段原子性,不可(再)分割,关系型数据库默认满足第一范式
第二范式(第一范式基础上)
- 概念:消除对主键的部分依赖
- 主键:可以唯一标识记录的字段或字段的集合
- 依赖:A字段可以确定B字段,则B字段依赖A字段
- 对主键部分依赖:如果某个字段依赖复合主键的一部分字段,则称之为对主键的部分依赖
- 第二范式要去消除对主键的部分依赖
解决方案: 单独加入属性列id作为主键,即可消除
第三范式(第二范式基础上)
- 概念:消除对主键的传递依赖
- 传递依赖:
C依赖于B,B依赖于主键,则C对主键存在传递依赖关系 - 解决方案:
将独立数据单独建表,使用关联字段进行存储 - 总结:
怎样做到满足三范式:
独立数据独立建表
表中存在于业务逻辑无关的ID主键
表之间的关系由关联字段(关联表)进行表示
优势
典型优势:
- 减少数据冗余
- 易于维护更新
存储引擎选择storage engine
MySQL中的数据,索引,以及其他数据是如何存储的。是一套文件系统的实现。
Innodb和MyISAM的差异
数据和索引的存储 | 文件移动 | 插入顺序 | 是否产生空间碎片 | 事物 | 外键 | 锁定级别 | 并发处理能力 | |
---|---|---|---|---|---|---|---|---|
MyISAM | 分开存储(.myd和ymi) | 支持 | 表末尾插入 | 会 | 不支持 | 不支持 | 支持表级锁定 | 弱 |
Inoodb | 集中存储 .idb | 不支持 | 主键顺序插入(插入需进行排序) | 不会 | 支持 | 支持 | 表级锁定和行级锁定 | 强 |
选择结论
- 如果没有需求,请选择Innodb
- MyISAM:以读写和插入为主的应用程序,例如:新闻、博客、门户网站
- Innodb:更新、删除操作频繁,或者要求数据完整性比较强。并发性好,支持事物和外键保证事物完整性
MySQL中的锁
锁的作用
- 避免资源争用的机制功能
- 资源争用:多个任务同时使用同一个资源,对该资源产生争用。
- 数据库中,记录(数据)就是资源。不同的客户端对记录的CRUD操作就是任务。
- 思路
就是在某个任务使用该资源时,对其标识出来,其他任务就不能同时使用了,等待或者放弃了。
加锁的流程:
先尝试加锁,如果锁定成功就使用资源。否则等待或者放弃。
锁的类型
不同类型的锁,导致的并发操作是不一样的。
- 共享锁,读锁,S-lock:在需要读操作时增加锁的类型。会共享读操作,其他的任务也可以进行读操作,阻塞写操作。
- 排他锁,独占锁,写锁,X-lock:在需要执行写操作时会独占该资源,其他任何任务既不能读也不能写。
锁的粒度(范围),仅仅是数据库中的概念
- 在MySQL中,实现了不同粒度的锁。
- MySQL的Innodb实现了表级和行级锁定
- 一旦加锁,锁定的记录数量是不同的
- 表级锁定:table-level:操作会锁定整张表,无论是共享锁还是独占锁。
- 行及锁定:row-level:操作会锁定操作的记录。
MySQL中锁的相关语法
innodb和myisam都支持表锁
- 加锁:lock tables table-name1,table-name2 READ|WRITE;
- 解锁:unlock tables;
对于innodb支持行锁:
innodb的行锁,其实是一个子范围锁,依据条件锁定的部分范围。学名:间隙锁
- 对查询的记录增加共享锁:select * from table where LOCK IN share mode
- 对查询记录增加排他锁:select * form table where UPDATE
索引,最重要的优化手段
索引是什么
- 索引(index):关键字与索引位置之间的映射关系,称之为索引。
- 关键字:从数据中提取,用于标识,检索数据的特定内容。
- 使用索引的目的:加快检索
- 索引为什么能加快检索?
- 关键字相对于数据本身,数据量较小。
- 关键字都是排序的,遍历可以确定位置。
MySQL中的索引类型
不同的类型对关键字的限制不同,其他都相同
- 普通索引(index):对关键字没有限制
- 唯一索引(unique index):要求记录提供的关键字不能重复
- 主键索引(primary key)要求关键字不能重复,且不能为null
- 全文索引(fulltext index)
索引管理语法
- 查看索引:
- show create table table-name;
- desc table-name;也可以查看部分索引
- 创建索引:
基于不同的索引类型,会使用不同的语法。
-
在创建表时完成。
-- 创建了一个主键索引,通常都是自动增长的与业务逻辑无关的id id int auto_increment primary key, first_name varchar(16), last_name varchar(16), sn varchar(16), information text, -- 创建一个基于first_name、last_name上的复合索引,并命名为name key name (first_name,last_name), -- 创建了基于sn的唯一索引,没有命名,默认以字段名命名索引 unique key(sn), -- 创建了全文索引 fulltext key(information)
-
在修改表结构时完成
-- 假设有一个user表 alter table user(表名) -- 创建一个基于first_name、last_name上的复合索引,并命名为name add key name (first_name,last_name), -- 创建了基于sn的唯一索引,没有命名,默认以字段名命名索引 add unique key(sn), -- 创建了全文索引 add fulltext key(information)
- 删除索引
- alter table table-name(表名) drop primary key;删除主键索引
- alter table table-name(表名) drop key key-name;利用索引名可以删除普通、唯一、全文索引
注意:删除主键时,通常伴随着自增长,自增还依赖于主键,需要先去掉自增,再删除主键
执行计划explain(execution plain)
在执行的select前,使用关键字explain,可以获取该查询语句的执行计划。若要查询更完整的,就在语句的最后再加上/G
当执行SQL时,先分析优化,形成执行计划,按照计划执行
索引的使用场景(重点)
建立的索引,会在哪些情况下被使用,根据这些情况去建立索引
-
where查询
当一个字段没有索引时,我们要根据这个字段查询时,可以先对该字段建立索引然后再查询((alter table 表名 add index(字段名)))。这样会大大加快查询效率。若某个字段有多个索引,系统会选择一个最优的使用 -
order by排序
filesort,外部排序。是相对于内部排序(排序的数据可以被一次性的载入到内存中进行排序,就叫做内排序)来说的,性能较低。需要将数据读取到内存,但是不会一次性全读取,需要分段读取。合并排序结果,叫做外部排序。外部排序一个很大的瓶颈就是要先将数据读取到内存。
如
select username from user where ordey by username limit 5;
- 若没有对username字段添加索引(用了性能较低的外部排序),则排序操作是全表扫描,整张表排好序之后再取前五条。
- 若对username添加了索引,则排序操作只需要根据索引取数据的前5条记录即可,不需要全表扫描,也不用重新排序。
-
join连接
如
select c.* , count(s.id) from class c join student s on c.id=s.class_id group by c.id limit 5
- 在关联字段s.class_id上没有索引,执行查询会导致全表扫描,执行时间长
- 还会导致外部查询
- 增加了s.class_id索引之后,关联查询效率会大大提高
-
索引覆盖
是一种现象:指的是索引中提供的关键字覆盖了需要查询的字段数据,此时查询会由索引提供,而不是数据表提供。
-
假如给student表的first_name字段和last_name字段加了复合索引。查询语句如下:
select first_name,last_name form student limit 5
则查看执行计划,发现此种情况并没有在where查询、order by排序、join连接这三种情况中,但是还是用了索引查询,这就是索引覆盖的现象。
-
如假如给student表的first_name字段和last_name字段加了复合索引。但是查询语句如下:
select first_name,last_name,user form student limit 5
发现更改查询组合后,在执行计划中可以看到查询并没有用到索引,发生了全表扫描
分析:第一次查询的两个字段刚好是建立索引的两个字段,而第二次查询字段相对于建立的索引多了一个,所以第一次查询发生了索引覆盖,所以建议select查询时只写必要的字段,该覆盖的可能性会提升
-
语法应该注意的细节
字段需要独立出现
字段要独立出现在等式的一侧若没有独立出现则不能触发该字段上的索引
例如:在student表中,id为主键。则下列语句:
select * from student where id=20
或select * from student where 21-1=id
都使用了主键索引
但如下语句就不能触发主键索引:
select * from student where id+1=20
like查询不能以通配符开头
MySQL中的通配符:
%,任意字符的任意数量,正则表示:/.*/
_,任意的一个字符。以正则表示/./
-
如:在user表中的name字段已经建立了索引,则:
select * from user where name like '%亮'
此查询语句就不会使用索引查询,因为like查询%在开头 -
select * from user where name like '张%'
此like查询语句中,%没有在开头,所以次查询用到了索引
注意:
-
字符串比较时,不能使用包含的逻辑,标题中包含“PHP”的内容:
subject like '%PHP%'
,实际操作中一定不能使用。包含逻辑的解决方案:全文索引。第三方提供的全文索引完成。
-
实际开发中like可以使用,但是仅仅可以使用在匹配开头的情况。例如,搜索建议:PHP:
like ‘PHP%’
PHP培训
PHP框架
PHP开发
复合索引的非最左侧字段,不能使用独立索引
index name(first_name,last_name);
独立对first_name和last_name去执行索引,效果是不同的
使用左侧字段查询
- 如
select * from student where first_name=?
;采用了索引查询
使用右侧字段查询
select * from student where last_name=?
;没有采用索引查询
注意:复合索引,右边字段相当于没有检索索引
原因:复合索引是先根据左侧字段排序,如果左侧字段相同,再根据右侧字段排序。意味着总体上右侧字段是没有排序的。有点类似于order by查询(order by first_name,last_name)
复合索引的使用场景是?组合条件查询
常规的查询,是first_name 和 last_name通常一起出现。
where first_name like’xxx%’ and last_name like ‘xxx%’
此时就要比分别在first_name 和 last_name上建立索引要高效一些
分别建立索引,查询时就要使用两个索引,计算两个索引的交集才可以
or,保证两边都有索引可用
cond1 or cond2
同时保证cond1和cond2都有索引才可以。
状态值不容易使用到索引
gender 0,1,2,表示男,女,保密
值的数量较少
支付状态:未支付,已支付
配送状态:未配送,配送中,已收货
这样的状态值即使在字段上加了索引,往往也不能起作用。
测试相同时才查询比对状态值字段和 非状态值字段
语法原因:
会导致一个状态值同时匹配大量的记录,查询大量的记录,MySQL有时会认为用索引的开销要比全表扫描还要大,主动放弃索引。
举例:
一个大厅一楼有一个公司门牌号的表,假如要去找一个公司,直接看门牌号的表,然后去找对应的公司即可。但是如果总共有100个公司,要找50家公司,每次找了一个公司,然后再返回一楼寻找下一家公司的门牌号,这样还不如不看门牌号,直接把所有的公司都看一遍。
如何创建索引
-
建立基础索引,在where , order ,join 字段上建立索引
-
优化组合索引,基于业务逻辑
- 如果条件经常出现在一起,将多字段索引升级为复合索引。
- 如果通过增加个别字段,就可以出现索引覆盖,就增加个别字段。
- 查询时不会用到的索引应该删除。否则数据库维护索引会增加开销。
前缀索引
语法:alter table 表名 create index(field(10))
含义:使用field字段的前10个字符建立索引。默认情况使用字段的全部内容建立索引。
前缀的标识度足够的情况下,需要使用前缀索引
例如:密码字段就适合使用前缀索引
- 主要难度:怎样选取前缀的长度。需要分析多长的前缀标识度才足够:
- 将数据库中(密码的所有记录)/(不重复的密码记录):
select count(*) / count(distinct (password)) from student
,算出使用整个的密码字段所能达到的最大密码标识度。假设等于1.0038. - 然后再使用不同的前缀数来计算标识度:
select count(*) / count(distinct left(password,1)) from student
,逐个的改变前缀数,直到标识度能够达到1.0038为止。
存储的存储结构(了解)
Btree索引、hash索引、聚簇索引
以上概念,指的是,索引的存储结构是数据结构上的概念(与使用关系不大)
Btree树索引
-
索引存储在磁盘上所用的基础的通用存储结构,无论MySQL、MongoDB,或者其他的数据库,在磁盘上存储索引时,用的都是Btree树结构。
-
特点:
-
一个Btree节点存储多个索引关键字。多少是由节点的大小和关键字的大小来确定的,通常节点的大小是固定的,由计算机文件系统来确定,磁盘一次性读取内存容量(512kb)就是一个节点的大小。
-
大量的关键字分散到多个节点上进行存储。
-
通过上层节点的子节点指针,指向下层节点,来关联所有的节点。节点指针位于关键字之间。
-
指针指向的关键字的顺序,一定位于指针两侧的关键字之间
-
无论是普通,唯一,主键,全文索引都存储为Btree结构
-
辨析:
Btree是多路平衡查找树,BinaryTree是二叉树。
聚簇索引
关键字和记录在一起存储,称之为聚簇结构,聚簇索引。
常规的,索引是关键字和位置的映射关系
而聚簇索引不是关键字和位置的映射,而是关键字和记录就存储在一起
在数据结构上聚簇索引采用的是B+Tree结构。
在MySQL中仅仅是innodb的主键索为聚簇索引。其他的索引包括innodb的非主键索引都是典型的Btree结构。空间固定,往往占用的空间比较小
hash索引
当数据被载入到内存时采用hash结构存储。
查询缓存,query cache
是MySQL层面提供的数据缓存,用于缓存select查询结果。
在配置文件中开启缓存
找到mysql安装位置的my.ini文件(linux下是my.cnf)
在[mysql]段中,添加配置
query _cache_type=0;
(0表示关闭缓存;1表示开启,但是默认缓存,需要加sql-no-cache提示放弃缓存;2表示开启,但是默认不开启缓存,需要加sql-cache开启缓存)
一般情况下以2为主
配置完成后,需要重启mysql才能生效
重启之后在客户端,使用:show variables like 'query_cache_type';
查看结果
[外链图片转存失败(img-gJ43KuRp-1568707495139)(C:\Users\liu\AppData\Roaming\Typora\typora-user-images\1553734791386.png)]
在客户端设置缓存大小
配置项:query_cache_sieze来配置
先查看一下:
show variables like 'query_cache_size';
[外链图片转存失败(img-GoOO9Knd-1568707495140)(C:\Users\liu\AppData\Roaming\Typora\typora-user-images\1553735080433.png)]
可以改变缓存的大小:
set gloabal query _cache_size = ·····;
[外链图片转存失败(img-LdD8suZj-1568707495140)(C:\Users\liu\AppData\Roaming\Typora\typora-user-images\1553735650285.png)]
查询缓存结果
-
如果缓存开启参数设置为1,那么直接查询即可,若要不缓存,则加上sql-no-cache即可
-
如果缓存参数设置为2,那么需要在查询时加上sql-cache提示使用缓存
如:
select sql-cache * from student limit 5
;查询消耗时间0.03s第二次再次执行查询语句
select sql-cache * from student limit 5
;查询消耗的时间为0s;
重置缓存
reset query cache 完成清空缓存。
缓存失效(严重问题)
当数据表发生变动时,基于该数据表的任何缓存都会被清空。表层面的管理。
注意
- 应用程序不应该关心查询缓存的使用情况。可以尝试使用,不能由查询缓存决定业务逻辑。(缓存使用情况应该由数据库管理员决定)
- 动态数据不能被缓存(比如说系统时间)
- 缓存检索依赖于sql字符串的语法规则,大小写、空格等都会导致缓存不匹配。(也就是说只有两次的sql语句完全一致时才能匹配缓存)
分区(partition)
当数据有明显的业务逻辑差异时,应选择条件分配,若只想让数据分开存储,应选择hash或者key这种均匀存储。
概述
将一张表中的数据和索引分散到不同的文件中进行存储,称为分区操作。划分出来的文件就是不同的分区。
- innodb:数据和索引存储在.ibd后缀的文件中
- myisam:数据和索引存储在.myd和.myi后缀的文件中。
- 通常一张表对应一组数据和索引文件。这张表的数据和索引集中存储在这组文件中。
- 当一个表出现了大量的记录时,可以将其分布到不同的数据和索引文件中存储。
分布之后,每个文件中包含的记录数量显著减少,保证单独文件的执行效率。
演示:最常见的分区语法,使用id,将数据分布到10个分区中
id主键是最主要的检索要素。
在设计表时,使用partition选项,可以完成分区配置:
--利用id字段,使用hash算法,将数据分布到10个分区内
create table articles(
id int unsigned auto_increment primary key,
subject varchar(255),
content text
)
partition by hash(id) partitions 10;
hash算法
使用一个整数值,将记录分布到分区中,采用求余方案。
hash:一类算法的总称,求余,md5,sha1,都是hash算法。只要可以使用某个输入,得到某个特定的输出的算法就是hash算法,要求相同的输入应该得到相同的输出。
hash-table,哈希表。特殊的数据结构。key就是输入,value就是输出。
hash分区算法,在逻辑上表示均匀分配。
key算法
也是一个hash算法,更加通用的hash算法。在hash中,仅仅可以对整数进行求余运算。在key算法中,可以使用非整型字段,输入数据不一定为整数。核心与hash一致,但是要先通过分区字段计算一个整数值,再完成求余操作。
也可以使用字符串将文章分布到10个区域中。
分区字段一定是主键的一部分,分区字段一定是强检索字段。
range算法
范围条件算法,主要使用<(小于)来实现条件,分区时严格指定每一个分区条件,不仅仅是指定分区的数量。
是一种分区算法,指的是,将数据使用某种条件分散到不同的区中。
演示:利用文章的不同发布时间 ,将分区分不到不同的区域中:
create table articles_range(
id int unsigned auto_increment;
subject varchar(255),
content text,
pubtime int,
primary key(id,pubtime)
) charset=utf8 engine=innodb
partition by range(pubtime)(
partition p201710 values less than (1509465599),
partition p201711 values less than (1512057599),
partition p201712 values less than (1509473599)
);
仅仅支持<less than运算,因此,需要将更小的放在前面。
测试:
insert into articles_range values(
null,
150965499
);
分析:150965499小于第一个分区,所以改动的是第一个分区的文件。
list算法
条件范围算法
create table articles_list(
id int unsigned auto_increment;
subject varchar(255),
content text,
pubtime int,
status tinyint,-- enum('写作中','完成','发布')
primary key(id,pubtime)
) charset=utf8 engine=innodb
partition by list(status)(
partition pwriting values in(1,2),
partition ppublished values in (3)
);
insert into articles_list values(
null,
150965499,
3
);
此时,改动的是第二个分区的文件。
分区管理语法
list,range可以删除或者新增分区
hash,key可以修改分区数量
list,range增加分区
alter table articles_range add partition(
partition p201801 values less than (1517414400),
partition p201802 values less than (1519833600)
);
list/range删除分区
alter table articles_range drop partition p201710
删除名为p201710的分区
key/hash增加分区
alter table articles add partition partitions 4
,在原来的基础上增加4个分区
key/hash减少分区
alter table articles coalesce partition 6
,在原来的基础上减少6个分区
key和hash的分区管理,不影响数据。在管理时,需要将数据重新分配到新的分区中,效率较低。尽量在设计阶段,考虑分区的总数,满足设计。
分区的使用
-
在客户端程序不变的情况下,将服务器的数据分不到不同的物理文件中,进而做到提高数据表的处理能力。
-
当数据表中数据量很大时,分区可以提升效率。
-
只有检索字段为分区字段时,分区效率才会比较明显。因此分区字段的选择很重要。业务逻辑应该根据分区字段做出调整。
-
可以将分区文件部署到不同的磁盘上,充分利用磁盘的性能
分表
通常指的是,通过应用程序,将数据划分到不同的表中进行存储。而分区是在服务器完成的分区算法。
分表会导致客户端明显改变,在服务器端,出现结构相同的多张表。
分区和分表都称之为水平分割
分表的原因
数据库减压,
分区算法的局限。(重要原因)
数据库支持不完善,分区在5.1之后才支持的
垂直分割和水平分割
水平分割:每个表的结构相同,分别存储数据
垂直分割:每个表的记录数量一致,但是字段不一致。分表之间是一对一的关系。分割一依据:在业务逻辑中经常在一起使用的字段分布到一个表中。
横向扩展技术
由多台MySQL服务器提供数据存储服务器。
横向扩展是根本提升数据库性能的手段,受限于单台计算机的硬件处理能力,使用多台计算机完成对同一个服务的支持。
比较典型的概念:
读写分离,负载均衡。
需要用到的技术:MySQL的复制技术,负载均衡中间件。