MySQL笔记_3
SQL
优化
insert
优化
- 使用批量插入,一次性插入500~1000条数据,而不是一条一条插入,也不能一次性插入过多
- 使用手动事务提交
- 主键顺序插入
- 如果需要插入大批量的数据,可以使用
MySQL
提供的load
指令,而不使用insert
1
2
3
4
5
6
7
8-- 使用load需要修改
-- 客户端链接服务端 加上--local-infile参数
mysql \-\-local-infile -u root -p
-- 设置全局参数local-infile为1,开启本地加载文件到数据库的开关
set global local-infile = 1;
-- 执行load指令 将准备好的数据加载到表结构中
load data local infile '/root/sql1.log' into table 'user' fields terminated by ',' lines terminated by '\n';
-- 每个字段之间使用,分割,每行之间使用'\n'分割 - 对于
load
命令而言,同样顺序插入比乱序插入更好
主键优化
- 表数据都是根据主键顺序存放的,这种存储方式的表称为索引组织表(
index organized table, IOT
) InnoDB
中的页可以为空,也可以填充一半,或者存满。规定每个页至少包含两行数据,最多包含N
行数据。因为如果只有一行数据,相当于形成了一个链表,如果一行数据比较大,超出于阈值后又会形成溢出现象。- 行与行之间根据主键排列
主键顺序插入
- 如果主键顺序插入,则一页存放满以后自动开启新页并将数据存放其中
主键乱序插入
- 如果主键乱序插入,存放满以后,开启新的数据页,找到本应该要存放的数据页的50%的位置,后面的行移动到新的数据页,再决定要插入的行存放在原来剩下的50%的数据页中还是新数据页中,最后改变链表指针, 称为页分裂
- 删除一行记录时,没有物理删除,只是标记为删除并且可以被其他记录使用
- 页中删除的记录达到
MERGE_THRESHOLD
,即默认页的50%时,InnoDB
就会查找最近的前后两个页是否可以合并,如果可以合并,就会优化空间,称为页合并
推荐使用主键自增
- 比如身份证号,如果拿身份证号作为主键,导致所有二级索引存储主键的值就会占用
20B
,如果是自增主键,大小就会很小,所以主键长度越小,普通索引的叶子节点就越小,占用空间也就越小。 - 但是如果只有一个唯一索引,并且是
K-V
键值对查询的索引,那就可以不用主键自增,避免回表
主键设计原则
- 满足业务需求的情况下尽量降低主键的长度。因为主键索引只有一个,二级索引的叶子节点中记录的是主键,如果主键较长,会占用较多的磁盘空间。
- 插入数据使用顺序插入,使用
auto_increment
- 不要使用
UUID
或者身份证号作为主键 - 避免对主键的修改
order by
优化
Using filesort
: 通过表索引或全表扫描,读取满足条件的数据行。在排序缓冲区中sort buffer
完成排序操作,所有不是通过索引直接返回排序结果的排序都是filesort
排序Using index
: 通过有序索引顺序扫描直接返回有序数据,就是using index
,不需要额外的排序,操作效率高。
- 当没有
(age, phone)
这个联合索引的时候,select id, age, phone from user order by age, phone;
这个查询语句使用的是using filesort
- 当有
(age, phone)
这个联合索引的时候,则该语句使用using index
- 若语句改变为
select id, age, phone from user order by age desc, phone desc;
依然是using index
- 但是如果语句改变为
select id, age, phone from user order by age, phone desc;
则会出现前一个使用using index
,后一个变为using filesort
这是因为创建索引的时候值都是从小到大排列的,可以通过show index from user
查看其中的Collection
列是A
就表示升序排列 - 为了解决这个问题,只需要在创建索引的时候指定顺序即可,比如
create index idx_user_age_phone_ad on user(age asc, phone desc);
此时在进行上一次的排序,就会发现using index
- 如果排序变为
select id, age, phone from user order by phone, age;
就会出现前一个使用using filesort
,后一个使用using index
,因为这个违背了最左前缀法则,所以会出现两个。这个不像where ... and ...
可以自动匹配顺序变化。
- 若语句改变为
- 如果有联合索引,则全部是升序排序,或者全部是降序排序,均可实现
using index
; - 但是如果一个是升序,另一个是降序,则需要在创建一个指定排序的索引才行
- 这些都是建立在覆盖索引,不需要回表查询的基础上。如果需要回表,则一定是
using filesort
,比如:select * from user order by age, phone;
排序设计原则
- 建立合适的索引,多字段排序也遵循最左前缀法则
- 使用覆盖索引
- 多字段排序,一个升序一个降序,注意联合索引在创建时的规则
- 不可避免出现
filesort
的情况下,出现大数据排序时可以增大缓冲区的大小,sort_buffer_size
默认为256K
select city,name,age from t where city='杭州' order by name limit 1000;
- 初始化
sort buffer
, 确定要放入city name age
字段 - 从索引
city
中根据索引找到第一个满足条件的主键ID - 根据主键ID返回
name city age
存入sort buffer
- 从索引ID取下一个符合要求的主键ID,直到取完
sort buffer
按照name
快排- 取
1000
行返回
这是全字段排序,可能在内存中完成,也可能在外部完成,取决于
sort_buffer_size
,number_of_tmp_files
表示排序的过程中需要使用的临时文件个数,所以配置的sort_buffer_size
越小,number_of_tmp_files
就越大
- 全字段排序如果一行内容很多,就会导致
sort buffer
放不下,产生很多临时文件,性能就不好 - 配置
max_length_for_sort_data
,如果单行长度超过这个值,MySQL
就会换一个排序方法- 如果
max_length_for_sort_data
变小,比如只能放入name
,就不能直接返回了 - 初始化
sort buffer
,确定放入name
和id(rowid)
- 根据
city
索引找到主键id,根据主键id返回name
和id
字段,存入sort buffer
中 - 从索引
city
取下一个主键ID,重复,直到不满足city
条件为止 sort buffer
对name
字段排序- 结果根据id找到
city name age
返回 - 所以比全字段排序多了一次回表
- 如果
- 如果建立
(city,name)
联合索引,这条语句就不会进行排序- 从索引
city
中找到第一个满足条件的主键ID - 从主键ID找到
city name age
作为结果集的一部分直接返回
- 从索引
- 如果建立
(city, name, age)
联合索引,则变成了覆盖索引,不需要回表,直接返回结果
- 如果
explain
的结果中,是using temporary, using filesort
的话,那么结果就是需要临时表,并且需要排序
如果现在有单词表,需要每次随机显示三个单词:select word from words order by rand() limit 3;
-
创建临时表,引擎
Memory
,两个字段,一个随机值R
,一个单词word
,没有索引 -
按照单词表主键顺序读出
word
,生成0-1
之间的随机数,存入临时表中 -
按照字段
R
排序- 初始化
sort buffer
, 两个字段,一个是double
,一个是整型 - 从内存临时表读取
R
和rowid
,存入sort buffer
中 sort buffer
根据R
进行排序- 取前三个
rowid
,根据rowid
在内存表找到对应的word
,返回
- 初始化
-
对于
InnoDB
表,全字段排序减少磁盘访问,会优先选择 -
order by rowid
用的就是内存表进行排序 -
对于内存表,回表就是根据行数据,直接访问内存得到数据,不会多访问磁盘,所以优化器优先考虑行越小越好,所以会选择根据
rowid
排序 -
rowid
就是InnoDB
唯一标识数据行的值,如果主键被删除了,InnoDB
会自动生成一个6B
的rowid
-
对于
Memory
引擎,rowid
就是数组下标。如果内存临时表中进行排序,这个临时表就是Memory
引擎的 -
tmp_table_size
参数设置了内存临时表的大小,默认16MB
,超过大小就是磁盘临时表,InnoDB
引擎,如果使用磁盘临时表,就是一个没有显式主键的InnoDB
表排序过程 -
如果
limit
值特别小的情况,引擎会使用优先队列排序优化
limit
优化
- 由于需要排序记录,并且将其他记录全部丢弃,查询排序的时间非常长。因此,
limit
越往后时间越长 - 可以使用覆盖索引 + 子查询的形式优化
- 原始语句:
select * from 表名 order by id limit 9000000, 10;
- 覆盖索引:
select id from 表名 order by id limit 9000000, 10;
- 子查询:
select * from 表名 where id in (select id from 表名 order by id limit 9000000, 10);
这句话会报错,因为不能在in
后面使用limit
select s.* from 表名 s, (select id from 表名 order by id limit 90000000, 10) a where s.id=a.id;
将limit查询结果作为一个表联合查询
count
优化
MyISAM
把表的总行数存储在磁盘上,count(*)
直接返回这个数,效率高,前提是没有where
条件InnoDB
即使没有where
条件,也需要将数据一行一行读出,然后累加- 这是因为
MVCC
,所以不能记录count(*)
的值直接返回,因为每一次返回的值是不确定的 InnoDB
会保证逻辑正确的情况下,找索引树最小的那棵来遍历计数
- 这是因为
- 优化只有自己计数,例如使用
redis
count(列名)
返回的是列中不为null
的个数,count(主键)
遍历整张表,把每一行主键id取出,返回给服务层,服务层拿到主键以后直接累加,因为主键不会为null
count(字段)
需要看字段是否有not null
约束,如果没有该约束,需要将每行字段值取出,返回给服务层。服务层判断是否为null
,不为null
才累加计数- 如果有
not null
约束,则跟主键处理一样
- 如果有
count(1)
遍历整张表,但是不取值,服务层对于返回的每一行,都放一个数字1进去,直接按行累加。参数是什么数字都可以,0,-1都可以,返回一样的结果count(*)
不会把全部字段取出来,而是专门优化了,直接在服务层累加- 所以最终的效率
count(*)
性能最高,类似于count(1)
,然后才是count(主键)
,性能最差的是count(字段)
, 推荐使用count(*)
缓存系统计数
- 使用
Redis
计数,插入一行,计数就加一,反之减一- 可能丢失更新,
Redis
在内存中,虽然可以持久化保存在磁盘中,如果没有保存的时候异常重启了,那么再启动时,Redis
从磁盘中得到的数据就丢失了,不过可以通过异常重启以后执行count(*)
更新值 - 就算这样,逻辑也会有问题,比如取出总数同时显示最近100条,就需要从
Redis
中取出数据,再从MySQL
中取100条- 返回的100条的时候可能新加入了数据
- 可能100条数据中有新数据,但是
Redis
没有 + 1 - 可能100条数据中没有新数据,但是
Redis
+ 1
- 可能丢失更新,
数据库计数
- 单独存储一张表用来计数
- 可以崩溃恢复
- 可以利用事务隔离保证计数和返回的结果不会有偏差
update
优化
- 更新数据时,根据索引字段级进行更新,否则行锁会升级成表锁。
update course set name='Java' where id=1;
这句话是行锁,别的事务更新别的行不会被卡住update course set name='Springboot' where name='kafka';
这句话是表锁,别的事务更新别的行会被卡住,因为name
字段没有索引- 如果建立了
name
的索引,则上句更新就是行锁了
- 如果建立了
- 一定要根据索引字段进行更新!否则表锁会锁住整张表。
存储对象: 视图、存储过程、触发器、存储函数
视图
- 视图是虚拟存在的表,视图中的数据不在数据库中真实存在,行列数据来自视图查询中使用的表,称为基表,在使用视图时动态生成
- 视图只保存
SQL
逻辑,不保存查询结果。创建视图主要工作在SQL
查询语句上
增
- 创建视图:
create [or replace] view 视图名[(列名列表)] as select语句 [with [cascade | local] check option];
- 替换视图就使用
or replace
select语句
关联的表就是基表- 创建视图的时候指定
with cascade check option
,并且设置了视图select语句
为where id < 20
,若此时增加了一条id = 30
的数据就会报错,提示无法插入。如果没有设置check option
就不会报错,可以正常插入
- 替换视图就使用
删
- 删除视图:
drop view [if exists] 视图名[, 视图名1, ...];
改
- 替换视图:
create or replace view 视图名[(列名列表)] as select语句 [with [cascade | local] check option];
- 修改视图:
alter view 视图名[(列名列表)] as select语句 [with [cascade | local] check option];
查
- 查询创建视图语句:
show create view 视图名;
- 查看视图数据:
select * from 视图名;
视图就是虚拟存在的表,所以可以跟操作表一样操作视图
- 在视图中插入数据会插入到对应的基表中,因为视图不会保存数据。
with check option
就是检查项,在增删改的时候都会检查是否符合视图创建时的定义。MySQL
允许创建视图的视图,检查依赖视图规则有两个选项,就是cascade
以及local
cascade
create view v1 as select id, name from student where id <= 20;
这里没有设置check option
,所以修改数据不会检查id <= 20
create view v2 as select id, name from v1 where id >= 10 with cascade check option;
这里修改了v2
的数据时,不仅会检查id >= 10
,还会检查id <= 20
create view v3 as select id, name from v2 where id <= 15;
这里修改v3
的数据> 15
也可以插入,但是如果> 20
则不能插入,因为不能满足v2
的检查项
local
create view v1 as select id, name from student where id <= 15;
修改v1
中的数据不会检查id <= 15
create view v2 as select id, name from v1 where id >= 10 with local check option;
先检查是否满足v2
条件,然后再找v1
是否需要检查,发现v1
不需要检查,则直接插入。所以插入id = 17
也可以create view v3 as select id, name from v2 where id < 20;
没有定义检查选项,会先找v2
的检查项,发现v2
需要检查id >= 10
,再发现v1
没有检查选项,所以最终id >= 10
即可
- 视图如果要能够更新,必须满足视图中的行与基表中的行一对一关系,只要包含下面任意一项,则视图不可更新
- 聚合函数或窗口函数
sum, min, max, count
distinct
group by
having
union
,union all
- 聚合函数或窗口函数
视图作用
- 操作简单,将复杂的经常使用的查询定义在视图中,不需要每次都指定同样的相同操作
- 安全,由于
MySQL
不能授权到特定的行列,只能授权到表。所以可以通过视图修改用户能见到的数据 - 数据独立: 可以帮助用户屏蔽真实表结构带来的变化
例子:
- 为了保证安全性,操作
user
表时,只能看到用户的基本字段,屏蔽手机号和邮箱两个字段:
create view user_view as select id, name, profession, age, gender, status, create_time from user;
- 为了简化操作,查询每个学生所选修课程,三表联查,定义一个视图:
1
2
3
4select s.name, s.no, c.name from student s, student_course sc, course c where s.id = sc.studentid and c.id = sc.courseid;
-- 封装成视图
create view stu_course_view as select s.name studentname, s.no studentno, c.name coursename
from student s, student_course sc, course c where s.id = sc.studentid and c.id = sc.courseid;
存储过程
- 很多地方禁止使用
- 事先编译并存储在数据库中的一段
SQL
语句集合,减少数据库和应用服务器之间的传输,提高数据处理效率 - 就是
SQL
语句封装重用 - 特点:封装、复用;可以接收参数和返回数据;减少网络交互,提升效率
增
- 创建存储过程
1
2
3
4
5
6
7create procedure 存储过程名称([in/out/inout参数列表])
begin
-- SQL语句
end; -- 这里有个分号
-- 如果是在命令行中创建存储过程,遇到分号就自动结束了,所以会报错,需要通过delimiter指定sql语句结束符
-- delimiter $$
-- 会一直到$$才认为是语句结束了
删
- 删除存储过程:
drop procedure if exists 存储过程名;
调用
- 调用存储过程:
call 存储过程名([参数])
查
- 查询指定数据库的存储过程以及状态信息:
select * from information_schema.routines where routine_schema='xxx';
- 查询存储过程定义:
show create procedure 存储过程名;
变量
MySQL
变量分为三类,系统变量,用户自定义变量以及局部变量
- 存储过程的参数类型:
in, out, inout
三种类型,默认是in
类型的,作为输入,out
类型作为输出,作为返回值,inout
既可以作为输入又可以作为输出
系统变量:
- 不需要用户自定义,属于服务器,分为全局变量
global
以及会话变量session
- 会话变量表示在当前会话内有效
- 查询所有系统变量:
show [session | global] variables;
- 模糊查询:
show [session | global] variables like ...;
- 查询指定的系统变量,不写默认是
session
:show @@[session | global].系统变量名;
MySQL
重启后,设置的global
会失效,需要在my.cnf
中完成设置才能保持
用户自定义变量
- 直接
@变量名
即可 - 赋值自定义变量:
1
2
3
4set @var_name = expr[, @var_name = expr, ...];
set @var_name := expr[, @var_name := expr, ...];
select @var_name := expr
select 字段名 into @var_name from 表名; - 使用
select @varname
,如果var_name
没有赋值,这个语句不会报错,只会显示null
局部变量
- 使用
declare
申明,存储过程内的局部变量和输入参数,在begin ... end
模块内生效 - 声明变量:
declare 变量名 变量类型 [default ...];
- 赋值局部变量:
1
2
3set 变量名 = 值;
set 变量名 := 值;
select 字段名 into 变量名 from 表名;
case
1 | case case_value |
while
1 | while 条件 do |
repeat
1 | repeat |
loop
1 | [begin_label:] loop |
游标
- 用于存储查询结果集的数据类型,在存储过程个函数中可以使用游标对结果集进行循环处理,包括游标声明,
open, fetch, close
- 声明游标:
declare 游标名称 cursor for 查询语句;
- 打开游标:
open 游标名称;
- 打开游标后才能获取游标记录:
fetch 游标名称 into 变量[, 变量];
- 关闭游标:
close 游标名称;
例子:
- 根据传入参数
uage
, 查询user
表,用户年龄小于等于uage
的用户姓名name
和专业profession
插入到创建的一张表id, name, profession
中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24create procedure p(in uage int)
begin
declare uname varchar(100);
declare upro varchar(100);
declare u_cursor cursor for select name, profession from user where age <= uage;
-- 游标的申明应该放在普通变量后面
declare exit handler for sqlstate '02000' close u_cursor;
-- 满足SQL状态码是02000的, 触发退出操作,关闭游标u_cursor
-- 同样可以将sqlstate '02000' 替换成not found
drop table if exists user_pro;
create table if not exists 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 user_pro values(null, uname, upro);
end while; -- 但是这里是死循环,因为不知道游标什么时候会结束
close u_cursor;
end;- 条件处理程序
handler
定义在流程遇到问题和相应的处理步骤 declare handler_action handler for condition_value[, condition_value ...] statement;
handler_action
包括两个内容,continue
以及exit
condition_value
有SQLSTATE sqlstate_value
状态码,比如02000
;sqlwarning
是所有以01
开头的sqlstate
代码的缩写,not found
是所有以02
开头的代码的缩写,sqlexception
是所有其他的sqlstate
代码的缩写
- 条件处理程序
存储函数
- 就是有返回值的存储过程,并且参数只能为
in
类型,可以被存储过程替代
1 | create function 存储函数名([参数列表]) |
characteristic
:
deterministic
: 相同的输入参数总是产生相同的结果no sql
: 不包含SQL
语句reads sql data
: 包含读取数据的语句,但是不包含写入数据的语句
触发器
- 不建议使用
MySQL
只支持行级触发器,不支持语句级触发器,因此一次修改多行,就会触发多次
触发器类型 | NEW 和 OLD |
---|---|
INSERT型 | NEW表示将要或者已经新增的数据 |
UPDATE型 | OLD表示修改之前的数据,NEW表示将要或已经修改的数据 |
DELETE型 | OLD表示将要或已经删除的数据 |
-
创建触发器:
1
2
3
4
5
6create trigger trigger_name
before/after insert/ update/ delete
on 表名 for eachrow
begin
-- trigger_stmt;
end; -
查看触发器:
show triggers;
-
删除触发器:
drop triggers [schema_name.]trigger_namel
如果没有指定schema_name
,默认为当前数据库