MySQL-视图-触发器-事务-存储过程-函数-流程控制-索引与慢查询优化-06

目录

小科普

核心业务逻辑代码一般都是放在服务端的 客户端容易被懂行点的人修改源码,造成损失

视图***

什么是视图

一个查询语句的结果是虚拟表,将(查询出)这张虚拟表(的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 status\G;  # 存储过程(\G 竖式排版查看,横的太长了 看不清)
       show function status\G;  # 函数

# 查看存储过程或函数的创建代码

  show create procedure proc_name\G;
  show create function func_name\G;

# 查看视图
  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 status\G;  # 查看存储过程(竖式展示排版)

在mysql中调用存储过程

存变量(设置初始值)

set @res=10; # 设置全局变量 @res 等于10select @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_insert1\G 

#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)
- 连表时注意条件类型需一致
- 索引散列值(重复少)不适合建索引,例:性别不适合(大量重复的,分层不好找)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券