课程地址
(基础篇)MySQL的启动
mysql 默认使用 3306 端口
在 centos下,启动 mysql 数据库:service mysqld start;
查看状态/启动/停止/重启:systemctl status/start/stop/restart mysqld;
登录到mysql数据库:mysql -uroot -psyc13140;
MySQL的层次结构为:用户,数据库,表
关系型数据库:RDBMS,Relational Database Management System。建立在关系模型理论的基础上,由多张连接的二维表组成的数据库
查看数据库和表:
show databases;
use db1;
select database(); # 查询当前数据库
show tables;
MySQL数据类型
SQL语言分类
CRUD:cerate retrive update delete
DDL
数据定义语言:数据库的定义,数据库表的定义,字段的定义
show databases; -- 查看所有数据库
select database(); -- 显示当前使用的数据库
create database [if not exists] mydb1 [default charset utf8mb4] [collate utf8mb4_general_ci];
alter database mydb1 character set utf8;
drop database [if exists] mydb1;
show tables;
create table emp(
id int [comment ""],
name varchar(20) [comment ""],
sex int [comment ""],
birthday date [comment ""],
salary double [comment ""],
hiredate date [comment ""],
resume text [comment ""]
) [comment ""];
desc emp; # describe emp
show create table emp; # 查看建表过程
drop table emp;
创建表
create table tb_user(
id int comment "id",
name varchar(20) comment "name",
age int comment "age",
gender varchar(1) comment "gender"
)comment "user table";
执行sql脚本:
source /home/daniel/scott.sql;
创建表练习:
create table emp(
id int,
work_no varchar(10),
name varchar(10),
gender char(1),
age tinyint unsigned,
id_card char(18),
entry_date date
);
修改表
alter table emp add nickname varchar(20) comment "昵称";
alter table emp modify nickname varchar(25);
alter table emp change nickname username varchar(30);
alter table emp drop username;
alter table emp rename to worker;
删除表
drop table worker;
truncate table worker; # 删除表,并重新创建该表
小结
# 数据库操作
show databases;
create database db_name;
user db_name;
select database();
drop database db_name;
# 表操作
show tables;
create table tb_name(field1, field2, ...);
desc tb_name;
show create table tb_name;
alter table tb_name add/modify/change/drop/rename to ...;
drop table tb_name;
DML
insert into tb_name(field1, field2, ...) values (value1, value2...);
insert into tb_name values(v1, v2);
# 批量添加数据
insert into tb_name (f1, f2, ...) values (value1, value2...), (value1, value2...), (value1, value2...);
insert into tb_name values (value1, value2...), (value1, value2...), (value1, value2...);
对于这样一张表:
create table emp(
id int,
name varchar(20),
sex int,
birthday date,
salary double,
hiredate date,
resume text
);
# insert into tb(field1, field2, field3) values(value1, value2, value3);
insert into emp values(1,'daniel',1,'1999-10-19',50000,'2023-10-10','i hope so');
update emp set salary=salary+5000;
delete from emp where name='daniel';
delete from emp; # 删除所有数据!
如果插入的数据已存在,则使用 ignore
修饰 insert
insert ignore into actor values(3, "ED", "CHASE", "2006-02-15 12:34:33");
DQL
SELECT
字段列表
FROM
表名列表
WHERE
条件列表
GROUP BY
分组字段列表
HAVING
分组后条件列表
ORDER BY
字段1 排序方式1, 字段2 排序方式2
LIMIT # 分页
[<offset>,] <row count>]
select deptno, dname, loc from dept;
select empno, ename, sal, sal*12 as package from emp; # as起别名
select distinct city from user_profile; # 去重
where
select * from emp where sal=800 and deptno=20; # 查找部门编号为20,薪水为800的员工
select * from emp where deptno in (20, 30); # 查找部门编号为20或30的员工
select name from emp where idcard is null;
在解析where条件时, 是从右向左解析的,所以应该将容易假的值放在右边,利用逻辑短路特性
like
%
:匹配任意多个字符
_
:匹配任意一个字符
select * from emp where ename like 'S%'; # 查询名字以S开头的员工
select * from emp where ename like '____'; # 查询名字中有四个字母的员工
group by
# 分组之后,查询的字段一般为聚合函数和分组字段,查询其他字段无任何意义
select gender, count(*) from emp group by gender;
select gender, avg(age) from emp group by gender;
# 查询年龄小于45岁的员工,并根据工作地分组,获取员工数量>=3的地址
select workaddress, count(*)
from emp
where age < 45
group by workaddress
having count(*) >= 3;
order by
select * from emp order by age asc;
select * from emp order by entrydate desc;
select * from emp order by age asc, entrydate desc;
limit
select * from article limit 1,3 # 取第2 3 4条数据
DQL练习:
select * from emp where age <= 23 and age >= 20;
select * from emp where gender = "男" and age between 20 and 40 and name like "___";
select gender, count(*) from emp where age < 60 group by gender;
select name, age from emp where age <= 35 order by age, entrydate desc;
select * from emp where gender = "男" and (age between 20 and 40) order by age, entrydate limit 5;
DQL的执行顺序
函数
聚合函数
将一列数据作为一个整体,进行纵向计算。包括:count max min avg sum等
主要应用于 group by 分组操作
select count(*) from emp; # null不参与聚合函数计算
select avg(age) from emp;
字符串函数
select concat("hello", "mysql");
select lower("Hello");
select upper("Hello");
select lpad("1", 5, "0");
select rpad("1", 5, "0");
select trim(" hello world ");
select substring("hello, mysql", 1, 5);
-- 使用逗号分隔结果
select dept_no, group_concat(emp_no) as employees
from dept_emp
group by dept_no;
update emp set workno = lpad(workno, 5, "0");
数值函数
select ceil(1.4);
select floor(1.9);
select mod(7, 4);
select rand(); // 生成 (0, 1) 之间的验证码
select round(3.1415926, 3);
通过数据库的函数,生成一个6位数的随机验证码:
select lpad(round(rand() * 1000000, 0), 6, "0");
日期函数
取日期中的年月日:
select curdate();
select curtime();
select now();
select year(now());
select month(now());
select day(now());
select date_add(now, interval 70 day);
select datediff("2021-12-01", "2021-11-01");
查询所有员工的入职天数,并根据入职天数倒序排序:
select name, datediff(now(), entrydate) as days from emp order by days desc;
求时间差:
timestampdiff(MINUTE, start_time, submit_time)
流程控制函数
select if(true, "ok", "default");
select ifnull("ok", "default");
select ifnull(null, "default");
查询emp表的员工姓名和工作地址,如果工作地址为北京或上海,则显示一线城市;否则显示二线城市
select
name,
( case workaddress
when "北京" then "一线城市"
when "上海" then "一线城市"
else "二线城市"
end ) as "工作地点"
from emp;
根据成绩显示结果:
select
name,
( case
when score >= 85 then "优秀"
when score >= 60 then "及格"
else "不及格"
end) as "成绩"
from stu;
约束
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)
)comment "用户表"
外键约束
外键用于建立两张表数据之间的连接,从而保证数据的一致性和完整性
alter table emp add constraint fk_emp_dept_id foreign key (dept_id) references dept(id);
alter table emp drop foreign key fk_emp_dept_id;
一般是不允许直接删除父表的外键记录的,因为子表有很多记录关联到了父表
删除更新行为
alter table emp
add constraint fk_emp_dept_id
foreign key (dept_id)
references dept(id)
on update cascade
on delete cascade;
多表查询
多表关系
多对一:
多对多:创建一张中间表
一对一:一般用作单表拆分
多表查询
多表查询的过程其实就是构造多个表的一个笛卡尔积,然后利用连接条件筛选出正确的结果,消除无效的笛卡尔积
select * from emp, dept where emp.dept_id = dept.id;
内连接
查询两张表的交集部分
隐式内连接:
select emp.name, dept.name from emp, dept where emp.dept_id = dept.id;
select e.name, d.name
from emp as e, dept as d -- 如果已经为表起了别名,则后面引用表时不能再用原表名,必须使用别名
where e.dept_id = d.id;
显式内连接(使用 on 连接):
select e.name, dept.name
from emp as e inner join dept as d
on emp.dept_id = dept.id;
外连接
select * from tb1 left outer join tb2 on cond; -- 查询左表
select * from tb1 right outer join tb2 on cond; -- 查询右表
-- 查询emp表的所有数据,和对应的部门信息
select e.*, d.name
from emp as e left outer join dept as d
on e.dept_id = d.id;
-- 查询dept表的所有数据,和对应的员工信息
select d.*, e.*
from emp as e right outer join dept as d
on e.dept_id = d.id;
自连接
自连接查询,可以是内连接查询,也可以是外连接查询
查询员工,及其所属领导的名字:
select worker.name, leader.name from emp as worker, emp as leader where worker.managerid = leader.id;
select worker.name, leader.name
from emp as worker inner join emp as leader
on worker.managerid = leader.id;
查询所有员工emp及其领导的名字emp,如果员工没有领导,也需要查询出来(使用外连接):
select a.name as "worker", b.name as "leader"
from emp as a left outer join emp as b
on a.managerid = b.id;
联合查询
将多次的查询结果合并起来,形成一个新的查询结果集
-- 将薪资 < 5000的员工和年龄 > 50的员工全部查询出来
select * from emp where salary < 5000
union all
# union -- 会去重
select * from emp where age > 50;
联合查询的多张表的列数和每列的字段类型必须保持一致。union
会将结果去重;union all
会直接将数据合并在一起
子查询
标量子查询
查询“销售部”的所有员工信息:
select * from emp where dept_it = (select id from dept where name = "销售部");
查询在“方东白”入职之后的员工信息:
select * from emp where entrydate > (select entrydate from emp where name = "方东白");
列子查询
-- 查询销售部和市场部的所有员工信息
select * from emp where dept_id in (select id from dept where name = "销售部" or name = "市场部");
-- 查询比财务部所有人工资都高的员工信息
select *
from emp
where salary > all( -- max(
select salary
from emp
where dept_id = (
select id
from dept
where name = "财务部"
)
);
-- 查询比研发部任意一人工资高的员工信息
select *
from emp
where salary > any( -- min( -- some(
select salary
from emp
where dept_id = (
select id
from dept
where name = "研发部"
)
);
行子查询
-- 查询与“张无忌”薪资和直属领导相同的员工信息
select *
from emp
where (salary, managerid) = ( --( tuple equals
select salary, managerid
from emp
where name = "张无忌"
);
表子查询
-- 查询与“鹿仗客”,“宋远桥”的职位和薪资相同的员工信息
select *
from emp
where (job, salary) in (
select job, salary
from emp
where name = "鹿仗客" or name = "宋远桥"
);
-- 查询入职日期是“2006-01-01”之后的员工信息和部门信息
select e.*, d.*
from (
select *
from emp
where entrydate > "2006-01-01"
) as e left join dept as d
on e.dept_id = d.id;
案例练习
三张表:emp dept salgrade
-- 1 查询员工的姓名,年龄,职位,部门信息 隐式内连接
select e.name, e.age, e.job, d.name
from emp as e, dept as d
where e.dept_id = d.id;
-- 2 查询年龄小于30岁的员工姓名,年龄,职位,部门信息 显式内连接
select e.name, e.age, e.job d.name
from emp as e inner join dept as d
on e.dept_id = d.id
where e.age < 30;
-- 3 查询拥有员工的部门id,部门名称
select distinct d.id, d.name
from emp as e, dept as d
where e.dept_id = d.id;
-- 4 查询所有age > 40的员工,及其归属的部门名称;如果员工没有分配部门,也需要展示出来 左外连接
select e.*, d.*
from emp as e left outer join dept as d
on e.dept_id = d.id
where e.age > 40;
-- 5 查询所有员工的工资等级
select e.*, s.grade
from emp as e, salgrade as s
where e.salary >= s.lowsal and e.salary <= s.highsal;
-- 6 查询“研发部”所有员工的信息及工资等级
select e.*, s.grade
from emp as e,
salgrade as s,
dept as d
where
e.dept_id = d.id and
e.salary between s.lowsal and s.highsal and
d.name = "研发部";
-- 7 查询“研发部”员工的平均工资
select avg(e.salary)
from emp as e, dept as d
where e.dept_id = d.id and d.name = "研发部";
-- 8 查询工资比“灭绝”高的员工信息 标量子查询
select *
from emp
where salary > (
select salary from emp where name = "灭绝"
);
-- 9 查询比平均薪资高的员工信息
select *
from emp
where salary > (
select avg(salary) from emp
);
-- 10 查询低于本部门平均工资的员工信息
select *
from emp as e1
where e1.salary < (
select avg(e2.salary)
from emp as e2
where e1.dept_id = e2.dept_id
);
-- 11 查询所有部门的信息,并统计部门的员工人数
select
d.id,
d.name,
(select count(*) from emp as e where e.dept_id = d.id) as "人数"
from dept as d;
-- 12 查询所有学生的选课情况,展示出学生名称,学号,课程名称
select s.name, s.no, c.name
from student as s, course as c, student_course as sc
where s.id = sc.studentid and c.id = sc.courseid;
事务
事务是指一组操作的集合,它是一个不可分割的工作单位,事务会把所有操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败
MySQL的事务默认是自动提交的,也就是说,当执行一条DML语句,MySQL会立即地隐式提交事务
建表:
create table account (
id int primary key auto_increment,
name varchar(10),
money int
);
insert into account
values
(1, "zhangsan", 2000),
(2, "lisi", 2000);
-- 恢复数据
update account set money = 2000 where name = "zhangsan" or name = "lisi";
-- 查看/设置事务提交方式
select @@autocommit;
set @@autocommit = 0;
-- 提交事务
commit
-- 回滚事务
rollback
如果设置了手动提交,则执行DML语句后,数据库的内容不会发生变化,直到commit
set @@autocommit = 0;
update account set money = money - 1000 where name = "zhangsan";
update account set money = money + 1000 where name = "lisi";
-- 直到执行commit
commit
临时开启一个事务:
begin -- start transaction
总结,使用事务有2种方式:
- 通过
set @@autocommit = 0
关闭自动提交,全部手动commit/rollback
- 通过
begin
开启一个事务
ACID
并发事务的3个问题,多个并发事务在操作数据库时引发的问题:
- 脏读:一个事务读到另一个事务还没有提交的数据
- 不可重复读:一个事务先后读取同一条记录,但两次读取的数据不同(中间被update了)
- 幻读:一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在
事务的隔离级别
查看/设置事务的隔离级别:
select @@transaction_isolation;
-- set [session | global] transaction isolation level {read uncommitted | read committed | repeatable read | serializable};
set session transaction isolation level read uncommitted;
事务隔离级别越高,数据越安全,但性能越低
总结:
事务简介:一组操作的集合,这组操作要么全部执行成功,要么全部执行失败
开启事务:start transcaction
提交/回滚事务:commit/rollback
事务四大特性:原子性、一致性、隔离性、持久性
并发事务问题:脏读、不可重复度、幻读
事务隔离级别:read uncommitted/read committed/repeatable read/serializable
建表:
create table account(
name varchar(10),
money int
);
insert into account values ("zhangsan", 2000), ("lisi", 2000);
脏读演示
不可重复读演示
幻读演示
幻读会抛出主键重复异常:查询时查询不到,但是插入时报告主键重复
(进阶篇)存储引擎
存储引擎:存储数据,建立索引,更新/查询数据等技术的实现方式。存储引擎是基于表的,而不是基于库的(同一个库下的多张表可以有不同的存储引擎),所以存储引擎通常也被称为表类型
show create table account;
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) DEFAULT NULL,
`money` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1
默认的存储引擎是 InnoDB
查看支持的存储引擎:
show engines;
-- 创建表my_myisam,并指定MyISAM存储引擎
create table my_myisam (
id int,
name varchar(10)
) engine = MyISAM;
-- 创建表my_memory,并指定Memory存储引擎
create table my_memory (
id int,
name varchar(10)
) engine = Memory;
InnoDB
InnoDB 引擎的3大特性(优点):事务、外键、行级锁
- DML操作遵循ACID模型,支持事务
- 行级锁,提高并发访问性能
- 支持外键约束,保证数据的完整性和正确性
innodb_file_per_table
MyISAM
- 不支持事务和外键
- 只支持表锁,不支持行锁
- 访问速度快
Memory
表数据存放在内存中,只能作为临时表或缓存来使用。支持hash索引(只能进行等值匹配,不能进行范围查找)
索引
索引(index)是帮助MySQL高效获取数据的有序数据结构。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引
select * from user where age = 45;
索引的缺点:
- 占用更多的空间
- 增加了insert update delete的开销
B 树结构:
面试题:为什么InnoDB存储引擎选用B+tree索引结构?
索引的分类:
对于这样一条查询语句:
select * from user where name = "Arm";
它会先走二级索引查到id值,然后再用id值从聚集索引中查到row数据,这个过程被称为回表查询
以下的2条SQL语句,哪个执行效率高?
select * from user where id = 10; -- id是主键
select * from user where name = "Arm"; -- name有索引
答:第一个效率更高,因为它无需回表查询
索引语法
create [unique|fulltext] index index_name on table_name(col_name, ...); -- 创建索引
show index from table_name;
drop index index_name from table_name;
索引可以关联到多个列(字段):
- 如果索引关联到单个字段,称之为单列索引
- 如果索引关联到单个字段,称之为多列索引,或联合索引
create index idx_user_name on tb_user(name);
create unique index idx_user_phone on tb_user(phone);
create index idx_user_pro_age_stu on tb_user(profession, age, status);
create index idx_user_email on tb_user(email);
drop index idx_user_email from tb_user;
SQL性能分析
查看执行频次
查看当前数据库的操作是以“增删改”等 DML 语句为主,还是以查询 DQL 语句为主。如果主要是 DQL,可以考虑建立索引加速查询
mysql> show global status like "Com_______";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Com_binlog | 0 |
| Com_commit | 0 |
| Com_delete | 0 |
| Com_insert | 0 |
| Com_repair | 0 |
| Com_revoke | 0 |
| Com_select | 125 |
| Com_signal | 0 |
| Com_update | 0 |
| Com_xa_end | 0 |
+---------------+-------+
10 rows in set (0.00 sec)
慢查询日志
记录了所有执行时间超过指定参数(long_query_time
,单位:秒,默认10s)的所有SQL语句的日志。MySQL的慢查询日志默认没有开启,需要在MySQL的配置文件 /etc/my.cnf
中配置如下信息:
slow_query_log=1
long_query_time=2
show_profiles
select @@have_profiling; # 查看是否支持profile
select @@profiling; # 查看profiling是否打开
set profiling=1; # 打开profiling
show profiles;
show profile for query query_id;
show profile cpu for query query_id;
explain
explain
或者 desc
获取MySQL如何执行 select
语句的信息,包括在select语句执行过程中表如何连接和连接的顺序(sql语句的执行计划)
explain select * from tb1 where cond1; # 结果的id值越大,越优先执行;如果id相同,从上到下执行
explain 执行计划各字段含义:
- id:select 查询的序列号,表示查询中执行 select 子句或操作表的顺序(结果的id值越大,越优先执行;如果id相同,从上到下执行)
- select_type:simple(简单表,不需要表连接或者子查询)、primary(主查询,即外层的查询)、union(联合查询 union 后面的查询语句)、subquery(子查询)
- type:连接类型,性能由好到差:NULL、system、const、eq_ref、range、index、all
- possible_key:可能使用的索引
- key:实际使用的索引
- key_len:索引使用的字节数,该值为索引字段最大可能长度
- rows:MySQL 预估的必须执行查询的行数
- filtered:返回结果行数占需要读取行数的百分比,值越大越好
最左前缀法则
例如对于联合索引 idx_user_profession_age_status
最左索引存在即可,与顺序无关(会自动调整),比如下面的 sql,索引长度仍然是 54(全部索引):
select * from tb_user where status = "0" and age = 31 and profession = "软件工程"
在联合查询中,如果出现范围查询 <>
,范围查询右侧的列索引失效
尽量使用 >=
或 <=
,这样索引不会失效
select * from tb_user where profession = "SE" and age > 30 and status = '0'; # status失效
select * from tb_user where profession = "SE" and age >= 30 and status = '0'; # status正常
索引失效
不要在索引列上进行运算操作,否则索引将会失效
select * from tb_user where phone = "19858190715"; # 索引有效
select * from tb_user where substring(phone, 10, 2) = "15"; # 索引失效
字符串不加引号,索引会失效(隐式类型转换)
select * from tb_user where phone = 19858190715; # 索引失效
select * from tb_user where profession = "SE" and age = 30 and status = 0; # status索引失效
如果仅在尾部进行模糊匹配,索引不会失效。如果是在头部进行模糊匹配,索引将会失效
select * from tb_user where profession like "软件%"; # 索引有效
select * from tb_user where profession like "%工程"; # 索引失效
select * from tb_user where profession like "%工%"; # 索引失效
对于or连接的条件,如果一侧有索引,另一侧没有索引,则全部索引失效
select * from tb_user where phone = "19858190715" or age = 23; # 索引失效
select * from tb_user where id = 10 or age = 23; # 索引失效
数据分布对索引的影响:如果MySQL评估使用索引比全表扫描还慢,则不使用索引(如结果集就是全表数据)
select * from tb_user where id >= 1; # 直接全表,不走索引
select * from tb_user where profession is not null; # 直接全表,不走索引
SQL提示
在SQL语句中加入一些人为的提示来达到优化的操作目的(同时存在联合索引和单列索引时指定使用单列索引)
# use index
select * from tb_user use index(idx_user_pro) where profession = "SE";
# ignore index
select * from tb_user ignore index(idx_user_pro) where profession = "SE";
# force index
select * from tb_user force index(idx_user_pro) where profession = "SE";
覆盖索引
explain 返回的 Extra 的 2 种情况:
- using index condition:查询使用了索引,但是需要回表查询数据(低效)
- using where; using index:查询使用了索引,但是需要的数据在索引列中都能直接找到,所以不需要回表查询数据(高效)
select id, profession, age, status from tb_user where profession = "SE" and age = 31 and status = '0';
-- name 字段必须通过回表查询才能得到
select id, profession, age, status, name from tb_user where profession = "SE" and age = 31 and status = '0';
-- 一般会引发回表查询
select * from tb_user where profession = "SE" and age = 31 and status = "0";
为什么不推荐 select *
:极易产生回表查询
练习题:下面哪条SQL语句会产生回表查询?
select * from tb_user where id = 2;
select id, name from tb_user where name = "Arm";
select * from tb_user where name = "Arm"; # 这条语句会产生回表查询,性能低
一张表,有4个字段(id username password status),由于数据量大,需要对以下SQL语句进行优化,该如何进行?
select id, username, password from tb_user where username = "itcast";
答:对username password建立联合索引,避免回表查询(username 加速查询;passwrod 避免回表)
前缀索引
在保证选择性的前提下,取很长的字符串的一部分建立索引
create index idx_user_email_5 on tb_user(email(5));
通过前缀索引获取到数据后需要再检查是否满足where条件
在业务场景中,如果存在多个查询条件,考虑针对查询字段建立索引时,建议建立联合索引,而非单列索引
select id, phone, name from tb_user where phone = "19888876539" and name = "daniel";
create unique index idx_user_phone_name on tb_user(phone, name);
select id, phone, name from tb_user use index(idx_user_phone_name) where phone = "19888876539" and name = "daniel"; # 建立联合索引后,不再需要回表查询
联合索引的B+树结构:
每个节点有2个键值数据
索引失效情景:
- 不遵循“最左前缀法则”
- 在索引列上进行函数运算
- 字符串不加引号
- like模糊匹配,
%
在前面 - or连接的条件,一侧有索引,另一侧没有索引,索引失效
- MySQL评估,走全表扫描比走索引还快,索引失效(数据分布的影响)
SQL优化
insert优化
-
使用批量插入
-
手动提交事务
-
start transaction; insert into tb_test values(1, "tom"), (2, "cat"), (3, "jerry"); insert into tb_test values(4, "tom"), (5, "cat"), (6, "jerry"); insert into tb_test values(7, "tom"), (8, "cat"), (9, "jerry"); commit;
-
-
主键顺序插入
-
如果一次性插入大量数据,使用
insert
插入性能低,此时可以使用load
指令进行插入-
mysql --local-infile -uroot -ppswd # 连接数据库时加入--local-infile选项 set global local_infile = 1; load data local infile "/root/sql1.log" into table "tb_user" fields terminated by "," lines terminated by "\n";
-
主键优化
根据叶子节点的有序性,主键应顺序插入;如果主键乱序插入,会产生页分裂现象,浪费了时间
当删除一行记录时,并不会进行物理删除,而只会进行标记。当删除的记录达到 50% 时,会进行页合并以节省空间
主键设计原则:
- 尽量降低主键长度
- 插入数据时尽量选择顺序插入,选择使用
AUTO_INCREMENT
自增主键 - 尽量不要使用UUID做主键或其他自然主键
- 业务操作时,尽量避免对主键的修改
order by优化
mysql 有 2 种排序方式:
- using index:排序过程直接通过有序索引直接返回数据,效率高
- using filesort:需要将返回的结果在排序缓冲区中排序,所有不是通过索引直接返回排序结果的排序都称为 filesort 排序
-- age 和 phone 没有索引
select id, age, phone from tb_user order by age; -- using filesort
select id, age, phone from tb_user order by age, phone; -- using filesort
create index idx_user_id_age on tb_user(age, phone);
select id, age, phone from tb_user order by age; -- using index
select id, age, phone from tb_user order by age, phone; -- using index
select id, age, phone from tb_user order by age desc, phone desc; -- backward index scan; using index
select id, age, phone from tb_user order by phone, age; -- using index; using filesort
select id, age, phone from tb_user order by age asc, phone desc; -- using index; using filesort
-- 建立排序方式不同的索引
create index idx_user_id_age_ad on tb_user(age asc, phone desc);
select id, age, phone from tb_user order by age asc, phone desc; -- using index
order by优化:
- 根据排序字段建立适当的索引,多字段排序时也遵循最左前缀法则
- 尽量使用覆盖索引
- 多字段排序,一个升序一个降序,需要注意联合索引在创建时的规则(一个升序一个降序)
- 如果不可变地出现filesort,大数据量排序时,可以适当增大排序缓冲区大小
sort_buffer_size
group by优化
select profession, count(*) from tb_user group by profession; -- using temporary
create index idx_user_pro_age_sta on tb_user(profession, age, status); -- 创建联合索引
select profession, count(*) from tb_user group by profession; -- using index
select profession, count(*) from tb_user group by age; -- using index; using temporary
select profession, count(*) from tb_user group by profession, age; -- using index
select profession, count(*) from tb_user where profession = "SE" group by age; -- using index
在分组操作时,可以通过索引来提高效率
在分组操作时,索引的使用也是满足最左前缀法则
limit优化
select * from tb_sku limit 10, 10; -- 0s
select * from tb_sku limit 1000000, 10; -- 3s
select * from tb_sku limit 5000000, 10; -- 11s
limit
的查询时间随着 offset
的增大而增大。因为它需要将前面的所有数据都排序,仅返回少量数据
使用覆盖索引 + 多表联查(子查询)进行优化:
select s.* from tb_sku as s, (select id from tb_sku order by id limit 9000000, 10) as a where s.id = a.id;
count优化
count的几种用法:
-- count(*) count(pk) count(字段) count(1)
select count(1) from tb_user; -- 24
update优化
核心:尽量根据主键/索引字段进行数据更新,避免行锁升级为表锁
对于这样一张表:
create table course (
id int primary key auto_increment,
name varchar(10)
);
insert into course (id, name)
values (1, "Java"),
(2, "PHP"),
(3, "MySQL"),
(4, "Hadoop");
InnoDB引擎3大特性:事务,外键,行级锁
在行级锁的条件下,下面的两个事务得以成功并发执行:
begin; -- transcation1
update course set name = "javaEE" where id = 1;
commit;
begin; -- transcation2
update course set name = "Kafuka" where id = 4;
commit;
但是如果where条件使用name:
begin; -- transcation1
update course set name = "springboot" where name = "PHP"; -- 行锁升级为表锁
commit;
begin; -- transcation2
update course set name = "Kafka" where id = 4; -- 等待上一条update提交
commit;
name没有索引,被加了表锁。所以使用update更新数据时要根据索引字段进行更新,解决上面的问题的办法就是为 name 字段建立索引:
create index idx_course_name on course(name);
小结
视图
视图(View)是一种虚拟存在的表。视图中的数据并不在数据库中实际存在,行和列数据来自定义视图的查询中使用的表,并且是在使用视图时动态生成的。
通俗的讲,视图只保存了查询的SQL逻辑,不保存查询结果。所以我们在创建视图的时候,主要的工作就落在创建这条SQL查询语句上。
-- 创建视图
create [or replace] view view_name[(column_name)] as (select from 基表) [with [cascaded | local] check option];
对于这样一张表:
create table student (
id int primary key auto_increment,
name varchar(20),
no char(10)
);
insert into student (id, name, no)
values (1, "daiqisi", "2000100100"),
(2, "xiexun", "2000100102"),
(3, "yintianzheng", "2000100103"),
(4, "weiyixiao", "2000100104");
创建一个视图:
create or replace view stu_v1 as (select id, name from student where id <= 10);
-- 查看创建视图的语句
show create view view_name;
-- 查看视图数据
select * from view_name;
查询视图:
select * from stu_v1 where id < 3;
修改视图:
create or replace view view_name[(column_name)] as (select from 基表) [with [cascaded | local] check option];
alter view view_name[(column_name)] as (select from 基表) [with [cascaded | local] check option];
alter view stu_v1 as select id, name from student where id <= 10;
删除视图:
drop view if exists view_name;
检查选项
drop view if exists stu_v1;
create or replace view stu_v1 as select id, name from student where id <= 20;
select * from stu_v1;
insert into stu_v1 values(6, "Tom"); -- 插到了基表
insert into stu_v1 values(30, "Tom"); -- 插入了基表,但是在本视图查不到
为了避免出现上面这种基表和视图数据不一致的情况,增加检查选项
cascaded
create or replace view stu_v1 as select id, name from student where id <= 20 with cascaded check option;
select * from stu_v1;
insert into stu_v1 values(6, "Tom"); -- 插到了基表
insert into stu_v1 values(30, "Tom"); -- 插入失败
视图可以基于视图创建,对于级联的 check option,不仅会检查当前的 check option,还会检查上一级视图的 check option
local
create or replace view stu_v4 as select id, name from student where id <= 15;
insert into stu_v4 values(6, "Tom"); -- 成功
insert into stu_v4 values(16, "Tom"); -- 成功
create or replace view stu_v5 as select id, name from stu_v4 where id >= 10 with local check option;
insert into stu_v5 values(13, "Tom"); -- 成功
insert into stu_v5 values(17, "Tom"); -- 成功,因为 stu_v4 没有定义 local check option.如果是 cascaded 则不会成功
create or replace view stu_v6 as select id, name from stu_v5 where id < 20;
insert into stu_v6 values(14, "Tom"); -- 成功
视图更新
只能在视图和基表的行是一对一的关系下才能更新视图
create view stu_v_count as select count(*) from student;
insert into stu_v_count values(10); -- The target table stu_v_count of the INSERT is not insertable-into
视图的作用:
- 简单:视图不仅可以简化用户对数据的理解,也可以简化他们的操作那些被经常使用的查询可以被定义为视图,从而使得用户不必为以后的操作每次指定全部的条件
- 安全:通过视图用户只能查询和修改他们所能见到的数据数据库可以授权,但不能授权到数据库特定行和特定的列上
- 数据独立:视图可帮助用户屏蔽真实表结构变化带来的影响
视图案例
根据如下需求,定义视图:
- 为了保证数据库表的安全性,开发人员在操作
tb_user
表时,只能看到的用户的基本字段,屏蔽手机号和邮箱两个字段 - 查询每个学生所选修的课程(三张表联查),这个功能在很多的业务中都有使用到,为了简化操作,定义一个视图
create view tb_user_v1 as select id, name, profession, age, gender, status, createtime from tb_user;
select * from tb_user_v1;
create view tb_student_course_v1 as
select s.name as student_name,
s.no as studnet_no,
c.name as course_name
from
student as s,
student_course as sc,
course as c
where
s.id = sc.studnetid and c.id = sc.courseid;
存储过程
存储过程是事先经过编译并存储在数据库中的一段 SQL 语句的集合,调用存储过程可以简化应用开发人员的很多工作,减少数据在数据库和应用服务器之间的传输,对于提高数据处理的效率是有好处的。存储过程思想上很简单,就是数据库 SQL 语言层面的代码封装与重用
特点:
- 封装,复用
- 可以接受参数,也可以返回数据
- 减少网络交互,效率提升
create procedure p1()
begin
select count(*) from student;
end;
call p1();
select * from information_schema.ROUTINES where ROUTINE_SCHEMA = "itcast";
show create procedure p1;
drop procedure if exists p1;
在命令行中,执行创建存储过程的 SQL 时,需要通过关键字 delimiter 指定 SQL 语句的结束符(在 datagrip 则没有这个问题)
delimiter $$
变量
系统变量
查看系统变量:
show variables;
show session variables;
show session variables like 'auto%';
select @@autocommit; -- 查看特定系统变量的值
select @@global.autocommit;
设置系统变量:
set session autocommit = 0;
select @@session.autocommit;
如果没有指定 SESSION/GLOBAL
,默认是 SESSION
mysqld 重启后,全局参数会失效,若不想失效可以在 /etc/my.cnf
配置
用户自定义变量
set @myname = 'itcast'; -- 无需提前声明
set @myage := 10;
select count(*) into @mycnt from student;
select @myname, @myage, @mycnt;
局部变量
create procedure p2()
begin
declare stu_count int default 0;
select count(*) into stu_count from student;
select stu_count;
end;
call p2();
if
if c1 then
...
elseif c2 then
...
else
...
end if;
create procedure p3()
begin
declare score int default 58;
declare result varchar(10);
if score >= 85 then
set result := 'great';
elseif score >= 60 then
set result := 'pass';
else
set result := 'failure';
end if;
select result;
end;
call p3();
参数
create procedure p4(in score int, out result varchar(10))
begin
if score >= 85 then
set result := 'great';
elseif score >= 60 then
set result := 'pass';
else
set result := 'failure';
end if;
end;
call p4(90, @result);
select @result;
-- 将 200 分制的分数转化为百分制并返回
create procedure p5(inout score double)
begin
set score := score / 2;
end;
set @score = 180;
call p5(@score);
select @score; -- 90
case
create procedure p6(in month int)
begin
declare result varchar(10);
case
when month >= 1 and month <= 3 then
set result := '1';
when month >= 4 and month <= 6 then
set result := '2';
when month >= 7 and month <= 9 then
set result := '3';
when month >= 10 and month <= 12 then
set result := '4';
else
set result := 'invalid';
end case;
select concat(month, ': ', result);
end;
call p6(8);
while
create procedure p7(in n int)
begin
declare total int default 0;
while n > 0 do
set total := total + n;
set n := n - 1;
end while;
select total;
end;
call p7(10);
repeat
create procedure p8(in n int)
begin
declare total int default 0;
repeat
set total := total + n;
set n := n - 1;
until n <= 0 end repeat;
select total;
end;
call p8(100);
loop
求从 1 ~ n 的和:
create procedure p9(in n int)
begin
declare total int default 0;
sum: loop
if n <= 0 then
leave sum;
end if;
set total := total + n;
set n := n - 1;
end loop;
select total;
end;
call p9(100);
计算 1 ~ n 之间偶数累加的值:
create procedure p10(in n int)
begin
declare total int default 0;
sum: loop
if n <= 0 then
leave sum;
end if;
if n % 2 = 1 then
set n := n - 1;
iterate sum;
end if;
set total := total + n;
set n := n - 1;
end loop;
select total;
end;
call p10(100);
cursor
需求:根据传入的参数 uage,来查询用户表tb_user中,所有的用户年龄小于等于 uage 的用户姓名(name)和专业(profession),并将用户的姓名和专业插入到所创建的一张新表(id,name,profession)中
create procedure p11(in uage int)
begin
declare uname varchar(100);
declare upro varchar(100);
declare u_cursor cursor for select name, profession from tb_user where age <= uage;
drop table if exists tb_user_pro;
create table if not exists tb_user_pro(
id int primary key auto_increment,
name varchar(100),
profession varchar(100)
);
open u_cursor;
while true do
fetch u_cursor into uname, upro;
insert into tb_user_pro values (null, uname, upro);
end while;
close u_cursor;
end;
call p11(30);
handler
declare exit handler for SQLSTATE '02000' close u_cursor; -- 类似于出错后的中断回调
declare exit handler for not found close u_cursor; -- 类似于出错后的中断回调
create procedure p11(in uage int)
begin
declare uname varchar(100);
declare upro varchar(100);
declare u_cursor cursor for select name, profession from tb_user where age <= uage;
declare exit handler for SQLSTATE '02000' close u_cursor; -- 类似于出错后的中断回调
-- declare exit handler for not found close u_cursor; -- 类似于出错后的中断回调
drop table if exists tb_user_pro;
create table if not exists tb_user_pro(
id int primary key auto_increment,
name varchar(100),
profession varchar(100)
);
open u_cursor;
while true do
fetch u_cursor into uname, upro;
insert into tb_user_pro values (null, uname, upro);
end while;
close u_cursor;
end;
call p11(30);
存储函数
需求:使用存储函数实现从 1 ~ n 的累计
create function f1(n int)
returns int deterministic
begin
declare total int default 0;
while n > 0 do
set total := total + n;
set n := n - 1;
end while;
return total;
end;
select f1(100);
触发器
行级触发器和语句级触发器的区别:比如一条 update 语句一次更新 5 行,行级触发器会被触发 5 次,而语句级触发器会被触发 1 次
insert
需求:通过触发器记录 user 表的数据变更日志(user_logs),包含增加,修改,删除
创建日志表:
create table if not exists user_logs(
id int primary key auto_increment,
operation varchar(20) not null,
operate_time datetime not null,
operate_id int not null,
operate_params varchar(500)
) default charset=utf8;
插入数据触发器:
create trigger tb_user_insert_trigger after insert on tb_user for each row
begin
insert into user_logs(id, operation, operate_time, operate_id, operate_params)
values (null, 'insert', now(), new.id, concat('inserted data: id = ', new.id, ', name = ', new.name));
end;
update
create trigger tb_user_update_trigger after update on tb_user for each row
begin
insert into user_logs(id, operation, operate_time, operate_id, operate_params)
values (null, 'update', now(), new.id, concat('updated data: id = ', new.id, ', name = ', new.name));
end;
delete
create trigger tb_user_delete_trigger after delete on tb_user for each row
begin
insert into user_logs(id, operation, operate_time, operate_id, operate_params)
values (null, 'delete', now(), old.id, concat('deleted data: id = ', old.id, ', name = ', old.name));
end;
view:
- 虚拟存在的表,不保存查询结果,只保存查询的 SQL 逻辑
- 简单,安全,数据独立
procedure:
- 事先定义并存储在数据库中的一段 SQL 语句的集合
- 减少网络交互,提高性能,封装重用
- 变量、if、case、参数(in/out/inout)、while、repeat、loop、cursor、handler
function:
- 存储函数是有返回值的存储过程,参数类型只能为 IN 类型
- 存储函数可以被存储过程替代
trigger:
- 可以在表数据进行 insert、update、delete 之前或之后触发
- 保证数据完整性、日志记录、数据校验
锁
- 全局锁:锁住数据库中的所有表
- 表级锁:每次操作锁住整张表
- 行级锁:每次操作锁住对应的行数据
全局锁
对整个数据库加锁,加锁后整个库就处于只读状态,后续的 DML语句、DDL 语句、已经更新操作的事务提交语句都将被阻塞
典型的使用场景就是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性
数据库中加全局锁,是一个比较重的操作,存在以下问题:
- 如果在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆
- 如果在从库上备份,那么在备份期间从库不能执行主库同步过来的二进制日志(binlog),会导致主从延迟
在InnoDB引擎中,我们可以在备份时加上参数 --single-transaction
参数来完成不加锁的一致性数据备份:
mysqldump --single-transaction -uroot -p123456 itcast> itcast.sql
表级锁
表级锁,每次操作锁住整张表。锁定粒度大,发生锁冲突的概率最高,并发度最低。应用在MyISAM、InnoDB、BDB等存储引擎中
表级锁主要分为3类:
- 表锁
- 元数据锁(meta data lock,MDL)
- 意向锁
表锁
表锁又可被分为2类:共享读锁&独占写锁
- read lock(共享)
- write lock(独占)
语法:
lock tables tb read/write;
unlock tables;
对于读锁,当前客户端和其他客户端都能读,但是都不能写
对于写锁,其他客户端不能读写,当前客户端能够读写
元数据锁
意向锁
如下图,线程 A 根据主键 update id = 3 的数据时,会自动加上行锁。此时如果线程 B 过来加表锁,它需要逐行检查每一行是否添加了行锁,效率很差
为了解决这个问题,引入意向锁:为了避免行锁与表锁之间的冲突,在 InnoDB 中引入了意向锁,使表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查
如果另一个任务试图在该表级别上应用共享或排它锁,则受到由第一个任务控制的表级别意向锁的阻塞。第二个任务在锁定该表前不必检查各个页或行锁,而只需检查表上的意向锁
意向锁不会与行级的共享 / 排他锁互斥
行级锁
行锁
如果不通过索引检索数据,InnoDB 会对表中的所有记录加锁,升级为表锁:
间隙锁
间隙锁可以防止幻读
小结
概述:
- 在并发访问时,解决数据访问的一致性、有效性问题
- 全局锁、表级锁、行级锁
全局锁:
- 对整个数据库实例加锁,加锁后整个实例就处于只读状态
- 性能较差,数据逻辑备份时使用
表级锁:
- 操作锁住整张表,锁定粒度大,发生锁冲突的概率高
- 表锁、元数据锁、意向锁
行级锁:
- 操作锁住对应的行数据,锁定粒度最小,发生锁冲突的概率最低
- 行锁、间隙锁、临键锁
InnoDB 引擎
逻辑存储结构
架构
内存结构
磁盘结构
后台线程
将 InnoDB 缓存中存在的数据在合适的时机刷新到磁盘当中
事务原理
redo log
用于刷新脏页到磁盘发生错误时,进行数据恢复使用
刷新 redo log file 要比刷新 ibd 文件快得多,因为前者是随机磁盘 IO,后者是顺序磁盘 IO,性能更高
2个 redo log 是循环写的,并不会永久保留下来,占用空间不大
undo log
undo log 主要用于实现事务的原子性
回滚日志,用于记录数据被修改前的信息,作用含两个:提供回滚 和 MVCC(多版本并发控制)
undo log 和 redo log记录物理日志不一样,它是逻辑日志。可以认为当 delete 一条记录时,undo log 中会记录一条对应的 insert 记录,反之亦然。当 update 一条记录时,它记录一条对应相反的 update 记录。当执行 rollback 时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚
undo log 销毁:undo log 在事务执行时产生,事务提交时,并不会立即删除 undo log,因为这些日志可能还用于MVCC
undo log 存储:undo log 采用段的方式进行管理和记录,存放在前面介绍的 rollback segment 回滚段中,内部包含 1024 个 undo log segment
MVCC
隐式字段
undo log
回滚日志,在 insert、update、delete 的时候产生的便于数据回滚的日志
当 insert 的时候,产生的 undo log 日志只在回滚时需要,在事务提交后,可被立即删除
而 update、delete 的时候,产生的 undo log 日志不仅在回滚时需要,在快照读时也需要,不会立即被删除
对于这样一条记录:
其版本链如下:
read view
RC 级别分析
下图说明了事务 5 的第二个查询在 MVCC 版本链上匹配的过程,通过直接观察可以看出,事务 5 的第二次读取,能够读取到事务 3 提交的记录
首先匹配表中的记录,事务 ID 为 4,所有规则均不成立
接着匹配 undo log 版本链上的第一条数据,事务 ID 为3,满足第二条匹配规则:记录的事务 ID 小于当前活跃的最小事务 ID,说明该事务已经提交,可以读取
RR 级别分析
小结
逻辑存储结构:表空间、段、区、页、行
架构:内存结构&磁盘结构
MVCC:
- 记录隐藏字段
- undo log 版本链
- readView
MySQL 管理
常用工具:
mysql:客户端工具,-e 执行 SQL 并退出
mysqladmin:MySQL 管理工具
mysqlbinlog:二进制查看工具
mysqlshow:查看数据库,表,字段的统计信息
mysqldump:数据备份工具
mysqlimport/source:数据导入工具
(运维篇)日志
错误日志
错误日志是 MySOL 中最重要的日志之一,它记录了当 mysqld 启动和停止时,以及服务器在运行过程中发生任何严重错误时的相关信息
当数据库出现任何故障导致无法正常使用时,建议首先查看此日志
该日志是默认开启的,默认存放目录 /var/log/,默认的日志文件名为 mysqld.log
通过如下命令查看错误日志位置:
show variables like '%log_error%';
查看错误日志:
tail -f /var/log/mysqld.log
二进制日志
binlog 记录了所有 DDL 和 DML 语句,但不包括查询语句
作用:
- 灾难时的数据恢复
- MySQL 的主从复制
show variables like '%log_bin%';
查询日志(所有日志)
查询日志中记录了客户端的所有操作语句,而二进制日志不包含查询数据的SQL语句。默认情况下,查询日志是未开启的
show variables like '%general%';
打开该选项:
// /etc/my.cnf
general_log=1
general_log_file=/var/lib/mysql/mysql_query.log
慢查询日志
主从复制
主库配置
首先关闭主机和从机的防火墙
从库配置
分库分表
拆分策略
垂直拆分:
- 垂直分库:根据业务将不同的表拆分到不同的库中
- 垂直分表:根据字段属性将不同的字段拆分到不同的表中(例如将占用空间较大而又不重要的字段拆分出去)
水平拆分:
- 水平分库:对库中的每个表做水平拆分
- 水平分表:将表按照水平方向“剪开”,每张表的结构都一样
MyCat
Mycat:数据库分库分表中间件,不用调整代码即可实现分库分表,支持多种语言