内容回顾
1、MySQL8默认字符集utf8
2、sql_mode
- 编写分组语句,select后面出现分组字段和聚合函数
3、MySQL存储引擎
4、练习sql语句
5、MySQL索引
-
高效获取数据的数据结构
-
索引缺点:占用空间、维护成本高
5、树
-
二叉树
-
二叉搜索树
-
平衡二叉树
-
多叉树:树高度和io次数相关的,树高度越低,io次数越少
-
B-Tree 和 B+Tree
6、InnoDB聚簇和非聚簇
7、覆盖索引
今天内容
1、MyISAM中的索引
-
MySQL数据库支持多种存储引擎,默认存储引擎是InnoDB,还有很多其他存储引擎
-
MyISAM引擎使用 B+Tree 作为索引结构 ,
叶子节点存放的是数据记录的地址 -
MyISAM索引都是非聚簇,但是和InnoDB里面非聚簇不一样的
-
执行概述:没有严格区分主键和非主键,根据条件(主键或者非主键)找到对应地址,再根据地址查询其他数据(回表)
2、MyISAM与InnoDB对比
-
InnoDB支持事务、外键、行锁;MyISAM不支持事务、不支持外键、使用表锁
-
从其他角度比较:
1、InnoDB的数据索引在一个文件中,而MyISAM索引和数据在不同的文件里面:
2、InnoDB中主键索引是聚簇索引,其他索引是非聚簇索引 。MyISAM索引都是非聚簇。
3、InnoDB要求表必须有主键 ( MyISAM可以没有 )。如果没有显式指定,则MySQL系统会自动选择一个可以非空且唯一标识数据记录的列作为主键。
4、MyISAM叶子节点记录的是数据的地址。InnoDB叶子节点存储数据完整的值。
3、索引操作
-
索引创建语句
-
create index 索引名称 on 表名称(字段1 , 字段2....)
# 创建表时候,设置主键,mysql自动为主键创建索引
# 创建索引语句
# 单值索引
CREATE INDEX idx_name ON customer1(customer_name);
# 复合索引
CREATE INDEX idx_no_name ON customer1(customer_no,customer_name);
4、索引的使用场景(背)
哪些情况适合创建索引:
-
频繁作为WHERE查询条件的字段
-
经常GROUP BY 和 ORDER BY的列
-
字段的值有唯一性的限制
-
DISTINCT去重字段需要创建索引
-
多表JOIN时,对连接字段创建索引
-
使用字符串前缀创建索引
-
使用频繁的列,放到联合索引的左侧 (最佳左前缀法则)
哪些情况不要创建索引:
-
WHERE、GROUP BY 、ORDER BY里用不到的字段不创建索引
-
表的数据记录太少
-
避免对经常增删改的表创建索引
-
不要定义冗余或重复的索引
5、索引优化
性能分析工具EXPLAIN
-
explain:查看SQL执行计划:使用EXPLAIN关键字可以
模拟优化器执行SQL查询语句 -
使用explain分析sql性能如何的
-
用法:explain + sql语句
explain select * from customer1
- 返回结果,各个字段表示性能指标,根据这些指标分析处理sql语句性能

id: 语句执行顺序,得到结果查询趟数
**type:**性能类型,ALL表示全表检索,效率最低
system > const > eq_ref > ref > range > index > ALL`
SQL 性能优化的目标:至少要达到
range级别,要求是ref级别,最好是const级别。(阿里巴巴开发手册要求)
possible_keys 和 key : possible_keys 可能使用的索引,key实际使用的索引
**key_len:**查看索引使用充分程度
-
先看索引上字段的类型+长度。比如:int=4 ; varchar(20) =20 ; char(20) =20
-
如果是varchar或者char这种字符串字段,视字符集要乘不同的值,比如utf8要乘 3(MySQL5.7),如果是utf8mb4要乘4,GBK要乘2
-
varchar这种动态字符串要加2个字节
-
允许为空的字段要加1个字节
-- 创建索引 (4 + 4 + 20*3+2) + 3 = 73 CREATE INDEX idx_age_deptid_name ON emp(age,deptid,name);
rows: 查询出结果检索多少行数据,肯定越少越好
**filtered:**命中率
**Extra:**额外,有些信息在前面字段无法显示,在这个字段显示
准备工作-添加几十万数据(了解)
CREATE TABLE `dept` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`deptName` VARCHAR(30) DEFAULT NULL,
`address` VARCHAR(40) DEFAULT NULL,
ceo INT NULL ,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1;
CREATE TABLE `emp` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`empno` INT NOT NULL ,
`name` VARCHAR(20) DEFAULT NULL,
`age` INT(3) DEFAULT NULL,
`deptId` INT(11) DEFAULT NULL,
PRIMARY KEY (`id`)
#CONSTRAINT `fk_dept_id` FOREIGN KEY (`deptId`) REFERENCES `t_dept` (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1;
使用存储过程+函数添加(了解)
# 创建表
CREATE TABLE `dept` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`deptName` VARCHAR(30) DEFAULT NULL,
`address` VARCHAR(40) DEFAULT NULL,
ceo INT NULL ,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1;
CREATE TABLE `emp` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`empno` INT NOT NULL ,
`name` VARCHAR(20) DEFAULT NULL,
`age` INT(3) DEFAULT NULL,
`deptId` INT(11) DEFAULT NULL,
PRIMARY KEY (`id`)
#CONSTRAINT `fk_dept_id` FOREIGN KEY (`deptId`) REFERENCES `t_dept` (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1;
# 创建函数
-- 查看mysql是否允许创建函数:
SHOW VARIABLES LIKE 'log_bin_trust_function_creators';
-- 命令开启:允许创建函数设置:(global-所有session都生效)
SET GLOBAL log_bin_trust_function_creators=1;
-- 随机产生字符串
DELIMITER $$
CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255)
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 $$
-- 用于随机产生区间数字
DELIMITER $$
CREATE FUNCTION rand_num (from_num INT ,to_num INT) RETURNS INT(11)
BEGIN
DECLARE i INT DEFAULT 0;
SET i = FLOOR(from_num +RAND()*(to_num -from_num+1));
RETURN i;
END$$
# 创建存储过程
-- 插入员工数据
DELIMITER $$
CREATE PROCEDURE insert_emp(START INT, max_num INT)
BEGIN
DECLARE i INT DEFAULT 0;
#set autocommit =0 把autocommit设置成0
SET autocommit = 0;
REPEAT
SET i = i + 1;
INSERT INTO emp (empno, NAME, age, deptid ) VALUES ((START+i) ,rand_string(6), rand_num(30,50), rand_num(1,10000));
UNTIL i = max_num
END REPEAT;
COMMIT;
END$$
-- 插入部门数据
DELIMITER $$
CREATE PROCEDURE insert_dept(max_num INT)
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit = 0;
REPEAT
SET i = i + 1;
INSERT INTO dept ( deptname,address,ceo ) VALUES (rand_string(8),rand_string(10),rand_num(1,500000));
UNTIL i = max_num
END REPEAT;
COMMIT;
END$$
-- 批量删除某个表上的所有索引 建议使用可视化工具执行
DELIMITER $$
CREATE PROCEDURE `proc_drop_index`(dbname VARCHAR(200),tablename VARCHAR(200))
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE ct INT DEFAULT 0;
DECLARE _index VARCHAR(200) DEFAULT '';
DECLARE _cur CURSOR FOR SELECT index_name FROM information_schema.STATISTICS WHERE table_schema=dbname AND table_name=tablename AND seq_in_index=1 AND index_name <>'PRIMARY' ;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done=2 ;
OPEN _cur;
FETCH _cur INTO _index;
WHILE _index<>'' DO
SET @str = CONCAT("drop index ",_index," on ",tablename );
PREPARE sql_str FROM @str ;
EXECUTE sql_str;
DEALLOCATE PREPARE sql_str;
SET _index='';
FETCH _cur INTO _index;
END WHILE;
CLOSE _cur;
END$$
# 调用存储过程
-- 执行存储过程,往dept表添加1万条数据
CALL insert_dept(10000);
-- 执行存储过程,往emp表添加50万条数据,编号从100000开始
CALL insert_emp(100000,500000);
单表查询优化(背)

-- 创建索引
CREATE INDEX idx_name ON emp(`name`);
EXPLAIN SELECT * FROM emp WHERE emp.age=12 and name='lucy';
EXPLAIN SELECT * FROM emp WHERE emp.name=123;
#### 单表查询优化
# 计算或者函数导致索引失效
EXPLAIN SELECT * FROM emp WHERE LEFT(emp.name,3) = 'abc';
# LIKE以%开头索引失效
EXPLAIN SELECT * FROM emp WHERE emp.name like '%abc%';
# 不等于(!= 或者<>)索引失效
EXPLAIN SELECT * FROM emp WHERE emp.name <> 'abc' ;
# is not null索引失效
EXPLAIN SELECT * FROM emp WHERE emp.name IS NOT NULL;
# 类型转换导致索引失效
EXPLAIN SELECT * FROM emp WHERE name= 123;
# 最佳左前缀法则
CALL proc_drop_index("atguigudb","emp");
-- 创建索引 (4 + 4 + 20*3+2) + 3 = 73
CREATE INDEX idx_age_deptid_name ON emp(age,deptid,`name`);
-- 1. 先看索引上字段的类型+长度。比如:int=4 ; varchar(20) =20 ; char(20) =20
-- 2. 如果是varchar或者char这种字符串字段,视字符集要乘不同的值,
-- 比如utf8mb3要乘 3(MySQL5.7),如果是utf8mb4要乘4,GBK要乘2
-- 3. varchar这种动态字符串要加2个字节
-- 4. 允许为空的字段要加1个字节
EXPLAIN SELECT * FROM emp WHERE emp.deptid=1
AND emp.name = 'abcd'
AND emp.age = 30;
-- EXPLAIN结果:
-- 索引查找的顺序为 age、deptid、name,匹配所有索引字段
EXPLAIN SELECT * FROM emp WHERE emp.age = 30
AND emp.deptid=1
AND emp.name = 'abcd';
-- EXPLAIN结果:
-- 索引查找的顺序为 age、deptid、name,匹配所有索引字段
EXPLAIN SELECT * FROM emp WHERE emp.deptid=1 AND emp.name = 'abcd';
-- EXPLAIN结果:
-- type: ALL, 执行了全表扫描
-- key_len: NULL, 索引失效
-- 索引查找的顺序为 age、deptid、name,查询条件中不包含age,无法使用整个索引
EXPLAIN SELECT * FROM emp WHERE emp.name = 'abcd' and emp.age=30
-- EXPLAIN结果:
-- key_len:5 只使用了age索引
-- 索引查找的顺序为 age、deptid、name,查询条件中不包含deptid,无法使用deptid和name索引
SHOW VARIABLES LIKE '%char%';
# 全值匹配,条件有几个字段,为这些字段创建索引,复合索引优于单值索引
# 索引中范围条件右边的列失效
CALL proc_drop_index("atguigudb","emp");
create index idx_name on emp(age,deptId,`name`)
-- 把范围字段放到索引最右边
create index idx_name1 on emp(age,`name`,deptId)
EXPLAIN SELECT * FROM emp WHERE emp.age=30
AND emp.deptId>1000
AND emp.name = 'abc';
多表查询优化(背)
1、数据准备
-- 分类
CREATE TABLE IF NOT EXISTS `class` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
);
-- 图书
CREATE TABLE IF NOT EXISTS `book` (
`bookid` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`bookid`)
);
-- 插入16条记录
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
-- 插入20条记录
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
2、左外连接优化
-- 创建索引
CREATE INDEX idx_class_card ON class(card);
-- 创建索引
CREATE INDEX idx_book_card ON book(card);
EXPLAIN SELECT * FROM class
LEFT JOIN
book
ON class.card = book.card;
# 左外连接创建索引规则
## 左外连接时候,左表是驱动表,右表被驱动表
-- 左表class:驱动表、右表book:被驱动表
-- 驱动表会全表检索,被驱动只是关联数据
## 在被驱动表创建索引
## 把数据量少的表作为驱动表(小表作为驱动表)
3、内连接优化
- 内连接查询,MySQL自动优化,优先把数据量少的表作为驱动表,这个过程自动完成的
其他优化
- 子查询优化
因为子查询会有2趟操作,效率很低,不推荐使用子查询,使用关联查询替代子查询
6、慢查询日志
- **作用:**一种日志记录,查看哪些SQL超出了我们的最大忍耐时间值。
比如设置sql语句最大运行时间5s,某些sql语句运行时间超过了5s,把超过5s语句记录在日志里面
-
**场景:**经常使用在线上环境中,排查接口中sql语句运行慢的问题
-
使用过程:
第一步,MySQL慢查询日志默认没有开启的,如果使用需要开启慢查询日志
第二步,设置sql语句可以接受运行时间,比如3s
第三步,开始测试,执行程序(语句),当某些sql语句运行时间超过3s,这些语句记录到慢查询日志里面
第四步,如果找到运行慢的sql语句,可以使用explain进行分析,进行调优
7、View视图
-
将一段查询sql封装为一个虚拟的表。
-
这个虚拟表只保存了sql逻辑,不会保存任何查询结果。
-
语法 CREATE VIEW 视图名称 AS 查询sql语句
SELECT emp.name, ceo.name AS ceoname
FROM t_emp emp
LEFT JOIN t_dept dept ON emp.deptid = dept.id
LEFT JOIN t_emp ceo ON dept.ceo = ceo.id;
-- 使用视图
select * from v_ceo
8、读写分离和分库分表理论
数据库优化方案
问题:
哪些方法可以进行数据库调优?
解决方案:
1、创建索引,按照规则进行创建
-
频繁作为WHERE查询条件的字段
-
经常GROUP BY 和 ORDER BY的列
-
字段的值有唯一性的限制
-
DISTINCT去重字段需要创建索引
-
多表JOIN时,对连接字段创建索引
-
使用字符串前缀创建索引
-
使用频繁的列,放到联合索引的左侧 (最佳左前缀法则)
2、对sql语句进行优化,包含单表和多表优化
- 单表

- 多表
-- 左外连接,左表驱动表,右表被驱动表,在被驱动表创建索引,把小表作为驱动表
-- 内连接,MySQL自动选择小表作为驱动表
-- 使用关联查询替代子查询
3、搭建数据库集群,采用主从复制+读写分离机制进行优化
4、如果数据过多,考虑进行分库分表(数据库分片)
-- 按照阿里巴巴分库分表原则:单表500W行 或者 单表容量达到2GB
读写分离(主从复制)

概述
-
搭建数据库主从环境,需要有且只有一台主机,可以有多台从机,比如一主两从
-
读写分离:主机里面做写操作,从机里面做读操作
-
要添加数据,在主机里面进行添加,主机添加完成之后,同步到多个从机里面
-
主从复制有缺陷:数据延迟问题
-- 在主机里面添加完成之后,马上到从机里面去读取数据,可能读取不到,因为主机数据可能还没有同步过去
-- 解决方案:
第一种,在主机里面写完之后,等一会再去读
第二种,主机里面写马上去读,在主机写完成之后就在主机进行读操作
数据库分片
- 数据库分片称为分库分表
垂直分片
- 在设计表时候,设计有几个数据库有几个表
垂直分库

垂直分表
-
把一张表里面一部分字段创建一张表,另一部分字段创建另外一张表
-
比如用户基本表和用户详情表,商品基本表和商品详情表

水平分片
水平分表:针对数据存放地方
-
比如有一张表有1w条记录,进行水平拆分,可以创建两张表,每张放其中5000条记录
-
水平分表规则:取模(求余)
-- 根据id % 表数量
-- 假如2张表,任何数字对2取模,值 0 1,0放到第一张表,1 放到第二张表
阿里巴巴Java开发手册:
【推荐】单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。
说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。
- 实现读写分离和分库分表可以有两种方式实现:
第一种,编写程序代码实现
第二种,使用数据库中间件实现,比如ShardingSphere、MyCat
- ShardingSphere主要包含两个产品
-
程序代码封装:ShardingSphere-JDBC
-
中间件封装:ShardingSphere-Proxy