当前位置: 首页 > news >正文

【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 …

案例

删除表中的重复记录,重复的数据只能有一份

  1. 创建一个空表,表结构和原表结构相同

    • create tabke 新表名 like 原表名
  2. 将原表进行去重查询,将这个结果放到空表中

    • insert into 新表名 select distinct * from 原表名;
  3. 将原表名重命名,将创建的表重命名为原表

    • 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的次数)

理解:

  1. 我们向一个具有主键的表中,乱序插入数据,发现数据会自动排序。谁做的?为什么这么做?

mysql服务做的,

  1. 重谈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+树,唯一的不同在于除了最后一层每个结点要用指针连接起来,其他层的结点是不需要连接的

  1. 叶子结点保存有数据,路上节点没有,非叶子节点,不要数据,只要目录项

    • 非叶子节点不存数据,可以存储更多的目录项,意味着一个目录页可以管理更多的叶子page,这颗树一定是一个 矮胖型 的树(矮胖型,途径的路上节点减少,找到目标数据只需要更少的page,IO次数更少,提高了效率)
    • 表是被加载到文件缓冲区中的,但mysql的CURD操作是在自己的buffer pool操作的,那么操作前是要把整个B+树加载到buffer pool中吗?不需要,可以先加载头节点,根据要查的数据编号确定是从哪个分支下查找,就加载哪个分支,其他分支就不用加载了(mysql中IO一次,加载一个page,其他分支不用加载了,说明大量的page不用加载,减少了IO次数,提高了效率)
  2. 叶子结点全部用链表连起来

    • B+树的特点
    • 可以支持我们进行范围查找,如果要查找10~20之间的数据,因为是树形结构,同一层节点是没有联系的,难道要把10~20之间的数据挨个查找一遍吗?因此由于只有最后一层才存的是数据,所以只需在最后一层的结点用链表连接起来,这样只需找到10和20的结点,以10为起始,20为终止结点遍历
  3. mysql中innodb下存储的 表 大多是以B+树的结构存储,在我们对表进行CURD操作时,就是在这个结构下进行的

  4. 我的表没设置主键怎么办?也是这样的结构吗?是的,我们没有设置主键,但是系统会设置一个隐藏主键

  5. innodb下索引就是B+树结构

为什么不用其他的结构构成索引

  • 链表?线性遍历,太慢了
  • 二叉搜索树?每次是以二分之一的数据减少,相比B+树,属于瘦高型,要加载的page就相对多,IO次数就多
  • AVL && 红黑树?也是瘦高型,要加载的page就相对多,IO次数就多
  • 哈希?搜索效率确实快O(1),但是有短板,不能进行范围查找

B+树 vs B树

  1. B树的每一层的每一个节点存有目录也有数据,这样会导致一个page能存的目录变少了(管理的page变少了),所以相比B+树可能相对较高,要加载的page就多,IO次数多,效率稍低。

  2. 但由于B树每个结点都会存数据,因此可能在没达到最后一层就找到数据了,这不就比B+树要快了?那为什么不选B树?

    • 因为B树有个缺点,不能进行范围查找,B树最后一层的结点不是连接起来的,所以查找范围数据时,要进行多次遍历B树
  3. 简单总结上面两点:

    • B+树节点不存储数据,这样一个节点可以存储更多目录,可以使树更矮,加载page更少,IO次数更少
    • B+树叶子节点相连,可以进行范围查找

聚簇索引和非聚簇索引

  1. innodb会将数据放在B+树最后一层的节点中 — 索引和数据在一起 — 聚簇索引

    • 一张表中可以有多个索引,也就是多个B+树,如果我们想给某一列添加索引,那么就会生成一个B+树,最后一层存放的是主键值,然后再拿着主键值去主键索引找主键对应的结点,节点中存放的是该主键对应的所有数据。这过程叫做回调查询。
  2. MyISAM存储引擎也是采用B+树作为索引结构,但是最后一层的节点存的是数据的地址,B+树和数据分离 — 索引和数据分离 — 非聚簇索引

索引操作

主键索引、唯一键索引、普通索引

索引创建原则:

  • 频繁的作为查询条件的字段应该创建索引
  • 唯一性太差的字段不适合单独创建索引
  • 更新非常频繁的字段不适合创建索引
  • 不会出现在where子句中的字段(不会作为筛选条件)不该创建索引

主键索引的特点:

  • 一个表中只能有一个主键索引,可以是复合主键
  • 主键索引的效率高(主键不能重复)
  • 主键索引的列基本是int

创建:

  1. 主键索引:

    • 方式一:创建表的时候设置主键

    • 方式二:alter table table_name add primary key(字段名);

  2. 唯一键索引

    • 方式一:创建表的时候给字段加唯一键约束(加了唯一键约束后,会在表中为该列创建一个B+树)
    • 方式二:alter table table_name add unique(字段名);
  3. 普通索引

    • 方式一:在创建表的时候,在表的定义最后,指定某列为索引:index(字段名)

    • 方式二:alter table table_name add index(字段名);

    • 方式三:可以给普通索引起名:create index 索引名 on table_name(字段名);

  4. 复合索引:普通索引的一种,多个列进行复合索引

    • 方式一:alter table table_name add index(字段1,字段2);

    • 方式二:给复合索引起索引名:create index 索引名 on table_name(字段1,字段2);

    • 复合索引只能拿左侧逐个拿字段名开始查找,不能直接只拿右侧的字段名查找 — 索引最左匹配原则

查看表中的所有索引信息:

show index from 表名 \G;

删除索引:

  1. 删除表中的主键索引:

    • alter table table_name drop primary key;
  2. 删除表中唯一键索引:

    • alter table table_name drop index 索引名; — 索引名就是查看表中所有索引信息中的key_name
  3. 删除表中普通键索引:

    • alter table table_name drop index 索引名;— 和唯一键索引删除方式一样
    • drop index 索引名 on 表名;

事务(重点)

事务的概念:

mysql中只有innodb存储引擎支持事务

什么是事务?

在ACID四大属性的加持下,由一条或多条SQL语句共同构成的就叫事务

mysql保证事务在并发访问时不会出现问题,就必须保证四大属性:ACID

  • 原子性:一个事务中所有的操作,要么全部完成,要么全部不完成,不会停留在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从没有执行过一样
  • 一致性:由另外三个属性做支撑并且需要用户在逻辑上完成一致性
  • 隔离性:
  • 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障数据也不会丢失

为什么要存在事务?

保证数据的一致性,保证并发操作的安全性,对故障进行恢复

事务的操作:

事务由两种提交方式:自动和手动提交

  1. 用set来设置自动提交的模式

    • set autocommit=0; — 禁止自动提交

    • set autocommit=1; — 开启自动提交

    • select @@autocommit; — 查看autocommit的值,返回 1 表示自动提交,0 表示手动提交‌

    • show variables like ‘autocommit’; — 查看autocommit信息

  2. 自动提交:

    • 自动提交只支持单语句的事务,将autocommit设置为自动提交后,每个语句都是一个事务

    • 多语句的事务需要手动提交

  3. 手动提交:

    • 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默认的隔离级别是可重复读

四种隔离级别下遇到的问题:

  1. 读未提交:Read Uncommitted

    • 一个事务在执行中,读到另一个执行中更新但没有提交的事务中的数据,这个现象叫做脏读
  2. 读提交:Read Committed

    • 一个正在执行的事务,能读到另一个事务修改并提交后的数据,可能会出现执行中的事务读取另一个事务中的修改的数据多次,但读取结果却不同,这个现象叫做不可重复读。
  3. 可重复读:Repeatable Read

    • 两个事务并发执行时,看不到彼此之间进行操作的数据(即使一方提交了,另一方也看不到),只有都提交后才能看到。

    • 但是在一般的数据库在可重复读情况的时候,无法屏蔽其他事务insert操作的数据(因为隔离性的实现是通过加锁完成的,而inser待插入的数据不存在,那么一般加锁无法屏蔽这类问题),一个事务insert数据提交后,另一个事务多次查找时,会查找出新的记录,前后不一致,就如同产生了幻觉,这种现象叫做幻读。mysql在可重复读级别下解决了幻读问题。

  4. 串行化:Serializable

    • 强制事务按顺序执行来确保最高的数据一致性

深刻理解事务隔离性:

数据库一共有三种并发场景:

  • 读读并发:不存在任何安全问题,不需要并发控制
  • 读写并发:有线程安全问题,可能会造成事务隔离性问题,如脏读,不可重复读,幻读
  • 写写并发:有线程安全问题,会导致数据不一致
读写:

多版本并发控制(MVCC)是一种用来解决读写冲突的无锁并发控制

每个事务都有自己的事务ID,可以根据事务的ID大小,来决定事务到来的先后顺序

mysqld可能会面临处理多个事务的情况,事务也有自己的生命周期,mysqld要对多个事务进行管理,先描述再组织,所以mysqld看待事物就是在看一个类或结构体

理解MVCC需要知道的三个前提知识:

  1. 3个记录隐藏字段 和 版本链
    • DB_TRX_ID:6byte,最近修改/插入的事务ID,记录创建这条记录/最后一次修改该记录的事务ID
    • DB_ROLL_PTR:7byte,回滚指针,指向这条记录的上一个版本(在修改一个数据时,会对旧数据进行拷贝,然后将拷贝的放到undo log中,最新数据会覆盖原数据并且最新数据的回滚指针指向拷贝的数据)
    • DB_ROW_ID:6byte,隐藏的自增ID(隐藏主键),如果数据表没有主键,innodb会自动以隐藏主键产生一个聚簇索引。
      • 为什么没有设置主键的表,我们在查询时,效率还是那么慢?不是有隐藏主键建立的聚簇索引吗?因为在查询的时候是对B+树的叶子节点进行逐个遍历,所以慢
    • flag:记录该条记录是被更新还是被删除,删除一条记录不是真的删除,而是将flag对应的比特位修改就行,最终在刷盘的时候把这条记录去除就行
    • 版本链:数据更新时,将旧数据插入版本链中,版本链是在undo log中放的,版本链是通过回滚指针将旧版本(旧数据)进行连接的链式结构
  2. undo log
    • 是mysql维护的一块内存缓冲区,属于buffer pool的一部分。保存事务运行中数据的历史版本,用来进行回滚。
      1. update时:会将数据拷贝后放入Undo log中,新数据中的回滚指针指向旧数据
      2. delete时:将数据拷贝一份放入undo log中,再将当前数据中的flag设置为删除状态
        • update和delete可以形成版本链,回滚时按照版本链的顺序用就数据覆盖当前数据,实现回滚操作
      3. insert时:插入数据的同时,会将相反的delete语句放入undo log中
        • 回滚时,执行undo log中的SQL语句就能实现回滚
    • 删除undo log中数据的时机:无活跃事务引用的旧版本数据
  3. Read View
    • 对事物的可见性进行判断
    • 当某个事物进行快照读时,会对该记录创建一个Read View读视图(就是一个类),用来判断当前事务能看到哪个版本的数据
    • Read View这个类中有四个重要的变量:
      1. m_ids; — 用来维护Read View生成的时刻,系统正在活跃的事务ID
      2. up_limit_id; — 记录活跃的事务ID中最小的ID
      3. low_limit_id; — 记录已经存在的最大事务ID+1
      4. creator_trx_id; — 创建Read View的事务ID

事务读数据的方式有两种:

  • 当前读:读取最新记录,增删改都叫做当前读,是需要加锁的。select也可能有当前读(select有当前读也有快照读)

    • 什么决定了select是当前读还是快照读?隔离级别。
      • 如果隔离级别是读未提交,那么select就是当前读,因为在这个级别下事务是能看到其他事务执行的操作,所以要读取最新数据
      • 如果隔离级别是读提交或可重复读,那么select就是快照读,读的是历史版本。
  • 快照读:读取历史版本,是不用加锁的 — 既提高了效率(读写可以并发),又为隔离性提供了底层支持(读的版本不同)

为什么可以读写并发?

  • 因为写的是最新的数据,读的是历史版本,所以不会出现访问同一个位置,就不需要加锁,不需要加锁,就不会有互相等待的情况,就可以并发执行读写操作

  • 隔离性的本质上是在数据层面上隔离,不同隔离级别看到的是不同的数据版本,隔离性决定看到的是哪个版本的数据,所以隔离性本质是用MVCC多版本控制来实现的,事务回滚也是用MVCC多版本控制实现的。

为什么要有隔离性?

  • 事务都是原子的,所以,在并发执行时事务一定是有先有后的。但是事务从begin->CURD->commit这个过程是有阶段的,多个事务执行时,他们CURD操作是会交织在一起的,为了保证事务的有先有后,就应该让事务看到他该看到的内容,这就是隔离性与隔离级别要解决的问题。

如何保证,不同的事务看到不同的内容?也就是如何实现隔离级别?下面是MVCC多版本控制的实现过程,可以解释这个问题。

MVCC的实现过程:四步:

  1. 版本链生成:

    • 事务在修改数据时,会生成新数据和旧版本数据,这些旧数据中有修改该数据的事务ID和回滚指针,旧版本数据会放到undo log中形成版本链。
  2. Read View创建:

    • 多个事务在读写并发下,某一个事务进行快照读的时候,会创建一个Read View,Read View就是一个类,类内有四个变量,1.所有活跃的事务ID表、2.活跃事务ID中最小的ID、3.已经存在的最大事务ID+1、4.当前创建Read View的事务ID。
    • 注意:是以事务进行快照读这一时刻为时间点来记录这四个变量,而不是以创建当前事务的时刻为时间点,其他事务提交没提交,是以快照读的时刻来判断。
  3. 可见性判断:

    • 创建好后,快照读访问历史版本数据,每条数据都有最后一次操作该数据的事务ID,会将四个变量和这个事务ID进行一次比对
      1. 如果这个事务ID小于最小的事务ID,说明这个数据在这些并发的事务之前就提交了,所以可以查询
      2. 如果这个事务ID不在事务ID表中,说明这个数据是在事务并发时,快照读之前就提交了,所以可以查询
      3. 如果这个事务ID大于等于最大的事务ID+1,说明这个数据是在快照后才有的,所以不能查
  4. 旧版本清理:

    • 清楚所有活跃事务都不会访问的旧版本

上面这个过程就是读提交、可重复读的隔离级别下操作,多版本控制主要是对这两种隔离级别进行控制。

读未提交中事务采用的是当前读的方式,读的都是最新数据。

串行化不会有事务并发。

那么读提交和可重复读区别到底是什么?

  • 根据快照读的时机不同,能看到的数据就不同(快照读的时机不同,那么其他事务提交的情况就不同)
  • 读提交的隔离级别下,事务每次进行快照读时,会根据时间不同创建出不同的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语句,怎么才能拿到查询的数据?

  1. 查询到的数据是放在了MYSQL对象中的缓冲区,

    • MYSQL_RES* mysql_store_result(MYSQL* mysql);

    • 这个函数会将查询到的数据从MYSQL对象中提取到MYSQL_RES对象中

    • 返回值为NULL表示失败

  2. 如何理解MYSQL_RES这个对象?

    • MYSQL_RES中维护了一个二级指针数组,用来存放每一行数据的起始地址

    • 下面要先知道每一行数据是怎么存储的?

      • mysql中的每一个字段都是以字符串的形式存储,每一行数据都是由一个个字段构成的,而这每一个字段都是用字符数组存储的,因此一行数据就是一个指针数组,存放的是每个字段的地址。

      • 数据有多行,所以就需要一个二级指针数组来存放每一行数据的起始地址,也就是存放指针数组的起始地址。

    • 因此,通过这个二级指针数组,就可以获取任意行任意字段。

  3. 要获取任意行任意字段,就要知道有多少行,多少列

    1. 获取行数:my_ulonglong mysql_num_rows(MYSQL_RES* res);
    2. 获取列数:unsigned int mysql_num_fields(MYSQL_RES* res);
    3. MYSQL_ROW mysql_fetch_row(MYSQL_RES* res);
      • 功能:类似一个迭代器,可以遍历每一行数据
      • MYSQL_ROW本质就是一个char**的类型,就是将二级指针数组中的值依次交给char**的变量
      • 由于内部维护一个计数器,通过这个计数器的值当做数组下标来访问每一行的地址,因此只能遍历一遍,想再遍历,就只能重置计数器,mysql_data_seek(MYSQL_RES* res, 0);
    4. 获取列名: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; }
  4. 释放结果集,结果集就是MYSQL_RES对象,是mysql在堆区开辟的内存,所以要释放掉。

    • mysql_free_result(MYSQL_RES* res);

相关文章:

  • 基于大模型的腹股沟疝全流程预测与诊疗方案研究报告
  • 掌握常见 HTTP 方法:GET、POST、PUT 到 CONNECT 全面梳理
  • Transformer中Post-Norm和Pre-Norm如何选择?
  • 影像数据处理
  • P5670 秘籍-反复异或 Solution
  • 日语学习-日语知识点小记-构建基础-JLPT-N4阶段(8): - (1)复习一些语法(2)「~ています」
  • C++中函数的实现写在头文件内
  • 第 6 篇:衡量预测好坏 - 评估指标
  • 机器视觉lcd屏增光片贴合应用
  • unity基础自学2.3:移动和抓握物品
  • Qt项目——汽车仪表盘
  • Git SSH 密钥多个 Git 来源
  • 研究夜间灯光数据在估计出行需求方面的潜力
  • MySQL 按照日期统计记录数量
  • python 练习
  • 基于LoRA的Llama 2二次预训练实践:高效低成本的大模型领域适配
  • 使用c++调用deepseek的api(附带源码)
  • AI律师匹配AI分析法律需求意图并匹配律师
  • 为什么在TCP层(即传输层)没有解决半包、粘包的问题
  • 基于SpringBoot的在线抽奖系统测试用例报告
  • 在现代东京,便利店如何塑造了饮食潮流、生活方式和日本社会
  • 教育部召开全国中小学幼儿园安全工作视频会议:加强校园安防建设
  • 著名作家、中国艺术研究院原常务副院长曲润海逝世
  • 95后男中音胡斯豪敲开芝加哥抒情歌剧院大门
  • 海南开展药品安全“清源”行动,严查非法渠道购药等违法行为
  • 白宫慌了!将设工作组紧急处理对中国加征关税危机