MySQL-视图-触发器-事务-存储过程-函数-流程控制-索引与慢查询优化-06
- 2019 年 10 月 7 日
- 筆記
目录
小科普
核心业务逻辑代码一般都是放在服务端的 客户端容易被懂行点的人修改源码,造成损失
视图***
什么是视图
一个查询语句的结果是虚拟表,将(查询出)这张虚拟表(的sql语句)保存下来,他就变成了一个视图(mysql中还是以表的形式存在的)
为什么要用视图
当频繁需要用到多张表的联表结果,你就可以事先生成好视图,之后直接调用即可,避免了反复写联表操作的 sql 语句(实际效果相当于再次执行语句)
如何生成视图
# 语法 create view 视图名 as 生成虚拟表的查询语句 create view teacher_course as select * from teacher inner join course on teacher.tid = course.teacher_id; show tables # 即可看到该视图(当做表了) select * from teacher_course; # 看到的就是
修改视图 –> 最好(千万)不要

关联表数据改动前

关联表数据改动之后
视图中的数据自动更新了–> 执行查询视图记录语句 = 重新执行了创建视图的那个sql 语句

注意: 1.视图只有表结构,视图中的数据还是来源于原来的表 2.不要改动视图表中的数据(可能会报错,也可能会改其他表的数据) —> 我在cmd终端改动影响到了其他表 3.不要太依赖视图,尽量少用视图来写业务逻辑
- 视图会影响性能,占用硬盘资源、数据库资源
- 工作中可能是别的部门的人管理数据库,跨部门交流比较麻烦,而且如果别人改动了视图关联表,或删了视图那涉及到的业务逻辑就很危险了。
触发器
什么是触发器
到达某个条件自动触发
触发条件
当你在对数据进行增删改的情况下会自动触发触发器的运行
触发器语法结构
修改mysql的默认结束符(;)
delimiter $$ # --> 默认 ; 改成了 $$
,只对当前窗口有效,重新登录也会无效
delimiter $$ # 改变当前窗口sql 语句的结束符 create trigger 触发器的名字 after/before insert/update/delete on 表名 for each row begin # 操作其他表的sql 语句 end $$ delimiter ; # 把sql结束符改回 ; # 触发器常见命名格式:tri_before/after_insert/update/delete_表名
触发器死循环
触发器里面的代码会触发触发器本身的执行,造成了死循环
在触发器里面千万不要写操作本表的语句,增删改查都不要,会报错
比如:user表的 新增前触发器 代码块内写的是 user表的新增插值操作,就会造成触发器死循环(直接报错)
六个触发器的执行时机
新增前、新增后、删除前、删除后、更新前、更新后
小案例(新增后)
create table user( id int primary key auto_increment, name varchar(32) not null, password varchar(255) not null ) create table log( id int primary key auto_increment, message varchar(255) ); drop trigger tri_after_insert_user; # 删除已存在的触发器 tri_after_insert_user delimiter $$ create trigger tri_after_insert_user after insert on user for each row begin # insert into user(name, password) values('老子翻车了', '123'); # 死循环了,像递归,不断触发这个触发器 # select * from user; # 也会报错,触发器里不能返回值 Not allowed to return a result set from a trigger insert into log(message) values ('看到我就说明你没翻车'); end $$ delimiter ; insert into user(name, password) values('会翻车吗', '可能吧'); select * from user; select * from log; # 删除触发器 drop trigger tri_after_insert_user;
扩展: 视图、存储过程的查看
摘抄自:mysql查看存储过程函数
# 查询数据库中的存储过程和函数 select `name` from mysql.proc where db = 'xx' and `type` = 'PROCEDURE' # 存储过程 select `name` from mysql.proc where db = 'xx' and `type` = 'FUNCTION' # 函数 show procedure statusG; # 存储过程(G 竖式排版查看,横的太长了 看不清) show function statusG; # 函数 # 查看存储过程或函数的创建代码 show create procedure proc_nameG; show create function func_nameG; # 查看视图 SELECT * from information_schema.VIEWS; # 视图 SELECT * from information_schema.TABLES; # 表 # 查看触发器 SHOW TRIGGERS [FROM db_name] [LIKE expr] SELECT * FROM triggers T WHERE trigger_name="mytrigger"G;
NEW对象指代的就是当前记录(对象)
NEW 对象可以取到触发这个触发器的sql语句的记录对象,通过 . 字段名
的方式来获取到字段值
# 案例 (mysql大小写不敏感) CREATE TABLE cmd ( id INT PRIMARY KEY auto_increment, USER CHAR (32), priv CHAR (10), cmd CHAR (64), sub_time datetime, #提交时间 success enum ('yes', 'no') #0代表执行失败 ); CREATE TABLE errlog ( id INT PRIMARY KEY auto_increment, err_cmd CHAR (64), err_time datetime ); delimiter $$ # 将mysql默认的结束符由;换成$$ create trigger tri_after_insert_cmd after insert on cmd for each row begin if NEW.success = 'no' then # 新记录都会被MySQL封装成NEW对象 insert into errlog(err_cmd,err_time) values(NEW.cmd,NEW.sub_time); end if; end $$ delimiter ; # 结束之后记得再改回来,不然后面结束符就都是$$了 #往表cmd中插入记录,触发触发器,根据IF的条件决定是否插入错误日志 INSERT INTO cmd ( USER, priv, cmd, sub_time, success ) VALUES ('egon','0755','ls -l /etc',NOW(),'yes'), ('egon','0755','cat /etc/passwd',NOW(),'no'), ('egon','0755','useradd xxx',NOW(),'no'), ('egon','0755','ps aux',NOW(),'yes'); # 查询errlog表记录 select * from errlog; # 删除触发器 drop trigger tri_after_insert_cmd;

事务 *****
什么是事务
事务包含一堆sql语句,要么全部成功,要么都不成功
事务的四大特性 ACID
用自己的话背下来
A:原子性 atomicity
一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。
C:一致性 consistency
事务必须是使数据库从一个一致性状态变到另一个一致性状态。 一致性与原子性是密切相关的。
I:隔离性 isolation
一个事务的执行不能被其他事务干扰。 即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
D:持久性 durability
持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。 接下来的其他操作或故障不应该对其有任何影响。
如何开启事务
start transaction
标志下面的语句都是 事务
事务开始后,只会在内存中修改
只有commit 之后才会写到硬盘上
事务回滚
rollback
事务开始的地方到这里的语句都会回滚(失效)
永久性更改
commit
把数据刷在硬盘上,后面再 rollback
就回滚不回去了
start transaction、rollback、commit 有点像python异常捕获的 try … except … else …
小案例
create table user( id int primary key auto_increment, name char(32), balance int ); insert into user(name,balance) values ('wsb',1000), ('egon',1000), ('ysb',1000); # 修改数据之前先开启事务操作 start transaction; # 修改操作 update user set balance=900 where name='wsb'; #买支付100元 update user set balance=1010 where name='egon'; #中介拿走10元 update user set balance=1090 where name='ysb'; #卖家拿到90元 # 回滚到上一个状态 rollback; # 开启事务之后,只要没有执行commit操作,数据其实都没有真正刷新到硬盘 commit; """开启事务检测操作是否完整,不完整主动回滚到上一个状态,如果完整就应该执行commit操作""" # 站在python代码的角度,应该实现的伪代码逻辑, try: update user set balance=900 where name='wsb'; #买支付100元 update user set balance=1010 where name='egon'; #中介拿走10元 update user set balance=1090 where name='ysb'; #卖家拿到90元 except 异常: rollback; else: commit; # 那如何检测异常?
存储过程
什么是存储过程
就类似于python中的自定义函数
内部封装了 sql 语句,后续想要实现相应的操作,只需要调用存储过程即可
如何创建存储过程
语法结构
# 无参数版 delimiter $$ # 改mysql的结束符 create procedure 存储结构名字() begin sql 语句; end delimiter ; # 改回来 call 存储结构名字() # 调用写好的存储过程 # 有参数版 delimiter $$ # 改mysql的结束符 create procedure 存储结构名字( in m int, # in 只能传进来,不能返回 in n int, # 参数对应的意思---> 进还是出, 变量名, 数据类型 out res int, # out 只能返回,不能传 inout xxx int, # inout 可以传进来,也可以被返回 ) begin sql 语句; end delimiter ; # 改回来 call 存储结构名字(m, n) # 参数怎么传不知道。。
案例
存储过程在哪个库里定义就只能在哪个库里面使用
定义存储过程
delimiter $$ create procedure p1( in m int, # in 只能传进来,不能被返回 in n int, # 参数对应的意思---> 进还是出, 变量名, 数据类型 out res int # out 只能被返回,不能传入 ) begin select tname from teacher where tid > m and tid < n; set res=0; # 就类似于一个标志位,用来标识存储器是否执行成功 end $$ delimiter ; show procedure statusG; # 查看存储过程(竖式展示排版)
在mysql中调用存储过程
存变量(设置初始值)
set @res=10; # 设置全局变量 @res 等于10
,select @res; # 查看全局变量 @res 的值

调用
call p1(1,5,@res);
将变量 @res
传入,之后可以通过 select @res
来查看存储过程执行完成后的返回结果

在pymysql中调用存储过程
# 使用的是上一步创建的存储过程 import pymysql conn = pymysql.connect( host='127.0.0.1', port=3306, user='root', password='000000', database='day38', charset='utf8', autocommit=True, ) cursor = conn.cursor(pymysql.cursors.DictCursor) # -------------------------------------------- # cursor.callproc() 调用存储过程 # 内部自动用变量名存储对应值(看下面案例注释) # -------------------------------------------- cursor.callproc('p1', (1, 5, 10)) # 这里就不需要设置那个全局变量了(@res=10),内部自动用变量名存储了对应的值 print(cursor.fetchall()) # [{'tname': '李平老师'}, {'tname': '刘海燕老师'}, {'tname': '朱云海老师'}] ''' callproc 内部自动用变量名存储了对应的值 @_p1_0=1 @_p1_1=5 @_p1_2=10 # 自动取名规律: @_存储过程名_标号 ''' cursor.execute('select @_p1_0=1') print(cursor.fetchall()) # [{'@_p1_0=1': 1}] cursor.execute('select @_p1_1=5') print(cursor.fetchall()) # [{'@_p1_1=5': 1}] cursor.execute('select @_p1_2=10') print(cursor.fetchall()) # [{'@_p1_2=10': 0}]

案例– 使用存储过程监测事务
监测一个事务是否成功,通常使用存储过程包起来
# 大前提:存储过程在哪个库下面创建的只能在对应的库下面才能使用!!! delimiter // create PROCEDURE p5( OUT p_return_code tinyint ) BEGIN DECLARE exit handler for sqlexception BEGIN -- ERROR set p_return_code = 1; rollback; END; DECLARE exit handler for sqlwarning BEGIN -- WARNING set p_return_code = 2; rollback; END; START TRANSACTION; update user set balance=900 where id =1; update user123 set balance=1010 where id = 2; update user set balance=1090 where id =3; COMMIT; -- SUCCESS set p_return_code = 0; #0代表执行成功 END // delimiter ;
函数
注意与存储过程的区别,mysql内置的函数只能在sql语句中使用!
参考博客:函数
MySQL内置函数
常见函数及练习
一、数学函数 ROUND(x,y) 返回参数x的四舍五入的有y位小数的值 RAND() 返回0到1内的随机值,可以通过提供一个参数(种子)使RAND()随机数生成器生成一个指定的值。 二、聚合函数(常用于GROUP BY从句的SELECT查询中) AVG(col)返回指定列的平均值 COUNT(col)返回指定列中非NULL值的个数 MIN(col)返回指定列的最小值 MAX(col)返回指定列的最大值 SUM(col)返回指定列的所有值之和 GROUP_CONCAT(col) 返回由属于一组的列值连接组合而成的结果 三、字符串函数 CHAR_LENGTH(str) 返回值为字符串str 的长度,长度的单位为字符。一个多字节字符算作一个单字符。 CONCAT(str1,str2,...) 字符串拼接 如有任何一个参数为NULL ,则返回值为 NULL。 CONCAT_WS(separator,str1,str2,...) 字符串拼接(自定义连接符) CONCAT_WS()不会忽略任何空字符串。 (然而会忽略所有的 NULL)。 CONV(N,from_base,to_base) 进制转换 例如: SELECT CONV('a',16,2); 表示将 a 由16进制转换为2进制字符串表示 FORMAT(X,D) 将数字X 的格式写为'#,###,###.##',以四舍五入的方式保留小数点后 D 位, 并将结果以字符串的形式返回。若 D 为 0, 则返回结果不带有小数点,或不含小数部分。 例如: SELECT FORMAT(12332.1,4); 结果为: '12,332.1000' INSERT(str,pos,len,newstr) 在str的指定位置插入字符串 pos:要替换位置其实位置 len:替换的长度 newstr:新字符串 特别的: 如果pos超过原字符串长度,则返回原字符串 如果len超过原字符串长度,则由新字符串完全替换 INSTR(str,substr) 返回字符串 str 中子字符串的第一个出现位置。 LEFT(str,len) 返回字符串str 从开始的len位置的子序列字符。 LOWER(str) 变小写 UPPER(str) 变大写 REVERSE(str) 返回字符串 str ,顺序和字符顺序相反。 SUBSTRING(str,pos) , SUBSTRING(str FROM pos) SUBSTRING(str,pos,len) , SUBSTRING(str FROM pos FOR len) 不带有len 参数的格式从字符串str返回一个子字符串,起始于位置 pos。带有len参数的格式从字符串str返回一个长度同len字符相同的子字符串,起始于位置 pos。 使用 FROM的格式为标准 SQL 语法。也可能对pos使用一个负值。假若这样,则子字符串的位置起始于字符串结尾的pos 字符,而不是字符串的开头位置。在以下格式的函数中可以对pos 使用一个负值。 mysql> SELECT SUBSTRING('Quadratically',5); -> 'ratically' mysql> SELECT SUBSTRING('foobarbar' FROM 4); -> 'barbar' mysql> SELECT SUBSTRING('Quadratically',5,6); -> 'ratica' mysql> SELECT SUBSTRING('Sakila', -3); -> 'ila' mysql> SELECT SUBSTRING('Sakila', -5, 3); -> 'aki' mysql> SELECT SUBSTRING('Sakila' FROM -4 FOR 2); -> 'ki' 四、日期和时间函数 CURDATE()或CURRENT_DATE() 返回当前的日期 CURTIME()或CURRENT_TIME() 返回当前的时间 DAYOFWEEK(date) 返回date所代表的一星期中的第几天(1~7) DAYOFMONTH(date) 返回date是一个月的第几天(1~31) DAYOFYEAR(date) 返回date是一年的第几天(1~366) DAYNAME(date) 返回date的星期名,如:SELECT DAYNAME(CURRENT_DATE); FROM_UNIXTIME(ts,fmt) 根据指定的fmt格式,格式化UNIX时间戳ts HOUR(time) 返回time的小时值(0~23) MINUTE(time) 返回time的分钟值(0~59) MONTH(date) 返回date的月份值(1~12) MONTHNAME(date) 返回date的月份名,如:SELECT MONTHNAME(CURRENT_DATE); NOW() 返回当前的日期和时间 QUARTER(date) 返回date在一年中的季度(1~4),如SELECT QUARTER(CURRENT_DATE); WEEK(date) 返回日期date为一年中第几周(0~53) YEAR(date) 返回日期date的年份(1000~9999) 重点: DATE_FORMAT(date,format) 根据format字符串格式化date值 mysql> SELECT DATE_FORMAT('2009-10-04 22:23:00', '%W %M %Y'); -> 'Sunday October 2009' mysql> SELECT DATE_FORMAT('2007-10-04 22:23:00', '%H:%i:%s'); -> '22:23:00' mysql> SELECT DATE_FORMAT('1900-10-04 22:23:00', -> '%D %y %a %d %m %b %j'); -> '4th 00 Thu 04 10 Oct 277' mysql> SELECT DATE_FORMAT('1997-10-04 22:23:00', -> '%H %k %I %r %T %S %w'); -> '22 22 10 10:23:00 PM 22:23:00 00 6' mysql> SELECT DATE_FORMAT('1999-01-01', '%X %V'); -> '1998 52' mysql> SELECT DATE_FORMAT('2006-06-00', '%d'); -> '00' 五、加密函数 MD5() 计算字符串str的MD5校验和 PASSWORD(str) 返回字符串str的加密版本,这个加密过程是不可逆转的,和UNIX密码加密过程使用不同的算法。 六、控制流函数 CASE WHEN[test1] THEN [result1]...ELSE [default] END 如果testN是真,则返回resultN,否则返回default CASE [test] WHEN[val1] THEN [result]...ELSE [default]END 如果test和valN相等,则返回resultN,否则返回default IF(test,t,f) 如果test是真,返回t;否则返回f IFNULL(arg1,arg2) 如果arg1不是空,返回arg1,否则返回arg2 NULLIF(arg1,arg2) 如果arg1=arg2返回NULL;否则返回arg1 七、控制流函数小练习 #7.1、准备表 /* Navicat MySQL Data Transfer Source Server : localhost_3306 Source Server Version : 50720 Source Host : localhost:3306 Source Database : student Target Server Type : MYSQL Target Server Version : 50720 File Encoding : 65001 Date: 2018-01-02 12:05:30 */ SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for course -- ---------------------------- DROP TABLE IF EXISTS `course`; CREATE TABLE `course` ( `c_id` int(11) NOT NULL, `c_name` varchar(255) DEFAULT NULL, `t_id` int(11) DEFAULT NULL, PRIMARY KEY (`c_id`), KEY `t_id` (`t_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of course -- ---------------------------- INSERT INTO `course` VALUES ('1', 'python', '1'); INSERT INTO `course` VALUES ('2', 'java', '2'); INSERT INTO `course` VALUES ('3', 'linux', '3'); INSERT INTO `course` VALUES ('4', 'web', '2'); -- ---------------------------- -- Table structure for score -- ---------------------------- DROP TABLE IF EXISTS `score`; CREATE TABLE `score` ( `id` int(11) NOT NULL AUTO_INCREMENT, `s_id` int(10) DEFAULT NULL, `c_id` int(11) DEFAULT NULL, `num` double DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of score -- ---------------------------- INSERT INTO `score` VALUES ('1', '1', '1', '79'); INSERT INTO `score` VALUES ('2', '1', '2', '78'); INSERT INTO `score` VALUES ('3', '1', '3', '35'); INSERT INTO `score` VALUES ('4', '2', '2', '32'); INSERT INTO `score` VALUES ('5', '3', '1', '66'); INSERT INTO `score` VALUES ('6', '4', '2', '77'); INSERT INTO `score` VALUES ('7', '4', '1', '68'); INSERT INTO `score` VALUES ('8', '5', '1', '66'); INSERT INTO `score` VALUES ('9', '2', '1', '69'); INSERT INTO `score` VALUES ('10', '4', '4', '75'); INSERT INTO `score` VALUES ('11', '5', '4', '66.7'); -- ---------------------------- -- Table structure for student -- ---------------------------- DROP TABLE IF EXISTS `student`; CREATE TABLE `student` ( `s_id` varchar(20) NOT NULL, `s_name` varchar(255) DEFAULT NULL, `s_age` int(10) DEFAULT NULL, `s_sex` char(1) DEFAULT NULL, PRIMARY KEY (`s_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of student -- ---------------------------- INSERT INTO `student` VALUES ('1', '鲁班', '12', '男'); INSERT INTO `student` VALUES ('2', '貂蝉', '20', '女'); INSERT INTO `student` VALUES ('3', '刘备', '35', '男'); INSERT INTO `student` VALUES ('4', '关羽', '34', '男'); INSERT INTO `student` VALUES ('5', '张飞', '33', '女'); -- ---------------------------- -- Table structure for teacher -- ---------------------------- DROP TABLE IF EXISTS `teacher`; CREATE TABLE `teacher` ( `t_id` int(10) NOT NULL, `t_name` varchar(50) DEFAULT NULL, PRIMARY KEY (`t_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of teacher -- ---------------------------- INSERT INTO `teacher` VALUES ('1', '大王'); INSERT INTO `teacher` VALUES ('2', 'alex'); INSERT INTO `teacher` VALUES ('3', 'egon'); INSERT INTO `teacher` VALUES ('4', 'peiqi'); #7.2、统计各科各分数段人数.显示格式:课程ID,课程名称,[100-85],[85-70],[70-60],[ <60] select score.c_id, course.c_name, sum(CASE WHEN num BETWEEN 85 and 100 THEN 1 ELSE 0 END) as '[100-85]', sum(CASE WHEN num BETWEEN 70 and 85 THEN 1 ELSE 0 END) as '[85-70]', sum(CASE WHEN num BETWEEN 60 and 70 THEN 1 ELSE 0 END) as '[70-60]', sum(CASE WHEN num < 60 THEN 1 ELSE 0 END) as '[ <60]' from score,course where score.c_id=course.c_id GROUP BY score.c_id;
date_format() 函数(需掌握)
CREATE TABLE blog ( id INT PRIMARY KEY auto_increment, NAME CHAR (32), sub_time datetime ); INSERT INTO blog (NAME, sub_time) VALUES ('第1篇','2015-03-01 11:31:21'), ('第2篇','2015-03-11 16:31:21'), ('第3篇','2016-07-01 10:21:31'), ('第4篇','2016-07-22 09:23:21'), ('第5篇','2016-07-23 10:11:11'), ('第6篇','2016-07-25 11:21:31'), ('第7篇','2017-03-01 15:33:21'), ('第8篇','2017-03-01 17:32:21'), ('第9篇','2017-03-01 18:31:21'); select date_format(sub_time,'%Y-%m'),count(id) from blog group by date_format(sub_time,'%Y-%m');
自定义函数
注意
- 函数中不要写sql语句(否则会报错),函数仅仅只是一个功能,是一个在sql中被应用的功能
- 若要想在begin…end…中写sql,请用存储过程
delimiter // create function f1( i1 int, i2 int) returns int BEGIN declare num int; set num = i1 + i2; return(num); END // delimiter ; # 在查询中使用函数 select f1(11,nid) ,name from tb2; # 删除函数 drop function func_name;
流程控制
if 条件语句
# if条件语句 delimiter // # 修改mysql 默认的语句结束符 CREATE PROCEDURE proc_if () BEGIN declare i int default 0; if i = 1 THEN SELECT 1; ELSEIF i = 2 THEN SELECT 2; ELSE SELECT 7; END IF; END // delimiter ; # 将sql语句默认结束符改回 ;
while 循环
# while循环 delimiter // CREATE PROCEDURE proc_while () BEGIN DECLARE num INT ; SET num = 0 ; WHILE num < 10 DO SELECT num ; SET num = num + 1 ; END WHILE ; END // delimiter ;
索引与慢查询优化 **
mysql 默认有查询优化机制,我们不需要再上面花多少精力,能优化的,mysql基本都直接给我们优化了(可适当增加几个索引)
详细内容参考egon 的博客
第八篇:索引原理与慢查询优化 ,写的很详细,案例也很充分,我这个写的不好?
本篇仅作快速了解,第一遍最好是去看 egon 的博客(能有个详细的了解)感觉写的着实不错,直接复制过来了。。。
前言(摘抄)
索引是应用程序设计和开发的一个重要方面。
若索引太多,应用程序的性能可能会受到影响,而索引太少,对查询性能又会产生影响,要找到一个平衡点,这对应用程序的性能至关重要。
一些开发人员总是在事后才想起添加索引—-我一直认为,这源于一种错误的开发模式,如果知道数据的使用,从一开始就应该在需要处添加索引。
开发人员往往对数据库的使用停留在应用的层面,比如编写SQL语句、存储过程之类,他们甚至可能不知道索引的存在,或认为事后让相关DBA加上即可,但DBA往往不够了解业务的数据流,而添加索引需要通过监控大量的SQL语句进而从中找到问题,这个步骤所需的时间肯定是远大于初始添加索引所需的时间,并且可能会遗漏一部分的索引。
当然索引也并不是越多越好,我曾经遇到过这样一个问题: 某台MySQL服务器io stat显示磁盘使用率一直处于100%,经过分析后发现是由于开发人员添加了太多的索引,在删除一些不必要的索引之后,磁盘使用率马上下降为20%。 可见索引的添加也是非常有技术含量的。
这一块了解个大概即可,能在写sql语句时稍微考虑下性能就可以了,毕竟我是要搞开发的呀~
索引
索引的目的在于提高查询效率,与我们查阅图书所用的目录是一个道理:先定位到章,然后定位到该章下的一个小节,然后找到页数。
索引在MySQL中也叫“键 key”, 是存储引擎用于快速找到记录的一种数据结构
扩展阅读
索引原理
数据库比起查字典,飞机航班等显然要复杂的多,因为不仅面临着等值查询,还有范围查询(>、<、between、in)、模糊查询(like)、并集查询(or)等等。 那数据库应该选择怎么样的方式来应对所有的问题呢?我们回想字典的例子,能不能把数据分成段,然后分段查询呢? 最简单的如果1000条数据,1到100分成第一段,101到200分成第二段,201到300分成第三段……这样查第250条数据,只要找第三段就可以了,一下子去除了90%的无效数据。 但如果是1千万的记录呢,分成几段比较好?稍有算法基础的同学会想到搜索树,其平均复杂度是lgN,具有不错的查询性能。 但这里我们忽略了一个关键的问题,复杂度模型是基于每次相同的操作成本来考虑的。 而数据库实现比较复杂,一方面数据是保存在磁盘上的,另外一方面为了提高性能,每次又可以把部分数据读入内存来计算,因为我们知道访问磁盘的成本大概是访问内存的十万倍左右,所以简单的搜索树难以满足复杂的应用场景。
磁盘IO与预读
前面提到了访问磁盘,那么这里先简单介绍一下磁盘IO和预读。 磁盘读取数据靠的是机械运动,每次读取数据花费的时间可以分为寻道时间、旋转延迟、传输时间三个部分, 寻道时间指的是磁臂移动到指定磁道所需要的时间,主流磁盘一般在5ms以下; 旋转延迟就是我们经常听说的磁盘转速,比如一个磁盘7200转,表示每分钟能转7200次,也就是说1秒钟能转120次,旋转延迟就是1/120/2 = 4.17ms; 传输时间指的是从磁盘读出或将数据写入磁盘的时间,一般在零点几毫秒,相对于前两个时间可以忽略不计。 那么访问一次磁盘的时间,即一次磁盘IO的时间约等于5+4.17 = 9ms左右,听起来还挺不错的,但要知道一台500 -MIPS(Million Instructions Per Second)的机器每秒可以执行5亿条指令,因为指令依靠的是电的性质,换句话说执行一次IO的时间可以执行约450万条指令,数据库动辄十万百万乃至千万级数据,每次9毫秒的时间,显然是个灾难。 下图是计算机硬件延迟的对比图,供大家参考:

考虑到磁盘IO是非常高昂的操作,计算机操作系统做了一些优化,当一次IO时,不光把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区内,因为局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。 每一次IO读取的数据我们称之为一页(page)。具体一页有多大数据跟操作系统有关,一般为4k或8k,也就是我们读取一页内的数据时候,实际上才发生了一次IO,这个理论对于索引的数据结构设计非常有帮助。
常见索引
- primary key 主键索引
- unique key 唯一性索引
- index key 普通索引
- 联合索引(上面三个展开),下面的联合索引有介绍
上述三个键都可以加快查询,primary key 和 unique key 除索引外还有额外的约束 外键是用来创建表与表之间关联关系的,不算索引
索引一定是自己建的(key),普通字段没有 索引(之前我理解错了)
索引的本质
通过不断地缩小想要获取数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺序的事件,也就是说,有了这种索引机制,我们可以总是用同一种查找方式来锁定数据。
索引的缺点
- 在表中有大量数据时,创建索引速度会很慢
- 在索引创建完毕后,对表的查询性能会大幅度提升
- 往创建好索引的表(有大量数据)里插入数据会变得非常慢(更新索引导致慢)
小结
虽然索引好用,但应该在合理范围内去用,并不是越多越好
索引的数据结构 — B+ 树
前面讲了索引的基本原理,数据库的复杂性,又讲了操作系统的相关知识,目的就是让大家了解,任何一种数据结构都不是凭空产生的,一定会有它的背景和使用场景,我们现在总结一下:
我们需要这种数据结构能够做些什么,其实很简单,那就是:每次查找数据时把磁盘IO次数控制在一个很小的数量级,最好是常数数量级。
那么我们就想到如果一个高度可控的多路搜索树是否能满足需求呢?
就这样,b+树应运而生(B+树是通过二叉查找树,再由平衡二叉树,B树演化而来)。

如上图,是一颗b+树,关于b+树的定义可以参见B+树,这里只说一些重点.
浅蓝色的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据项(深蓝色所示)和指针(黄色所示),
如磁盘块1包含数据项17和35,包含指针P1、P2、P3,P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。
真实的数据存在于叶子节点即3、5、9、10、13、15、28、29、36、60、75、79、90、99。
非叶子节点只不存储真实的数据,只存储指引搜索方向的数据项,如17、35并不真实存在于数据表中。
b+树的查找过程
如图所示,如果要查找数据项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,显然成本非常非常高。
案例:查 71

b+树性质
索引字段要尽量的小
通过上面的分析,我们知道IO次数取决于b+数的高度h,假设当前数据表的数据为N,每个磁盘块的数据项的数量是m,则有h=㏒(m+1)N,当数据量N一定的情况下,m越大,h越小;而m = 磁盘块的大小 / 数据项的大小,磁盘块的大小也就是一个数据页的大小,是固定的,如果数据项占的空间越小,数据项的数量越多,树的高度越低。
这就是为什么每个数据项,即索引字段都要尽量的小,比如int占4字节,要比bigint8字节少一半。这也是为什么b+树要求把真实的数据放到叶子节点而不是内层节点,一旦放到内层节点,磁盘块的数据项会大幅度下降,导致树增高。当数据项等于1时将会退化成线性表。
索引的最左匹配特性
当b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+数是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了, 这个是非常重要的性质,即索引的最左匹配特性。
只有叶子结点存放真实数据,根和树枝节点存的仅仅是虚拟数据
查询次数由树的层级决定,层级越低次数越少查询速度越快(磁盘块存的数据越多层级越少越容易拿到数据)
————> 这也是把 id 作为主键的原因
一个磁盘块儿的大小是一定的,那也就意味着能存的数据量是一定的。
如何保证树的层级最低呢?一个磁盘块儿存放占用空间比较小的数据项
聚集索引非聚集索引
索引也有不同的种类,按不同的要求去分(不单单是一个字段作为索引)
聚集索引(primary key)
聚集索引其实指的就是表的主键(一般都是 id字段)
InnoDB 引擎规定一张表中必须要有主键 InnoDB 在建表的时候对应到硬盘上是两个文件,
.frm 表结构文件
只存放表结构,不可能放索引,也就意味着 InnoDB 的索引跟数据都放在.ibd 表数据文件
中
聚集索引特点:叶子结点放的是一条条完整的记录
辅助索引(unique,index)
查询数据的时候不可能都是用 id 作为筛选条件,也可能会用 id 之外的 name,password 等字段信息,那么这个时候就无法利用到聚集索引的加速查询优势。
此时就需要给其他字段建立(的)索引,这些索引就叫辅助索引
辅助索引特点:叶子结点存放的是辅助索引字段对应的那条记录的主键的值(比如:按照name字段创建索引,那么叶子节点存放的是:{name对应的值:name所在的那条记录的主键值})找到后再拿着id 去聚集索引里面去查

聚集索引和非聚集索引的不同
叶子结点存放的是否是一整行的信息
覆盖索引
InnoDB存储引擎支持覆盖索引(covering index,或称索引覆盖) 使用覆盖索引的一个好处是:辅助索引不包含整行记录的所有信息,故其大小要远小于聚集索引,因此可以减少大量的IO操作
select name from user where name='jason';
覆盖索引:只在辅助索引的叶子节点中就已经找到了所有我们想要的数据(条件和要查的字段相同的时候)
where 条件后面的字段作为辅助索引,select 后面的字段正好是(没有多余的字段)要找的数据,如果辅助索引和要找的数据一样,那就找到了,就是覆盖索引 –> 个人解释,没有权威性
非覆盖索引
select age from user where name='jason';
虽然查询的时候查到了辅助索引name,但是要查的是age字段,所以查到name字段后还需要利用查到的聚集索引id 才去查找那条记录中 age 字段的值
联合索引
- primary key(host, port) 联合主键索引(一般不用联合主键,主键一般都是专门的 id 字段)
- unique(host, port) 联合唯一索引
- index(host, port) 联合普通索引
select count(id) from s1 where name='jason' and gender = 'male' and id > 3 and email = 'xxx'; # 如果上述四个字段区分度都很高,那给谁建都能加速查询 # 给email加然而不用email字段 select count(id) from s1 where name='jason' and gender = 'male' and id > 3; # 给name加然而不用name字段 select count(id) from s1 where gender = 'male' and id > 3; # 给gender加然而不用gender字段 select count(id) from s1 where id > 3; # 带来的问题是所有的字段都建了索引然而都没有用到,还需要花费四次建立的时间 create index idx_all on s1(email,name,gender,id); # 最左匹配原则,区分度高的往左放 select count(id) from s1 where name='jason' and gender = 'male' and id > 3 and email = 'xxx'; # 速度变快
慢查询优化
查询优化神器-explain 通过分析sql语句来提升效率
设定一个时间检测所有超出改时间的sql语句,然后针对性的进行优化!
根据使用场景管理并设置合适的索引
注意这块的小点能提高sql语句性能
摘自egon博客:,放自己博客方便看
测试索引
数据准备
#1. 准备表 create table s1( id int, name varchar(20), gender char(6), email varchar(50) ); #2. 创建存储过程,实现批量插入记录 delimiter $$ #声明存储过程的结束符号为$$ create procedure auto_insert1() BEGIN declare i int default 1; while(i<3000000)do insert into s1 values(i,'egon','male',concat('egon',i,'@oldboy')); set i=i+1; end while; END$$ #$$结束 delimiter ; #重新声明分号为结束符号 #3. 查看存储过程 show create procedure auto_insert1G #4. 调用存储过程 call auto_insert1();
在没有索引的前提下测试查询速度
#无索引:mysql根本就不知道到底是否存在id等于333333333的记录,只能把数据表从头到尾扫描一遍,此时有多少个磁盘块就需要进行多少IO操作,所以查询速度很慢 mysql> select * from s1 where id=333333333; Empty set (0.33 sec)
在表中已经存在大量数据的前提下,为某个字段段建立索引,建立速度会很慢

在索引建立完毕后,以该字段为查询条件时,查询速度提升明显

1. mysql先去索引表里根据b+树的搜索原理很快搜索到id等于333333333的记录不存在,IO大大降低,因而速度明显提升 2. 我们可以去mysql的data目录下找到该表,可以看到占用的硬盘空间多了 3.需要注意,如下图

总结
#1. 一定是为搜索条件的字段创建索引,比如select * from s1 where id = 333;就需要为id加上索引 #2. 在表中已经有大量数据的情况下,建索引会很慢,且占用硬盘空间,建完后查询速度加快 比如create index idx on s1(id);会扫描表中所有的数据,然后以id为数据项,创建索引结构,存放于硬盘的表中。 建完以后,再查询就会很快了。 #3. 需要注意的是:innodb表的索引会存放于s1.ibd文件中,而myisam表的索引则会有单独的索引文件table1.MYI MySAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而在innodb中,表数据文件本身就是按照B+Tree(BTree即Balance True)组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此innodb表数据文件本身就是主索引。 因为inndob的数据文件要按照主键聚集,所以innodb要求表必须要有主键(Myisam可以没有),如果没有显式定义,则mysql系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则mysql会自动为innodb表生成一个隐含字段作为主键,这字段的长度为6个字节,类型为长整型.
正确使用索引
索引未命中
并不是说我们创建了索引就一定会加快查询速度, 若想利用索引达到预想的提高查询速度的效果,我们在添加索引时,必须遵循以下问题
范围问题
范围问题,或者说条件不明确,条件中出现这些符号或关键字:>、>=、<、<=、!= 、between…and…、like、
大于号、小于号

不等于!=

between …and…

like

建立索引的字段选择
尽量选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条记录
#先把表中的索引都删除,让我们专心研究区分度的问题 mysql> desc s1; +--------+-------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +--------+-------------+------+-----+---------+-------+ | id | int(11) | YES | MUL | NULL | | | name | varchar(20) | YES | | NULL | | | gender | char(5) | YES | | NULL | | | email | varchar(50) | YES | MUL | NULL | | +--------+-------------+------+-----+---------+-------+ rows in set (0.00 sec) mysql> drop index a on s1; Query OK, 0 rows affected (0.20 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> drop index d on s1; Query OK, 0 rows affected (0.18 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> desc s1; +--------+-------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +--------+-------------+------+-----+---------+-------+ | id | int(11) | YES | | NULL | | | name | varchar(20) | YES | | NULL | | | gender | char(5) | YES | | NULL | | | email | varchar(50) | YES | | NULL | | +--------+-------------+------+-----+---------+-------+ rows in set (0.00 sec) #先把表中的索引都删除,让我们专心研究区分度的问题

我们编写存储过程为表s1批量添加记录,name字段的值均为egon,也就是说name这个字段的区分度很低(gender字段也是一样的,我们稍后再搭理它) 回忆b+树的结构,查询的速度与树的高度成反比,要想将树的高低控制的很低,需要保证:在某一层内数据项均是按照从左到右,从小到大的顺序依次排开,即左1<左2<左3<... 而对于区分度低的字段,无法找到大小关系,因为值都是相等的,毫无疑问,还想要用b+树存放这些等值的数据,只能增加树的高度,字段的区分度越低,则树的高度越高。极端的情况,索引字段的值都一样,那么b+树几乎成了一根棍。本例中就是这种极端的情况,name字段所有的值均为'egon' #现在我们得出一个结论:为区分度低的字段建立索引,索引树的高度会很高,然而这具体会带来什么影响呢??? #1:如果条件是name='xxxx',那么肯定是可以第一时间判断出'xxxx'是不在索引树中的(因为树中所有的值均为'egon’),所以查询速度很快 #2:如果条件正好是name='egon',查询时,我们永远无法从树的某个位置得到一个明确的范围,只能往下找,往下找,往下找。。。这与全表扫描的IO次数没有多大区别,所以速度很慢
条件中的 = 和 in 可以乱序(mysql查询优化器自动优化)
=和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式
索引列不能参加运算
索引列不能参与计算,保持列“干净”,比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’)

and/or
#1、and与or的逻辑 条件1 and 条件2:所有条件都成立才算成立,但凡要有一个条件不成立则最终结果不成立 条件1 or 条件2:只要有一个条件成立则最终结果就成立 #2、and的工作原理 条件: a = 10 and b = 'xxx' and c > 3 and d =4 索引: 制作联合索引(d,a,b,c) 工作原理: 对于连续多个and:mysql会按照联合索引,从左到右的顺序找一个区分度高的索引字段(这样便可以快速锁定很小的范围),加速查询,即按照d—>a->b->c的顺序 #3、or的工作原理 条件: a = 10 or b = 'xxx' or c > 3 or d =4 索引: 制作联合索引(d,a,b,c) 工作原理: 对于连续多个or:mysql会按照条件的顺序,从左到右依次判断,即a->b->c->d

在左边条件成立但是索引字段的区分度低的情况下(name与gender均属于这种情况),会依次往右找到一个区分度高的索引字段,加速查询


经过分析,在条件为name='egon' and gender='male' and id>333 and email='xxx'的情况下,我们完全没必要为前三个条件的字段加索引,因为只能用上email字段的索引,前三个字段的索引反而会降低我们的查询效率

最左匹配原则
最左前缀匹配原则,是非常重要的原则,对于组合索引mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配(指的是范围大了,有索引速度也慢),比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。

其他情况
- 使用函数 select * from tb1 where reverse(email) = 'egon'; - 类型不一致 如果列是字符串类型,传入条件是必须用引号引起来,不然... select * from tb1 where email = 999; #排序条件为索引,则select字段必须也是索引字段,否则无法命中 - order by select name from s1 order by email desc; 当根据索引排序时候,select查询的字段如果不是索引,则速度仍然很慢 select email from s1 order by email desc; 特别的:如果对主键排序,则还是速度很快: select * from tb1 order by nid desc; - 组合索引最左前缀 如果组合索引为:(name,email) name and email -- 命中索引 name -- 命中索引 email -- 未命中索引 - count(1)或count(列)代替count(*)在mysql中没有差别了 - create index xxxx on tb(title(19)) #text类型,必须制定长度
其他注意事项
- 避免使用select * - count(1)或count(列) 代替 count(*) (默认是用id 聚集索引去查,效率会高很多) - 创建表时尽量时 char 代替 varchar (自行选择执行效率还是硬盘资源) - 表的字段顺序固定长度的字段优先 - 组合索引代替多个单列索引(经常使用多个条件查询时) - 尽量使用短索引 (单个磁盘片上的数据多,层级少,查的快) - 使用连接(JOIN)来代替子查询(Sub-Queries) - 连表时注意条件类型需一致 - 索引散列值(重复少)不适合建索引,例:性别不适合(大量重复的,分层不好找)