mysql数据库学习笔记

开始重新学习mysql数据库,这个文章将记录我学习mysql时的笔记以及遇到的问题

额外补充

1. mac/linux如何启动mysql

mac如何启动mysql:

  1. 首先系统设置中启动mysql
  2. 终端中输入mysql -u root -p

2. MySQL8.0.26-Linux版安装

  1. 安装前先检查
    卸载mariadb,会与MySQL安装冲突。
  • rpm -qa | grep mariadb查看有无mariadb
  • 如果有则删除yum -y remove mariadb-libs.x86_64

安装numactl

  • yum list installed|grep numactl
  • 如果没有则yum install numactl,否则会在安装时报错
  1. 上传压缩包,上传完后解压
mkdir mysql

tar -xvf mysql-8.0.26-1.el7.x86_64.rpm-bundle.tar -C mysql
  1. 安装
    记得按顺序安装
cd mysql

rpm -ivh mysql-community-common-8.0.26-1.el7.x86_64.rpm 

rpm -ivh mysql-community-client-plugins-8.0.26-1.el7.x86_64.rpm 

rpm -ivh mysql-community-libs-8.0.26-1.el7.x86_64.rpm 

rpm -ivh mysql-community-libs-compat-8.0.26-1.el7.x86_64.rpm

yum install openssl-devel

rpm -ivh  mysql-community-devel-8.0.26-1.el7.x86_64.rpm

rpm -ivh mysql-community-client-8.0.26-1.el7.x86_64.rpm

rpm -ivh  mysql-community-server-8.0.26-1.el7.x86_64.rpm
  1. 启动MySQL服务
systemctl start mysqld
systemctl restart mysqld
systemctl stop mysqld
  1. 查询自动生成的root用户密码
grep 'temporary password' /var/log/mysqld.log

命令行执行指令 :

mysql -u root -p

然后输入上述查询到的自动生成的密码, 完成登录 .

  1. 修改root用户密码

登录到MySQL之后,需要将自动生成的不便记忆的密码修改了,修改成自己熟悉的便于记忆的密码。

ALTER  USER  'root'@'localhost'  IDENTIFIED BY '1234';

执行上述的SQL会报错,原因是因为设置的密码太简单,密码复杂度不够。我们可以设置密码的复杂度为简单类型,密码长度为4。

set global validate_password.policy = 0;
set global validate_password.length = 4;

降低密码的校验规则之后,再次执行上述修改密码的指令。

  1. 创建用户

默认的root用户只能当前节点localhost访问,是无法远程访问的,我们还需要创建一个root账户,用户远程访问

create user 'root'@'%' IDENTIFIED WITH mysql_native_password BY '1234';

注意:当你创建了一个可以远程连接的root用户以后,和本地root用户是同一个,只是登陆密码是可以不一样的。

  1. 并给root用户分配权限
grant all on *.* to 'root'@'%';
  1. 重新连接MySQL
mysql -u root -p

然后输入密码

  1. 通过DataGrip远程连接MySQL

第一章:sql语句

sql注释:

  • 单行注释:-- 注释内容 或 # 注释内容(MySQL特有)
  • 多行注释: /* 注释内容 */

SQL又分为4类

  • DDL :定义语言,用来定义数据库对象(数据库,表,字段)
  • DML:操作语言,用来对数据库表中的数据进行增删改
  • DQL:查询语言,用来查询数据库中表的记录
  • DCL:控制语言,管理用户、控制访问权限

1. DDL:定义数据库对象(数据库,表,字段)

1.1数据类型

MySQL中的数据类型有很多,主要分为三类:

  • 数值类型(整型,浮点型)
    mysql数据库学习笔记_第1张图片

  • 字符串类型

    • char(10),定长字符串:性能好,但是size固定
    • vachar(10),变长字符串:性能差,但是size随着元素增加而增加
  • 日期时间类型
    mysql数据库学习笔记_第2张图片

1.2 数据库操作

  • 查询
    • 查询所有数据库
      • show databases;
    • 查询当前数据库
      • select databases();
  • 创建
    • create database 数据库名;
  • 删除
    • drop database [IF EXISTS]数据库名;
  • 使用
    • use 数据库名;

注意:实际代码不需要加中括号[],[]只是表示 可选

1.3 数据表操作

1.3.1 表查询:
  • 查询当前库的所有表: show tables;
  • 查询表结构:desc 表名;
  • 查询指定表的建表语句:show create table 表名
1.3.2 表创建:
CREATE TABLE 表名(
	字段1 字段1类型[COMMENT 字段1注释],
	字段2 字段2类型[COMMENT 字段2注释],
	字段3 字段3类型[COMMENT 宇段了注释]...
	字段n 字段n类型[COMMENT 字段n注释]
)[COMMENT 表注释];
注意 :[ ... ] 为可选参数, 最后一个字段后面没有逗号
1.3.3 表修改字段:

添加字段:
alter table 表名 add 字段名 类型 [comment 注释] [约束];

修改

  • 单纯修改数据类型:
    • alter table 表名 modify 字段名 新数据类型;
  • 修改数据类型和字段名
    • alter table 表名 change 旧名 新名 数据类型;

表删除字段:

  • alter table 表名 drop 字段名

表名修改:

  • alter table 表名 rename to 新表名
1.3.4 表删除:

表删除:

  • drop table 表名;

2 DML:对数据库表中的数据进行增删改

  • 增加数据:insert into
    • 给指定字段添加数据:insert into 表名(字段1,字段2) values(值1,值2);
    • 给全部字段添加数据:insert into 表名 values (值1,值2,…);
    • 批量插入多条数据:
      • insert into 表名(字段1,字段2) values(值1,值2),(值1,值2),(值1,值2) ;
      • insert into 表名 values (值1,值2,…),(值1,值2,…),(值1,值2,…);
  • 修改数据:update
    • update 表名 set 字段名1 = 值1 , 字段名2 = 值2 , …[WHERE 条件] 注意:如果没有条件则默认修改整张表所有数据
  • 删除数据:delete
    • delete from 表名 [where 条件]

3 DQL:查询数据库中表的记录

编写顺序

SELECT
    字段列表
FROM
    表名字段
WHERE
    条件列表
GROUP BY
    分组字段列表
HAVING
    分组后的条件列表
ORDER BY
    排序字段列表 			升序:asc, 降序desc
LIMIT
    分页参数

3.1 基本查询:select

select:查询哪些字段

  1. 查询多个字段
    • select 字段1, 字段2 from 表名;
    • select * from 表名;
  2. 设置别名
    • select 字段1 [as 别名1] , 字段2 [as 别名2 ] … from 表名;
      select name ‘名字’ from emp;//as可以省略
  3. 去除重复记录
    • select distinct 字段列表 from 表名;

3.2 条件查询: where

语法:SELECT 字段列表 FROM 表名 WHERE 条件列表;
条件:
mysql数据库学习笔记_第3张图片
例子:

-- 年龄等于30
select * from employee where age = 30;
-- 年龄小于30
select * from employee where age < 30;
-- 小于等于
select * from employee where age <= 30;
-- 没有身份证
select * from employee where idcard is null or idcard = '';
-- 有身份证
select * from employee where idcard;
select * from employee where idcard is not null;
-- 不等于
select * from employee where age != 30;
-- 年龄在20到30之间
select * from employee where age between 20 and 30;
select * from employee where age >= 20 and age <= 30;
-- 下面语句不报错,但查不到任何信息
select * from employee where age between 30 and 20;
-- 性别为女且年龄小于30
select * from employee where age < 30 and gender = '女';
-- 年龄等于25或30或35
select * from employee where age = 25 or age = 30 or age = 35;
select * from employee where age in (25, 30, 35);
-- 姓名为两个字
select * from employee where name like '__';
-- 身份证最后为X
select * from employee where idcard like '%X';

3.3 聚合查询(聚合函数)

常见聚合函数:
mysql数据库学习笔记_第4张图片
语法:

SELECT 聚合函数(字段列表) FROM 表名;

例:

SELECT count(id) from employee where workaddress = "广东省";

注意:null不参与聚合函数的运算

3.4 分组查询:group by

语法:

SELECT 字段列表 FROM 表名 [ WHERE 条件 ] GROUP BY 分组字段名 [ HAVING 分组后的过滤条件 ];

分组之前的过滤用where,分组之后的过滤用having。

where 和 having 的区别:

  • 执行时机不同:where是分组之前进行过滤,不满足where条件不参与分组;having是分组后对结果进行过滤。
  • 判断条件不同:where不能对聚合函数进行判断,而having可以。

例子:

-- 根据性别分组,统计男性和女性数量(只显示分组数量,不显示哪个是男哪个是女)
select count(*) from employee group by gender;
-- 根据性别分组,统计男性和女性数量
select gender, count(*) from employee group by gender;
-- 根据性别分组,统计男性和女性的平均年龄
select gender, avg(age) from employee group by gender;
-- 年龄小于45,并根据工作地址分组
select workaddress, count(*) from employee where age < 45 group by workaddress;
-- 年龄小于45,并根据工作地址分组,获取员工数量大于等于3的工作地址
select workaddress, count(*) address_count from employee where age < 45 group by workaddress having address_count >= 3;

3.5 排序查询: order by

语法:

SELECT 字段列表 FROM 表名 ORDER BY 字段1 排序方式1, 字段 2 排序方式2;

排序方式:

  • ASC: 升序(默认)
  • DESC: 降序

例子:

-- 根据年龄升序排序
SELECT * FROM employee ORDER BY age ASC;
SELECT * FROM employee ORDER BY age;
-- 两字段排序,根据年龄升序排序,入职时间降序排序
SELECT * FROM employee ORDER BY age ASC, entrydate DESC;

注意事项

如果是多字段排序,当第一个字段值相同时,才会根据第二个字段进行排序

3.6 分页查询 limit

语法:

SELECT 字段列表 FROM 表名 LIMIT 起始索引, 查询记录数;

例子:

-- 查询第一页数据,展示10条
SELECT * FROM employee LIMIT 0, 10;
-- 查询第二页
SELECT * FROM employee LIMIT 10, 10;

注意事项

  • 起始索引从0开始,起始索引 = (查询页码 - 1) * 每页显示记录数
  • 分页查询不同数据库函数不同,MySQL是LIMIT
  • 如果查询的是第一页数据,起始索引可以省略,直接简写 LIMIT 10### 3.

4 DCL:管理用户、控制访问权限

4.1 管理用户

查询用户:

USE mysql;
SELECT * FROM user;

创建用户:

CREATE USER '用户名'@'主机名' identified by '密码';

修改用户密码:

alter user '用户名'@'主机名' identified WITH mysql_native_password BY '新密码';

删除用户:

DROP USER '用户名'@'主机名';

例子:

-- 创建用户test,只能在当前主机localhost访问
create user 'test'@'localhost' identified by '123456';
-- 创建用户test,能在任意主机访问
create user 'test'@'%' identified by '123456';
-- 修改密码
alter user 'test'@'localhost' identified with mysql_native_password by '1234';
-- 删除用户
drop user 'test'@'localhost';

注意事项

  • 主机名可以使用 % 通配

4.2 权限控制

常用权限
mysql数据库学习笔记_第5张图片
查询权限:

show grants FOR '用户名'@'主机名';

授予权限:

grant 权限列表 ON 数据库名.表名 TO '用户名'@'主机名';

撤销权限:

revoke 权限列表 ON 数据库名.表名 FROM '用户名'@'主机名';

注意事项

  • 多个权限用逗号分隔
  • 授权时,数据库名和表名可以用 * 进行通配,代表所有

第二章:函数

1 字符串函数

常用函数:
mysql数据库学习笔记_第6张图片
例子

-- 拼接
SELECT CONCAT('Hello', 'World');
-- 小写
SELECT LOWER('Hello');
-- 大写
SELECT UPPER('Hello');
-- 左填充
SELECT LPAD('01', 5, '-');
-- 右填充
SELECT RPAD('01', 5, '-');
-- 去除空格
SELECT TRIM(' Hello World ');
-- 切片(起始索引为1)
SELECT SUBSTRING('Hello World', 1, 5);

2 数值函数

常见函数:
mysql数据库学习笔记_第7张图片

--生成六位随机验证码
select lpad(ceil(rand()*1000000),6,0);

3 日期函数

常用函数:
mysql数据库学习笔记_第8张图片
例子:

-- DATE_ADD
SELECT DATE_ADD(NOW(), INTERVAL 70 YEAR);

4 流程函数

常用函数:
mysql数据库学习笔记_第9张图片
例子:

select
    name,
    (case when age > 30 then '中年' else '青年' end)
from employee;

select
    name,
    (case workaddress when '北京市' then '一线城市' when '上海市' then '一线城市' else '二线城市' end) as '工作地址'
from employee;

第三章:约束

1. 约束概念

概念:约束是作用于表中字段上的规则,用于限制存储在表中的数据。

目的:保证数据库中数据的正确、有效性和完整性。

分类:
mysql数据库学习笔记_第10张图片

注意: 约束是作用于表中字段上的,可以在创建表/修改表的时候添加约束。

2. 常用约束关键字

mysql数据库学习笔记_第11张图片

例子:

create table user(
    id int primary key auto_increment,
    name varchar(10) not null unique,
    age int check(age > 0 and age < 120),
    status char(1) default '1',
    gender char(1)
);

3. 外键约束

概念 :外键用来让两张表的数据之间建立连接,从而保证数据的一致性和完整性。

例如,如下图所示
mysql数据库学习笔记_第12张图片
emp表的最后一个字段关联了另一张表的主键,则最后一个字段称为外键。通过外键让两张表数据产生连接。

语法:

--创建时设置
CREATE TABLE 表名(
    字段名 字段类型,
    ...
    [constraint] [外键名称] foreign key(外键字段名) references 主表(主表列名)
);
--修改
alter table 表名 add constraint 外键名称 foreign key (外键字段名) references 主表(主表列名);

--删除 
ALTER TABLE 表名 DROP FOREIGN KEY 外键名;

例子:

alter table emp add constraint fk_emp_dept_id foreign key(dept_id) references dept(id);

可选用的删除/更新行为的参数:
mysql数据库学习笔记_第13张图片

alter table 表名 add constraint 外键名称 foreign key (外键字段名) references 主表(主表列名) on update 行为 on delete 行为;

第四章:多表查询

1. 多表关系

多表关系:

  • 一对多(多对一)
  • 多对多
  • 一对一

一对多:

  • 例子:部门与员工
  • 关系:一个部门对应多个员工,一个员工对应一个部门
  • 实现:在多的一方建立外键,指向一的一方的主键

多对多:

  • 例子:学生与课程
  • 关系:一个学生可以选多门课程,一门课程也可以供多个学生选修
  • 实现:建立一张中间表,中间表至少包含两个外键,分别关联两方主键

一对一:

  • 例子:用户与用户详情
  • 关系:一对一关系,多用于单表拆分,将一张表的基础字段放在一张表中,其他详情字段放在另一张表中,以提升操作效率
  • 实现:在任意一方加入外键,关联另外一方的主键,并且设置外键为唯一的(UNIQUE)
  • 一对一其实就是一张表,只是为了效率,拆分成两张表

2. 多表查询----处理笛卡尔积

直接合并查询,会产生笛卡尔积,即会展示所有组合结果:
select * from employee, dept;
如下图所示:
mysql数据库学习笔记_第14张图片
因此,在多表查询时,需要消除无效的笛卡尔积

消除无效笛卡尔积:只需要让子表中的外键=父表中的主键即可

select * from employee, dept where employee.dept = dept.id;

2.1 多表查询的分类

多表查询的分类

  • 连接查询
    • 内连接:查询A、B表交集部分数据
    • 外连接
      • 左外连接:左外连接:查询左表所有数据,以及两张表交集部分数据
      • 右外连接:右外连接:查询右表所有数据,以及两张表交集部分数据
    • 自连接:当前表与自身的连接查询(可内可外),自连接必须使用表别名
  • 子查询:SQL语句中嵌套SELECT语句,称为嵌套查询,又称子查询。
    mysql数据库学习笔记_第15张图片

3. 连接查询

3.1 内连接

内连接查询的是两张表交集的部分

隐式内连接:
SELECT 字段列表 FROM 表1, 表2 WHERE 条件 …;

显式内连接:
SELECT 字段列表 FROM 表1 inner join 表2 on 连接条件 …; //inner可省略

显式性能比隐式高

-- 查询员工姓名,及关联的部门的名称
-- 隐式
select e.name, d.name from employee as e, dept as d where e.dept = d.id;
-- 显式
select e.name, d.name from employee as e inner join dept as d on e.dept = d.id where e.age < 30;

3.2 外连接

左外连接:
查询左表所有数据,以及两张表交集部分数据

SELECT 字段列表 FROM 表1 left outer join 表2 on 条件 …; //outer可省略
相当于查询表1的所有数据,包含表1和表2交集部分数据

在实际开发中,右外也可以改成左外,所以只用了解左外即可。

3.3 自连接

子连接:
自身的表与自身的表连接查询

当前表与自身的连接查询,自连接必须使用表别名

自连接查询,可以是内连接查询,也可以是外连接查询

语法:

  • 内连接:
    select 字段列表 from 表A 别名A join 表A 别名B on 条件 …;
  • 外连接:
    select 字段列表 from 表A 别名A left join 表A 别名B on 条件 …;

例子:

-- 查询员工及其所属领导的名字
select a.name, b.name from employee a, employee b where a.manager = b.id;
-- 没有领导的也查询出来
select a.name, b.name from employee a left join employee b on a.manager = b.id;

3.4 联合查询

把多次查询的结果合并,形成一个新的查询集

语法:
SELECT 字段列表 FROM 表A …
union [all]
SELECT 字段列表 FROM 表B …

注意事项

  • UNION ALL 会有重复结果,UNION 不会
  • 多张表的查询结果的列数量、类型必须一样。
  • 联合查询比使用or效率高,不会使索引失效

4. 子查询

子查询:
SQL语句中嵌套SELECT语句,那个被嵌套的select的查询称为嵌套查询,又称子查询。
SELECT * FROM t1 WHERE column1 = ( SELECT column1 FROM t2);
子查询外部的语句可以是 insert / update / delete / select /where/ from的任何一个

根据子查询结果可以分为:

  • 标量子查询(子查询结果为单个值)
  • 列子查询(子查询结果为一列)
  • 行子查询(子查询结果为一行)
  • 表子查询(子查询结果为多行多列)

根据子查询位置可分为:

  • WHERE 之后
  • FROM 之后
  • SELECT 之后

4.1 标量子查询:子查询结果为单个值

子查询返回的结果是单个值(数字、字符串、日期等)。

其实标量子查询就是编程的思想,把这个查询的语句当作返回的值直接当结果输入到另一个查询的语句中使用。

常用操作符:- < > > >= < <=

-- 查询销售部所有员工
select id from dept where name = '销售部';
-- 根据销售部部门ID,查询员工信息
select * from employee where dept = 4;
-- 合并(子查询)
select * from employee where dept = (select id from dept where name = '销售部');
-- 查询xxx入职之后的员工信息
select * from employee where entrydate > (select entrydate from employee where name = 'xxx');

4.2 列子查询:子查询结果为一列

子查询返回的结果是一列(可以是多行)。

常用操作符:
mysql数据库学习笔记_第16张图片

例子:

-- 查询销售部和市场部的所有员工信息
select * from employee where dept in (select id from dept where name = '销售部' or name = '市场部');
-- 查询比财务部所有人工资都高的员工信息
select * from employee where salary > all(select salary from employee where dept = (select id from dept where name = '财务部'));
-- 查询比研发部任意一人工资高的员工信息
select * from employee where salary > any (select salary from employee where dept = (select id from dept where name = '研发部'));

4.3 行子查询:子查询结果为一行

返回的结果是一行(可以是多列)。
常用操作符:=, <, >, IN, NOT IN

例子:

-- 查询与xxx的薪资及直属领导相同的员工信息
select * from employee where (salary, manager) = (12500, 1);
select * from employee where (salary, manager) = (select salary, manager from employee where name = 'xxx');

4.4 表子查询:子查询结果为多行多列

返回的结果是多行多列
常用操作符:IN

例子:

-- 查询与xxx1,xxx2的职位和薪资相同的员工
select * from employee where (job, salary) in (select job, salary from employee where name = 'xxx1' or name = 'xxx2');
-- 查询入职日期是2006-01-01之后的员工,及其部门信息
select e.*, d.* from (select * from employee where entrydate > '2006-01-01') as e left join dept as d on e.dept = d.id;

第五章:事务

事务是一组操作的集合,事务会把所有操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么一起成功,要么一起失败。

想象一个例子:
张三要转账给李四,在数据库来说有三个操作:

  • 查询张三余额
  • 张三账号-1000
  • 李四账号+1000

上述三个操作需要看作一个事务,不然就错了。

1. 事务基本操作

利用事务的操作方式一:

-- 查看事务提交方式
SELECT @@AUTOCOMMIT;
-- 设置事务提交方式,1为自动提交,0为手动提交,该设置只对当前会话有效
SET @@AUTOCOMMIT = 0;
-- 提交事务
commit;
-- 回滚事务
rollback;

操作实例:

-- 设置手动提交
SELECT @@AUTOCOMMIT;
SET @@AUTOCOMMIT = 1;
-- 操作数据
select * from account where name = '张三';
update account set money = money - 1000 where name = '张三';
update account set money = money + 1000 where name = '李四';
-- 提交事务:将数据真正提交到数据库
commit;

如果设置成手动提交事务的话,执行sql语句后,数据不会更新到数据库本身,只有输入commit才会真正更新数据库

利用事务的操作方式二:

开启事务:
start transactionbegin transaction;
提交事务:
commit;
回滚事务:
rollback;

操作实例:

-- 开启事务
start transaction;
-- 操作数据
select * from account where name = '张三';
update account set money = money - 1000 where name = '张三';
update account set money = money + 1000 where name = '李四';
-- 提交事务
commit;

2. 事务的四大特性

四大特性ACID:

  • 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败
  • 一致性(Consistency):事务的一致性是指事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处以一致性状态。
    比如:如果从A账户转账到B账户,不可能因为A账户扣了钱,而B账户没有加钱
  • 隔离性(Isolation):事务的隔离性是指在并发环境中,并发的事务是互相隔离的,一个事务的执行不能被其它事务干扰。也就是说,不同的事务并发操作相同的数据时,每个事务都有各自完整的数据空间
  • 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久

3. 并发事务

多个并发事务会产生一系列问题,最常见的是如下三个:

  • 脏读:一个事务读到另一个事务还没提交的数据
  • 不可重复读:一个事务先后读取同一条记录,但两次读取的数据不同
  • 幻读:一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。幻读仅专指新插入的行,且在当前读的情况下。

4. 事务的隔离级别

上述三个并发事务的问题可以通过事务的隔离级别来解决

隔离级别 脏读 不可重复读 幻读
Read uncommitted(读未提交) 存在 存在 存在
Read committed(读提交) 不存在 存在 存在
Repeatable Read(默认)(可重复读) 不存在 不存在 存在
Serializable(串行化) 不存在 不存在 不存在

从上到下数据的安全性越来越高,但是性能越来越差。

查看事务隔离级别:
SELECT @@transaction_isolation;

设置事务隔离级别:
set [ session | global ] transaction isolation level {Read uncommitted | Read committed | Repeatable Read | Serializable };

SESSION 是会话级别,表示只针对当前会话有效,GLOBAL 表示对所有会话有效

第六章:存储引擎

1. 介绍

MySQL体系结构:

  • 连接层
    • 最上层是一些客户端和链接服务,主要完成一些类似于连接处理、授权认证、及相关的安全方案。服务器也会为安全接入的每个客户
      端验证它所具有的操作权限。
  • 服务层
    • 第二层架构主要完成大多数的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化,部分内置函数的执行。所有跨存
      储引擎的功能也在这一层实现,如过程、函数等
  • 引擎层
    • 存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过APl和存储引擎进行通信。不同的存储引擎具有不同的功能,这样我们可以根据自己的需要,来选取合适的存储引擎。
  • 存储层
    • 主要是将数据存储在文件系统之上,并完成与存储引擎的交互。

如下图所示:
mysql数据库学习笔记_第17张图片
存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式。存储引擎是基于表而不是基于库的,所以存储引擎也可以被称为表引擎

注意:索引是在存储引擎层实现的,所以不同的存储引擎的索引结构是不同的。

建表时如何指定存储引擎:

-- 建表时指定存储引擎
CREATE TABLE 表名(
    ...
) ENGINE=INNODB;

2. innoDB

innoDB 是一种兼顾高可靠性高性能的通用存储引擎,在 MySQL 5.5 之后,InnoDB 是默认的 MySQL 引擎。

特点:

  • DML 操作遵循 ACID 模型,支持事务
  • 行级锁,提高并发访问性能
  • 支持外键约束,保证数据的完整性和正确性

文件形式:

  • xxx.ibd: xxx代表表名,InnoDB 引擎的每张表都会对应这样一个表空间文件,存储该表的表结构(frm、sdi)、数据和索引。

存储引擎特点:

在innodb中,page页是磁盘操作的最小单元。

3. Myisam

mysql早期的默认存储引擎。

特点:

  • 不支持事务,不支持外键
  • 支持表锁,不支持行锁
  • 访问速度快

文件:

  • xxx.sdi: 存储表结构信息
  • xxx.MYD: 存储数据
  • xxx.MYI: 存储索引

4. Memory

Memory 引擎的表数据是存储在内存中的,受硬件问题、断电问题的影响,只能将这些表作为临时表或缓存使用。

特点:

  • 存放在内存中,速度快
  • hash索引(默认)

文件:

  • xxx.sdi: 存储表结构信息

5. 三种存储引擎的特点总结

mysql数据库学习笔记_第18张图片

6.存储引擎的选择

在选择存储引擎时,应该根据应用系统的特点选择合适的存储引擎。对于复杂的应用系统,还可以根据实际情况选择多种存储引擎进行组合。

  • InnoDB: 如果应用对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,数据操作除了插入和查询之外,还包含很多的更新、删除操作,则 InnoDB 是比较合适的选择
  • MyISAM: 如果应用是以读操作和插入操作为主,只有很少的更新和删除操作,并且对事务的完整性、并发性要求不高,那这个存储引擎是非常合适的。(日志、电商评论/足迹)
  • Memory: 将所有数据保存在内存中,访问速度快,通常用于临时表及缓存。Memory 的缺陷是对表的大小有限制,太大的表无法缓存在内存中,而且无法保障数据的安全性。(现在一般用redis代替memory)

第七章:索引

1. 索引概述:

索引是帮助 MySQL 高效获取数据数据结构有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查询算法,这种数据结构就是索引。

优缺点:

  • 优点:
    • 提高数据检索效率,降低数据库的IO成本
    • 通过索引列对数据进行排序,降低数据排序的成本,降低CPU的消耗
  • 缺点:
    • 索引列也是要占用空间
    • 索引大大提高了查询效率,但降低了更新的速度,比如 INSERT、UPDATE、DELETE

2.索引的数据结构

索引是在第三层存储引擎层实现的,所以存储引擎的不同,索引也不同。
mysql数据库学习笔记_第19张图片
索引在不同存储引擎的支持情况:
mysql数据库学习笔记_第20张图片
一般索引都默认是B+树索引,后文如果没有特意强调,都指的是B+树索引。

2.1 B树


就是二叉查找树的多叉版。
注意:B树的分叉是在节点的左右两边的。B树的叶子节点是最下面的空节点

那么如何保证m阶的B树的查找效率呢?(如何维护B树,B树的条件)

  • 每个节点最多有m个子树(m个孩子节点),即m-1个关键字
  • 假设B树有m阶(阶:所有节点的孩子节点个数的最大值称为B树的阶,比如上图的阶就是5),那么除了根节点外,任何节点的分叉至少要有⌈m/2⌉,即关键字至少要有⌈m/2⌉-1
  • 所有子树的高度相同,所以,所有失败节点都在同一层,即图里最下面的空节点
  • 所有非叶节点的关键字是有序的

2.2 B+树

B+树是应对数据库所需而出现的一种B树的变形树。

B树和B+树的区别:

  • B树的分叉是在节点的左右两边,而B+树的分叉是在节点本身,即有多少个节点就有多少个孩子

一棵m阶的B+树的条件:

  • 每个分支节点最多有m棵子树(m个孩子节点)
  • 如果一个根节点不是叶子节点,则必须要有两棵子树,其他每个分支节点至少有⌈m/2⌉棵子树。
  • 节点的子树(孩子)个数=关键字个数
  • 所有的叶子节点包含了所有的关键字以及指向相应记录的指针,叶子节点的关键字按大小顺序排序,相邻叶子节点按大小顺序相互连接。

B+树有两种查找方式:

  • 从根节点向下查找,只有找到最下面的一个节点,才能找到该节点实际对应的记录。
  • 直接从叶子节点从左往右进行顺序查找

为什么B+树更适合存储数据?
  首先明确,在B/B+树中,一个节点存放在一个磁盘块中。在B树中,非叶节点存放了该关键字对应的存储地址,而在B+树中,只有叶子节点才会从存放关键字对应的存储地址,所以可以使一个磁盘块可以包含更多的关键字,使得B+树的阶更大,树更矮,读取磁盘的次数更少,查找更快。
   那为什么不使用红黑树或者平衡二叉树来存? 因为相比于二叉树,B+树的高度更低,搜索效率高

注意:数据库的B+树的叶子节点是双向链表(原版是单向链表)

3. 索引的分类

mysql数据库学习笔记_第21张图片

在 InnoDB 存储引擎中,根据索引的存储形式,又可以分为以下两种:

  • 聚集索引(Clustered Index)(必须有,而且只能有一个)
    • 聚集索引的叶子节点保存了具体的主键对应的每一行的数据
  • 二级索引(Secondary Index)(可以有多个)
    • 二级索引的叶子节点存放的是对应的主键

为什么二级索引不存储行数据?
  因为如果二级索引也存行数据,那就太冗余了。所以二级索引存的是主键的值。

由于聚集索引必须要有,且只能有一个,所以聚集索引存在一个选取规则:

  • 如果存在主键,主键索引就是聚集索引
  • 如果不存在主键,将使用第一个唯一(UNIQUE)索引作为聚集索引
  • 如果表没有主键或没有合适的唯一索引,则 InnoDB 会自动生成一个 rowid 作为隐藏的聚集索引

聚集索引和二级索引如下图所示:

3.1 思考题

注意:聚集索引和二级索引对应的叶子节点存放的内容需要记住-----聚集索引的叶子节点存放的具体的主键对应的每一行的数据而二级索引存放的是主键的值。这是sql优化的关键!

  1. 以下 SQL 语句,哪个执行效率高?为什么?
select * from user where id = 10;
select * from user where name = 'Arm';
-- 备注:id为主键,name字段创建的有索引

答:第一条语句,因为第二条需要回表查询----先查询二级索引,在通过二级索引的结果得知主键,再根据主键去查询聚集索引得到表的内容。

  1. 已知InnoDB 主键索引的 B+Tree 高度,求可以存储的数据量的大小

答:假设一行数据大小为1k,一页中可以存储16行这样的数据。InnoDB 的指针占用6个字节的空间,主键假设为bigint,占用字节数为8.

设n为key的数量:

可得公式:n * 8 + (n + 1) * 6 = 16 * 1024

其中 8 表示 bigint 占用的字节数,n 表示当前节点存储的key的数量,(n + 1) 表示指针数量(比key多一个)。算出n约为1170。

如果树的高度为2,那么他能存储的数据量大概为:1171 * 16 = 18736;
如果树的高度为3,那么他能存储的数据量大概为:1171 * 1171 * 16 = 21939856。

另外,如果有成千上万的数据,那么就要考虑分表,涉及运维篇知识。

4. 索引的操作语法

回顾索引的分类:
mysql数据库学习笔记_第22张图片

创建索引:

CREATE [ UNIQUE | FULLTEXT ] INDEX index_name ON table_name (index_col_name, ...);
  • 如果create后面加了索引类型UNIQUE/FULLTEXT,则创建的是唯一索引/全文索引
  • 如果不加 CREATE 后面不加索引类型参数,则创建的是常规索引
  • table_name可以有多个,如果有多个则创建的是联合索引

查看索引:

SHOW INDEX FROM table_name;

删除索引:

DROP INDEX index_name ON table_name;

5. SQL性能分析

查询增删改查的执行频次

SHOW GLOBAL/SESSION STATUS LIKE 'Com_______'; --7个下划线,模糊匹配
-- 看全局或者看当前会话的

第八章:Mysql事务日志

8.1 redolog基本介绍

事务有4种特性:原子性、一致性、隔离性和持久性。那么事务的四种特性到底是基于什么机制实现呢?

  • 事务的隔离性由锁机制实现。
  • 而事务的原子性、一致性和持久性由事务的 redo 日志和undo 日志来保证。
    • REDO LOG 称为 重做日志 ,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性
    • UNDO LOG 称为 回滚日志 ,回滚行记录到某个特定版本,用来保证事务的原子性、一致性。

有的DBA或许会认为 UNDO 是 REDO 的逆过程,其实不然。

为什么需要redolog?

  • 首先确定的是mysql在执行sql语句的时候,是先把磁盘的页缓存刀内存中的buffer pool,先更新缓冲池中的数据,然后缓冲池中的脏页(内存已经更新,但是磁盘未更新的数据称为脏页)会以一定的频率刷入磁盘。但是这样有个问题,当更新了缓冲池的数据后,在准备刷入磁盘时,机器发生了宕机,那么对事务的修改就没办法保存到磁盘了,也就违背了持久性的问题。此时可以通过redo日志来解决。
  • 如何通过redo日志解决持久性问题:每次对内存数据的刷新,都把操作记录下来就好了。就算数据库宕机,我们也可以重启数据库后,通过Redo日志来恢复,就可以将没有成功刷新的数据刷新到磁盘上。
  • Innodb采用了"WAL"策略,每次修改,先写日志,再写磁盘,只有日志写入成功,才算事务提交成功。

redo log整体流程:
mysql数据库学习笔记_第23张图片

第1步:先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝
第2步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值
第3步:当事务commit时,将redo log buffer中的内容刷新到 redo log file,对 redo log file采用追加写的方式
第4步:定期将内存中修改的数据刷新到磁盘中

注意:Write-Ahead Log(预先日志持久化):先写日志,再写磁盘。并且,在事务commit以后,redo buffer才会开始根据策略提交缓存中的数据到redo log(磁盘)

分析:其实只要第三步,能将redo buffer刷新到磁盘的日志文件中,那么一定可以保证数据的持久化,所以需要研究下redo buffer是如何刷新到磁盘中的。

8.2 redo log 的刷盘策略

首先明确:InnoDB引擎并不是直接将命令写到redo log磁盘中的,是先同步到redo log buffer,然后事务提交后,在通过一定的策略,将redo log buffer内容刷新到redo log(磁盘)中。
mysql数据库学习笔记_第24张图片
注意,redo log buffer刷盘到redo log file的过程并不是真正的刷到磁盘中去,只是刷入到 文件系统缓存(page cache)中去(这是现代操作系统为了提高文件写入效率做的一个优化),真正的写入会交给系统自己来决定(比如page cache足够大了)。那么对于InnoDB来说就存在一个问题,如果交给系统来同步,同样如果系统宕机,那么数据也丢失了(虽然整个系统宕机的概率还是比较小的)。

那么策略是什么呢,以及把写入交给系统也会存在问题,如何解决呢?针对这种情况,InnoDB给出 innodb_flush_log_at_trx_commit 参数,该参数控制 commit提交事务时,如何将 redo log buffer 中的日志刷新到 redo log file 中。它支持三种策略:

  • 设置为0 :表示每次事务提交时不进行刷盘操作。(系统默认后台线程每隔1s进行一次重做日志的同步)
  • 设置为1 :表示每次事务提交时都将进行同步,刷盘操作( 默认值 )
  • 设置为2 :表示每次事务提交时都只把 redo log buffer 内容写入 page cache,但不进行刷盘。由os自己决定什么时候同步到磁盘文件。

注意:InnoDB引擎有个后台线程,每隔1s,自动将redo buffer中的内容写到page cache,并调用刷盘操作mysql数据库学习笔记_第25张图片
上图是参数为1的流程图,其他参数的类似,先看下面的分析,再来看上图的流程图。

分析每个参数的策略(注意,这一块有点像redis的AOF持久化部分,问题也是aof缓存什么时候将内容写到aof文件中,P140)

  • 当设置为0时:执行命令时会将命令写到redo buffer,但是事务提交时不会进行刷盘操作,而是会通过后台线程,每隔1s自动写到page cache,并调用刷盘操作。性能最好,安全性最差。
  • 当设置为1时:只要一提交事务,立马将redo buffer的内容写到page cache,并且page cache立刻刷新到redo log中。结论:每次事务提交,都一定会刷新到磁盘中。是最安全的,也是一定能保证持久化的。性能最差,安全性最好。
  • 当设置为2时:每次一提交事务,就会将内容写到page cache中,但page cache不会立刻将内容刷新到磁盘,由os自己决定什么时候同步到磁盘文件(例如满)。这么做的好处是:数据库就算宕机了,事务的内容其实还是保存在os的缓存中的,不会受到影响。一定程度上保证了持久性,性能比1好,但是安全性比1差一点。

8.3 写入redo buffer/log的过程

如何写入redo buffer:

Mini-Transaction(mtr)
一个事务可以包含若干条语句,每一条语句其实是由若干个 mtr 组成,每一个 mtr 又可以包含若干条 redo日志,画个图表示它们的关系就是这样:
mysql数据库学习笔记_第26张图片
不同的事务可能是并发执行的,所以事务 T1 、 事务T2 之间的 mtr 可能是交替执行的。所以存入redo buffer中,同一个事务的不同mtr可能不会存在一块,但是一个属于同一个mtr的redo日志一定存在同一块。如下图所示
mysql数据库学习笔记_第27张图片
如何写入redo log:
mysql数据库学习笔记_第28张图片

日志中有两个关键的属性:

  • write pos是当前记录的位置,一边写一边后移,写到第3号文件末尾后就回到0号文件开头。
  • checkpoint是当前要擦除的位置,也是往后推移并且循环的。每当把相应数据从内存刷新到磁盘时(不是保存在redo log,就是传统意义的持久化),那么redo log中的checkpoint也会相应的后移,来擦除数据。

mysql数据库学习笔记_第29张图片
有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个 能力称为crash-safe。

8.4 undo日志

redo log是事务持久性的保证,undo log是事务原子性的保证。在事务中更新数据的前置操作 其实是要 先写入一个 undo log 。 如下图所示

mysql数据库学习笔记_第30张图片
事务需要保证原子性 ,也就是事务中的操作要么全部完成,要么什么也不做。但有时候事务执行到一半会出现一些情况,比如:

  • 情况一:事务执行过程中可能遇到各种错误,比如 服务器本身的错误 ,操作系统错误 ,甚至是突然断电导致的错误。
  • 情况二:程序员可以在事务执行过程中手动输入 ROLLBACK 语句结束当前事务的执行。

以上情况出现,我们需要把数据改回原先的样子,这时候就需要undo log帮忙,这个过程称之为 回滚 ,这样就可以造成一个假象:这个事务看起来什么都没做,所以符合原子性要求。

注意:undo可以将数据回滚到执行语句/事务之前的样子。但是只是逻辑上回复到原来的样子,物理上页可能会发生变化,这没办法变回去(不理解这句话就跳过,无所谓。)

总结undo 日志的作用:

  • 回滚数据:
  • MVCC:undo 日志的另一个作用是MVCC,InnoDB引擎中,MVCC的实现是通过undo日志来完成的。具体来说,当用户读取一行记录时,该记录已经被其他事务占用,那么当前事务可以通过undo读取之前的行版本信息,以此实现非锁定读取。

当事务提交时,InnoDB存储引擎会做以下两件事情:

  • 将undo log放入链表中,以供其他事务通过undo log来获得行记录之前的版本。最后是否删除通过purge线程来判断。(如果是插入的记录则会直接删除)
  • 判断undo log所在的页是否可以重用,若可以分配给下个事务使用

8.4.1 undo log的流程

下图所示的是undo+redo事务的简化过程
mysql数据库学习笔记_第31张图片

第九章:锁

事务的隔离性由 锁机制 实现。

同时 锁机制 也为实现MySQL 的各个隔离级别提供了保证。 锁冲突 也是影响数据库 并发访问性能的一个重要因素。所以锁对数据库而言显得尤其重要,也更加复杂。

9.1 探索多个并发事务访问相同记录

假设两个并发事务对同一个记录进行操作,会分为三种情况

  • 读-读
  • 写-写
  • 读-写/写-读

9.1.1 读-读

读-读 情况,即并发事务相继 读取相同的记录 。读取操作本身不会对记录有任何影响,并不会引起什么问题,所以允许这种情况的发生。

9.1.2 写-写

在这种情况下会发生 脏写 的问题,任何一种隔离级别都不允许这种问题的发生。通过锁机制来解决脏写问题。具体来说:所以在多个未提交事务 相继对一条记录做改动时,需要让它们 排队执行 ,这个排队的过程其实是通过 来实现的。这个所谓的锁其实是一个内存中的结构 ,在事务执行前记录本来是没有锁的,也就是说一开始是没有锁结构和记录进行关联的,如图所示:
mysql数据库学习笔记_第32张图片
当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的锁结构 ,当没有的时候就会在内存中生成一个锁结构与之关联。比如,事务 T1 要对这条记录做改动,就需要生成一个锁结构与T1关联:
mysql数据库学习笔记_第33张图片
当又有一个事务T2想要操作这条记录时,又会生成一个锁结构与T2关联:
mysql数据库学习笔记_第34张图片

9.1.3 读-写/写-读

读-写 或 写-读 ,即一个事务进行读取操作,另一个进行改动操作。这种情况下可能发生 脏读 、不可重复读 、幻读 的问题。

两种方案解决脏读 、不可重复读 、幻读这些问题

  • 1、读操作用多版本并发控制(MVCC),写操作加锁
  • 2、读写都采用加锁

小结对比发现:

  • 采用 MVCC 方式的话, 读-写 操作彼此并不冲突, 性能更高 。
  • 采用 加锁 方式的话, 读-写 操作彼此需要 排队执行 ,影响性能。

一般情况下我们当然愿意采用 MVCC 来解决 读-写 操作并发执行的问题,但是业务在某些特殊情况 下,要求必须采用加锁的方式执行。下面就讲解下MySQL中不同类别的锁。

9.2 锁的分类

根据不同角度,可将锁划分为不同的类型:
mysql数据库学习笔记_第35张图片

9.2.1 由对数据操作类型划分----读写锁

  • 读锁 :也称为 共享锁 、英文用 S 表示。针对同一份数据,多个事务的读操作可以同时进行而不会互相影响,相互不阻塞的
  • 写锁 :也称为 排他锁 、英文用 X 表示。当**前写操作没有完成前,它会阻断其他写锁和读锁。**这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源。

例如(行级读写锁):如果一个事务T1 已经获得了某个行r的读锁,那么此时另外的一个事务 T2 是可以去获得这个行r的读锁的,因为读取探作井没有改变行r的数据;但是,如果某个事务 T3 想获得行r的写锁,则它必须等待事务T1、T2 释放掉行r上的读锁才行。

总结:S锁遇到S锁不会阻塞,X锁(序号1)遇到S锁或者X锁(序号2),当前X锁(序号1)就会阻塞,直到其他S锁或者X锁提交事务。

如何加锁?

SELECT ... LOCK IN share mode; //加共享锁
SELECT ... for share;//加共享锁

SELECT ... for update; //加排他锁(是的,读锁也能加排他锁)

mysql8.0新特性:在select … for update, select … for share后添加nowaitskip locked语法,可以跳过锁等待。
具体来说:

  • 如果查询的行已加锁
    • 那么NOWAIT参数回立刻报错返回。
    • 而skip locked也会立即返回,只是返回结果中不包含被锁定的行。

9.2.2 从数据操作粒度划分

从数据操作粒度划分,可将锁划分为:

  • 表级锁
  • 页级锁
  • 行锁

为了提高数据库的并发度,每次锁的数据范围越小越好,但是管理锁又是一件很消耗资源的操作。因此,数据库需要在高并发与系统性能之间做平衡,这样就产生了锁粒度的概念

当只对一条记录加锁时,我们可以称锁的粒度比较细。当对整张表加锁时,可以称锁的粒度比较粗。

1 表锁

表锁会锁住整个表,他是Mysql中最基本的锁策略,并不依赖于存储引擎(什么存储引擎对于表锁的策略都是一样的)。表锁开销最小(粒度大),但是并发性差。由于表锁一次锁住整张表,可以避免死锁

MySQL的表级锁有两种模式:(以MyISAM表进行操作的演示)

  • 表共享读锁(表级别S锁)
  • 表独占写锁(表级别X锁)

由于Innodb实现了粒度更细的行锁,所以Innodb尽量不使用表锁。只有MyISAM才会用表锁。

命令:

lock tables 表名 read/write;

unlock tables;

9.2.3 从对待锁的态度分类----乐观/悲观锁

从对待锁的态度来看锁的话,可以将锁分成乐观锁和悲观锁,从名字中也可以看出这两种锁是两种看待数据并发的思维方式 。需要注意的是,乐观锁和悲观锁并不是锁,而是锁的设计思想 。

悲观锁:悲观锁是一种思想,顾名思义,就是很悲观,对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。

悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 阻塞 直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞, 用完后再把资源转让给其它线程)。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当 其他线程想要访问数据时,都需要阻塞挂起。

乐观锁
乐观锁认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上,我们可以采用 版本号机制 或者 CAS机制 实现。 乐观锁适用于多读的应用类型, 这样可以提高吞吐量。

两种锁的适用场景:

  1. 乐观锁 适合 读操作多 的场景,相对来说写的操作比较少。它的优点在于 程序实现,不存在死锁问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。
  2. 悲观锁 适合 写操作多 的场景,因为写的操作具有排它性 。采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止 读 - 写 和 写 - 写 的冲突。

第十章:MVCC-多版本并发控制

10.1 什么是MVCC

MVCC依赖于:隐藏字段,undo log,read view(等学完就懂为什么依赖这三个了)

MVCC (Multiversion Concurrency Control)多版本并发控制:顾名思义,MVCC 是通过数据行的多个版本管理来实现数据库的并发控制 。这项技术使得在InnoDB的事务隔离级别下执行一致性读操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。

注意。上述有一句话:多个版本管理来实现数据库的并发控制。多个版本通过undo log实现,管理通过read view(视图)实现

使用MVCC主要就是为了解决上一章说过的一个场景,一个事务在写A数据,另一个事务在读A数据,如何解决这个冲突问题(会产生脏读,不可重复读,幻读的现象)?

所以,在我看来,MVCC和加锁,是两种解决两个事务同时读-写操作的方法。MVCC更加灵活,可以防止加锁带来的性能损失。

10.2 快照读与当前读

MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能用更好的方式去处理 读-写冲突做到即使有读-写冲突时,也能做到不加锁 , 非阻塞并发读 ,而这个读指的就是快照读 , 而非当前读 。当前 读实际上是写操作的时候读,读的是记录的最新版本。

  • 快照读:通过MVCC的视图来读取数据。既然基于多版本,那么快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
  • 当前读:当前读读取的是记录的最新版本(最新数据,而不是历史版本的数据),读取时还要保证其他并发事务 不能修改当前记录,会对读取的记录进行加锁。加锁的 SELECT,或者对数据进行增删改都会进行当前 读。

10.3 根据MVCC在谈隔离级别

MYSQL中,默认的隔离级别是可重复读。从定义来看,可重复读解决了脏读和不可重复读的问题。但是其实,可重复读通过MVCC的方法一并解决了幻读的问题

10.4 MACC实现原理之Readview

在MVCC机制中,多个事务对同一个行记录进行操作时,会产生多个历史快照,这些历史快照保存在undo log中,如果一个事务想要查询这个行记录,系统需要知道他要读取哪个版本的的行记录,那么就需要用到readview进行管理。

Readview就是事务A在使用MVCC机制进行读操作时,产生的一个读视图。

补充章:Mysql API库

在使用mysql的库的时候 需要使用链接库:

gcc hello.c -o hello -I/usr/include/mysql/ -L/usr/lib64/mysql/ -lmysqlclient 

总体使用的函数有:

a. 初始化:MYSQL *mysql_init(MYSQL *mysql)

b. 错误处理	:unsigned int mysql_errno(MYSQL *mysql) 

					char *mysql_error(MYSQL *mysql);
c. 建立连接:
MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const char
	*passwd,const char *db, unsigned int port, const char *unix_socket, unsigned long client_flag);
	
d. 执行SQL语句:int mysql_query(MYSQL *mysql, const char *stmt_str)

e. 获取结果:	MYSQL_RES *mysql_store_result(MYSQL *mysql)
		MYSQL_ROW mysql_fetch_row(MYSQL_RES *result)
		
f. 释放内存:	void mysql_free_result(MYSQL_RES *result)

1. 初始化----mysql_init

MYSQL *mysql_init(MYSQL *mysql)

MYSQL * mysql = mysql_init(NULL);
    if(mysql == NULL)
    {
        printf("mysql init error \n");
        return -1;
    }
printf("mysql init ok\n");

2. 连接与关闭数据库----mysql_real_connect

//参数依次为 mysql通道,
//ip,用户名(如用户名为主机,则填"localhost"),
//密码,数据库名,
//端口号,socket(设置为NULL的时候表示不使用socket),客户端标识符(一般设置为0)
//mysql_real_connect函数若返回NULL表示则表示连接失败
MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const char *passwd
, const char *db, unsigned int port, const char *unix_socket, un signed long client_flag);
//初始化
MYSQL * mysql = mysql_init(NULL);
if(mysql == NULL)
{
    printf("mysql init error ! \n");
    return -1;
}
//连接数据库
MYSQL * conn = mysql_real_connect(mysql, "120.25.144.39", "root", "1234", "heima", 0, NULL, 0);
if(conn == NULL)
{
    printf("mysql connect error ! \n");
    return -1;
}
printf("connect mysql OK! \n");

mysql_close(conn);
printf("mysql close!\n");

3. 执行sql语句----mysql_query

mysql_query:只要mysql可以执行的sql语句,都可以用mysql_query执行。

当连接处于活动状态时,可以使用C语言 mysql_query()或mysql_real_query() 向服务器发出SQL查询。两者的差别在于,mysql_query()预期的查询为指定的、由Null终结的字符串,而mysql_real_query()预期的是计数字符串。

int mysql_query(MYSQL *mysql, const char *query)

int mysql_real_query(MYSQL *mysql, const char *query, unsigned long length)

//执行sql语句
int ret = mysql_query(conn, "insert into student values (11, '小路')");
if( ret != 0)
{
    printf("mysql_query 失败!\n");
    return -1;
}
printf("mysql_query 成功!\n");

4. 获取结果集----mysql_store_result

获取结果集就是对数据库进行SELECT、SHOW等获取操作。

一种方式是通过 mysql_store_result() 将整个结果集全部取回来。另一种方式则是调用 mysql_use_result() 初始化获取操作,但暂时不取回任何记录。视结果集的条目数选择获取结果集的函数。两种方法均通过 mysql_fetch_row() 来访问每一条记录。

函数原型:

  • MYSQL_RES *mysql_store_result(MYSQL *mysql)

返回值:

  • 成功返回MYSQL_RES结果集指针
  • 失败返回NULL。

整体获取的结果集,保存在 MYSQL_RES 结构体指针中,通过检查mysql_store_result()是否返回NULL,可检测函数执行是否成功:

MYSQL_RES *result = mysql_store_result(mysql);
if (result == NULL)
{
	ret = mysql_errno(mysql);
	printf("查询失败%s\n", mysql_error(mysql));
	return ret;	
}

该函数调用成功,则SQL查询的结果被保存在result中,但我们不清楚有多少条数据。

获取列数、获取行数:

一个函数获取行数,两个函数具备获取列数的功能:

  • mysql_num_rows(MYSQL *mysql) 获取行数
  • unsigned int mysql_field_count(MYSQL *mysql) 从mysql句柄中获取有多少列。
  • unsigned int mysql_num_fields(MYSQL_RES *result) 从返回的结果集中获取有多少列。
//获取行的个数
int rows = mysql_num_rows(result);//一般用不到

//获取结果集中字段的个数
int fields = mysql_num_fields(result);

接着使用mysql_fetch_row的方式将结果集中的数据逐条取出:

函数原型:

  • MYSQL_ROW mysql_fetch_row(MYSQL_RES *result)

返回值:

  • 成功返回下一行的MYSQL_ROW结构。
  • 如果没有更多要检索的行或出现了错误,返回NULL。

获取表头信息:
获取表头的API函数有两个:

  • MYSQL_FIELD *mysql_fetch_fields(MYSQL_RES *result) 全部获取
  • MYSQL_FIELD *mysql_fetch_field(MYSQL_RES *result) 获取单个

示例:

//执行select查询语句
int ret = mysql_query(mysql, "select * from student")
if( ret != 0)
{
    printf("mysql_query 失败!\n");
    return -1;
}
printf("mysql_query 成功!\n");

//获取结果集
MYSQL_RES *results = mysql_store_result(conn);
if(results==NULL)
{
	printf("mysql_store_result error, [%s]\n",mysql_error(mysql));
	return -1;
}
printf("mysql_store_result ok\n");

//获取列数
unsigned int row_num = mysql_num_fields(result);

//获取所有的字段名
MYSQL_FIELD *fields = NULL;
fields = mysql_fetch_fields(result);	//得到表头的结构体数组
//打印表头
for(int i=0; i<row_num; ++i)
{
    cout<<fields[i].row_num<<"\t";
}

//获取结果集中每一行记录
MYSQL_ROW row;
while( row=mysql_fetch_row(results) )
{
	for(i=0; i<row_num; i++)
	{
		printf("%s\t", row[i]);	
	}
	printf("\n");
}

释放结果集
结果集处理完成,应调用对应的函数释放所占用的内存。

函数原型:

  • void mysql_free_result(MYSQL_RES *result);

返回值:

  • 成功释放参数传递的结果集。没有失败情况。
mysql_free_result(result);

5. 附录,如何链接上Mysql API

想使用mysql, 需要先安装libmysqlclien,然后必须链接上libmysqlclient.a的库和mysql.h的头文件。通过调用命令find查询库文件和头文件的地址。

mysql客户端的开发

1. 代码开发思路

mysql客户端编写思路分析:
1 mysql初始化--mysql_init
2 连接mysql数据库---mysql_real_connect
3 while(1)
  {
  	//打印提示符:write(STDOUT_FILENO, "mysql >", strlen("mysql >"));
  	//读取用户输入: read(STDIN_FILENO, buf, sizeof(buf))
  	//判断用户输入的是否为退出: QUIT quit exit EXIT
  	if(strncasecmp(buf, "exit", 4)==0 || strncasecmp(buf, "quit", 4)==0)
  	{
  		//关闭连接---mysql_close();
  		exit();
  	}
  	
  	//执行sql语句--mysql_query();
  	
  	//若不是select查询, 打印执行sql语句影响的行数--mysql_affected_rows();
  	if(strncasecmp(buf, "select", 6)!=0)
  	{
  		printf("Query OK, %d row affected", mysql_affected_rows());
  		continue;
  	}
  	
  	//若是select查询的情况
  		---//获取列数: mysql_field_count()
  	//获取结果集: mysql_store_result()
  		--获取列数: int mysql_num_fields();
  	//获取表头信息并打印表头信息:mysql_fetch_fields();
  		
  	//循环获取每一行记录并打印: mysql_fetch_row()
  	//释放结果集: mysql_free_result()
  	
  }
  
4 关闭连接: mysql_close();

2. C++下的mysql客户端开发

需求:在C++下,利用mysql API函数,实现一个类mysql的客户端窗口,实现对mysql数据库的操作

//C++实现mysql客户端开发
#include 
#include 
#include 
#include 
using namespace std;

class Mysql
{
public:
    //初始化数据库,连接数据库
    Mysql(char* ip, char* user, char* password, char* db, int port);

    //增删改操作----update\insert\delete
    void db_CUD(char * buf);

    //查操作----select
    void db_select(char * buf);

    //判断是什么语句,然后自动调用相应的操作
    void execute_sql(char * buf);

    //断开数据库的连接
    void db_close();

    MYSQL * m_mysql;
    MYSQL * m_conn;
    MYSQL_RES * m_result;
};

//初始化数据库,连接数据库
Mysql::Mysql(char* ip, char* user, char* password, char* db, int port)
{
    //初始化数据库
    this->m_mysql = mysql_init(NULL);
    if(this->m_mysql == NULL)
    {
        printf("mysql init error \n");
        exit(-1);
    }

    //连接数据库
    this->m_conn = mysql_real_connect(this->m_mysql, ip, user, password, db, port, NULL, 0);
    if(this->m_conn==NULL)
    {
        printf("mysql connect error ! \n");
        exit(-1);
    }

    //设置字符集----解决中文问题
    cout<<"before:"<<mysql_character_set_name(this->m_conn)<<endl;
    mysql_set_character_set(this->m_conn,"utf8");//设置字符集为 utf8
    cout<<"after:"<<mysql_character_set_name(this->m_conn)<<endl;

}

//增删改操作----update\insert\delete
void Mysql::db_CUD(char * buf)
{
    int ret = mysql_query(this->m_conn, buf);

    //通过调用mysql_affected_rows(),可发现有多少行已被改变(影响)。
    if( ret == 0)//mysql_query调用成功
    {
        ret = mysql_affected_rows(this->m_conn);
        cout<<"Query OK, "<< ret <<" rows affected"<<endl;
        cout<<"Rows matched: "<<ret<<"  Changed: "<<ret<<"  Warnings: 0"<<endl;
    }
    else//mysql_query调用失败---sql语句出错
        cout<<"ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 1"<<endl;
}

//查操作----select
void Mysql::db_select(char *buf)
{
    int ret = mysql_query(this->m_conn, buf);
    if(ret != 0)
        cout<<"mysql_query--select失败!"<<endl;
    else
    {
        //获取数据集
        this->m_result = mysql_store_result(this->m_conn);

        //获取失败
        if( this->m_result == NULL)
        {
            cout<<"mysql_store_result error";
            return;
        }

        //获取成功
        else
        {
            //获取列数
            int row_num = mysql_num_fields(this->m_result);

            //打印表头信息
            MYSQL_FIELD *fields = NULL;
            fields = mysql_fetch_fields(this->m_result);	//得到表头的结构体数组
            cout<<"+----+-----------+------+--------------------+--------+------------+-----------+---------+"<<endl;
            for(int i=0; i<row_num; ++i)
            {
                cout<<fields[i].name<<"\t";
            }
            cout<<endl;
            cout<<"+----+-----------+------+--------------------+--------+------------+-----------+---------+"<<endl;

            //获取结果集每一行记录
            MYSQL_ROW row;
            while( row = mysql_fetch_row(this->m_result) )
            {
                for (int i=0; i<row_num; i++)
                {
                    cout<<row[i]<<"\t";
                }
                cout<<endl;
            }
            cout<<"+----+-----------+------+--------------------+--------+------------+-----------+---------+"<<endl;

            //释放结果集
            mysql_free_result(this->m_result);
        }
    }

}

//判断是什么语句,然后自动调用相应的操作
void Mysql::execute_sql(char * buf)
{
    //先判断是不是QUIT quit EXIT exit
    if(strncasecmp(buf, "quit", 4)==0 || strncasecmp(buf, "exit", 4) == 0)
    {
        this->db_close();
    }

    //在判断是增删改还是查
     else if( strncasecmp(buf, "insert", 6) == 0 || strncasecmp(buf, "update", 6) == 0 || strncasecmp(buf, "delete", 6) == 0)
     {
         db_CUD(buf);
     }

     else if(strncasecmp(buf, "select", 6) == 0)
     {
         db_select(buf);
     }

     //输入错误指令
     else
     {
        cout<<"ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near"<<"\'"<<buf<<"\'at line 1"<<endl;
     }

}

//关闭连接
void Mysql::db_close()
{
    mysql_close(this->m_conn);
    cout<<"Bye"<<endl;
    exit(1);
}

//处理buf异常情况
int clean_buf(char * buf)
{
    //处理输入回车报错的情况
    if(buf[0]=='\n')
        return 1;

    //去掉末尾的;
    char * p = strrchr(buf, ';');
    if(p!=NULL)
        *p = 0x00;

    //去除前面的空格
    int i;
    for(i=0; strlen(buf);++i)
    {
        if(buf[i]!=' ')//只要前面是空格,指针往前走
            break;
    }
    int n = strlen(buf);
    memmove(buf, buf+i, n-i+1);//memmove拷贝字符串。+1是因为多拷贝一个\0

    return 0;
}

int main()
{
    char buf[1024];
    //初始化、连接数据库
    Mysql mysql=Mysql("localhost", "root", "password", "da tabasename", 0);
    while(1)
    {
        //将"mysql"输出到终端
        write(STDIN_FILENO,"mysql> ",strlen("mysql> "));

        //获取用户输入
        memset(buf,0x00,sizeof(buf));
        read(STDOUT_FILENO, buf, sizeof(buf));

        //处理buf异常情况
        int flag = clean_buf(buf);
        if(flag==1)
            continue;

        //判断是什么语句,然后自动执行相应的语句
        mysql.execute_sql(buf);
    }
}

你可能感兴趣的:(数据库,mysql,学习,1024程序员节)