一、Mysql逻辑架构介绍

相关课件:
04尚硅谷_MySQL高级_JAVA版.pdf 尚硅谷MySQL高级_思维导图.pdf
MYSQL高级.mmap mysql.docx
参考笔记:https://www.yuque.com/cessstudy/mysql/ikewnr

1. 安装前准备

Centos6:
rpm -qa | grep mysql Centos6在安装时,自带了mysql相关的组件。
卸载自带的mysql: rpm -e --nodeps mysql-libs
Centos7:
rpm -qa | grep mariadb Centos7默认装有mariadb
image.png
卸载mariadb:rpm -e --nodeps mariadb-libs
image.png

1.1 检查/tmp文件夹权限

默认路径是/root image.pngcd .. 回到上级目录。image.png
输入ll查看tmp文件夹权限
image.png
给/tmp文件夹最大权限 chmod -R 777 /tmp
image.png

2. Linux安装mysql

下载地址:https://dev.mysql.com/downloads/mysql/
Ubuntu安装:Mysql Ubuntu安装教程
CentOs7安装:
文档里面用的是rpm安装rpm安装教程非rpm安装教程
官网下载mysql-8.0.22-1.el7.x86_64.rpm-bundle.tarimage.png

在/usr/local/module下创建mysql目录,将下载的文件上传并解压
tar -xvf mysql-8.0.22-1.el7.x86_64.rpm-bundle.tar
image.png

开始安装mysql,执行以下命令

  1. rpm -ivh mysql-community-common-8.0.22-1.el7.x86_64.rpm --nodeps --force
  2. rpm -ivh mysql-community-libs-8.0.22-1.el7.x86_64.rpm --nodeps --force
  3. rpm -ivh mysql-community-client-8.0.22-1.el7.x86_64.rpm --nodeps --force
  4. rpm -ivh mysql-community-server-8.0.22-1.el7.x86_64.rpm --nodeps --force

查看mysql安装时创建的mysql用户和mysql组
cat /etc/passwd|grep mysql
image.png
输出形式:name:password:uid:gid:comment:home:shell
name:用户登录名;
Password:用户口令。此域中的口令是加密的。当用户登录系统时,系统对输入的口令采取相同的算法,与此域中的内容进行比较。如果此域为空,表明该用户登录时不需要口令。
Uid:指定用户的 UID。用户登录进系统后,系统通过该值,而不是用户名来识别用户。
gid:GID。如果系统要对相同的一群人赋予相同的权利,则使用该值。
comment:用来保存用户的真实姓名和个人细节。
home:指定用户的主目录的绝对路径。
shell:如果用户登录成功,则要执行的命令的绝对路径放在这一区域中。它可以是任何命令。
cat /etc/group|grep mysql
image.png

2.1 对Mysql数据库进行初始化等相关配置

依次执行以下命令
mysqld --initialize ## 第一安装才需要
chown mysql:mysql /var/lib/mysql -R
systemctl start mysqld.service ## 启动mysql
systemctl enable mysqld; ## 设置mysql开机自启动

通过**cat /var/log/mysqld.log | grep password** 命令查看数据库的密码
然后 mysql -uroot -p 回车键输入密码后进入数据库页面
通过ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '自己设置的密码';命令修改密码

2.2 查看mysql的路径

ps -ef | grep mysql 可以查看mysql的运行路径;
image.png
mysqld是mysql的守护进程。

which mysql:Which命令是通过 PATH环境变量查找可执行文件路径,用于查找指向这个命令所在的文件夹
image.png 这是mysql的运行路径。

find / -name mysql:查找所有包含mysqk关键字的文件路径
image.png
/var/lib/mysql:mysql数据库文件的存放路径;
/usr/lib/mysql :mysql的安装路径。

whereis mysqlWhereis命令和find类似,不过不同的是whereis是通过本地架构好的数据库索引查找会比较快。如果没有更新到数据库里面的文件或命令则无法查找到信息。
image.png

mysql5配置文件地址:

mysql5不同的版本配置文件的地址不一样,下图是基于mysql5.5
image.png

mysql8配置文件的地址:

查看mysql的配置my.cnf位置:ps aux | grep mysql | grep 'my.cnf' 如果没有输出内容则是使用的默认配置位置。
默认配置my.cnf位置:mysql --help | grep 'my.cnf'
image.png顺序排前的优先。
可以在/etc文件夹下找到my.cnf文件:
image.png

修改mysql字符集:

查看mysql字符集,mysql -u root -p 登陆mysql,然后执行
SHOW VARIABLES LIKE ‘character%’; 或 show variables like ‘%char%’;
image.png
mysql8 字符集直接就是utf-8了,所以不需要修改。

如果是mysql5,则需要修改/etc文件夹下的my.cnf文件:
image.pngimage.pngimage.png
修改完成后记得重启mysql,不然不生效。
注意:在修改字符集之前建立的库,在修改字符集之后还是使用原字符集,所以最好刚安装时就直接修改字符集。

2.3 远程访问的授权

1. 创建远程访问用户

通过Xshell等工具进行远程访问之前,需要创建远程访问用户。

  1. # 使用mysql数据库
  2. use mysql;
  3. # 查看mysql.`user`表中的用户以及访问范围
  4. select `user`, `host` from user;

image.png 一般默认情况下root默认只有localhost,也就是只有本机能访问。如果没有则创建一个不受ip限制的user(如果有root@%可以直接修改其加密规则,直接进行第四步操作修改已存在用户的加密规则):
create user '用户名'@'%' identified with 加密规则 by '密码';
注意:@后面可以跟如下几种:

  • %:表示该用户可以通过任意非本地IP地址登陆
  • localhost:表示该用户只能本机登陆
  • ip地址:表示该用户只能通过固定的ip地址登陆

登陆密码的加密规则有两种:mysql_native_password(mysql8之前默认)、caching_sha2_password(mysql8之后默认)

  1. # 我这里创建了一个名为root的可以通过任意ip登陆的账户,加密规则为mysql_native_password
  2. create user 'root'@'%' identified with mysql_native_password by '10086';
  3. # 设置 'root'@'%' 的密码永不过期
  4. ALTER USER 'root'@'%' IDENTIFIED BY '10086' PASSWORD EXPIRE NEVER;

2. 授予用户访问库和表的权限

grant 操作权限 on 数据库.表 to '用户名'@'登陆ip' with grant option;
权限选择:update、select、delete、drop、create、alter等用,分开,如果授予全部权限使用all;
数据库.表:指定数据库下的表可访问,如果需要指定多个名用,分开,如果授予全部数据库或者全部表则使用*
with grant option这个选项表示改用户可以把自己已有的权限授权给别人。

  1. # 这里为刚才创建的root@% 用户授予所有数据库的所有表的所有操作访问权限
  2. grant all privileges on *.* to 'root'@'%' with grant option;
  3. # 刷新权限
  4. flush privileges;

3. 防火墙开放端口/关闭防火墙

我的Centos防火墙之前因为使用zookeeper直接关闭了,所以就不用开启3306的端口号,如果防火墙是开启的,需要配置开放3306的端口号。
开放3306端口号

  1. firewall-cmd --list-all # 查看所有开放的端口号
  2. firewall-cmd --add-service=http --permanent # 开启http访问方式
  3. firewall-cmd --add-port=3306/tcp --permanent # 开启3306端口访问
  4. firewall-cmd --reload # 重启防火墙

注意:Centos7中,service被systemctl取代, systemctl start/stop/status mysqld

4. 备注(修改已存在用户的密码加密规则)

Mysql8之前默认加密规则是mysql_native_password,而在Mysql8之后,加密规则是caching_sha2_password。如果使用caching_sha2_password进行远程连接可能会报以下错误:
image.png
解决办法有两种:第一种是升级图形界面工具版本,第二种是把MySQL8用户登录密码加密规则还原成mysql_native_password
修改登陆密码加密规则为mysql_native_password如下:

  1. #使用mysql数据库
  2. USE mysql;
  3. #修改'root'@'localhost'用户的密码规则和密码
  4. ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '10086';
  5. # 设置密码永不过期
  6. ALTER USER 'root'@'localhost' IDENTIFIED BY '10086' PASSWORD EXPIRE NEVER;
  7. #刷新权限
  8. FLUSH PRIVILEGES;

3. mysql配置文件——主要配置

  • Windows下的mysql配置文件:my.ini
  • Linux下的mysql配置文件:/etc/my.cnf

    3.1 二进制日志文件log_bin

    用于主从复制。会记录主机上记录变化。
    image.png
    mysql8 log_bin默认开启
    image.png

    3.2 错误日志log-error

    记录严重的警告和错误信息,每次启动和关闭的详细信息等。Linux端mysql5默认关闭,mysql8默认开启。
    image.png
    windows端需要自己在my.ini中进行配置。

    3.3 查询日志

    默认关闭,记录查询的sql语句,如果开启会降低MySQL整体的性能,因为记录日志需要消耗系统资源。
    在mysql中输入 show variables like '%log%'; 可以查看各种日志的开启情况以及存放位置。
    mysql8中查询日志叫做general_log,默认关闭。
    image.png
    慢查询日志:
    image.png
    如果需要开启查询日志,需要在配置文件中设定其值为1(0表示关闭,1表示开启)

    3.4 数据文件

    俗称mysql数据库的数据库,就是记录各数据库的信息的数据文件。

    ①两系统

    windows:my.ini 配置文件设定的datadir目录下,存放了数据库中各种库。
    image.png
    linux:my.cnf 配置文件设定的datadir目录下
    image.png

    ② myisam和innodb引擎下对应的不同数据文件

    myisam:
  1. frm文件:用于存储表结构。
  2. myd(mydata)文件:用于存放表数据。
  3. myi(myindex)文件:用于存放表索引。

innodb:

  1. frm文件:用于存储表结构
  2. idb文件:用于存放表数据和索引的文件。该表的索引(B+树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据。

Innodb存储引擎可将所有数据存放于ibdata*的共享表空间,也可将每张表存放于独立的.ibd文件的独立表空间。
共享表空间以及独立表空间都是针对数据的存储方式而言的。

  • 共享表空间: 某一个数据库的所有的表数据,索引文件全部放在一个文件中,默认这个共享表空间的文件路径在data目录下。 默认的文件名为:ibdata1 初始化为10M
  • 独立表空间: 每一个表都将会生成以独立的文件方式来进行存储,每一个表都有一个.frm表描述文件,还有一个.ibd文件。 其中这个文件包括了单独一个表的数据内容以及索引内容,默认情况下它的存储位置也是在表的位置之中。在配置文件my.cnf中设置: innodb_file_per_table

注意:Innodb存储引擎下,mysql5中有frm文件用于存放表结构;mysql8中,Oracle将frm文件的信息及更多信息移动到叫做序列化字典信息(Serialized Dictionary Information,SDI),SDI被写在.ibd文件内部,它是数据字典包含的信息的一个冗余副本。为了从IBD文件中提取SDI信息,Oracle提供了一个应用程序 ibd2sdi。此应用程序解析SDI信息,并以JSON文件输出,该JSON文件便于操作提取和构建表定义。

3.5 mysql的逻辑架构介绍

image.png

  • Connectors:指的是不同语言中与SQL的交互。
  • Connection Pool:管理缓冲用户连接,线程处理等需要缓存的需求。MySQL数据库的连接层。
  • Management Serveices & Utilities:系统管理和控制工具。备份、安全、复制、集群等等。。
  • SQL Interface:接受用户的SQL命令,并且返回用户需要查询的结果。
  • Parser:SQL语句解析器。
  • Optimizer:查询优化器,SQL语句在查询之前会使用查询优化器对查询进行优化。就是优化客户端请求query,根据客户端请求的 query 语句,和数据库中的一些统计信息,在一系列算法的基础上进行分析,得出一个最优的策略,告诉后面的程序如何取得这个 query 语句的结果。For Exampleselect uid,name from user where gender = 1;这个select查询先从from开始找到表,根据where语句进行选取,而不是先将表全部查询出来以后再进行gender过滤;然后根据uidname进行属性投影,而不是将属性全部取出以后再进行过滤。最后将这两个查询条件联接起来生成最终查询结果。
  • Caches & Buffers:查询缓存。
  • Pluggable Storage Engines存储引擎接口。MySQL区别于其他数据库的最重要的特点就是其插件式的表存储引擎(注意:存储引擎是基于表的,而不是数据库)。
  • File System:数据持久化到磁盘上,就是文件的存储。

和其他数据库相比,MySQL有点与众不同,主要体现在存储引擎的架构上,插件式的存储引擎架构将查询处理和其他的系统任务以及数据的存储提取相分离。这种架构可以根据业务的需求和实际需求选择合适的存储引擎。简单来说就是分层可拔插。

MySQL逻辑架构分层

image.png

  • 连接层:最上层是一些客户端和连接服务,包含本地sock通信和大多数基于客户端/服务端工具实现的类似于tcp/ip的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程池的概念,为通过认证安全接入的客户端提供线程。同样在该层上可以实现基于SSL的安全链接。服务器也会为安全接入的每个客户端验证它所具有的操作权限。
  • 服务层:MySQL的核心服务功能层,该层是MySQL的核心,包括查询缓存,解析器,解析树,预处理器,查询优化器。主要进行查询解析、分析、查询缓存、内置函数、存储过程、触发器、视图等,select操作会先检查是否命中查询缓存,命中则直接返回缓存数据,否则解析查询并创建对应的解析树。
  • 引擎层:存储引擎层,存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API与存储引擎进行通信。不同的存储引擎具有的功能不同,这样我们可以根据自己的实际需要进行选取。
  • 存储层:数据存储层,主要是将数据存储在运行于裸设备的文件系统之上,并完成与存储引擎的交互。

    3.6 Mysql存储引擎

    MYISAM与INNODB

    通过show engines;查看支持的存储引擎。
    image.png
    通过show variables like 'default_storage_engine%';命令查看当前正在使用的存储引擎
    image.png
    引擎对比
对比项 MyISAM InnoDB
外键 不支持 支持
事务 不支持 支持
行表锁 表锁,即使操作一条记录也会锁住整张表,不适合高并发操作 行锁,操作时只锁某一行,不对其他行有影响,适合高并发操作
缓存 只缓存索引,不缓存真实数据 不仅缓存索引还要缓存真实数据,対内存要求较高,而且内存大小対性能有决定性影响
表空间
关注点 性能 事务
默认安装 Y Y

XtraDB

image.png
Percona为MySQL数据库服务器进行了改进,在功能和性能上较MySQL有着很显著的提升。该版本提升了在高负载情况下的 InnoDB的性能、为DBA提供一些非常有用的性能诊断工具;另外有更多的参数和命令来控制服务器行为。
该公司新建了一款存储引擎叫xtradb完全可以替代innodb,并且在性能和并发上做得更好。

二、索引优化分析

2.1 性能下降SQL慢的原因

  • 查询语句写得烂
  • 索引失效:索引建了没用上。(索引分为单值索引跟复合索引(联合索引)以及唯一索引)
    • 单值索引:只使用表中的某一个字段建索引,一个索引只包含一个列,一个表中可以有多个单列索引。
    • 复合索引:基于多个数据列建立的索引,即一个索引包含多个列
    • 唯一索引:索引列的值必须唯一,但可以为null;
  • 关联查询太多join(设计缺陷或者有不得已的需求。)会导致执行时间、等待时间长
  • 服务器调优以及各个参数设置(缓冲、线程数等。)

    2.2 常见的join查询

    1. SQL执行顺序

    手写顺序:
    1. SELECT DISTINCT <select_list>
    2. FROM <left_table>
    3. <join_type> JOIN <right_table>
    4. ON <join_condition>
    5. WHERE <where_condition>
    6. GROUP BY <group_by_list>
    7. HAVING <having_condition>
    8. ORDER BY <order_by_condition>
    9. LIMIT <limit number>
    机读顺序:
    1. FROM <left_table>
    2. ON <join_condition>
    3. <join_type> JOIN <right_table>
    4. WHERE <where_condition>
    5. GROUP BY <group_by_list>
    6. HAVING <having_condition>
    7. SELECT
    8. DISTINCT <select_list>
    9. ORDER BY <order_by_condition>
    10. LIMIT <limit number>
    直观顺序:
    1. SELECT # 7
    2. FROM # 1
    3. JOIN # 3
    4. ON # 2
    5. WHERE # 4
    6. GROUP BY # 5
    7. HAVING # 6
    8. DISTINCT # 8
    9. ORDER BY # 9
    10. LIMIT # 10

    2. 总结

    image.png

3 Join关系图——7种

image.png

4. 建表SQL

  1. CREATE TABLE `tbl_dept` (
  2. `id` INT(11) NOT NULL AUTO_INCREMENT,
  3. `deptName` VARCHAR(30) DEFAULT NULL,
  4. `locAdd` VARCHAR(40) DEFAULT NULL,
  5. PRIMARY KEY(`id`)
  6. ) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
  7. CREATE TABLE `tbl_emp` (
  8. `id` INT(11) NOT NULL AUTO_INCREMENT,
  9. `name` VARCHAR(20) DEFAULT NULL,
  10. `deptId` INT(11) DEFAULT NULL,
  11. PRIMARY KEY (`id`) ,
  12. KEY `fk_dept_id`(`deptId`) # key表示对deptId列建立索引
  13. # CONSTRAINT `fk_dept_id` FOREIGN KEY (`deptId`) REFERENCES `tbl_dept` (`id`)
  14. )ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET= utf8;
  15. insert into tbl_dept(deptName,locAdd) values('RD',11);
  16. INSERT INTO tbl_dept(deptName,locAdd) VALUES('HR',12);
  17. INSERT INTO tbl_dept(deptName,locAdd)VALUES('MK',13);
  18. INSERT INTO tbl_dept(deptName,locAdd) VALUES('MIS',14);
  19. INSERT INTO tbl_dept(deptName,locAdd)VALUES('FD',15);
  20. INSERT INTO tbl_emp(NAME,deptId) VALUES('z3',1);
  21. INSERT INTO tbl_emp(NAME,deptId)VALUES('z4',1);
  22. INSERT INTO tbl_emp(NAME,deptId)VALUES('z5',1);
  23. INSERT INTO tbl_emp(NAME,deptId) VALUES('w5',2);
  24. INSERT INTO tbl_emp(NAME,deptId)VALUES('w6',2);
  25. INSERT INTO tbl_emp(NAME,deptId) VALUES('s7',3);
  26. INSERT INTO tbl_emp(NAME,deptId) VALUES('s8',4);
  27. INSERT INTO tbl_emp(NAME,deptId) VALUES('s9',51);
  1. # 1 查询tbl_dept跟tbl_emp的共有部分 s9跟部门5都不在结果中
  2. select * from tbl_dept a inner join tbl_emp b on a.id = b.deptId;
  3. # 2 左外连接查询主表的所有记录,从表中没有的补null
  4. select * from tbl_dept a left join tbl_emp b on a.id = b.deptId;
  5. # 3 右外连接与左外连接类似
  6. select * from tbl_dept a right join tbl_emp b on a.id = b.deptId;
  7. # 4 类似于集合操作的A-B,集合A中去除与B的公共部分
  8. select * from tbl_dept a left join tbl_emp b on a.id = b.deptId where b.deptId is null;
  9. # 5 同理 B-A
  10. select * from tbl_dept a right join tbl_emp b on a.id = b.deptId where a.id is null;
  11. # 6 全外连接,mysqk不支持,可以使用union连接查询 连接2跟5 或者连接3跟4 或者2跟3 类似于集合A+B union=合并加去重
  12. select * from tbl_dept a full outer join tbl_emp b on a.id = b.deptId;
  13. # 2+5
  14. select * from tbl_dept a right join tbl_emp b on a.id = b.deptId where a.id is null union select * from tbl_dept a left join tbl_emp b on a.id = b.deptId;
  15. # 3+4
  16. select * from tbl_dept a right join tbl_emp b on a.id = b.deptId union select * from tbl_dept a left join tbl_emp b on a.id = b.deptId where b.deptId is null;
  17. # 7 与6类似 这里类似于集合 (A-B)+(B-A) 可以使用4+5的连接查询
  18. select * from tbl_dept a full outer join tbl_emp b on a.id = b.deptId where a.id is null or b.deptId is null;
  19. # 4+5
  20. select * from tbl_dept a left join tbl_emp b on a.id = b.deptId where b.deptId is null union select * from tbl_dept a right join tbl_emp b on a.id = b.deptId where a.id is null;

image.pngimage.png
image.png
image.png
image.png
image.png
image.png

2.3 索引简介

1. 索引是什么

MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构
可以得到索引的本质:索引是数据结构,可以简单的理解为索引是排好序的快速查找数据结构
索引会影响到Mysql的查找(where的条件查询)和排序(order by)两大功能

除了数据本身之外,数据库还维护着一个满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构的基础上实现高级查找算法,这种数据结构就是索引。 下图就是一种可能的索引方式示例:
image.png
为了加快Col2的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据记录物理地址的指针,这样就可以运用二叉查找在一定的复杂度内获取到相应数据,从而快速的检索出符合条件的记录。

一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储在磁盘上。
我们平时所说的索引,如果没有特别指明,都是指B树(多路搜索树,并不一定是二叉的)结构组织的索引。其中聚集索引,次要索引,覆盖索引,复合索引,前缀索引,唯一索引默认都是使用B+树索引,统称索引。当然,除了B+树这种数据结构的索引之外,还有哈希索引(Hash Index)等。
linux下df命令用来检查linux服务器的文件系统的磁盘空间占用情况。df -h以方便阅读的方式显示,df -i以inode模式来显示磁盘使用情况

  1. [root@mrlinxi ~]# df -i
  2. 文件系统 Inode 已用(I) 可用(I) 已用(I)% 挂载点
  3. devtmpfs 479648 373 479275 1% /dev
  4. tmpfs 482661 1 482660 1% /dev/shm
  5. tmpfs 482661 1266 481395 1% /run
  6. tmpfs 482661 16 482645 1% /sys/fs/cgroup
  7. /dev/mapper/centos-root 8910848 44101 8866747 1% /
  8. /dev/sda1 524288 327 523961 1% /boot
  9. tmpfs 482661 1 482660 1% /run/user/0
  10. [root@mrlinxi ~]# df -h
  11. 文件系统 容量 已用 可用 已用% 挂载点
  12. devtmpfs 1.9G 0 1.9G 0% /dev
  13. tmpfs 1.9G 0 1.9G 0% /dev/shm
  14. tmpfs 1.9G 20M 1.9G 2% /run
  15. tmpfs 1.9G 0 1.9G 0% /sys/fs/cgroup
  16. /dev/mapper/centos-root 17G 7.2G 9.9G 42% /
  17. /dev/sda1 1014M 151M 864M 15% /boot
  18. tmpfs 378M 0 378M 0% /run/user/0

2. 索引的优势与劣势

优势

  • 查找:类似大学图书馆的书目索引,提高数据检索的效率,降低数据库的IO成本
  • 排序:通过索引対数据进行排序,降低数据排序的成本,降低了CPU的消耗

劣势

  • 索引也是一张表,该表保存了主键和索引字段,并指向实体表的数据,所以索引也是需要占用磁盘空间的。
  • 虽然索引极大提高了查询速度,但同时却会降低更新表的速度,比如对表进行insert、update、delete操作。因为更新表时,MySQL不仅要保存数据,还要保存索引文件每次更新后添加的索引字段。会调整因为更新所带来的键值变化后的索引信息。
  • 索引只是提高效率的一个因素,如果MySQL有大数据量的表,就需要花时间研究建立最优秀的索引。

    3. 索引分类

  • 单值索引:即一个索引只包含单个列,一个表中可以有多个单列索引。

  • 唯一索引:索引列的值必须唯一,但允许有空值。
  • 复合索引:即一个索引包含多个列。

建议:一张表的索引最好不要超过5个。
基本语法:

  1. /* 1、创建索引 [UNIQUE]可以省略*/
  2. /* 如果只写一个字段就是单值索引,写多个字段就是复合索引 */
  3. CREATE [UNIQUE] INDEX indexName ON tabName(columnName(length),[columnName2(length)]);
  4. ALTER TABLE tabName ADD [UNIQUE] INDEX indexName ON (columnName(length));
  5. /* 2、删除索引 */
  6. DROP INDEX [indexName] ON tabName;
  7. /* 3、查看索引 */
  8. /* 加上\G就可以以列的形式查看了 不加\G就是以表的形式查看 */
  9. SHOW INDEX FROM tabName \G;

image.png
使用**ALTER**命令来为数据表添加索引

  1. /* 1、该语句添加一个主键,这意味着索引值必须是唯一的,并且不能为NULL */
  2. ALTER TABLE tabName ADD PRIMARY KEY(column_list);
  3. /* 2、该语句创建索引的键值必须是唯一的(除了NULL之外,NULL可能会出现多次) */
  4. ALTER TABLE tabName ADD UNIQUE indexName(column_list);
  5. /* 3、该语句创建普通索引,索引值可以出现多次 */
  6. ALTER TABLE tabName ADD INDEX indexName(column_list);
  7. /* 4、该语句指定了索引为FULLTEXT,用于全文检索 */
  8. ALTER TABLE tabName ADD FULLTEXT indexName(column_list);

4. mysql索引结构

索引结构可以分为四种:

  • BTree索引
  • Hash索引
  • full-text全文索引
  • R-Tree索引

B+Tree索引检索原理
image.png
初始化
一颗b+树,浅蓝色的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据项(深蓝色所示)和指针(黄色所示),如磁盘块1包含关键字3、25和60,包含指针P1、P2、P3,P1表示小于25的磁盘块,P2表示在25和60之间的磁盘块,P3表示大于60的磁盘块。真实的数据存在于叶子节点,即中间节点不存储真实数据
非叶子节点不存储真实的数据,只存关键字和指向下一个节点的索引。
查找过程
如果要查找数据项29,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO。在内存中用二分查找确定29在17和35之前,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘的IO)可以忽略不计,通过磁盘块1的P2指针的磁盘地址吧磁盘块3由磁盘加载到内存,发生第二次IO。29在26和30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存中,发生第三次IO,同时内存中做二分查找找到29,结束查询,总计三次IO。

真实的情况时,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将会是巨大的,如果没有索引,每个数据项都要发成一次IO,那么总共需要宝万次的IO,显然成本非常非常高。

5. 哪些情况需要/不需要创建索引

需要创建

  • 主键自动建立唯一索引(唯一+非空)
  • 频繁作为查询条件的字段应该创建索引
  • 查询中与其他表关联的字段,外键关系建立索引
  • 查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度,建的复合索引尽量与Order by 一致
  • 查询中统计或者分组字段(group by也和索引有关)

注:单值/组合索引的选择问题?(在高并发下倾向于创建组合索引)

不需要创建

  • 频繁更新的字段不适合创建索引(因为每次更新不止更新实体表数据,还会更新索引文件。)
  • where条件里用不到的字段不适合创建索引。
  • 表记录太少,经常增删改的表
  • 如果某个数据列包含众多重复的内容,为它建立索引,没有太大的实际效果。假如一个表有10万行记录,有一个字段A只有true和false两种值,并且每个值的分布概率大约为50%,那么对A字段建索引一般不会提高数据库的查询速度。索引的选择性是指索引列中不同值的数目与表中记录数的比。如果一个表中有2000条记录,表索引列有1980个不同的值,那么这个索引的选择性就是1980/2000=0.99。一个索引的选择性越接近于1,这个索引的效率就越高。

    2.4 性能分析(重点)

    1. Mysql Query Optimizer

  • MySQL中专门负责优化SELECT语句的优化器模块。主要功能:通过计算分析系统中收集到的统计信息,为客户端请求的Query提供它认为最优的执行计划(它认为最优的数据检索方式,但不见得是DBA认为最优的,这部分最耗费时间)

  • 当客户端像MySQL请求一条Query,命令解析器模块完成请求分类,去别处是SELECT并转发给MySQL Query Optimizer ,MySQL Query Optimizer首先会对整条Query进行优化,处理掉一些常量表达式的预算,直接换算成常量值。并对Query中的查询条件进行简化和转换,如去掉一些无用或显而易见的条件,结构调整等。然后分析Query中的Hint信息(如果有),看显示Hint信息是否可以完全确定该Query的执行计划。如果没有Hint或Hint信息还不足以完全确定执行计划,则会读区所涉及对象的统计信息,根据Query进行写相应的计算分析,然后再得出最后的执行计划。

    2. Mysql常见瓶颈

    CPU:CPU再饱和的时候一般发生再数据装入内存或从磁盘上读区数据的时候
    IO:磁盘IO瓶颈发生再装入数据远大于内存容量的时候
    服务器硬件的性能瓶颈:top,free,iostat和vmstat来查看系统的性能状态

    3. Explain

    ① EXPLAIN是什么?(查看执行计划)

    使用EXPLAIN关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理SQL语句的。 用来分析查询语句或表结构的性能瓶颈。

    ② EXPLAIN能干嘛

    可以查看SQL语句以下信息:

  • **id**:表的读取顺序

  • **select_type**:数据读取操作的操作类型
  • **possible_keys**:哪些索引可以使用
  • **key**:哪些索引被实际使用
  • **ref**:表之间的引用
  • **rows**:每张表有多少行被优化器查询

    ③ EXPLAIN怎么使用?

    语法: explain + SQL 语句
    注意: MYSQL 5.6.3以前只能EXPLAIN SELECT,5.6.3以后可以EXPLAIN SELECT, UPDATE, DELETE
    image.png

    4. EXPLAIN字段解释

    ① id

    **id**:序列号,包含一组数字,表示查询中执行select子句或操作表的顺序。 (表的读取和加载顺序)
    id的值有一下三种情况:

  • id相同,执行顺序由上到下,在前面的先执行

    • image.png
  • id不同,如果是子查询,id的序号会递增,id越大的优先级越高。
    • image.png
  • id相同和id不同同时存在, 先执行序号大的,相同序号的由上而下依次执行
    • image.png

② select_type

**select_type**:数据查询的类型,主要是用于区别,普通查询、联合查询、子查询等的复杂查询。

  • **SIMPLE**:简单的SELECT查询,查询中不包含子查询或者UNION
  • **PRIMARY**:查询中如果包含任何复杂的子部分,最外层查询则被标记为PRIMARY
  • **SUBQUERY**:在SELECT或者WHERE子句中包含了子查询。
  • **DERIVED**:在FROM子句中包含的子查询被标记为DERIVED(衍生),MySQL会递归执行这些子查询,把结果放在临时表中。
  • **UNION**:如果第二个SELECT出现在UNION之后,则被标记为UNION;若UNION包含在FROM子句的子查询中,外层SELECT将被标记为DERIVED
  • **UNION RESULT**:从UNION表获取结果的SELECT

③ type

**type**:访问类型排列
从最好到最差依次是:**system**>**const**>**eq_ref**>**ref**>**range**>**index**>**ALL**。除了ALL没有用到索引,其他级别都用到索引了。
一般来说,得保证查询至少达到**range**级别,最好达到**ref**

  • **system**:表只有一行记录(等于系统表),相当于const的特例,平时不会出现,可以忽略不计。
  • **const**:表示通过索引一次性找到结果,const用于比较primary key或unique索引。 因为只需匹配一行数据,所以查询速度极快。若将主键置于where条件中,mysql就能将该查询转换为一个常量。
  • **eq_ref**:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键或唯一索引扫描。
  • **ref**:非唯一性扫描,返回匹配的某个单独值的所有行。本质上也是一种索引扫描,它返回所有匹配某个单独值的行,然而它可能会找到多个符合条件的行,所以属于查找和扫描的混合体。
  • **range**:只检索给定某个范围的行,使用一个索引来选择行,key列显示了使用哪个索引,一般在where语句中出现between、>、<、in等查询。这种范围扫描比全表扫描要好,因为只需要开始于索引的某一点,结束于另一点即可,缩小了扫描范围。
  • **index**:Full Index Scan,index与ALL的区别为:index类型只遍历索引树,这通常比ALL类型快。 因为索引文件比数据文件小(也就是说index与ALL都是扫描全表,但index类型从索引中扫描, 而ALL类型从磁盘中扫描。)
  • **ALL**:遍历整张表去找匹配的行,效率极低。

④ possible_keys

**possible_keys**:显示理论上应用在这张表的索引,一个或多个。查询涉及到的字段上若存在索引,则列出。但不一定被实际查询所使用。

⑤ key

**key**:实际使用的索引,如果为null,则没有使用索引。
查询中若使用了覆盖索引(Covering Index),则该索引仅出现在key列表中(与Extra有关)。
覆盖索引:select 查询的字段和复合索引建立时的字段刚好吻合。(与顺序无关)

⑥ key_len

**key_len**:表示索引中使用的字节数(字段长度字符集字节数),可通过该列计算查询中使用的索引的长度。key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的。在不损失精度的情况下,长度越短越好。
如果是单列索引,那就把整个索引长度算进去,如果是多列索引,那么查询不一定都能使用到所有的列,用多少算多少。key_len只计算where条件用到的索引长度,而排序和分组就算用到了索引,也不会计算到key_len中。
key_len计算规则:https://blog.csdn.net/qq_34930488/article/details/102931490
— 索引列为字符串类型的情况
1) 列长度:
2) 列是否为空: NULL(+1),NOT NULL(+0)
3) 字符集: 如 utf8mb4=4,utf8=3,gbk=2,latin1=1
4) 列类型为字符: 如 varchar(+2), char(+0)
计算公式:key_len=(表字符集长度)
列字段长度 + 1(null) + 2(变长列)
— 数值数据的key_len计算公式:
TINYINT允许NULL = 1 + 1(NULL)
SMALLINT允许为NULL = 2 + 1(NULL)
INT允许为NULL = 4 + 1(NULL)
— 日期时间型的key_len计算:(针对mysql5.5及之前版本)
DATETIME允许为NULL = 8 + 1(NULL)
TIMESTAMP允许为NULL = 4 + 1(NULL)

⑦ ref

显示索引的哪一列被使用了,如果可能的话是一个常数。表示哪些列或常量被用于查找索引列上的值。
image.png

⑧ rows

根据表统计信息及索引选用情况,大致估算出找到所需的记录需要读取的行数。
image.png

⑨ Extra

包含不适合在其他列中显示但十分重要的额外信息。

  1. Using filesort:说明mysql会对数据使用外部的索引排序,而不是按照表内的索引顺序读取。mysql中无法利用索引完成的排序操作,称为”文件排序”

image.png
这里建的复合索引是 col1 col2 col3 在查询时第一次order by 只用到了col3,此时mysql无法根据当前索引直接完成排序操作,所以产生了filesort。第二次可以看到order by用到了col2与col3,此时mysql可以直接通过索引完成排序,因此性能更优。

  1. Using temporary:表示使用了临时表保存中间结果,mysql在对查询结果排序时,使用了临时表。常见于排序order by和分组查询group by。临时表对系统性能的损耗很高

image.png

  1. Using index:表示select操作中,使用了覆盖索引(Covering Index), 避免了访问表的数据行,效率很高。 如果同时出现Using where,表明索引被用来执行索引键值的查找;如果没有同时出现Using where, 则表示索引用来读取数据,而非执行索引查找动作。

官网的解释:https://dev.mysql.com/doc/refman/8.0/en/explain-output.html

覆盖索引:就是select的数据列只用从索引中就能够取得,不必从数据表中读取,换句话说查询列要被所使用的索引覆盖。如果select查询的列只要都同时属于同一个组合索引中的全部或部分列时,都是索引覆盖查询
注意:如果要使用覆盖索引,一定不能写SELECT ,要写出具体的字段。
覆盖索引博客,建议看这里
*回表查询
:先通过普通索引的值定位聚簇索引值,再通过聚簇索引的值定位行记录数据,需要扫描两次索引B+树,它的性能较扫一遍索引树更低。

  1. Using where:表明使用了where过滤。
  2. Using join buffer:使用了连接缓存。
  3. impossible where:表示where子句的值永远是false,不能用来获取任何元祖。
  4. select tables optimized away:在没有group by子句的情况下, 基于索引优化min/max操作或对于MyISAM存储引擎优化的count(*)操作, 不必等到执行阶段再进行计算,查询执行计划生成的阶段即可完成优化。
  5. distinct:优化distinct操作,在找到第一次匹配的元组后,立即停止找相同值的动作。

    5. 例子说明

    image.png
    看explain的结果:
    第一行(执行顺序4):id列为1,表示是union里的第一个select,select_type列的primary表示该查询为外层查询,table列被标记为,表示查询结果来自一个衍生表,其中derived3中3表示查询衍生自第三个select查询,即id为3的select。【select d1.name …】
    第二行(执行顺序2):id为3,是整个查询中第三个select的一部分。因查询包含在from中,所以为derived。【select id,name from t1 where other_column=’’】
    第三行(执行顺序3):id为2,select列表中的子查询select_type为subquery,为整个查询中的第二个select。【select id from t3】
    第四行(执行顺序1):id为4,select_type为union,结合第五行来看,说明第四个select是union里的第二个select,最先执行。【select name,id from t2】
    第五行(执行顺序5):代表从 union的临时表中读取行的阶段,table列的表示用第一个和第四个select结果进行union操作。

    2.5 索引优化

    2.5.1 索引分析

    1. 单表

    建表 ``sql CREATE TABLE IF NOT EXISTSarticle(idINT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,author_idINT (10) UNSIGNED NOT NULL,category_idINT(10) UNSIGNED NOT NULL ,viewsINT(10) UNSIGNED NOT NULL ,commentsINT(10) UNSIGNED NOT NULL,titleVARBINARY(255) NOT NULL,content` TEXT NOT NULL );

INSERT INTO article(author_id,category_id,views,comments,title,content) VALUES (1,1,1,1,’1’,’1’), (2,2,2,2,’2’,’2’), (1,1,3,3,’3’,’3’);

  1. 案例:查询`category_id`1`comments`大于1的情况下,`views`最多的`article_id`。<br />**编写SQL语句并查看SQL执行计划**:
  2. ```sql
  3. # 1、sql语句
  4. select id, author_id from article where category_id = 1 and comments > 1 order by views desc limit 1;
  5. +----+-----------+
  6. | id | author_id |
  7. +----+-----------+
  8. | 3 | 1 |
  9. +----+-----------+
  10. # 2、sql执行计划
  11. explain select id, author_id from article where category_id = 1 and comments > 1 order by views desc limit 1 \G
  12. *************************** 1. row ***************************
  13. id: 1
  14. select_type: SIMPLE
  15. table: article
  16. partitions: NULL
  17. type: ALL
  18. possible_keys: NULL
  19. key: NULL
  20. key_len: NULL
  21. ref: NULL
  22. rows: 3
  23. filtered: 33.33
  24. Extra: Using where; Using filesort
  25. 1 row in set, 1 warning (0.00 sec)

可以看到type是ALL,最坏的情况(全表扫描,虽然只有三条),EXTRA出现了Using filesort,说明性能有待优化。

优化:创建索引,根据where和group后面的字段建立索引,新建一个由category_id, comments, views组成的复合索引

  1. create index idx_article_ccv on article(category_id, comments, views);

查看增加索引之后的执行计划:
image.png
我们发现,创建符合索引idx_article_ccv之后,虽然解决了全表扫描的问题,但是在order by排序的时候没有用到索引,MySQL居然还是用的Using filesort,为什么?
这是因为按照B+Tree索引的工作原理,先排序category_id,如果遇到相同的 category_id则再排序comments,如果遇到相同的comments再排序views。当comments字段在联合索引中处于中间位置时,因comments>1条件是一个范围(所谓range),MYSQL无法利用索引在对后面的views部分进行检索,即range类型查询字段后面的索引无效。

我们试试修改SQL,看看SQL的查询计划:

  1. EXPLAIN SELECT id,author_id FROM article
  2. WHERE category_id = 1 AND comments = 1 ORDER BY views DESC LIMIT 1;

image.png

推论:当comments > 1的时候order by排序views字段索引就用不上,但是当comments = 1的时候order by排序views字段索引就可以用上!!!所以,范围之后的索引会失效。

我们现在知道范围之后的索引会失效,原来的索引 idx_article_ccv 最后一个字段views会失效,那么我们如果删除这个索引,创建 idx_article_cv (category_id和views)索引呢????

  1. drop index idx_article_ccv on article; # 删除索引
  2. create index index_article_cv on article(category_id, views); #新建索引
  3. show index from article; # 查看article表的索引
  4. explain select id, author_id from article where category_id = 1 and comments > 1 order by views desc limit 1 \G

image.png
可以看到此时type变为了ref检索+排序同时用到了我们的索引,同时EXTRA中也没有Using file sort,结果十分理想。

2. 两表

建表、插数据:

  1. DROP TABLE IF EXISTS `class`;
  2. DROP TABLE IF EXISTS `book`;
  3. CREATE TABLE IF NOT EXISTS `class`(
  4. `id` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
  5. `card` INT(10) UNSIGNED NOT NULL COMMENT '分类'
  6. ) COMMENT '商品类别';
  7. CREATE TABLE IF NOT EXISTS `book`(
  8. `bookid` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
  9. `card` INT(10) UNSIGNED NOT NULL COMMENT '分类'
  10. ) COMMENT '书籍';
  11. insert into class(card) values(floor(1+(rand()*20)));
  12. insert into class(card) values(floor(1+(rand()*20)));
  13. insert into class(card) values(floor(1+(rand()*20)));
  14. insert into class(card) values(floor(1+(rand()*20)));
  15. insert into class(card) values(floor(1+(rand()*20)));
  16. insert into class(card) values(floor(1+(rand()*20)));
  17. insert into class(card) values(floor(1+(rand()*20)));
  18. insert into class(card) values(floor(1+(rand()*20)));
  19. insert into class(card) values(floor(1+(rand()*20)));
  20. insert into class(card) values(floor(1+(rand()*20)));
  21. insert into class(card) values(floor(1+(rand()*20)));
  22. insert into class(card) values(floor(1+(rand()*20)));
  23. insert into class(card) values(floor(1+(rand()*20)));
  24. insert into class(card) values(floor(1+(rand()*20)));
  25. insert into class(card) values(floor(1+(rand()*20)));
  26. insert into class(card) values(floor(1+(rand()*20)));
  27. insert into class(card) values(floor(1+(rand()*20)));
  28. insert into class(card) values(floor(1+(rand()*20)));
  29. insert into class(card) values(floor(1+(rand()*20)));
  30. insert into class(card) values(floor(1+(rand()*20)));
  31. insert into book(card) values(floor(1+(rand()*20)));
  32. insert into book(card) values(floor(1+(rand()*20)));
  33. insert into book(card) values(floor(1+(rand()*20)));
  34. insert into book(card) values(floor(1+(rand()*20)));
  35. insert into book(card) values(floor(1+(rand()*20)));
  36. insert into book(card) values(floor(1+(rand()*20)));
  37. insert into book(card) values(floor(1+(rand()*20)));
  38. insert into book(card) values(floor(1+(rand()*20)));
  39. insert into book(card) values(floor(1+(rand()*20)));
  40. insert into book(card) values(floor(1+(rand()*20)));
  41. insert into book(card) values(floor(1+(rand()*20)));
  42. insert into book(card) values(floor(1+(rand()*20)));
  43. insert into book(card) values(floor(1+(rand()*20)));
  44. insert into book(card) values(floor(1+(rand()*20)));
  45. insert into book(card) values(floor(1+(rand()*20)));
  46. insert into book(card) values(floor(1+(rand()*20)));
  47. insert into book(card) values(floor(1+(rand()*20)));
  48. insert into book(card) values(floor(1+(rand()*20)));
  49. insert into book(card) values(floor(1+(rand()*20)));
  50. insert into book(card) values(floor(1+(rand()*20)));

不创建索引的情况下,SQL的执行计划:
image.png
book和class两张表都是没有使用索引,全表扫描,那么如果进行优化,索引是创建在book表还是创建在class表呢?下面进行大胆的尝试!
给从表book创建索引:alter table book add index Y(card);
再次查看SQL执行计划:
image.png
可以看到第二行的type变为了ref,rows也变成了1,优化明显。
这是由左连接的特性决定的,left join条件用于确定如何从从表(右表)搜索行,左边一定都有,所以右边是我们的关键点,一定需要建立索引。

现在删除从表(book)的索引,给主表class新建索引,然后在查询执行计划:

  1. drop index Y on book;
  2. alter table class add index X(card);

image.png
此时第一行的type是index,第二行是All,在主表上加索引的方式不如在从表上建索引的方式好。
以上是左连接的情况,右连接的情况类似;由此可见,左连接/右连接将索引创建在从表上更加合适,即左/右连接建在右/左表上

3. 三表

建表,插数据:

  1. CREATE TABLE IF NOT EXISTS `phone`(
  2. `phoneid` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
  3. `card` INT (10) UNSIGNED NOT NULL
  4. )ENGINE = INNODB;
  5. insert into phone(card) values(floor(1+(rand()*20)));
  6. insert into phone(card) values(floor(1+(rand()*20)));
  7. insert into phone(card) values(floor(1+(rand()*20)));
  8. insert into phone(card) values(floor(1+(rand()*20)));
  9. insert into phone(card) values(floor(1+(rand()*20)));
  10. insert into phone(card) values(floor(1+(rand()*20)));
  11. insert into phone(card) values(floor(1+(rand()*20)));
  12. insert into phone(card) values(floor(1+(rand()*20)));
  13. insert into phone(card) values(floor(1+(rand()*20)));
  14. insert into phone(card) values(floor(1+(rand()*20)));
  15. insert into phone(card) values(floor(1+(rand()*20)));
  16. insert into phone(card) values(floor(1+(rand()*20)));
  17. insert into phone(card) values(floor(1+(rand()*20)));
  18. insert into phone(card) values(floor(1+(rand()*20)));
  19. insert into phone(card) values(floor(1+(rand()*20)));
  20. insert into phone(card) values(floor(1+(rand()*20)));
  21. insert into phone(card) values(floor(1+(rand()*20)));
  22. insert into phone(card) values(floor(1+(rand()*20)));
  23. insert into phone(card) values(floor(1+(rand()*20)));
  24. insert into phone(card) values(floor(1+(rand()*20)));

三张表上都没有索引

  1. EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card=book.card LEFT JOIN phone ON book.card = phone.card;

image.png
可以看到,全部都是ALL,全表扫描,非常不好。
给phone表和book表上建索引:

  1. alter table phone add index z (card);
  2. alter table book add index Y (card);

image.png
后两行的type都是ref且总rows优化很好,效果不错。因此索引最好设置在需要经常查询的字段中。

4. 结论

Join语句的优化:

  • 尽可能减少Join语句中的NestedLoop(循环嵌套)的循环总次数:”永远用小结果集驱动大结果集”。
  • 优先优化NestedLoop的内存循环;
  • 保证Join语句中被驱动表上Join条件字段已经被索引;
  • 当无法保证被驱动表的Join条件字段被索引且内存资源充足的前提下,不要太吝惜JoinBuffer的设置。

2.5.2 索引失效(应避免)

建表插数据、建索引:

  1. create table staffs(
  2. id int auto_increment,
  3. name varchar(24) not null default '' comment '姓名',
  4. age int not null default 0 comment '年龄',
  5. pos varchar(20) not null default '' comment '职位',
  6. add_time timestamp not null default current_timestamp comment '入职时间',
  7. primary key(id)
  8. );charset utf8 comment '员工记录表';
  9. insert into staffs(name,age,pos,add_time) values('z3',22,'manager',now());
  10. insert into staffs(name,age,pos,add_time) values('July',23,'dev',now());
  11. insert into staffs(name,age,pos,add_time) values('2000',23,'dev',now());
  12. select * from staffs;
  13. create index idx_staffs_nameAgePos on staffs(name,age,pos);

1、索引失效的情况

  1. 全值匹配我最爱。
  2. 最佳左前缀法则(带头大哥不能死,中间兄弟不能断)。案例跳转
  3. 不要在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描。 案例跳转
  4. 索引中范围条件右边的字段会全部失效。 案例跳转
  5. 尽量使用覆盖索引(只访问索引的查询,索引列和查询列一致),减少SELECT *案例跳转
  6. MySQL在使用!=或者<>的时候无法使用索引会导致全表扫描(mysql5会,mysql8不会)。 案例跳转
  7. is nullis not null也无法使用索引。 案例跳转
  8. like以通配符开头%abc索引失效会变成全表扫描(使用覆盖索引就不会全表扫描了)(百分like加右边)。 案例跳转
  9. 字符串不加单引号索引失效。 案例跳转
  10. 少用or,用它来连接时会索引失效。 案例跳转

全值匹配&最左前缀法则
  1. # 用到了idx_staffs_name_age_pos索引中的name字段
  2. EXPLAIN SELECT * FROM `staffs` WHERE `name` = 'july';
  3. # 用到了idx_staffs_name_age_pos索引中的name, age字段
  4. EXPLAIN SELECT * FROM `staffs` WHERE `name` = 'july' AND `age` = 25;
  5. # 用到了idx_staffs_name_age_pos索引中的name,age,pos字段 这是属于全值匹配情况
  6. EXPLAIN SELECT * FROM `staffs` WHERE `name` = 'july' AND `age` = 25 AND `pos` = 'dev';
  7. # 用到了idx_staffs_name_age_pos索引中的name,age,pos字段 这是属于全值匹配情况
  8. # 因为Mysql中有一个优化器,会将where 优化成`name` = 'july' AND `age` = 25 AND `pos` = 'dev'
  9. EXPLAIN SELECT * FROM `staffs` WHERE `age` = 25 AND `pos` = 'dev' AND `name` = 'july' AND;
  10. # 索引失效 type为ALL 全表扫描
  11. EXPLAIN SELECT * FROM `staffs` WHERE `age` = 25 AND `pos` = 'dev';
  12. # 索引失效 type为ALL 全表扫描
  13. EXPLAIN SELECT * FROM `staffs` WHERE `pos` = 'dev';
  14. # 用到了idx_staffs_name_age_pos索引中的name字段,pos字段索引失效
  15. EXPLAIN SELECT * FROM `staffs` WHERE `name` = 'july' AND `pos` = 'dev';

image.png
以上几句SQL都用到了索引。然后我们去掉name和age的筛选:
image.png
image.png
发现两种情况下,type都由ref变成了ALL,索引失效了。这是因为违背了最佳左前缀法则。
如果索引了多个列,要遵循左前缀法则。最左前缀法则是指查询需要从索引的最左前列开始,并且按照索引列中的顺序去查询并且不跳过索引中间的列,若跳过则索引失效。
image.png
这里虽然用到了索引,但是对比name跟age的情况,发现ref列下面少了一个const,也就是说只用到了索引中的name字段,pos字段索引失效。
结论:过滤条件要使用索引必须按照索引建立时的顺序,依次满足,一旦跳过某个字段,索引后面的字段都无法被使用。
总结口诀:带头大哥不能死,中间兄弟不能断。

不要在索引列上做任何操作
  1. # 用到了idx_staffs_name_age_pos索引中的name字段
  2. EXPLAIN SELECT * FROM `staffs` WHERE `name` = 'july';
  3. # 索引失效 LEFT(NAME, 4) 类属于java的substring,截取前4个字符,这里在索引列上做了操作,导致索引失效
  4. EXPLAIN SELECT * FROM `staffs` WHERE LEFT(`name`,4) = 'july';

image.png

索引中范围条件右边的字段会全部失效
  1. # 用到了idx_staffs_name_age_pos索引中的name,age,pos字段 这是属于全值匹配情况
  2. EXPLAIN SELECT * FROM `staffs` WHERE `name` = 'july' AND `age` = 25 AND `pos` = 'dev';
  3. # 索引失效
  4. EXPLAIN SELECT * FROM `staffs` WHERE `name` = 'july' AND `age` > 25 AND `pos` = 'dev';

image.png
这里只用到了name跟age的索引,pos的失效了,从key_len可以看出来。
建议将可能做范围查询的字段的索引顺序放在最后

尽量使用覆盖索引

尽量少用select *
image.png

使用!=或者<>的时候,可能索引失效导致全表扫描

!=和<>(这两个都表示不等于,给忘记了)
mysql5,使用!=会导致索引失效
image.png
mysql8会走索引,type为range
image.png

字段的is not null 和 is null

在字段属性不允许为null的条件下:is not null用不到索引,会导致全表扫描
image.png
在字段属性允许为null的条件下:
mysql5:is not null用不到索引,is null可以用到索引;
image.png
mysql8:都可以用到索引
image.png

like的前后模糊匹配

image.png
前缀不能出现模糊匹配,否则索引失效。
问题:如何解决【like’%字符串%’】时,索引不被使用的问题??
使用覆盖索引即可。

  1. CREATE TABLE tbl_user(
  2. `id` INT(11) NOT NULL AUTO_INCREMENT,
  3. `name` VARCHAR(20) DEFAULT NULL,
  4. `age`INT(11) DEFAULT NULL,
  5. `email` VARCHAR(20) DEFAULT NULL,
  6. PRIMARY KEY(`id`)
  7. )ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
  8. insert into tbl_user(NAME,age,email) values('1aa1',21,'b@163.com');
  9. insert into tbl_user(NAME,age,email) values('2aa2',222,'a@163.com');
  10. insert into tbl_user(NAME,age,email) values('3aa3',265,'c@163.com');
  11. insert into tbl_user(NAME,age,email) values('4aa4',21,'d@163.com');
  1. # 没建索引,以下语句都不会用到索引
  2. explain select id from tbl_user where name like '%aa%';
  3. explain select name from tbl_user where name like '%aa%';
  4. explain select age from tbl_user where name like '%aa%';
  5. explain select id, name from tbl_user where name like '%aa%';
  6. explain select id, name, age from tbl_user where name like '%aa%';
  7. explain select name, age from tbl_user where name like '%aa%';
  8. explain select * from tbl_user where name like '%aa%';
  9. explain select id, name, age, email from tbl_user where name like '%aa%';
  10. # 新建一个name,age的复合索引
  11. create index idx_user_nameAge on tbl_user(name, age);
  12. # alter table tbl_user add index idx_user_nameAge(name, age);
  13. # 再查看之前的语句
  14. # 覆盖索引
  15. explain select id from tbl_user where name like '%aa%';
  16. # 覆盖索引
  17. explain select name from tbl_user where name like '%aa%';
  18. # 覆盖索引
  19. explain select age from tbl_user where name like '%aa%';
  20. # 覆盖索引
  21. explain select id, name from tbl_user where name like '%aa%';
  22. # 覆盖索引
  23. explain select id, name, age from tbl_user where name like '%aa%';
  24. # 覆盖索引
  25. explain select name, age from tbl_user where name like '%aa%';
  26. # 没有产生覆盖索引,索引失效
  27. explain select * from tbl_user where name like '%aa%';
  28. # 没有产生覆盖索引
  29. explain select id, name, age, email from tbl_user where name like '%aa%';

字符串不加单引号会导致索引失效

这里就是在索引上做了自动转换的操作,不要在索引列上做任何操作(计算、函数、(自动or手动)类型转换)。
image.png
这里索引上的name字段进行了自动类型转换,索引失效。

少用or

mysql5 type是all
image.png
mysql8 使用or不会导致索引失效,type是range
image.png

2、总结口诀

全职匹配我最爱,最左前缀要遵守;
带头大哥不能死,中间兄弟不能断;
索引列上少计算,范围之后全失效;
LIKE 百分写最右,覆盖索引不写*;
不等空值还有 OR,索引影响要注意;
VAR 引号不可丢,SQL 优化有诀窍。

假设index(a, b, c),判断下列语句是否使用到索引

Where语句 索引是否被使用
where a = 3 Y,使用到a
where a = 3 and b = 5 Y,使用到a,b
where a = 3 and b = 5 Y,使用到a,b,c
where b = 3 或者 where b = 3 and c = 4 或者 where c = 4 N,没有用到a字段
where a = 3 and c = 5 使用到a,但是没有用到c,因为b断了
where a = 3 and b > 4 and c = 5 使用到a,b,但是没有用到c,因为c在范围之后
where a = 3 and b like ‘kk%’ and c = 4 Y,a,b,c都用到,type=range
where a = 3 and b like ‘%kk’ and c = 4 只用到a,type=ref
where a = 3 and b like ‘%kk%’ and c = 4 只用到a,type=ref
where a = 3 and b like ‘k%kk%’ and c = 4 Y,a,b,c都用到,type=range

3、面试题分析

  1. /* 创建表 */
  2. CREATE TABLE `test03`(
  3. `id` INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
  4. `c1` CHAR(10),
  5. `c2` CHAR(10),
  6. `c3` CHAR(10),
  7. `c4` CHAR(10),
  8. `c5` CHAR(10)
  9. );
  10. /* 插入数据 */
  11. INSERT INTO `test03`(`c1`,`c2`,`c3`,`c4`,`c5`) VALUES('a1','a2','a3','a4','a5');
  12. INSERT INTO `test03`(`c1`,`c2`,`c3`,`c4`,`c5`) VALUES('b1','b2','b3','b4','b5');
  13. INSERT INTO `test03`(`c1`,`c2`,`c3`,`c4`,`c5`) VALUES('c1','c2','c3','c4','c5');
  14. INSERT INTO `test03`(`c1`,`c2`,`c3`,`c4`,`c5`) VALUES('d1','d2','d3','d4','d5');
  15. INSERT INTO `test03`(`c1`,`c2`,`c3`,`c4`,`c5`) VALUES('e1','e2','e3','e4','e5');
  16. /* 创建复合索引 */
  17. CREATE INDEX idx_test03_c1234 ON `test03`(`c1`,`c2`,`c3`,`c4`);
  18. # 问题:我们创建了复合索引idx_test03_c1234,根据以下SQL分析索引的使用情况?
  19. # 1. 下面都用到了索引,其中第四个是全值匹配
  20. explain select * from test03 where c1 = 'a1';
  21. explain select * from test03 where c1 = 'a1' and c2 = 'a2';
  22. explain select * from test03 where c1 = 'a1' and c2 = 'a2' and c3 = 'a3';
  23. explain select * from test03 where c1 = 'a1' and c2 = 'a2' and c3 = 'a3' and c4 = 'a4';
  24. # 2.用到索引c1 c2 c3 c4全字段 MySQL的查询优化器会优化SQL语句的顺序
  25. EXPLAIN SELECT * FROM `test03`
  26. WHERE `c1` = 'a1' AND `c2` = 'a2' AND `c4` = 'a4' AND `c3` = 'a3';
  27. # 3.用到索引c1 c2 c3字段,c4字段失效,范围之后全失效 type:range
  28. EXPLAIN SELECT * FROM `test03`
  29. WHERE `c1` = 'a1' AND `c2` = 'a2' AND `c3` > 'a3' AND `c4` = 'a4';
  30. # 4.用到索引c1 c2 c3 c4全字段 MySQL的查询优化器会优化SQL语句的顺序 type:range
  31. EXPLAIN SELECT * FROM `test03`
  32. WHERE `c1` = 'a1' AND `c2` = 'a2' AND `c4` > 'a4' AND `c3` = 'a3';
  33. # 5.用到了索引c1 c2 c3三个字段, c1和c2两个字段用于查找,c3字段用于排序了但是没有统计到key_len中,c4字段失效
  34. EXPLAIN SELECT * FROM `test03`
  35. WHERE `c1` = 'a1' AND `c2` = 'a2' AND `c4` = 'a4' ORDER BY `c3`;
  36. # 6.用到了索引c1 c2 c3三个字段,c1和c2两个字段用于查找, c3字段用于排序了但是没有统计到key_len中
  37. EXPLAIN SELECT * FROM `test03`
  38. WHERE `c1` = 'a1' AND `c2` = 'a2' ORDER BY `c3`;
  1. # 7.用到了索引c1 c2两个字段,c4失效,c1和c2两个字段用于查找,c4字段排序产生了 Using filesort 说明排序没有用到c4字段索引。中间兄弟不能断
  2. EXPLAIN SELECT * FROM `test03`
  3. WHERE `c1` = 'a1' AND `c2` = 'a2' ORDER BY `c4`;

image.png

  1. # 8.1.用到了索引c1 c2 c3三个字段,c1用于查找,c2和c3用于排序
  2. explain select * from test03 where c1 = 'a1' and c5 = 'a5' order by c2,c3;

image.png

  1. # 8.2.用到了c1一个字段,c1用于查找,c3和c2两个字段索引失效,因为建索引的顺序是1234,3和2颠倒了,产生了Using filesort
  2. explain select * from test03 where c1 = 'a1' and c5 = 'a5' order by c3,c2;

image.png

  1. # 9.用到了c1 c2 c3三个字段,c1 c2用于查找,c2 c3用于排序
  2. explain select * from test03 where c1 = 'a1' and c2 = 'a2' order by c2,c3;
  3. # 10.用到了c1 c2 c3三个字段,c1 c2用于查找,c2 c3用于排序
  4. explain select * from test03
  5. where c1 = 'a1' and c2 = 'a2' and c5 = 'a5' order by c2,c3;
  1. # 本例有常量c2的情况,和8.2对比,多了一个c2的常量
  2. # 用到了c1、c2、c3三个字段,c1、c2用于查找,c2、c3用于排序,并没有产生Using filesort。因为之前c2这个字段已经确定了是'a2'了,这是一个常量,再去ORDER BY c3,c2的时候c2已经不需要排序了,所以没有产生file sort,注意与8.2对比。
  3. explain select * from test03
  4. where c1 = 'a1' and c2 = 'a2' and c5 = 'a5' order by c3,c2;

image.png

  1. /* GROUP BY 表面上是叫做分组,但是分组之前必定排序。 */
  2. # 11.用到c1 c2 c3三个字段,c1用于查找,c2 c3用于排序,c4失效
  3. explain select * from test03 where c1 = 'a1' and c4 = 'a4' group by c2,c3;

我这一句报错:
Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column ‘mysqlpre.test03.id’ which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by。
我这里用的mysql8.0.22,mysql5.7.5及以上版本会默认设置ONLY_FULL_GROUP_BY限制(官方解释)。其大致意思就是对于GROUP_BY聚合操作,如果在SELECT中的列既没有在GROUP_BY中出现,本身也不是聚合列(使用SUM,ANG等修饰的列),那么这句SQL是不合法的,因为那一列是不确定的。
关闭ONLY_FULL_GROUP_BY限制:

  1. 修改全局变量(本次连接生效,重启后无效,exit后再登陆): ```sql

    mysql5 去掉ONLY_FULL_GROUP_BY

    set @@global.sql_mode = ‘STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION’

mysql8 去掉ONLY_FULL_GROUP_BY

set @@global.sql_mode =’STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION’;

  1. 2. 修改配置文件(重启,永久有效):
  2. 在配置文件的[mysqld]下设置:
  3. ```sql
  4. # MYSQL5
  5. sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
  6. # MYSQL8 没有NO_AUTO_CREATE_USER
  7. sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION

但是不建议修改设置,因为这是group by 的规范,出现这个错误表示我们写的SQL语句有问题。
再查看结果:
image.png

  1. # 12.用到c1这一个字段,c4索引失效,c3、c2顺序颠倒索引失效产生了temporary跟file sort(Mysql5)
  2. EXPLAIN SELECT * FROM `test03` WHERE `c1` = 'a1' AND `c4` = 'a4' GROUP BY `c3`,`c2`;

image.png
出现了temporary临时表。
总结:

  • GROUP BY基本上都需要进行排序,索引优化几乎与ORDER BY一致,但是GROUP BY会有临时表的产生。
  • 定值是常量,范围之后失效,最终看排序(索引就是排好序的快速查找的数据结构),一般ORDER BY是给个范围。

    2.6 总结

  • 对于单键索引,尽量选择针对当前query过滤性更好的索引。

  • 在选择组合索引的时候,当前query中过滤性最好的字段,在索引字段顺序中的位置越靠前(左)越好。
  • 在选择组合索引的时候,尽量选择可以包含当前query中的where子句里更多字段的索引。
  • 尽可能通过分析统计信息和调整query的写法,来达到选择合适索引的目的。

口诀:
带头大哥不能死。
中间兄弟不能断。
索引列上不计算。
范围之后全失效。
覆盖索引尽量用。
不等有时会失效。
like百分加右边。
字符要加单引号。
一般SQL少用or。

三、查询截取分析

慢SQL分析步骤:
  1. 观察,至少跑1天,看看生产的慢SQL情况;
  2. 开启慢查询日志,设置阈值,比如超过5秒钟的就是慢SQL,并将它抓取出来;
  3. explain + 慢SQL分析;
  4. 使用show profile进行更加细致的分析;
  5. 运维经理 OR DBA,进行MySQL数据库服务器的参数调优。

    步骤总结:
  6. 慢查询的开启并捕获;

  7. explain + 慢SQL分析。
  8. show Profile查询SQL在MySQL数据库中的执行细节和生命周期情况。
  9. MySQL数据库服务器的参数调优。

    3.1 查询优化

    3.1.1 永远小表驱动大表

    优化原则:对于Mysql数据库而言,永远都是远通过小表驱动大表!
    举个例子: ```java /**
  • 以下两个循环结果都是一样的,但是对于MySQL来说不一样,
  • 情况一可以理解为,和Mysql建立5次连接每次查询1000次。
  • 情况二可以理解为,和Mysql建立1000次连接每次查询5次。
  • 显然,情况一要优于情况二,1000连接过程显然耗费的时间要多于5次连接 */ // 情况一 for(int i = 1; i <= 5; i ++){ for(int j = 1; j <= 1000; j++){

    } } // 情况二 for(int i = 1; i <= 1000; i ++){ for(int j = 1; j <= 5; j++){

    } } IN和EXISTS举例:sql

    IN 适合B表比A表数据小的情况

    SELECT * FROM A WHERE id IN (SELECT id FROM B)

    等价于

    for SELECT id FROM B for SELECT * FROM A WHERE A.id = B.id

EXISTS 适合B表比A表数据大的情况

EXISTS 连接到外表的时候,就相当于for循环,A表中的每条都与EXISTS内的语句进行匹配,返回true则打印,false则跳过

这里 select 1 可以随便修改成别的

SELECT * FROM A WHERE EXISTS (SELECT 1 FROM B WHERE B.id = A.id);

等价于

for SELECT FROM A for SELECT FROM B WHERE B.id = A.id

注意:A表与B表的ID字段应建立索引。

  1. 其实就是sql的机读顺序问题,in的时候先执行括号里的查询,所以括号里的表要小,exists的时候先执行外查询,所以括号外的表要小。<br />**EXISTS**:<br />`SELECT ... FROM TABLE WHERE EXISTS (subquery)`
  2. - 该语法可以理解为:将主查询的数据,放到子查询中做条件验证,根据验证结果(TRUEFALSE)来决定主查询的数据结果是否保留。
  3. **提示**:
  4. - `EXISTS(subquery)`只返回truefalse,因此子查询中的select * 可以是select 1 select 'X',官方说法是实际执行时会忽略SELECT清单,因此没有区别。
  5. - `EXISTS(subquery)`子查询的询实际执行过程额能经过了优化而不是我们理解上的逐条对比,如果担心效率问题,可进行实际检验已确定是否有效率问题。
  6. - `EXISTS(subquery)`子查询往往也可以用条件变道时、其他子查询或者`JOIN`来替代,何种最优需要具体问题具体分析。
  7. **案例**:
  8. ```sql
  9. mysql> select * from tbl_emp e where e.deptId in (select id from tbl_dept d);
  10. +----+------+--------+
  11. | id | name | deptId |
  12. +----+------+--------+
  13. | 1 | z3 | 1 |
  14. | 2 | z4 | 1 |
  15. | 3 | z5 | 1 |
  16. | 4 | w5 | 2 |
  17. | 5 | w6 | 2 |
  18. | 6 | s7 | 3 |
  19. | 7 | s8 | 4 |
  20. +----+------+--------+
  21. 7 rows in set (0.01 sec)
  22. mysql> select * from tbl_emp e where exists(select id from tbl_dept d where e.deptId = d.id);
  23. +----+------+--------+
  24. | id | name | deptId |
  25. +----+------+--------+
  26. | 1 | z3 | 1 |
  27. | 2 | z4 | 1 |
  28. | 3 | z5 | 1 |
  29. | 4 | w5 | 2 |
  30. | 5 | w6 | 2 |
  31. | 6 | s7 | 3 |
  32. | 7 | s8 | 4 |
  33. +----+------+--------+
  34. 7 rows in set (0.01 sec)
  35. mysql> select e.id, name, deptId from tbl_emp e inner join tbl_dept d where e.deptId = d.id;
  36. +----+------+--------+
  37. | id | name | deptId |
  38. +----+------+--------+
  39. | 1 | z3 | 1 |
  40. | 2 | z4 | 1 |
  41. | 3 | z5 | 1 |
  42. | 4 | w5 | 2 |
  43. | 5 | w6 | 2 |
  44. | 6 | s7 | 3 |
  45. | 7 | s8 | 4 |
  46. +----+------+--------+
  47. 7 rows in set (0.00 sec)

3.1.2 order by关键字优化

ORDER BY子句,尽量使用index方式排序,避免使用filesort方式排序

  1. # 建表 插数据
  2. CREATE TABLE `tblA`(
  3. #id integer primary key auto_increment,
  4. `age` INT,
  5. `birth` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
  6. );
  7. INSERT INTO `talA`(`age`) VALUES(22);
  8. INSERT INTO `talA`(`age`) VALUES(23);
  9. INSERT INTO `talA`(`age`) VALUES(24);
  10. /* 创建索引 */
  11. CREATE INDEX idx_talA_age_birth ON `talA`(`age`, `birth`);

案例

  1. EXPLAIN SELECT * FROM tblA WHERE age > 20 ORDER BY age;

image.png
使用索引进行排序,不会产生using filesort。
这里select * 使用了覆盖索引,因为表中只有age跟birth,且这两个字段都在复合索引中。因为所有的age > 20,所以是全索引扫描(type = index),如果是age > 22那么type就是range。

  1. EXPLAIN SELECT * FROMtblAWHEREage> 20 ORDER BYage,birth;

image.png
同样用到了索引进行排序,没有产生filesort。

  1. EXPLAIN SELECT * FROMtblAWHEREage> 20 ORDER BYbirth;

image.png
解释:这里where 中 age > 20,那么从范围左侧的索引都会失效。如果想要order by里面的索引不失效,那么order by里的字段必须满足最左前缀原则,因此这里order by无法使用索引排序,产生了filesort。这也是为什么上面不会产生filesort的原因。(个人推测)

  1. EXPLAIN SELECT * FROMtalAWHEREage> 20 ORDER BYbirth,age;

image.png
排序没有用到索引,出现了filesort

  1. EXPLAIN SELECT * FROM tblA ORDER BY birth;

image.png
没有用到索引排序(不是没有用到索引),产生了filesort。

注意:我在这里测试了一下EXPLAIN SELECT birth FROM tblA ORDER BY birth;
image.png
发现用到了索引(覆盖索引),索引并不是没有使用,而是没有用到缩索引排序,因为没有满足条件。所以是根据3个地方来共同判断有没有用到索引的。 1.是select的字段,2是where条件,3.是order by的字段 4.是group字段;而在做排序跟查找时有没有用到索引需要看是否满足索引使用的条件。

  1. EXPLAIN SELECT * FROMtblAWHEREbirth> '2020-08-04 07:42:21' ORDER BYbirth;

image.png
产生了filesort。

  1. EXPLAIN SELECT * FROMtblAWHEREbirth> '2020-08-04 07:42:21' ORDER BYage;

image.png

  1. EXPLAIN SELECT * FROM tblA order by age asc, birth desc;

image.png
order by 默认升序。

结论

MySQL支持两种排序方式,filesort和index,index效率更高,它指MySQL扫描索引本身完成的排序。filesort效率较低。

ORDER BY 满足两种情况,会使用Index方式x排序:

  • order by子句使用索引的最左前列。
  • 使用where子句和order by子句里的条件列组合,满足索引最左前列时。

尽可能的在索引列上完成排序操作,遵照索引键的最佳左前缀原则。
如果不在索引列上,filesort有两种算法。双路排序、单路排序。

  • 双路排序:MySQL4.1之前使用的是双路排序,需要扫描两次磁盘才能得到最终数据。读取行指针和order by列,对它们进行排序,然后扫描已经排好序的列表,按照列表中的值,重新从列表中读取数据输出。即从磁盘里取排序字段,在buffer进行排序,再从磁盘取其它字段。

取一批数据,要对磁盘进行两次扫描,因为I/O是很耗时的,所以在MySQL4.1之后,出现了第二种改进的算法 -> 单路排序。

  • 单路排序:从磁盘中读取查询的所有列,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输出。它的效率更快,避免了第二次读取数据,并且把随机IO变为了顺序IO,但它会使用更多的空间,因为它把每一行都保存在了内存中。

单路弊端:由于单路排序算法后出,所以总体而言比双路的好。但在sort_buffer中,单路排序要比双路排序要多占用很多空间,因为单路排序是吧所有字段去除,所以有可能取出的数据的总大小大于sort_buffer的容量,导致每次只能取sort_buffer容量大小的数据进行排序(创建tmp文件,多路合并),排完再取sort_buffer容量大小,再排……从而多次I/O。本来想省一次I/O操作,反而导致了大量的I/O操作,反而得不偿失。

优化策略

优化策略:

  • 增大sort_buffer_size参数的设置
  • 增大max_length_for_sort_data参数的设置

为什么要这么做?

  1. Order by时select *是一个大忌,只Query需要的字段,这点非常重要。在这里的影响是:
    1. 当Query的字段大小综合下雨max_length_for_sort_data而且排序字段序不是TEXT|BLO类型时,会采用单路排序,则用多路排序。
    2. 两种算法的数据都有可能超出sort_buffer的糯高粱,超出之后,会创建tmp文件进行合并排序,导致多次I/O,但是用单路排序算法的风险会更大一些,所以要提高sort_buffer_size。
  2. 尝试提高sort_buffer_size

不管用哪种算法,提高这个参数都会提高效率,当然,要根据系统的能力去提高,因为这个参数是针对每个进程的。

  1. 尝试提高max_length_for_sort_data

提高这个参数,会增加改进算法的概率。但是如果设得太高,数据总容量超出sort_buffer_size的概率的就会增大,明显症状是高磁盘I/O活动和低处理器使用率。

小总结

image.png

3.1.3 Group by关键字优化

  • Group by实质是先排序后进行分组,遵照索引建的最佳左前缀。
  • 当无法使用索引列时,会使用Using filesort进行排序,增大max_length_for_sort_data参数的设置和增大sort_buffer_size参数的设置,会提高性能。
  • where执行顺序高于having,能写在where限定条件里的就不要写在having中了。

    3.2 慢查询日志

    3.2.1 什么是慢查询日志

  • MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过阈值的语句,具体指运行时间超过 **long_query_time** 值的SQL,则会被记录到慢查询日志中。

  • long_query_time 的默认值为10,意思是运行10秒以上的语句。
  • 由慢查询日志来查看哪些SQL超出了我们的最大忍耐时间值,比如一条SQL执行超过5秒钟,我们就算慢SQL,希望能收集超过5秒钟的SQL,结合之前 explain 进行全面分析。

    3.2.2 如何开启慢查询日志

    默认情况下,MySQL数据库没有开启慢查询日志,需要我们手动来设置这个参数。
    当然,如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件。

查看慢查询日志是否开启以及如何开启:

  • 查看是否开启:**SHOW VARIABLES LIKE '%slow_query_log%';**
    • image.png
  • 开启慢查询日志:**SET GLOBAL slow_query_log = 1;**当前生效,重启后失效(修改后需要重新登录mysql)。

如果要使慢查询日志永久开启,需要修改/etc/my.cnf文件(linux) my.ini(windows),在[mysqld]下增加修改参数(不建议这么做)。

  1. # my.cnf
  2. [mysqld]
  3. # 1.这个是开启慢查询。注意ON需要大写
  4. slow_query_log=ON
  5. # 2.这个是存储慢查询的日志文件。这个文件不存在的话,需要自己创建
  6. slow_query_log_file=/var/lib/mysql/my-slow.log

开启了慢查询日志后,什么样的SQL会记录到慢查询日志里呢?
这个是由参数long_query_time控制的,默认情况下long_query_time的值为10秒。
MySQL中查看long_query_time的时间:**SHOW VARIABLES LIKE 'long_query_time%';**
修改慢查询阈值:set global long_query_time=3;
同样的,修改完后需要重新连接一下数据库。 exit后登录
也可以在配置文件/etc/my.cnf中修改(永久生效):

  1. [mysqld]
  2. # 这个是设置慢查询的阈值
  3. long_query_time=3

注意:如果查询时间正好等与long_query_time的情况,并不会被记录下来。也就是说,在mysql源码里是判断大于long_query_time,而非大于等于。

查看慢查询日志:
cd 到log的文件夹/var/lib/mysqlcat或者vim mrlinxi-slow.log

查新慢查询日志的总记录条数:**SHOW GLOBAL STATUS LIKE '%Slow_queries%';**

  1. mysql> select sleep(4);
  2. +----------+
  3. | sleep(4) |
  4. +----------+
  5. | 0 |
  6. +----------+
  7. 1 row in set (4.00 sec)
  8. mysql> SHOW GLOBAL STATUS LIKE '%Slow_queries%';
  9. +---------------+-------+
  10. | Variable_name | Value |
  11. +---------------+-------+
  12. | Slow_queries | 1 |
  13. +---------------+-------+
  14. 1 row in set (0.02 sec)

3.2.3 日志分析工具MysqlDumpSlow

在生产环境中,如果要手工分析日志,查找、分析SQL,显然是个体力活,MySQL提供了日志分析工具MysqlDumpSlow

  1. # 1、mysqldumpslow --help 来查看mysqldumpslow的帮助信息
  2. root@1dcb5644392c:/usr/bin# mysqldumpslow --help
  3. Usage: mysqldumpslow [ OPTS... ] [ LOGS... ]
  4. Parse and summarize the MySQL slow query log. Options are
  5. --verbose verbose
  6. --debug debug
  7. --help write this text to standard output
  8. -v verbose
  9. -d debug
  10. -s ORDER what to sort by (al, at, ar, c, l, r, t), 'at' is default # 按照何种方式排序
  11. al: average lock time # 平均锁定时间
  12. ar: average rows sent # 平均返回记录数
  13. at: average query time # 平均查询时间
  14. c: count # 访问次数
  15. l: lock time # 锁定时间
  16. r: rows sent # 返回记录
  17. t: query time # 查询时间
  18. -r reverse the sort order (largest last instead of first)
  19. -t NUM just show the top n queries # 返回前面多少条记录
  20. -a don't abstract all numbers to N and strings to 'S'
  21. -n NUM abstract numbers with at least n digits within names
  22. -g PATTERN grep: only consider stmts that include this string # 后边搭配一个正则匹配模式,大小写不敏感的。匹配特定查询语句
  23. -h HOSTNAME hostname of db server for *-slow.log filename (can be wildcard),
  24. default is '*', i.e. match all
  25. -i NAME name of server instance (if using mysql.server startup script)
  26. -l don't subtract lock time from total time
  27. # 2、 案例
  28. # 2.1、得到返回记录集最多的10个SQL
  29. mysqldumpslow -s r -t 10 /var/lib/mysql/mrlinxi-slow.log
  30. # 2.2、得到访问次数最多的10个SQL
  31. mysqldumpslow -s c -t 10 /var/lib/mysql/slow.log
  32. # 2.3、得到按照时间排序的前10条里面含有左连接的查询语句
  33. mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/slow.log
  34. # 2.4、另外建议使用这些命令时结合|和more使用,否则出现爆屏的情况
  35. mysqldumpslow -s r -t 10 /var/lib/mysql/slow.log | more

3.3 批量插入数据脚本

3.3.1 建表

  1. /* 1.dept 部门表 */
  2. create table dept(
  3. id int unsigned primary key auto_increment,
  4. deptno mediumint unsigned not null default 0,
  5. dname varchar(20) not null default "",
  6. loc varchar(13) not null default ""
  7. )engine=innodb default charset=GBK;
  8. /* 2.emp 员工表 */
  9. CREATE TABLE emp(
  10. id int unsigned primary key auto_increment,
  11. empno mediumint unsigned not null default 0,
  12. ename varchar(20) not null default "",
  13. job varchar(9) not null default "",
  14. mgr mediumint unsigned not null default 0,
  15. hiredate date not null,
  16. sal decimal(7,2) not null,
  17. comm decimal(7,2) not null,
  18. deptno mediumint unsigned not null default 0
  19. )ENGINE=INNODB DEFAULT CHARSET=GBK;

3.3.2 设置参数log_bin_trust_function_creators

创建函数,假如报错:This function nhas none of DETERMINISTIC….
是因为开启过慢查询日志,因为我们开启了bin-log,我们就必须为我们的function指定一个参数,否则使用函数会报错。

  1. # 查看是否开启了bin-log mysql8 默认开启
  2. mysql> show variables like 'log_bin';
  3. +---------------+-------+
  4. | Variable_name | Value |
  5. +---------------+-------+
  6. | log_bin | ON |
  7. +---------------+-------+
  8. 1 row in set (0.00 sec)
  9. # 在mysql中设置
  10. # log_bin_trust_function_creators 默认是关闭的 需要手动开启
  11. mysql> SHOW VARIABLES LIKE 'log_bin_trust_function_creators';
  12. +---------------------------------+-------+
  13. | Variable_name | Value |
  14. +---------------------------------+-------+
  15. | log_bin_trust_function_creators | OFF |
  16. +---------------------------------+-------+
  17. 1 row in set (0.00 sec)
  18. mysql> SET GLOBAL log_bin_trust_function_creators=1;
  19. Query OK, 0 rows affected (0.00 sec)

上述修改方式MySQL重启后会失败,在my.cnf配置文件下修改永久有效。

  1. [mysqld]
  2. log_bin_trust_function_creators=ON

3.3.3 创建函数保证每条数据都不同

  1. # 1、函数:随机产生长度为n的字符串
  2. DELIMITER $$
  3. CREATE FUNCTION ran_string(n INT) RETURNS VARCHAR(255)
  4. BEGIN
  5. DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
  6. DECLARE return_str VARCHAR(255) DEFAULT '';
  7. DECLARE i INT DEFAULT 0;
  8. WHILE i < n DO
  9. SET return_str = CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1)); # SUBSTRING(string,position,length);
  10. SET i=i+1;
  11. END WHILE;
  12. RETURN return_str;
  13. END $$
  14. # 2、函数:随机产生部门编号
  15. DELIMITER $$
  16. CREATE FUNCTION rand_num() RETURNS INT(5)
  17. BEGIN
  18. DECLARE i INT DEFAULT 0;
  19. SET i=FLOOR(100+RAND()*10); # 生成一个100-110的编号
  20. RETURN i;
  21. END $$
  22. # 删除函数
  23. drop function function_name;

3.3.4 创建存储过程

  1. # 创建往emp表中插入数据的存储过程
  2. delimiter $$
  3. create procedure insert_emp(in start int(10),in max_num int(10))
  4. begin
  5. declare i int default 0;
  6. # 设置不自动提交,以免每插入一条提交一次,一次性提交全部
  7. set autocommit = 0;
  8. repeat
  9. set i = i+1;
  10. insert into emp(empno,ename,job,mgr,hiredate,sal,comm,deptno) values((start+i),ran_string(6),'salesman',0001,curdate(),2000,400,rand_num());
  11. until i=max_num
  12. end repeat;
  13. commit;
  14. end $$
  15. # 创建往dept表中插入数据的存储过程
  16. delimiter $$
  17. create procedure insert_dept(in start int(10),in max_num int(10))
  18. begin
  19. declare i int default 0;
  20. set autocommit = 0;
  21. repeat
  22. set i = i+1;
  23. insert into dept(deptno,dname,loc) values((start+i),ran_string(10),ran_string(8));
  24. until i=max_num
  25. end repeat;
  26. commit;
  27. end $$
  28. # 恢复;为结束符
  29. delimiter ;
  30. # 删除存储过程
  31. drop procedure insert_dept

3.3.5 调用存储过程

  1. # 以编号100开始创建十个部门
  2. CALL insert_dept(100, 10);
  3. # 执行存储过程,往emp表添加50万条数据
  4. CALL insert_emp(100001, 500000);

3.4 Show Profile

Show Profile是MySQL提供的可以用来分析当前会话中语句执行的资源消耗情况。可以用于SQl调优的测量。官网
默认情况下show profile处于关闭状态,开启后保存最近15次的运行结果。

3.4.1 分析步骤

  1. 是否支持ShowProfile,看看当前的MySQL版本是否支持。

通过SHOW VARIABLES LIKE 'profiling'; 查看showprofile是否开启。或者使用SHOW VARIABLES LIKE 'profiling%'

  1. mysql> SHOW VARIABLES LIKE 'profi%';
  2. +------------------------+-------+
  3. | Variable_name | Value |
  4. +------------------------+-------+
  5. | profiling | OFF | # 这个参数是控制ShowProfile是否开启
  6. | profiling_history_size | 15 | # 设置ShowProfile记录数,默认15
  7. +------------------------+-------+
  8. 2 rows in set (0.00 sec)
  1. 开启ShowProfile功能,默认为关闭状态,使用前需开启。

SET profiling=ON;

  1. 运行SQL ```sql

    这里group by 后边 % 的意思是按 id 取余分组

    SELECT * FROM emp GROUP BY id%10 LIMIT 150000;

SELECT * FROM emp GROUP BY id%20 ORDER BY 5;

  1. 4. 查看结果,执行`SHOW PROFILES;`
  2. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1637723217685-142ae594-cc76-4ca6-899c-72f7d16e17a3.png#clientId=u7079a2d8-a332-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=229&id=ue885ceae&name=image.png&originHeight=229&originWidth=754&originalType=binary&ratio=1&rotation=0&showTitle=false&size=27195&status=done&style=none&taskId=u0c6803ef-0ca4-4ccb-b23e-842ae9212a2&title=&width=754)<br />可以看到每条SQL的执行时间,以及具体的query语句。
  3. 5. 诊断SQL`show profile cpu,block io for query Query_ID;`
  4. 执行 `show profile cpu, block io for query 3;` 这里的3是通过show profiles 查出来的Query_ID.<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1637733760402-f8fb412f-ffbd-4a33-9780-41959783d8b9.png#clientId=u7079a2d8-a332-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=361&id=u8793ef54&name=image.png&originHeight=361&originWidth=786&originalType=binary&ratio=1&rotation=0&showTitle=false&size=34864&status=done&style=none&taskId=u78b1ea8c-d446-4722-a259-58ef0ec42e2&title=&width=786)<br />show profile 后面带的参数解释:
  5. | 参数 | 含义 |
  6. | --- | --- |
  7. | ALL | 显示所有的开销信息 |
  8. | BLOCK IO | 显示块IO相关开销 |
  9. | CONTEXT SWITCHES | 上下文切换相关开销 |
  10. | CPU | 显示CPU相关开销信息 |
  11. | IPC | 显示发送和接收相关开销信息 |
  12. | MEMORY | 显示内存相关开销信息 |
  13. | PAGE FAULTS | 显示页面错误相关开销信息 |
  14. | SOURCE | 显示Source_function,Source_file,Source_line相关的开销信息 |
  15. | SWAPS | 显示交换次数相关开销的信息 |
  16. 我这里mysql8废弃了查询缓存,所以查询得到的结果没有waiting for query cache。<br />通过`show variables like '%query_cache%';`可以查看查询缓存是否开启(MYSQL8已经废弃这个功能,mysql5应该是自动开启) mysql5会有 waiting for query cache这一项<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1637734112860-a4b146cf-1972-4e23-a091-ac67c45c0365.png#clientId=u7079a2d8-a332-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=127&id=ua56c7889&name=image.png&originHeight=127&originWidth=355&originalType=binary&ratio=1&rotation=0&showTitle=false&size=6115&status=done&style=none&taskId=ud8406427-13bd-4961-ad96-51d9466c73d&title=&width=355)
  17. 6. 日常开发需要注意的结论。
  18. 以下字段出现了,就表示性能出大问题了。必须要优化。
  19. | **Convering HEAP to MyISAM** | 查询结果太大,内存都不够用了,往磁盘上搬了。 |
  20. | --- | --- |
  21. | **Creating tmp table** | 创建临时表(拷贝数据到临时表,用完再删除),非常耗费数据库性能。 |
  22. | **Copying to tmp table on disk** | 把内存中的临时表复制到磁盘,危险!!! |
  23. | **locked** | 死锁 |
  24. <a name="NnRJ1"></a>
  25. ## 3.5 全局查询日志
  26. 全局查询日志会记录所有的sql。<br />**永远不要在生产环境开启这个功能**。仅测试环境下使用
  27. 配置启用:
  28. ```shell
  29. 在mysql的my.cnf中,设置如下
  30. #开启
  31. general_log=1
  32. #记录日志文件的路径
  33. general_log_file=/path/logfile
  34. #输出格式
  35. log_output=FILE

编码启用:

  1. set global general_log=1;
  2. set global log_output='TABLE'; # 设置输出类型为table
  3. #此后编写的sql语句,将会记录到mysql库里的general_log表,可以用下面的命令查看
  4. select * from mysql.general_log;
  5. # set global log_output='FILE'; # 设置输出类型为file

查看全局日志是否开启:SHOW VARIABLES LIKE 'general_log%';
image.png

问题:
mysql8l的 mysql.general_log表的argument字段是mediumblob格式,查看全局日志时argument是0x开头:
image.png
解决办法:修改general_log表中的argument字段为varchar(100) 这个长度可以自己调整。
alter table mysql.general_log modify column argument varchar(100) not null;

四、MySql锁机制

4.1 锁的分类

从对操作的类型(读/写)可以分为:

  • 读锁(共享锁):针对同一份数据,多个读的操作可以同时进行,而不会互相影响
  • 写锁(排它锁):当前写的操作没有完成之前,会阻塞其它读锁和写锁

按照数据操作的粒度分类:

  • 行锁
  • 表锁

    4.2 表锁(更偏向读操作)

    表锁的特点:表锁偏向 MyISAM 存储引擎,开销小,加锁快,无死锁,锁定粒度大,发生锁冲突的概率最高,并发度最低。

    4.2.1 表锁相关命令

    ```sql 【表级锁分析—建表SQL】 create table mylock( id int not null auto_increment, name varchar(20), primary key(id) )engine myisam; # 注意引擎是myisam

insert into mylock(name) values(‘a’); insert into mylock(name) values(‘b’); insert into mylock(name) values(‘c’); insert into mylock(name) values(‘d’); insert into mylock(name) values(‘e’);

  1. 查看各数据库表上加过的锁:`show open tables;`<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1637741404766-a1abe97c-4d88-43ce-9446-4962d999ea08.png#clientId=udc3e7711-9988-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=261&id=u2b437384&name=image.png&originHeight=261&originWidth=821&originalType=binary&ratio=1&rotation=0&showTitle=false&size=25928&status=done&style=none&taskId=u7d41d435-959c-4342-a6b2-8a7b1706780&title=&width=821)
  2. ```sql
  3. # 手动给表加锁 给mylock上读锁,给book上写锁
  4. # lock table 表名字 read(write),表名字2 read(write),其它;
  5. lock table `mylock` read, `book` write;

加锁之后,show open tables; 中加了锁的表in use 变成了1.
image.png
释放表锁:unlock tables;

4.2.2 案例分析

1. 读锁案例

分别打开两个会话,会话1为mylock表加读锁:

  1. # session1中为mylock表加读锁
  2. lock table mylock read;
session1 session2
session1可以正常读自己锁的表
image.png
session2可正常读取session1锁的表
image.png
session1不能够读取其他没有加锁的表
image.png
其他session可以查询或更新未锁定的表
image.png
session1不能够修改自己锁的表
image.png
session2修改加锁的表会一直等待获得锁(阻塞),直到表锁被释放
image.png
session1 unlock tables; session1释放表锁之后,其他session完成表更新
image.png

只要某个session给表加了读锁,在该session释放读锁之前,都无法操作其他未加锁的表。但不影响其他session操作未加锁的表。

2. 写锁案例

session1给mylock表上写锁:lock table mylock write;
image.png

session1 session2
session1不能够读取其他没有加锁的表
image.png
其他session可以查询或更新未锁定的表
image.png
当前session对(写锁)锁定表的查询+更新+插入操作都可以执行
image.png
其他session对锁定表的查询被阻塞,需要等待锁被释放:
image.png
备注:如果可以,请换成不同的id来进行测试,因为mysql有缓存,第2次的条件会从缓存取得,影响锁效果演示。
unlocktables; 释放锁后,查询返回
image.png

4.2.3 案例结论

MyISAM在执行查询语句(select)前,会自动给涉及的所有表加读锁,在执行增删改操作前,会自动给涉及的表加写锁。
MySQL的表级锁有两种模式:

  • 表共享读锁(Table Read Lock)
  • 表独占写锁(Table Write Lock)

对MyISAM表进行操作,会有以下情况:

  • 对MyISAM表加读锁,不会阻塞其他进程对同一表的读请求,但会阻塞对同一表的写请求。只有当读锁释放后,才会执行其他进程的写操作。
  • 对MyISAM表加写锁,会阻塞其他进程对同一表的读和写操作,只有当写锁释放后才会执行进程的读写操作。
  • 简而言之,就是读锁会阻塞写,但是不会阻塞读,而写锁会把读和写都阻塞

    4.2.4 表锁分析

    【看看哪些表被加锁了】
    show opentables;可以查看哪些表加锁了,哪些没有。
    【如何分析表锁定】
    可以通过检查table_locks_waitedtable_locks_immediate状态变量来分析系统上的表锁定:
    通过 show status like 'table%'; 命令查看
    image.png
    table_locks_waitedtable_locks_immediate状态变量记录MySQL内部表级锁定的情况,两个变量说明如下:

  • table_locks_immediate:产生表级锁定的次数,表示可以立即获取锁的查询次数,每立即获取锁值加1;

  • table_locks_waited:出现表级锁定争用而发生等待的次数(不能立即获取锁的次数,每等待一次锁值加1),此值高则说明存在较严重的表级锁争用情况。

此外,MyISAM的读写锁调度是写有限,这又是MyISAM不适合做以写为主的表的引擎。因为写锁后,其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞。

4.3 行锁(更偏向写操作)

行锁的特点:偏向InnoDB存储引擎开销大加锁慢会出现死锁锁定粒度最小发生锁冲突的概率最低并发度也最高
InnoDB与MyISAM的两个最大不同点:

  • InnoDB支持事务(TRANSACTION);
  • InnoDB采用了行级锁。

事务的ACID:

  • 原子性(Atomicity):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么都不执行。
  • 一致性(Consistency):在事务开始和完成时,事务都保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有内部数据结构(如B树索引或双向链表)也都必须是正确的。一个事务可以使数据从一个一致状态切换到另外一个一致的状态。
  • 隔离性(Isolation):数据库提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。一个事务不受其他事务的干扰,多个事务互相隔离。
  • 持久性(Durability):事务完成之后,它对与数据的修改是永久性的,即使出现系统故障也能够保持。

并发事务处理带来的问题:

  • 更新丢失(Lost Update):多个事务同时操作某个数据,由于每个事务不知道其他事务的存在,就会发生丢失更新——最后的更新覆盖了其他事务所做更新。
  • 脏读(Dirty Reads):事务A读取到了事务B已修改但尚未提交的数据,还在这个基础上做了操作。此时,如果B事务回滚,A读取的数据无效,不满足一致性。
  • 不可重复读(Non-Repeatable-Reads):事务A读取到了事务B已经提交的修改数据,不符合隔离性。一个事务多次读取,结果不一样。
  • 幻读(Phantom Reads):事务A读取到了事务B提交的新增数据,不符合隔离性。

多说一句:幻读和脏读有点类似,脏读是事务B里面修改了数据,幻读是事务B里面新增了数据。

事务隔离级别:

脏读 不可重复读 幻读
read uncommitted:读未提交 × × × 最低级别,只能保证不读取物理上损坏的数据
read committed:读已提交 × × 语句级
repeatable read:可重复读 × 事务级
serializable:串行化 最高级别,事务级

查看当前数据库的事务隔离级别:show variables like 'tx_isolation'; mysql5
show variables like 'transaction_isolation'; mysql 8
设置当前 mySQL 连接的隔离级别:
set session transaction isolation level read committed;
设置数据库系统的全局的隔离级别:
set global transaction isolation level read committed;

4.3.1 案例分析

  1. # 建表
  2. CREATE TABLE test_innodb_lock (
  3. a INT(11),
  4. b VARCHAR(16)
  5. )ENGINE=INNODB;
  6. # 插入数据
  7. INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(1, 'b2');
  8. INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(3, '3');
  9. INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(4, '4000');
  10. INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(5, '5000');
  11. INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(6, '6000');
  12. INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(7, '7000');
  13. INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(8, '8000');
  14. INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(9, '9000');
  15. INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(1, 'b1');
  16. # 创建索引
  17. CREATE INDEX idx_test_a ON `test_innodb_lock`(a);
  18. CREATE INDEX idx_test_b ON `test_innodb_lock`(b);

1. 行锁定基本演示

打开session1与session2,将两个会话的自动提交都关闭
set autocommit = 0;

session1 session2
更新数据但是不提交,没有手写commit
image.png
此时session1 查询表中的数据是修改之后的。
image.png
读 己之所写
session1 commit;
其他session读表数据还是之前的,这里是因为隔离级别是可重复读,不会出现脏读的问题。
image.png
session2再次查询,发现就算session1提交了,也还是读的修改之前的数据。
session2 commit之后,才能读到session1提交的修改。
这是因为session2也设定了手动提交事务。在可重复读(通过mvcc实现)隔离级别下,同一事物快照读只会生成一次Readview,之后的快照读都是读取第一次创建的,也称之为一致性快照。所以在session2提交之前,不会读到session1提交的数据。
更新但是不提交,没有手写commit;
image.png
session2更新被阻塞,只能等待
image.png
等待时间过长会报超时异常
提交更新
image.png
解除阻塞,更新正常进行
image.png
session2 提交 commit;
最终两边的数据都变成4003

现在我们试一下session1修改a=4的数据,session2修改a=9的数据看看情况如何。
image.png
可以看到,针对同一表的不同行两边互不影响,不会出现阻塞状态。

2. 无索引行锁升级为表锁

当前情况下,没有出现类型的转换,索引没有失效。
image.png
b本来是varchar类型,这里我们写成int类型,那么会产生自动类型转换,导致索引失效。那么行锁会升级为表锁。
image.png

3. 间隙锁的危害

MySQL的锁机制 - 记录锁、间隙锁、临键锁
什么是间隙锁
当我们用范围条件而不是等值条件检索数据,并请求共享或者排它锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Gap Locks)。
间隙锁有一个比较致命的弱点,就是当锁定一个范围键值之后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的危害。
产生间隙锁的条件(RR事务隔离级别下):

  • 使用普通索引锁定;
  • 使用多列唯一索引;
  • 使用唯一索引锁定多行记录;

以上情况,都会产生间隙锁。
临键锁(Next-key Locks),是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。
注:临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级为RC,临键锁则也会失效。

session1,进行范围更新操作,产生(1,2]的间隙锁以及{1,3,4,5}的记录锁,此时就是临建锁。
session2插入间隙锁范围的数据被阻塞。
image.png
待session1提交后,session2接触阻塞正常更新。
image.png

4. 面试题:常考如何锁定一行

image.png
for update是排它锁(悲观锁)。select for update 是为了在查询时,对这条数据进行加锁,避免其他用户以该表进行插入,修改或删除等操作,造成表的不一致性。
mysql InnoDB引擎默认的修改数据语句,update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,如果加排他锁可以使用select …for update语句,加共享锁可以使用select … lock in share mode语句。所以加过排他锁的数据行在其他事务中是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。

4.3.2 案例结论

InnoDB存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更高一些,但是在整体并发处理能力方面要远远优于MyISAM的表级锁定的。当系统并发量较高的时候,InnoDB的整体性能和MyISAM相比就会有比较明显的优势了。
但是,InnoDB的行级锁定同样也有其脆弱的一面,当我们使用不当的时候,可能会让InnoDB的整体性能表现不仅不能比MyISAM高,甚至可能会更差。

4.3.3 行锁分析

通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况

  1. mysql> show status like '%InnoDB_row_lock%';
  2. +-------------------------------+--------+
  3. | Variable_name | Value |
  4. +-------------------------------+--------+
  5. | Innodb_row_lock_current_waits | 0 |
  6. | Innodb_row_lock_time | 145327 |
  7. | Innodb_row_lock_time_avg | 24221 |
  8. | Innodb_row_lock_time_max | 51110 |
  9. | Innodb_row_lock_waits | 6 |
  10. +-------------------------------+--------+
  11. 5 rows in set (0.00 sec)

各个状态量的说明如下:

  • Innodb_row_lock_current_waits:当前正在等待锁定的数量;
  • Innodb_row_lock_time:从系统启动到现在锁定总时间长度(重要);
  • Innodb_row_lock_time_avg:每次等待所花的平均时间(重要);
  • Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间;
  • Innodb_row_lock_waits:系统启动后到现在总共等待的次数(重要)

尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手制定优化策略。

4.3.4 优化建议

  • 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁(自动类型转换)
  • 合理设计索引,尽量缩小锁的范围
  • 尽可能较少检索条件,避免间隙锁
  • 尽量控制事务大小,减少锁定资源量和时间长度
  • 尽可能低级别事务隔离

    4.4 页锁

    开销和加锁时间介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般。

    五、主从复制

    5.1 复制的基本原理

    slave(从机)会从mater(主机)读取binlog(mysql存储文件)来进行数据同步。
    image.png
    MySQL复制过程分为三步:

  • Master将改变记录到二进制日志(Binary Log)。这些记录过程叫做二进制日志事件,Binary Log Events

  • Slave将Master的Binary Log Events拷贝到它的中继日志(Replay Log);
  • Slave重做中继日志中的事件,将改变应用到自己的数据库中。MySQL复制是异步且串行化的。

    5.2 主从复制的基本原则

  • 每个slave只有一个master

  • 每个slave只能有一个唯一的服务器ID
  • 每个master可以有多个slave

主从复制的最大问题:延时

5.3 一主一从常见配置

MySQL 8.0主从(Master-Slave)配置

  1. 基本要求:Master和Slave的MySQL服务器版本一致且后台以服务运行。相互能够ping通
  2. 主从配置都是配在[mysqld]节点下,都是小写

我们这里使用windows和Linux两个平台来配置主从。
注意:MySQL 5.1.7版本之后,已经不支持把master配置属性写入my.cnf配置文件中了,只需要把同步的数据库和要忽略的数据库写入即可。

5.3.1 修改主机配置

这里列举了Linux和windows的主机配置,我的主机实际上在windows上

  1. # Master配置——Linux
  2. [mysqld]
  3. server-id=1 # 主服务器唯一ID 【必须】
  4. # mysql5【必须】启用二进制日志 mysqlbin.xxxxx就是
  5. log-bin=/var/lib/mysql/mysqlbin
  6. # mysql8 在/var/lib/mysql 文件夹下 binlog.xxxxx就是
  7. log-bin=/var/lib/mysql/binlog
  8. log-err=本地路径/mysql-err # 可选 启用错误日志
  9. basedir=本地路径 # 可选 根目录
  10. tmpdir=本地路径 # 可选 临时目录
  11. read-only=0 # 主机 读写都可以
  12. binlog-ignore-db=mysql # 可选 设置不要复制的数据库
  13. binlog-do-db=ww2 # 可选 设置需要复制的数据库
  14. # Master配置——Windows
  15. server-id=1 # 主服务器唯一ID 【必须】
  16. # 二进制日志
  17. # mysql5 【必须】启用二进制日志 mysql5视频里数据文件是mysqlbin.xxxxx
  18. log-bin=mysql设置的datadir路径/mysqlbin
  19. # mysql8 这里的路径要跟datadir设置的数据库数据存放路径一致,我的是mysql8 数据文件名字是binlog.xxxxx
  20. log-bin=mysql设置的datadir路径/binlog
  21. log-err=mysql设置的datadir路径/mysqlerr # 可选 启用错误日志
  22. basedir="mysql本地路径" # 可选 根目录 这个我在安装windows版的mysql的时候就设置了,就是mysql的安装目录
  23. tpmdir="mysql本地路径" # 可选 临时目录 和根目录一样
  24. datadir="数据存储路径" # 这个我是在安装mysql的时候就设定了
  25. read-only=0 # 只读,主机设置true或者false都行
  26. binlog-ignore-db=不要复制的数据库名称 #可选 设置不要复制的数据库,多个用空格隔开
  27. binlog-do-db=要复制的数据库名称 #可选 设置要复制的数据库,多个用空格隔开

备注:主从复制时默认是全库复制。
image.png
修改后重启mysql服务

5.3.2 修改从机配置

从机在Linux上修改/etc/my.cnf文件

  1. # 主从复制从机slave配置
  2. server-id=2 # 这个ID不能跟主机相同
  3. log-bin=/var/lib/mysql/binlog

修改后重启mysql systemctl stop mysql systemctl start mysql

5.3.3 主机从机防火墙设置

视频里面直接关闭了防火墙,不太推荐这种方式。其实只需要开放数据库的访问端口3306即可。如果你是别的端口就开放对应的。
2.3 远程访问授权 这里已经开过3306的端口号了。如果你不知道弄了没有执行
firewall-cmd --list-all 查看开放的端口号。如果没有数据库对应的端口号,添加端口号即可:firewall-cmd --add-port=3306/tcp --permanent # 开启3306端口访问

5.3.4 主机建立账户并授权给slave

mysql5跟mysql8的命令有区别。

  1. # Mysql5
  2. # GRANT REPLICATION SLAVE ON *.* TO 'username'@'从机IP地址' IDENTIFIED BY 'password';
  3. # Mysql8
  4. # 在主节点创建用户
  5. create user 用户名@'从机ip' identified by '密码';
  6. # 授权
  7. GRANT REPLICATION SLAVE ON *.* TO '用户名'@'从机ip';
  8. # 刷新授权表信息
  9. flush privileges;
  10. # 获取主节点当前binarylog文件名和位置(position)
  11. show master status;

image.png
image.png
image.png
执行完上个步骤后,不要再操作主服务器MySQL,防止主服务器的状态值变化。

5.3.5 在Linux从机上配置需要复制的主机

  1. change master to master_host='主机IP',
  2. master_user='主机上创建的用户名',
  3. master_password='主机上创建的密码',
  4. master_log_file='file名字',
  5. master_log_pos=Position数值;
  1. # 1. 使用主机创建的用户名和密码登陆进Mater
  2. mysql> change master to master_host='192.168.190.1',
  3. -> master_user='zhangsan',
  4. -> master_password='10086',
  5. -> master_log_file='binlog.000028',
  6. -> master_log_pos=862;
  7. Query OK, 0 rows affected, 2 warnings (0.04 sec)
  8. # 2. 开启slave从机的复制
  9. mysql> start slave;
  10. Query OK, 0 rows affected, 1 warning (0.02 sec)
  11. # 3. 查看slave状态
  12. # Slave_IO_Running 和 Slave_SQL_Running 必须同时为Yes 说明主从复制配置成功!
  13. show slave status \G

踩坑提示
我这里在第三步,连了半天没连上去。他会一直尝试重连。
image.png
原因是window防火墙没有开启主机数据库的访问端口3306。解决:如何开启windows防火墙以及防火墙端口
修改后再查看状态:两个yes表明ok了。
image.png

5.3.6 测试&停止从服务复制功能

主机创建一个数据库:
image.png
从机直接查询数据库,看同步没有:
image.png

停止从机复制功能:stop slave;
清楚主从复制关系:
从机在stop slave之后,执行reset slave all;会删除所有的relay log文件。
主机通过reset master; 命令删除所有的binlog日志文件。

备注:该模式下好像只能同步新建的数据库,已有数据库好像无法同步,需要其他的操作。