【MySQL】详细介绍(两万字)
库的操作
创建数据库
创建数据库:create database [if not exists] db_name; — 本质就是在/var/lib/mysql 创建一个目录
删除数据库:drop database db_name; — 删除目录
创建数据库的时候,有两个编码集:
1.数据库编码集 — 数据库未来存储数据(用哪个语言写)
2.数据库校验集 — 支持数据库,进行字段比较使用的编码,本质也是一种读取数据库中数据的采用的编码方式(翻译的语言)
数据库无论对数据做任何操作,都必须保证操作和编码是编码一致的(写的语言和翻译的语言是一致的)
字符集和校验规则
查看系统默认字符集以及校验规则
show variables like ‘character_set_database’;
show variables like ‘collation_database’;
查看数据库支持的字符集
show charset;
创建数据库时指明编码集和校验集
create database db_name 编码集 校验集;
数据库的删查改
查看数据库:show databases;
查看当前在哪个数据库:select database();
查看创建数据库时的指令:show create database db_name;
删除数据库:drop database [if exists] db_name;
修改数据库(修改编码集、校验集):alter database db_name 编码集 校验集
数据库的备份和恢复
linux中的操作,把数据库看做文件来备份、恢复
-
备份:mysqldump -P3306 -u root -p 密码 -B 数据库名 > 数据库备份的文件路径.sql
-
要备份的不是数据库而是几张表:mysqldump -P3306 -u root - p 密码 数据库名 表名1 表名2 > 备份的路径.sql
-
备份多个数据库:mysqldump -P3306 -u root - p 密码 -B 数据库1 数据库2 > 数据库备份路径.sql
-
还原:source 备份的文件路径(如果在备份的时候没有-B,还原的时候需要先创建一个数据库,在use这个数据库,然后在恢复;如果有-B就直接还原)
-B的作用:备份文件会添加 CREATE DATABASE 和 USE 语句,确保还原时自动创建同名数据库
如果备份一个数据库时,没有带-B参数(当初创建表的指令),在恢复数据库时,要先创建空的数据库,然后使用数据库(use),再使用source还原
查看当前数据库的链接情况,有多少用户在连接,在做什么操作:show procsslist;
库的名字不能随便改,库不能随便删
表结构的操作
创建表
create table table_name(
字段1 类型1,
字段2 类型2) character set 编码集 校验集 engine 存储引擎;
查看表
查看当前数据库的所有表:show tables;
具体查看一张表的所有信息:desc table_name;
查看当初建表的语法信息:show create table table_name \G
修改表
增加:
-
向表中插入数据:insert into table_name (字段名1,字段名2…) values (字段1,字段2…);
-
向表中新增一列:alter table table_name add 新字段名 数据类型 comment 字段名的解释 after 已存在的字段名;
- after 已存在的字段名 是将新字段插在这个字段的后面
修改:(字段名就是列名)
-
修改表名:alter table 旧表名 rename to 新表名;
-
修改字段类型/约束:alter table table_name modify 字段名 新的字段类型 字段属性(约束); — 将新的字段信息覆盖旧的字段信息
-
修改列名称:alter table table_name change 旧列名 新列名 新字段类型 列属性; — 修改列名时也要重新给类型
删除一列:alter table table_name drop 字段名;
尽量不要修改和删除表结构
删除表
drop table table_name;
数据类型
tinying类型
范围:-128 ~ 127
无符号类型的使用方式:tinying unsigned
无符号的范围:0 ~ 255
在C/C++中,char a = 1234567; 将一个超出变量范围的值给这个变量时,通常不会报错,编译器会对这个超出范围的值进行截断,再赋给变量。
而mysql中插入不合法的数据时会直接拦截,所以数据只要被插入到mysql中,插入的时候就一定是合法的
所以在mysql中,数据类型也是对数据的一种约束,这个约束是约束使用者。
bit类型
bit(M); M表示位数,1~64位,不设置M默认就是1
查看bit类型的字段时,会以ASCII码的形式显示,插入97显示a,插入0、1…不显示(ASCII码值的0、1…是没有显示的),那怎么才能看到,将ASCII码转化为十进制 — 利用dec(字段名)
小数类型
float
float[(m,d)]:[]中可以设置也可以不设置,m指定显示的长度,d指定小数位数
约束
为什么要有表的约束?
- 数据库通过技术手段来约束特定的一张表,让用户在插入数据的时候必须按照约束规则进行插入,倒逼程序员插入正确的数据,站在mysql的角度就是,凡是插进来的数据都是合法的。
null,not null, default, comment, zerofill, primary key, auto_increment, unique key
空属性
null 和 not null
设置一个字段为not null,在插入数据时,必须给该字段设值,不能不设值或设为null
默认值
default 默认值
给字段设值好默认值,用户在插入数据时,没有给字段设值就用默认的
not null 和 default 同时存在:用户在插入数据时,如果给字段值了,这个字段值不能是null值,如果没给字段值,就会用默认的值
null 和 default 同时存在:用户在插入数据时,如过给字段值了,这个字段值是可以是null值的,如果没给字段值,就会用默认的值
在设置字段时没有给约束,系统会自动加约束条件default null,默认值为空
列描述 comment
comment
对字段进行说明,给程序员在插入数据时看的
zerofill
字段类型都有自己本身的显示位数,比如int(n)型,默认显示的最大长度有n位,有符号的int,不指定n系统会将默认设置为int(11),无符号默认设置为(10),我们也可以自己设置为int(4)、int(5)
给字段加zerofill约束,在显示字段值时,是按照字段类型的最大长度显示,不够的补0
比如一个int(10)的2,显示的时候就是00000 00002,一共10位
如果是int(4)的2,显示的就是0002,但如果这个数本身超过了显示位数,int(2)的100,那么还是会显示100
zerofill对字段的约束不是在插入数据的时候,而是在显示字段值的时候
主键
primary key用来唯一的约束该字段的数据,不能重复,不能为空,一张表里只能有一个主键,但不意味着一个表中的逐渐只能添加给一列,一个主键可以被添加到一列,或者多列上(复合主键)
逐渐所在的列通常是整数类型
删除主键:alter table table_name drop primary key;
添加主键:alter table table_name add primary key(字段名);
最好在使用表之前就设置好主键
自增长 auto_increment
auto_increment
可以在创建表的时候设置起始的自增长值:
create table table_name( id int unsigned primary key auto_increment, …) auto_increment = 50;
没设置起始值就从1开始
自增长的特点:
- 任何一个字段要做自增长,前提是本身就是一个索引
- 自增长字段必须是整数,按照当前字段中最大的值增长
- 一张表只能有一个自增长
- 通常和主键搭配使用
唯一键 unique
unique
可以为null且可以有多个null
削弱版的主键,可以为空,但不能重复 – 保证某一列的唯一性
主键是保证某一行记录在整张表中的唯一性
唯一键是保证某一列中的值不重复
两个的侧重点不一样
外键 foreign key
现在有一张学生表和班级表,学生表中有一个字段是班级号,表示一个学生所在的班级,班级表中也有一个班级号
-
假如现在要向学生表中插入一行数据,可是插入的班级号在班级表中并不存在,那么能插入成功吗?能,但是在逻辑上不对,班级号不存在,就不能有学生在这个班级里
-
假如现在要删除班级表中的一个班级号,可是学生表中还有学生属于该班级号,在逻辑上是不对的,应该先将班级中的学生清空才能删班级号
上述两个问题在执行时都可以通过,但在逻辑上都是错误的,将学生插入不存在的班级,将有学生的班级删除,本质是因为这两张表并没有产生联系
外键的作用:1.让从表和主表产生关联 2.产生外键约束
外键约束:数据在删除时可以保证表和表之间的逻辑关系,以及数据的完整性
外键要定义在从表上,主表中对应的字段必须有主键约束或unique约束
在从表中加外键:foreign key (字段名) references 主表(列)
表中数据的增删查改
插入数据
insert [into] table_name [(字段名1、字段名2…)] values (字段值1、字段值2…);
由于主键或者唯一键对应的值已经存在导致插入失败,可以选择更新操作:
insert [into] table_name [(字段名1、字段名2…)] values (字段值1、字段值2…) on duplicate key update 字段值1,字段值2…;
— 0 row affected; 表中有冲突数据,更新的数据也冲突
— 1 row affected; 表中没有冲突数据,数据被插入
— 2 row affected; 表中有冲突数据,删除后重新插入
替换(也算是插入)
插入数据如果有唯一键或主键冲突,就用新数据覆盖旧数据
replace into table_name (字段名1、…) values (字段值1、…);
— 1 row affected; 表中没有冲突数据,数据被插入
— 2 row affected; 表中有冲突数据,删除后重新插入
查数据
全列查询:
select * from table_name;
不建议使用*进行全列查询
结果去重:distinct
select distinct 字段名 from table_name;
where条件:
from > where > 字段名
给查询结果的字段重命名的时机,实际是将查询的结果给select时才重命名
示例:查询总分小于200的人名和总分
select cname, math+chinese+english total from student where total <200; 错误 where这里不能使用别名
select cname, math+chinese+english total from student where math+chinese+english <200; 正确
结果排序:
order by 字段名 [排序方式]
排序方式:升序 asc, 降序 desc。 order by不指定排序方式会默认按升序排
- 示例1:查询同学各门成绩,一次按数学降序,英语升序,语文升序的方式显示(数学相同分的按英语升序排列)
select cname from student order by math desc, english asc, chinese asc;
- 示例2:将总分进行升序排序
select cname, math+chinese+english total from student order by total asc;
这里为什么可以用别名,因为执行顺序是from > where > 字段名 > order by > 显示
先将表中根据一些条件数据进行筛选,筛选后的表(字段名也相当筛选,将要显示的字段筛选出来)交个order by排序,最后在显示
先筛选,再排序
limit 筛选分页结果
将表的最终结果按行筛选
select … from table_name [where …] [order by …] limit n; — 从0开始,筛选n条结果
select … from table_name [where …] [order by …] limit s, n;
select … from table_name [where …] [order by …] limit n offset s; — 从s行开始,筛选n条结果
修改数据update
语法:update table_name set 字段名=字段值 [where…] [order by…] [limit…]
对查询到的结果进行字段值更新
示例1:将曹同学的英语成绩改为80,数学成绩改为70
update student set english=80, math=70 where cname=‘曹同学’;
示例2:将总分最低的3名同学数学加30分
update student set math=math+30 order by math+chinese+english asc limit 3;
更新全表的语句慎用
删除数据delete
语法:
delete from table_name [where…] [order by…] [limit…]
清空表中数据,但表还在,表的结构不会改变,比如表中的字段、字段类型、字段约束,包括约束条件auto_increment的值也不会更改
截断表:
truncate table_name;
这个操作慎用:
- 只能对整表操作,不像delete可以对部分数据操作
- 比delete快,但truncate在删除数据时,不经过真正的事务,所以无法回滚(不会将记录在日志中)
- 会重置auto_increment为1
插入查询结果
insert into table_name […] select …
案例
删除表中的重复记录,重复的数据只能有一份
-
创建一个空表,表结构和原表结构相同
- create tabke 新表名 like 原表名
-
将原表进行去重查询,将这个结果放到空表中
- insert into 新表名 select distinct * from 原表名;
-
将原表名重命名,将创建的表重命名为原表
- rename table 原表名 to 表名1;
- rename table 新表名 to 原表名;
函数
聚合函数
count()、sum()、avg()、max()、min()
都是对列进行聚合
分组group by
分组的目的是方便进行聚合统计,先分组,再聚合
分组,是将不同的行数据进行分组的,根据分组条件分好的组,组内一定是相同的,可以用聚合
分组(“分表”),把一张表按照条件拆成多个子表,然后分别对各自的子表进行聚合统计
示例1:显示每个部门的每种岗位的平均工资和最低工资
select avg(sal), min(sal) from emp group by deptno, job;
示例2:显示平均工资低于两千的部门和它的平均工资
- 先统计每个部门的平均工资 — 先分组再聚合
- 再进行判断,对聚合的结果进行判断
- select deptno, avg(sal) 平均工资 from emp group by deptno having 平均工资 < 2000;
having是对聚合后的统计数据 执行顺序 group > select后的字段名 > having
having 和 where的区别:
-
where是对具体的任意列进行条件筛选
-
having是对分组聚合后的结果进行条件筛选,只能对分组的条件字段和聚合统计进行条件筛选
-
条件筛选的阶段不同:where > group > select后的字段名 > having
不能单纯的认为,磁盘中的表结构加载到mysql中的表才叫做表,在查询过程中筛选出来的表,都是逻辑上的表。mysql一切皆表
未来只要我们能处理好单标的curd,所有的sql场景,我们都能用统一的方式进行
SQL查询中各个关键字的执行顺序:from > on > join > where > group by > with > having > select > distinct > order by > limit
一般只有出现在group by后的字段名才能出现在select后的字段名中,还有聚合统计也能出现在select后面的字段中,其他列不能出现。
-
分组后的子表,他们的条件字段列的值是相同的,所以select查询该列,显示的是每个组中该列的值(每个组中该列的值是相同的,只有一个)
-
如果是select查询聚合统计,每个组中聚合统计的值只有一个,所以能查
-
如果是select查询其他列,因为每个组中其他列的值会不相同,所以不能查
日期函数
示例:查看在两分钟内发送的信息
select content, sendtime from msg where sendtime > data_sub(now(), interval 2 minute);
字符串函数
数学函数
其他函数
查询当前用户:select user();
password()函数,使用该函数对用户加密。插入数据的时候把要加密的数据填到()中,查询的时候也要使用password()函数
isnull(val1, val2); 如果val1为null,返回val2,否则返回val1
复合查询
SQL查询中各个关键字的执行顺序:from > on > join > where > group by > with > having > select > distinct > order by > limit
示例1:查询工资最高的员工的名字和工作岗位 ~
select ename, job from emp where sal=(select max(sal) from emp);
示例2:查询工资高于平均工资的员工信息 ~
select * from emp where sal > (select avg(sal) from emp);
示例3:显示每个部门的平均工资和最高工资
select depto, avg(sal), max(sal) from emp group by depto;
示例4:显示平均工资低于2000的部门号和它的平均工资
select deptno, avg(sal) 平均工资 from emp group by depto having 平均工资 < 2000;
示例5:显示每种岗位的雇员总数和平均工资 ~
select job, count(*), avg(sal) from emp group by job;
多表查询
SQL查询中各个关键字的执行顺序:from > on > join > where > group by > with > having > select > distinct > order by > limit
假如现在要在两张表中查询信息,那么SQL语句就是select * from 表1,表2;查询的结果实际是将两张表合并成一张表,第一张表的每条信息要和第二张表的每条信息进行组合,假设第一张表有n条信息,第二张表有m条,合并后就是n*m条信息,这就是笛卡尔积
示例1:显示部门号为10的部门名,员工名和工资 ~
select emp.ename, emp.sal, dept.dname from emp, dept where emp.deptno = dept.deptno;
示例2:显示各个员工的姓名,工资,及工资级别
select ename, sal, grade from emp, salgrade where emp.sal between losal and hisal;
自连接
在同一张表进行连接查询,查询时需要给表起不同的别名
示例1:显示员工FORD的上级领导的标号和姓名(mgr是员工领导的编号—empno) ~
子查询方式:select empno ename from emp where empno=(select mgr from emp where ename=‘FORD’);
自连接方式:select leader.empno, leader.ename from emp leader, emp worker where woker.ename=’FORD’ and worker.mgr = leader.empno;
子查询(嵌套查询)
子查询和where
单行子查询
示例1:显示和SMITH同一部门的员工
select * from emp where depno=(select depno from emp where ename=‘SMITH’);
多行子查询:
关键字in
示例:查询和10号部门工作岗位相同的雇员的名字,岗位,工资,部门号,但是不包含在10自己的 ~
select ename, job, sal, depno from emp where job in(select job from emp where depno=10) and depno != 10;
多列子查询
all、any
子查询和from
示例1:显示每个高于自己部门平均工资的员工的姓名、部门、工资、平均工资 ~
select b.ename, b.depno, b.sal, a.avg_sal from (select avg(sal) avg_sal from emp group by depno) a, emp b from where a.depno=b.depno and b.sal > a.avg_sal;
查询的条件是要跟子查询结果中特定的行做条件筛选就要在from后用子查询
查询的条件是要跟子查询结果中所有行做条件筛选就要在where后用子查询
示例2:查找每个部门工资最高的人的姓名、工资、部门、最高工资
select depno, max(sal) from emp group by depno
select ename, sal, depno, a.max_sal from (select depno, max(sal) max_sal from emp group by depno) a, emp b where a.depno=b.depno and b.sal=a.max_sal;
合并查询
合并操作:union,union all
列数必须相同
union:取两个结果的并集,去重
示例:将工资大于25000或职位是MANAGER的人找出来
select * from where sal > 25000 union select * from where job=‘MANAGER’;
union all:取两个结果的并集,不去重
SQL查询中各个关键字的执行顺序:from > on > join > where > group by > with > having > select > distinct > order by > limit
表的内连接和外连接
内连接
语法:select 字段名 from 表1 inner join 表2 on 连接条件 and 其他条件
内连接就是将笛卡尔积去除(去除方式 示例:a.depno=b.depno)
示例:显示SMITH的名字和部门名称
select ename, dname from emp a, dept b where ename=‘SMITH’ and a.depno=b.depno; — 普通形式
select ename, dname from emp a inner join dept b on a.depno=b.depno where ename=‘SMITH’; — 内连接形式
- select ename, dname from emp a inner join dept b on a.depno=b.depno 先将两张表进行笛卡尔积的去除
- where ename=‘SMITH’再将去除后的表进行条件筛选
外连接
左外连接
select 字段名 from 表名1 left join 表2 on 连接条件
让左侧表全部显示
示例:查询所有学生的成绩,如果这个学生没有成绩,也要将这个学生的个人信息显示出来
-
-
select * from stu left join exam on stu.id=exam.id;
-
保留左侧表stu的所有信息,进行笛卡尔积去除,如果左表在进行笛卡尔积去除后没有对应的右表信息,那就显示null
右外连接
select 字段名 from 表名1 right join 表2 on 连接条件
让右侧表全部显示
索引(重点)
mysql的服务器,本质是在内存中的,所有的数据库的CURD操作,全部是在内存中进行的 — 索引也是如此
提高算法效率的因素:1. 组织数据的方式 2. 算法本身 — 索引就是组织数据的方式
预备:
Mysql与磁盘交互
mysql不是直接与磁盘交互,mysql查看一张表其实就是打开一个文件,文件是在磁盘中存放的,所以需要先将文件从磁盘加载到操作系统申请的缓冲区中,mysql中有自己的缓冲区,用来和操作系统的缓冲区进行交互,交互的基本单位是16KB(page=16KB,使用InnoDB存储引擎时的交互单位)
mysql属于应用层,拥有自己的缓冲区buffer pool,和操作系统的缓冲区进行交互是将数据读取到自己的buffer pool中
总结:
- mysql以16KBpage为基本单位进行mysql级别的IO
- mysql有自己的缓冲区buffer pool,mysql在进行IO的时候是将操作系统的缓冲区(文件缓存区)中的数据写到buffer pool中,刷新的时候将buffer pool中的数据刷到操作系统内部的缓冲区中,最终在刷到磁盘中。
- 在系统级IO时,要尽量减少系统和磁盘IO的次数(IO效率低不是因为IO的数据大小,而是IO的次数)
理解:
- 我们向一个具有主键的表中,乱序插入数据,发现数据会自动排序。谁做的?为什么这么做?
mysql服务做的,
- 重谈page,如何理解mysql中page的概念?
-
mysql中一定存在大量的page,所以必须将这些page管理起来,先描述,再组织
-
所以,不能将page简单的认为是一个内存块,page内部必须有对应的管理信息
-
struct page{ struct page* next; struct page* prev; char buffer[NUM]; }
-
将所有的page用特定的数据结构管理起来。
为什么要有page?
- 性能方面:规定每次IO的大小,对数据进行预加载(利用局部性原理提高IO效率),并且可以减少IO次数
单page中查找数据
- page是存放在16KB中的,page中维护着一张页目录,目录中根据数据范围划分数据,查找时先确定根据要查找数据的编号确定在哪个范围,再去这个范围中找数据,就不用挨个遍历每一条数据
- 这个页目录,相当于一个数组,数组的每一块内容存放的是一个范围的数据的起始地址,查的时候根据要查找数据的编号,进行计算得到数组下标,然后通过数组下标获取数据的起始地址,再根据起始地址逐个遍历
page是存放在16KB中的,page内维护了一个页目录,页目录就是一个数组,将16KB划分为许多数据段,数组中存放的是每个段的起始地址,要查找一个数据时,拿着数据编号,将数据编号换算成数组下标,找到数据所在数据段的起始地址,再将数据段挨个遍历找到数据。
- 所以要根据页目录查找数据,数据必须是有序的,因此有了主键后,在插入数据时会自动排序,有了主键后,查找的效率会提升
多page中查找page
-
要查找数据,先确认时哪个page,就要在多个page中找page,这就要遍历每个page,如果page非常多,这个做法的效率就不太高。
-
每个page都有自己管理的数据的范围,专门申请一些page充当页目录,这些page中不存放数据,而是用来存放刚才那些page管理的数据的起始编号和那些page的地址,
-
如果数据足够多,就需要更多的page存放数据,就需要更多的page管理这些page,在查找数据的时候就要遍历大量这样的page,因此我们可以再申请一些page来管理第二层的page
上述这个结构就是B+树,唯一的不同在于除了最后一层每个结点要用指针连接起来,其他层的结点是不需要连接的
-
叶子结点保存有数据,路上节点没有,非叶子节点,不要数据,只要目录项
- 非叶子节点不存数据,可以存储更多的目录项,意味着一个目录页可以管理更多的叶子page,这颗树一定是一个 矮胖型 的树(矮胖型,途径的路上节点减少,找到目标数据只需要更少的page,IO次数更少,提高了效率)
- 表是被加载到文件缓冲区中的,但mysql的CURD操作是在自己的buffer pool操作的,那么操作前是要把整个B+树加载到buffer pool中吗?不需要,可以先加载头节点,根据要查的数据编号确定是从哪个分支下查找,就加载哪个分支,其他分支就不用加载了(mysql中IO一次,加载一个page,其他分支不用加载了,说明大量的page不用加载,减少了IO次数,提高了效率)
-
叶子结点全部用链表连起来
- B+树的特点
- 可以支持我们进行范围查找,如果要查找10~20之间的数据,因为是树形结构,同一层节点是没有联系的,难道要把10~20之间的数据挨个查找一遍吗?因此由于只有最后一层才存的是数据,所以只需在最后一层的结点用链表连接起来,这样只需找到10和20的结点,以10为起始,20为终止结点遍历
-
mysql中innodb下存储的 表 大多是以B+树的结构存储,在我们对表进行CURD操作时,就是在这个结构下进行的
-
我的表没设置主键怎么办?也是这样的结构吗?是的,我们没有设置主键,但是系统会设置一个隐藏主键
-
innodb下索引就是B+树结构
为什么不用其他的结构构成索引
- 链表?线性遍历,太慢了
- 二叉搜索树?每次是以二分之一的数据减少,相比B+树,属于瘦高型,要加载的page就相对多,IO次数就多
- AVL && 红黑树?也是瘦高型,要加载的page就相对多,IO次数就多
- 哈希?搜索效率确实快O(1),但是有短板,不能进行范围查找
B+树 vs B树
-
B树的每一层的每一个节点存有目录也有数据,这样会导致一个page能存的目录变少了(管理的page变少了),所以相比B+树可能相对较高,要加载的page就多,IO次数多,效率稍低。
-
但由于B树每个结点都会存数据,因此可能在没达到最后一层就找到数据了,这不就比B+树要快了?那为什么不选B树?
- 因为B树有个缺点,不能进行范围查找,B树最后一层的结点不是连接起来的,所以查找范围数据时,要进行多次遍历B树
-
简单总结上面两点:
- B+树节点不存储数据,这样一个节点可以存储更多目录,可以使树更矮,加载page更少,IO次数更少
- B+树叶子节点相连,可以进行范围查找
聚簇索引和非聚簇索引
-
innodb会将数据放在B+树最后一层的节点中 — 索引和数据在一起 — 聚簇索引
- 一张表中可以有多个索引,也就是多个B+树,如果我们想给某一列添加索引,那么就会生成一个B+树,最后一层存放的是主键值,然后再拿着主键值去主键索引找主键对应的结点,节点中存放的是该主键对应的所有数据。这过程叫做回调查询。
-
MyISAM存储引擎也是采用B+树作为索引结构,但是最后一层的节点存的是数据的地址,B+树和数据分离 — 索引和数据分离 — 非聚簇索引
索引操作
主键索引、唯一键索引、普通索引
索引创建原则:
- 频繁的作为查询条件的字段应该创建索引
- 唯一性太差的字段不适合单独创建索引
- 更新非常频繁的字段不适合创建索引
- 不会出现在where子句中的字段(不会作为筛选条件)不该创建索引
主键索引的特点:
- 一个表中只能有一个主键索引,可以是复合主键
- 主键索引的效率高(主键不能重复)
- 主键索引的列基本是int
创建:
-
主键索引:
-
方式一:创建表的时候设置主键
-
方式二:alter table table_name add primary key(字段名);
-
-
唯一键索引
- 方式一:创建表的时候给字段加唯一键约束(加了唯一键约束后,会在表中为该列创建一个B+树)
- 方式二:alter table table_name add unique(字段名);
-
普通索引
-
方式一:在创建表的时候,在表的定义最后,指定某列为索引:index(字段名)
-
方式二:alter table table_name add index(字段名);
-
方式三:可以给普通索引起名:create index 索引名 on table_name(字段名);
-
-
复合索引:普通索引的一种,多个列进行复合索引
-
方式一:alter table table_name add index(字段1,字段2);
-
方式二:给复合索引起索引名:create index 索引名 on table_name(字段1,字段2);
-
复合索引只能拿左侧逐个拿字段名开始查找,不能直接只拿右侧的字段名查找 — 索引最左匹配原则
-
查看表中的所有索引信息:
show index from 表名 \G;
删除索引:
-
删除表中的主键索引:
- alter table table_name drop primary key;
-
删除表中唯一键索引:
- alter table table_name drop index 索引名; — 索引名就是查看表中所有索引信息中的key_name
-
删除表中普通键索引:
- alter table table_name drop index 索引名;— 和唯一键索引删除方式一样
- drop index 索引名 on 表名;
事务(重点)
事务的概念:
mysql中只有innodb存储引擎支持事务
什么是事务?
在ACID四大属性的加持下,由一条或多条SQL语句共同构成的就叫事务
mysql保证事务在并发访问时不会出现问题,就必须保证四大属性:ACID
- 原子性:一个事务中所有的操作,要么全部完成,要么全部不完成,不会停留在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从没有执行过一样
- 一致性:由另外三个属性做支撑并且需要用户在逻辑上完成一致性
- 隔离性:
- 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障数据也不会丢失
为什么要存在事务?
保证数据的一致性,保证并发操作的安全性,对故障进行恢复
事务的操作:
事务由两种提交方式:自动和手动提交
-
用set来设置自动提交的模式
-
set autocommit=0; — 禁止自动提交
-
set autocommit=1; — 开启自动提交
-
select @@autocommit; — 查看autocommit的值,返回 1 表示自动提交,0 表示手动提交
-
show variables like ‘autocommit’; — 查看autocommit信息
-
-
自动提交:
-
自动提交只支持单语句的事务,将autocommit设置为自动提交后,每个语句都是一个事务
-
多语句的事务需要手动提交
-
-
手动提交:
-
start transaction; — 手动启动事务
-
begin; — 手动启动事务
-
commit; — 提交事务
-
事务在开启后,可以设置保存点 — savepoint 保存点名,在事务执行过程中对中间某条或某几条语句不想执行了,可以用rollback to 保存点名,丢弃掉rollback到这个保存点之间的语句,如果没有设置保存点,使用rollback,会直接回滚到起始位置。
-
结论:
-
只要输入begin或start transaction,事务就必须用commit提交,才会持久化,与设置set autocommit无关
-
事务可以手动回滚,同时,当操作异常,mysql会自动回滚
-
对于innodb每一条SQL语句都默认封装成事务(autocommit默认为1,自动提交),自动提交(select有特殊情况,因为mysql有MVCC)
事务操作注意事项:
- 如果没有设置保存点,也可以回滚,只能回滚到事务的开始,直接使用rollback
- 如果一个事务commit提交了,就不能回滚了
- 可以选择回滚到哪个保存点
- innodb支持事务,MyISAM不支持事务
事务隔离性:
隔离是必要的,隔离性是保证事务在执行过程中相互之间尽量不要受干扰,根据受干扰程度的不同就有了隔离级别,mysql在读写并发的场景中隔离级别有四种
隔离级别:
- 读未提交
- 读提交
- 可重复读
- 串行化
写与写并发的隔离级别是串行化,读写并发通常是另外三种
查看和设置隔离级别:
查看:三种方式
- select @@global.tx_isolation; — 全局性隔离级别 — mysql版本8.0以上用select @@globaltransaction_isolation;
- select @@session.tx_isolation; — 会话级隔离级别 — mysql版本8.0以上用select @@transaction_isolation;
- select @@tx_isolation; — 会话级隔离级别 和第二个一样
设置:
set [global | session] transtion isolation level [read uncommitted | read committted | repeatable read | serializable];
不建议改隔离级别,mysql默认的隔离级别是可重复读
四种隔离级别下遇到的问题:
-
读未提交:Read Uncommitted
- 一个事务在执行中,读到另一个执行中更新但没有提交的事务中的数据,这个现象叫做脏读
-
读提交:Read Committed
- 一个正在执行的事务,能读到另一个事务修改并提交后的数据,可能会出现执行中的事务读取另一个事务中的修改的数据多次,但读取结果却不同,这个现象叫做不可重复读。
-
可重复读:Repeatable Read
-
两个事务并发执行时,看不到彼此之间进行操作的数据(即使一方提交了,另一方也看不到),只有都提交后才能看到。
-
但是在一般的数据库在可重复读情况的时候,无法屏蔽其他事务insert操作的数据(因为隔离性的实现是通过加锁完成的,而inser待插入的数据不存在,那么一般加锁无法屏蔽这类问题),一个事务insert数据提交后,另一个事务多次查找时,会查找出新的记录,前后不一致,就如同产生了幻觉,这种现象叫做幻读。mysql在可重复读级别下解决了幻读问题。
-
-
串行化:Serializable
- 强制事务按顺序执行来确保最高的数据一致性
深刻理解事务隔离性:
数据库一共有三种并发场景:
- 读读并发:不存在任何安全问题,不需要并发控制
- 读写并发:有线程安全问题,可能会造成事务隔离性问题,如脏读,不可重复读,幻读
- 写写并发:有线程安全问题,会导致数据不一致
读写:
多版本并发控制(MVCC)是一种用来解决读写冲突的无锁并发控制
每个事务都有自己的事务ID,可以根据事务的ID大小,来决定事务到来的先后顺序
mysqld可能会面临处理多个事务的情况,事务也有自己的生命周期,mysqld要对多个事务进行管理,先描述再组织,所以mysqld看待事物就是在看一个类或结构体
理解MVCC需要知道的三个前提知识:
- 3个记录隐藏字段 和 版本链
- DB_TRX_ID:6byte,最近修改/插入的事务ID,记录创建这条记录/最后一次修改该记录的事务ID
- DB_ROLL_PTR:7byte,回滚指针,指向这条记录的上一个版本(在修改一个数据时,会对旧数据进行拷贝,然后将拷贝的放到undo log中,最新数据会覆盖原数据并且最新数据的回滚指针指向拷贝的数据)
- DB_ROW_ID:6byte,隐藏的自增ID(隐藏主键),如果数据表没有主键,innodb会自动以隐藏主键产生一个聚簇索引。
- 为什么没有设置主键的表,我们在查询时,效率还是那么慢?不是有隐藏主键建立的聚簇索引吗?因为在查询的时候是对B+树的叶子节点进行逐个遍历,所以慢
- flag:记录该条记录是被更新还是被删除,删除一条记录不是真的删除,而是将flag对应的比特位修改就行,最终在刷盘的时候把这条记录去除就行
- 版本链:数据更新时,将旧数据插入版本链中,版本链是在undo log中放的,版本链是通过回滚指针将旧版本(旧数据)进行连接的链式结构
- undo log
- 是mysql维护的一块内存缓冲区,属于buffer pool的一部分。保存事务运行中数据的历史版本,用来进行回滚。
- update时:会将数据拷贝后放入Undo log中,新数据中的回滚指针指向旧数据
- delete时:将数据拷贝一份放入undo log中,再将当前数据中的flag设置为删除状态
- update和delete可以形成版本链,回滚时按照版本链的顺序用就数据覆盖当前数据,实现回滚操作
- insert时:插入数据的同时,会将相反的delete语句放入undo log中
- 回滚时,执行undo log中的SQL语句就能实现回滚
- 删除undo log中数据的时机:无活跃事务引用的旧版本数据
- 是mysql维护的一块内存缓冲区,属于buffer pool的一部分。保存事务运行中数据的历史版本,用来进行回滚。
- Read View
- 对事物的可见性进行判断
- 当某个事物进行快照读时,会对该记录创建一个Read View读视图(就是一个类),用来判断当前事务能看到哪个版本的数据
- Read View这个类中有四个重要的变量:
- m_ids; — 用来维护Read View生成的时刻,系统正在活跃的事务ID
- up_limit_id; — 记录活跃的事务ID中最小的ID
- low_limit_id; — 记录已经存在的最大事务ID+1
- creator_trx_id; — 创建Read View的事务ID
事务读数据的方式有两种:
-
当前读:读取最新记录,增删改都叫做当前读,是需要加锁的。select也可能有当前读(select有当前读也有快照读)
- 什么决定了select是当前读还是快照读?隔离级别。
- 如果隔离级别是读未提交,那么select就是当前读,因为在这个级别下事务是能看到其他事务执行的操作,所以要读取最新数据
- 如果隔离级别是读提交或可重复读,那么select就是快照读,读的是历史版本。
- 什么决定了select是当前读还是快照读?隔离级别。
-
快照读:读取历史版本,是不用加锁的 — 既提高了效率(读写可以并发),又为隔离性提供了底层支持(读的版本不同)
为什么可以读写并发?
-
因为写的是最新的数据,读的是历史版本,所以不会出现访问同一个位置,就不需要加锁,不需要加锁,就不会有互相等待的情况,就可以并发执行读写操作
-
隔离性的本质上是在数据层面上隔离,不同隔离级别看到的是不同的数据版本,隔离性决定看到的是哪个版本的数据,所以隔离性本质是用MVCC多版本控制来实现的,事务回滚也是用MVCC多版本控制实现的。
为什么要有隔离性?
- 事务都是原子的,所以,在并发执行时事务一定是有先有后的。但是事务从begin->CURD->commit这个过程是有阶段的,多个事务执行时,他们CURD操作是会交织在一起的,为了保证事务的有先有后,就应该让事务看到他该看到的内容,这就是隔离性与隔离级别要解决的问题。
如何保证,不同的事务看到不同的内容?也就是如何实现隔离级别?下面是MVCC多版本控制的实现过程,可以解释这个问题。
MVCC的实现过程:四步:
-
版本链生成:
- 事务在修改数据时,会生成新数据和旧版本数据,这些旧数据中有修改该数据的事务ID和回滚指针,旧版本数据会放到undo log中形成版本链。
-
Read View创建:
- 多个事务在读写并发下,某一个事务进行快照读的时候,会创建一个Read View,Read View就是一个类,类内有四个变量,1.所有活跃的事务ID表、2.活跃事务ID中最小的ID、3.已经存在的最大事务ID+1、4.当前创建Read View的事务ID。
- 注意:是以事务进行快照读这一时刻为时间点来记录这四个变量,而不是以创建当前事务的时刻为时间点,其他事务提交没提交,是以快照读的时刻来判断。
-
可见性判断:
- 创建好后,快照读访问历史版本数据,每条数据都有最后一次操作该数据的事务ID,会将四个变量和这个事务ID进行一次比对
- 如果这个事务ID小于最小的事务ID,说明这个数据在这些并发的事务之前就提交了,所以可以查询
- 如果这个事务ID不在事务ID表中,说明这个数据是在事务并发时,快照读之前就提交了,所以可以查询
- 如果这个事务ID大于等于最大的事务ID+1,说明这个数据是在快照后才有的,所以不能查
- 创建好后,快照读访问历史版本数据,每条数据都有最后一次操作该数据的事务ID,会将四个变量和这个事务ID进行一次比对
-
旧版本清理:
- 清楚所有活跃事务都不会访问的旧版本
上面这个过程就是读提交、可重复读的隔离级别下操作,多版本控制主要是对这两种隔离级别进行控制。
读未提交中事务采用的是当前读的方式,读的都是最新数据。
串行化不会有事务并发。
那么读提交和可重复读区别到底是什么?
- 根据快照读的时机不同,能看到的数据就不同(快照读的时机不同,那么其他事务提交的情况就不同)
- 读提交的隔离级别下,事务每次进行快照读时,会根据时间不同创建出不同的Read View,相当于每次都会更新Read View,就能看到每次更新前其他事务提交后的数据。
- 而可重复读,只有首次快照读才会创建Read View,之后的每次快照读都会用第一次的Read View,这样导致在首次快照读之后,即使其他事务提交了数据也看不到
视图
视图就是根据select查询结果创建的一张表,视图中的数据变化会影响到基表,基表的数据变化也会影响到视图
创建视图:create view 视图名 as select语句;
删除视图:drop view 视图名;
查询视图:跟正常的select查询一样
视图的规则和限制:
-
视图不能添加索引,也不能有关联的触发器或者默认值
-
order by可以用在视图中,但如果在创建视图时的select语句中也含有order by,那么该视图中的order by将被覆盖
-
视图可以和表一起使用
用户管理
用户创建
用户信息是存放在mysql这个数据库中的,要查询用户信息,先use mysql,再select * from user;
select user, host, authentication_string from user;
-
字段user — 用户名
-
字段host — 主机名
-
suthenticatication_string — 用户密码
-
表名user
创建用户:
create user ‘用户名’@‘登录主机/ip’ identified by ‘密码’; — 新建的用户不能远程登录
create user ‘用户名’@‘%’ identified by ‘密码’; — 任意主机都能登录
删除用户:
drop user ‘用户名’@‘主机名’;
修改密码:
set password for ‘用户名’@‘主机名’=password(‘新密码’);
修改密码就是对user表中的数据修改,因此也可以使用update语句修改表中的authentication_string
权限管理:
给用户分配权限:
grant 权限列表 on 数据库.表名 to ‘用户名’@‘主机名’;
-
权限列表就是操作库或表的SQL语句关键字,例如select、update、delete、alter…
-
权限列表是all,相当于把全部权限都分配给该用户
回收权限:
revoke 权限列表 on 数据库.表名 to ‘用户名’@‘主机名’;
查看权限:
show grants for ‘用户名’@‘主机名’;
mysql访问
用C/C++访问mysql
初始化mysql对象:
MYSQL* mysql_init(MYSQL* mysql);
- 参数:传入一个 MYSQL 类型指针。若参数为 NULL,函数会自动分配并初始化一个新对象;若传入已存在的对象指针,则直接初始化该对象15。
- 返回值:成功时返回初始化后的 MYSQL 对象指针;内存不足时返回 NULL。
释放mysql对象:
mysql_close(MYSQL* mysql);
连接mysql:
MYSQL *mysql_real_connect(
MYSQL *mysql, // 已初始化的 MYSQL 对象指针
const char *host, // 主机名(如 “localhost” 或 IP)
const char *user, // 用户名
const char *passwd, // 密码
const char *db, // 默认连接的数据库名(可留空)
unsigned int port, // 端口号(默认 3306,设为 0 时自动使用默认值)
const char *unix_socket, // Unix 套接字路径(通常为 NULL)
unsigned long client_flag // 客户端标志(如 CLIENT_SSL 加密连接,可设为0)
);
- 失败时返回空
下达mysql命令:
int mysql_query(MYSQL* mysql, const char* q);
- q:下达的命令
- 返回0,表示成功
如果连接mysql后向表中插入的数据是汉语,可能会在插入后查询出的是乱码,这是由于客户端与服务端的字符集不同,怎么解决?
mysql_set_character_set(mysql对象, “utf8”);
执行select语句,怎么才能拿到查询的数据?
-
查询到的数据是放在了MYSQL对象中的缓冲区,
-
MYSQL_RES* mysql_store_result(MYSQL* mysql);
-
这个函数会将查询到的数据从MYSQL对象中提取到MYSQL_RES对象中
-
返回值为NULL表示失败
-
-
如何理解MYSQL_RES这个对象?
-
MYSQL_RES中维护了一个二级指针数组,用来存放每一行数据的起始地址
-
下面要先知道每一行数据是怎么存储的?
-
mysql中的每一个字段都是以字符串的形式存储,每一行数据都是由一个个字段构成的,而这每一个字段都是用字符数组存储的,因此一行数据就是一个指针数组,存放的是每个字段的地址。
-
数据有多行,所以就需要一个二级指针数组来存放每一行数据的起始地址,也就是存放指针数组的起始地址。
-
-
因此,通过这个二级指针数组,就可以获取任意行任意字段。
-
-
要获取任意行任意字段,就要知道有多少行,多少列
- 获取行数:my_ulonglong mysql_num_rows(MYSQL_RES* res);
- 获取列数:unsigned int mysql_num_fields(MYSQL_RES* res);
- MYSQL_ROW mysql_fetch_row(MYSQL_RES* res);
- 功能:类似一个迭代器,可以遍历每一行数据
- MYSQL_ROW本质就是一个char**的类型,就是将二级指针数组中的值依次交给char**的变量
- 由于内部维护一个计数器,通过这个计数器的值当做数组下标来访问每一行的地址,因此只能遍历一遍,想再遍历,就只能重置计数器,mysql_data_seek(MYSQL_RES* res, 0);
- 获取列名:MYSQL_FIELD* mysql_fetch_fields(MYSQL_RES* res);
- MYSQL_FIELD是一个结构体类型,里面包含了表中的一些属性
- 在调用mysql_store_result时就会为每一列创建一个MYSQL_FIELD结构体,并且是以连续数组的形式存储
- 返回值是数组的起始地址
- 示例:获取字段名,MYSQL_FIELD* fileds = mysql_fetch_fileds(res); for(int i = 0; i < 列数; i++) { fileds[i].name; }
-
释放结果集,结果集就是MYSQL_RES对象,是mysql在堆区开辟的内存,所以要释放掉。
- mysql_free_result(MYSQL_RES* res);