逻辑架构
存储过程 procedure
存储过程是一组为了完成特定功能的SQL语句集合。使用存储过程的目的是将常用或复杂的工作预先用SQL语句写好并用一个指定名称存储起来,这个过程经编译和优化后存储在数据库服务器中,因此称为存储过程。当以后需要数据库提供与已定义好的存储过程的功能相同的服务时,只需用 CALL
调用这个存储过程即可。
创建
CREATE PROCEDURE 存储过程名(IN|OUT|INOUT 参数名 参数类型,...)
BEGIN
存储过程体
END
delimiter
命令:在 MySQL 中,服务器处理 SQL 语句默认是以分号作为语句结束标志的。然而,在创建存储过程时,存储过程体可能包含有多条 SQL 语句,这些 SQL 语句如果仍以分号作为语句结束符,那么 MySQL 服务器在处理时会以遇到的第一条 SQL 语句结尾处的分号作为整个程序的结束符,而不再去处理存储过程体中后面的 SQL 语句,这样显然不行。为解决这个问题,通常可使用 DELIMITER 命令将结束命令修改为其他字符。在创建存储过程最后的 end 后写上修改的结束符,表示语句结束。
delimiter //
将结束符修改为//
delimiter ;
将结束符修改为;
例:创建不带参数的存储过程
mysql> delimiter //
mysql> create procedure test()
-> begin
-> update t_user set c_name = "hello" where id = 1;
-> end //
mysql> delimiter ;
# 调用存储过程
mysql> call test;
例:创建带参数的存储过程
mysql> delimiter //
mysql> create procedure test
-> (IN name varchar(30))
-> begin
-> update t_user set c_name = name where id = 1;
-> end //
mysql> delimiter ;
# 调用存储过程
mysql> call test('hello')
删除
mysql> drop procudure test;
查看
# 查看所有存储过程
mysql> show procedure status;
# 查看指定存储过程
mysql> show procedure status like 存储过程名;
参考
InnoDB引擎
存储数据结构
索引
索引的声明和使用
索引分类
- 从功能逻辑上说,索引主要有 4 种,分别是普通索引、唯一索引、主键索引、全文索引。
- 按照物理实现方式,索引可以分为 2 种:一级索引(聚簇索引)和二级索引(非聚簇索引)。
- 一级索引:索引和数据存储在一起,都存储在同一个B+tree中的叶子节点。一般主键索引都是一级索引。
- 二级索引树的叶子节点存储的是主键而不是数据。也就是说,在找到索引后,得到对应的主键,再回到一级索引中找主键对应的数据记录。
- 按照作用字段个数进行划分,分成单列索引和联合索引。
创建索引
# 建表时建立索引
CREATE TABLE table_name [col_name data_type]
[UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY] [index_name] (col_name [length]) [ASC | DESC]
# 在已存在的表上创建索引
ALTER TABLE table_name
ADD [UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY] [index_name] (col_name[length],...) [ASC | DESC]
CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name
ON table_name (col_name[length],...) [ASC | DESC]
UNIQUE
、FULLTEXT
和SPATIAL
为可选参数,分别表示唯一索引、全文索引和空间索引;INDEX
与KEY
为同义词,两者的作用相同,用来指定创建索引;index_name
指定索引的名称,为可选参数,如果不指定,那么MySQL默认col_name为索引名;col_name
为需要创建索引的字段列,该列必须从数据表中定义的多个列中选择;length
为可选参数,表示索引的长度,只有字符串类型的字段才能指定索引长度;ASC
或DESC
指定升序或者降序的索引值存储。
查看索引
SHOW INDEX FROM table_name
删除索引
ALTER TABLE table_name DROP INDEX index_name;
或
DROP INDEX index_name ON table_name;
隐藏索引(8.0新特性)
从 MySQL8.x 开始支持隐藏索引(invisible indexes),只需要将待删除的索引设置为隐藏索引,使查询优化器不再使用这个索引(即使使用force index(强制使用索引),优化器也不会使用该索引),确认将索引设置为隐藏索引后系统不受任何响应,就可以彻底删除索引。这种通过先将索引设置为隐藏索引,再删除索引的方式就是软删除。
# 切换成隐藏索引
ALTER TABLE tablename ALTER INDEX index_name INVISIBLE;
# 切换成非隐藏索引
ALTER TABLE tablename ALTER INDEX index_name VISIBLE;
# 也可以在创建索引时指定 INVISIBLE
# 索引创建时默认是 VISIBLE
(当索引被隐藏时,其内容仍是实时更新的,因此不推荐长期隐藏索引)
创建索引原则(推荐创建索引的场景)
字段的数值有唯一性的限制
索引本身可以起到约束的作用,比如唯一索引、主键索引都可以起到唯一性约束的,因此在我们的数据表中,如果某个字段是唯一的,就可以直接创建唯一性索引,或者主键索引。这样可以更快速地通过该索引来确定某条记录。
业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。(来源:Alibaba)
说明:不要以为唯一索引影响了insert速度,这个速度损耗可以忽略,但提高查找速度是明显的。
频繁作为 WHERE 查询条件的字段
某个字段在SELECT语句的 WHERE 条件中经常被使用到,那么就需要给这个字段创建索引了。尤其是在数据量大的情况下,创建普通索引就可以大幅提升数据查询的效率。
经常 GROUP BY 和 ORDER BY 的列
某个字段在 SELECT 语句的 WHERE 条件中经常被使用到,那么就需要给这个字段创建索引了。尤其是在数据量大的情况下,创建普通索引就可以大幅提升数据查询的效率。索引就是让数据按照某种顺序进行存储或检索,因此当我们使用 GROUP BY 对数据进行分组查询,或者使用 ORDER BY 对数据进行排序的时候,就需要对分组或者排序的字段进行索引。如果待排序的列有多个,那么可以在这些列上建立组合索引。
UPDATE、DELETE 的 WHERE 条件列
对数据按照某个条件进行查询后再进行 UPDATE 或 DELETE 的操作,如果对 WHERE 字段创建了索引,就能大幅提升效率。原理是因为我们需要先根据 WHERE 条件列检索出来这条记录,然后再对它进行更新或删除。如果进行更新的时候,更新的字段是非索引字段,提升的效率会更明显,这是因为非索引字段更新不需要对索引进行维护。
查询 DISTINCT 字段
有时候我们需要对某个字段进行去重,使用 DISTINCT,那么对这个字段创建索引,也会提升查询效率。
多表 JOIN
- 连接表的数量尽量不要超过 3 张,因为每增加一张表就相当于增加了一次嵌套的循环,数量级增长会非常快,严重影响查询的效率。
- 对 WHERE 条件创建索引,因为 WHERE 才是对数据条件的过滤。如果在数据量非常大的情况下,没有 WHERE 条件过滤是非常可怕的。
- 对用于连接的字段创建索引,并且该字段在多张表中的类型必须一致。
使用列的类型小的创建索引
我们这里所说的类型大小指的就是该类型表示的数据范围的大小。
- 数据类型越小,在查询时进行的比较操作越快
- 数据类型越小,索引占用的存储空间就越少,在一个数据页内就可以放下更多的记录,从而减少磁盘 I/O 带来的性能损耗,也就意味着可以把更多的数据页缓存在内存中,从而加快读写效率。
这个建议对于表的主键来说更加适用,因为不仅是聚簇索引中会存储主键值,其他所有的二级索引的节点处都会存储一份记录的主键值,如果主键使用更小的数据类型,也就意味着节省更多的存储空间和更高效的I/O。
使用字符串前缀创建索引
如果某个字段是 varchar 类型的(或者 text 等等),那么其中可能存储了很长的字符串,当建立索引时,可以不用整个字符串建立索引,而是指截取前面部分建立索引
拓展:Alibaba《Java开发手册》
【强制】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。
说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90% 以上。
区分度计算公式:count(distinct left(列名, 索引长度))/count(*)
区分度高的列适合作为索引
列的基数指的是某一列中不重复数据的个数,比方说某个列包含值 2,5,8,2,5,8,2,5,8,虽然有 9 条记录,但该列的基数却是 3 。也就是说,在记录行数一定的情况下,列的基数越大,该列中的值越分散;列的基数越小,该列中的值越集中。这个列的基数指标非常重要,直接影响我们是否能有效的利用索引。最好为列的基数大的列建立索引,为基数太小的列建立索引效果可能不好。
可以使用公式 select count(distinct a)/count(*) from t1
计算区分度,越接近 1 越好,一般超过 33% 就算是比较高效的索引了。
拓展:联合索引把区分度高(散列性高)的列放在前面。
使用频率高的列放到联合索引的左侧
多个字段都要创建索引的情况下,联合索引优于单值索引
单张表的索引不超过6个
不适合创建索引的场景
- where 中使用不到的字段不要使用索引
- 数据量小的表不要使用索引
- 有大量重复数据的列上不要创建索引
- 避免对经常更新的表创建过多索引
- 不建议用无序的值作为索引
- 删除不再使用或者很少使用的索引
- 不要定义冗余或重复的索引
插入 1KW 条测试数据
创建测试表t_user
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`c_user_id` varchar(36) NOT NULL DEFAULT '' COMMENT '用户Id',
`c_name` varchar(22) NOT NULL DEFAULT '' COMMENT '用户名',
`c_province_id` int(11) NOT NULL COMMENT '省份Id',
`c_city_id` int(11) NOT NULL COMMENT '城市Id',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`c_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
创建临时表
CREATE TABLE `tmp_table` (
`id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
用 python 生成 1kw 个数据
[root@fengye tmp]# python -c "for i in range(1, 1+10000000): print(i)" > base.txt
在 mysql 中执行导入命令
load data infile '\home\tmp\base.txt' replace into table tmp_table
可能出现的报错
1290 - The MySQL server is running with the --secure-file-priv option so it cannot execute this statement
原因:mysql默认没有开启
secure_file_priv
解决:在
my.ini(/etc/my.cnf)
中的[mysqld]
节点中添加secure_file_priv = ”,然后重启
说明:
secure_file_prive = null 限制mysqld 不允许导入导出
secure_file_priv = /var/lib/mysql-files/ 限制mysqld的导入导出只能发生在/var/lib/mysql-files/目录下
secure_file_priv = ” 不对mysqld的导入导出做限制
从临时表中的数据插入到 t_user
中
insert into t_user
select id,uuid(),concat('userNickName',id),floor(rand()*1000),floor(rand()*100),now()
from tmp_table
打乱创建时间
UPDATE t_user SET create_time=date_add(create_time, interval FLOOR(1 + (RAND() * 7)) year);
性能分析工具
查看系统性能参数
SHOW STATUS LIKE '参数'
常用的性能参数:
Connections:连接 MySQL 服务器的次数
Uptime:MySQL服务器上线事件
Slow_queries:慢查询次数
Innodb_rows_read:已select的行数
Innodb_rows_inserted:已inserted的行数Innodb_rows_updated:已updated的行数Innodb_rows_deleted:已deleted的行数
Com_select:查询操作的次数
Com_insert:插入操作的次数
Com_update:更新操作的次数
Com_delete:删除操作的次数
查看上一条SQL语句的查询成本
SHOW STATUS LIKE 'last_query_cost'
SQL 查询是一个动态的过程,从页加载的角度来看,我们可以得到以下两点结论:
- 位置决定效率。如果页就在数据库缓冲池中,那么效率是最高的,否则还需要从内存或者磁盘中进行读取,当然针对单个页的读取来说,如果页存在于内存中,会比在磁盘中读取效率高很多。
- 批量决定效率。如果我们从磁盘中对单一页进行随机读取,那么效率是很低的(差不多10ms),而采用顺序读取的方式,批量对页进行读取,平均一页的读取效率就会提升很多,甚至要快于单个页面在内存中的随机读取。
所以说,遇到I/O并不用担心,方法找对了,效率还是很高的。我们首先要考虑数据存放的位置,如果是经常使用的数据就要尽量放到缓冲池中,其次我们可以充分利用磁盘的吞吐能力,一次性批量读取数据,这样单个页的读取效率也就得到了提升。
慢查询日志
MySQL的慢查询日志,用来记录在MySQL中响应时间超过阈值(即 long_query_time
,默认为10s
)的语句。
慢查询日志默认为关闭状态,需要手动开启,支持持久化到文件。
建议在测试环境中开启,正式环境中关闭,因为该功能会影响性能。
补充说明:
除了
long_query_time
外,还有一个系统变量min_examined_row_limit
,表示查询扫描过的最小记录数。当一条查询扫描的记录数大于
min_examined_row_limit
同时查询的执行事件超过long_query_time
,那么这个查询就会被记录到慢查询日志中。这个值默认是
0
,可以在my.ini
中修改,也可用SET
命令修改。
修改慢查询配置
查看慢查询是否开启
SHOW VARIABLES LIKE 'slow_query_log';
查看慢查询日志位置
SHOW VARIABLES LIKE 'slow_query_log_file';
开启慢查询
SET GLOBAL slow_query_log='ON';
查看慢查询阈值
show variables like '%long_query_time%';
修改慢查询阈值
#测试发现:设置global的方式对当前session的long_query_time失效。对新连接的客户端有效。所以可以一并 执行下述语句
mysql> set global long_query_time = 1;
mysql> show global variables like '%long_query_time%';
mysql> set long_query_time=1;
mysql> show variables like '%long_query_time%';
查看慢查询数量
SHOW GLOBAL STATUS LIKE '%Slow_queries%';
慢查询日志分析
#得到按照时间排序的前10条里面含有左连接的查询语句
mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/atguigu-slow.log
参数:
-s 表示按照何种方式排序
c 访问次数
l 锁定时间
r 返回记录
t 查询时间
al 平均锁定时间
ar 平均返回记录数
at 平均查询时间
-t 返回前面多少条数据
-g 后边搭配一个正则匹配模式,大小写不敏感
可能会出现的报错:
mysqldumpslow: command not found...
原因:系统默认去
/usr/bin
下查找命令解决方法:找到 mysql 的安装目录下的
/bin/mysqldumpslow
执行
ln -s /www/server/mysql/bin/mysqldumpslow /usr/bin
重置慢查询日志
mysqladmin -uroot -p flush-logs slow
EXPLAIN 分析查询语句
语法
EXPLAIN 查询语句
EXPLAIN 语句输出的各个列的作用如下
列名 | 描述 |
---|---|
id | 在一个大的查询语句中每个SELECT关键字都对应一个唯一的id |
select_type | SELECT关键字对应的那个查询的类型 |
table | 表名 |
partitions | 匹配的分区信息 |
type | 针对单表的访问方法 |
possible_keys | 可能用到的索引 |
key | 实际上使用的索引 |
key_len | 实际使用到的索引长度 |
ref | 当使用索引列等值查询时,与索引列进行等值匹配的对象信息 |
rows | 预估的需要读取的记录条数 |
filtered | 某个表经过搜索条件过滤后剩余记录条数的百分比 |
Extra | 一些额外的信息 |
id
SELECT识别符(执行顺序的标识)。
- id值越大优先级越高,越先被执行
- id相同时,执行顺序由上至下
select_type
- SIMPLE(简单SELECT,不使用UNION或子查询等)
- PRIMARY(子查询中最外层查询,查询中若包含任何复杂的子部分,最外层的select被标记为PRIMARY)
- UNION(UNION中的第二个或后面的SELECT语句)
- DEPENDENT UNION(UNION中的第二个或后面的SELECT语句,取决于外面的查询)
- UNION RESULT(UNION的结果,union语句中第二个select开始后面所有select)
- SUBQUERY(子查询中的第一个SELECT,结果不依赖于外部查询)
- DEPENDENT SUBQUERY(子查询中的第一个SELECT,依赖于外部查询)
- DERIVED(派生表的SELECT, FROM子句的子查询)
- UNCACHEABLE SUBQUERY(一个子查询的结果不能被缓存,必须重新评估外链接的第一行)
table
显示这一步所访问数据库中表名称(显示这一行的数据是关于哪张表的),有时不是真实的表名字,可能是简称,例如上面的e,d,也可能是第几步执行的结果的简称
type *
对表的访问方式,表示 mysql 在表中找到所需行的方式,又称访问类型。
ALL
不用索引,遍历全表(效率最低)
index
使用索引,遍历全表(效率还是很低)
select count(*) from user;
range
范围查询,用在 between/like/<=
等等
select * from user where user_name like 'test';
ref
通过普通的二级索引进行等值查询(普通指的是不唯一索引,因此查询结果可能有多条)
select * from user where user_name = 'test';
ref_or_null
与 ref
类似,条件中多了一个 is null
判断
select * from user where address = 'test' or address is null;
eq_ref
连表查询时,连接的条件是主键或唯一二级索引,这是连表查询中连接效率最高的
select * from user
join department
on department.user_id = user.id;
const
根据主键或唯一二级索引与常数进行等值匹配
`select * from user where id = 100;
system
const的特例,当查询的表只有一条数据时,用system(innodb不支持)
结果从好到坏:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
SQL性能优化的目标:至少达到range级别,要求是ref级别,最好是consts级别
possible_keys & key
possible_key
表示可能用到的索引
key
表示实际用到的索引
key_len
实际使用的索引的长度
ref
查询条件的类型
rows *
预计需要读取的行数(越小越好)
filtered
某个表经过搜索条件过滤后剩余记录条数的百分比
extra *
记录了一些额外的信息来帮助理解MySQL实际查询的过程
EXPLAIN 输出格式
EXPLAIN FORMAT=<xxx> ...
- 传统格式
- JSON 格式(最详细)
- TREE 格式(8.0.16+)
- 可视化输出(Mysql workbench)
分析优化执行计划:trace
# 开启
SET optimizer_trace="enabled=on",end_markers_in_json=on;
# 设置大小
set optimizer_trace_max_mem_size=1000000;
# 使用
select * from student where id < 10;
select * from information_schema.optimizer_trace\G
MySQL监控分析视图 sys schema
索引情况
#1. 查询冗余索引
select * from sys.schema_redundant_indexes;
#2. 查询未使用过的索引
select * from sys.schema_unused_indexes;
#3. 查询索引的使用情况
select index_name,rows_selected,rows_inserted,rows_updated,rows_deleted from sys.schema_index_statistics where table_schema='dbname' ;
表相关
# 1. 查询表的访问量
select table_schema,table_name,sum(io_read_requests+io_write_requests) as io from sys.schema_table_statistics group by table_schema,table_name order by io desc;
# 2. 查询占用bufferpool较多的表
select object_schema,object_name,allocated,data
from sys.innodb_buffer_stats_by_table order by allocated limit 10;
# 3. 查看表的全表扫描情况
select * from sys.statements_with_full_table_scans where db='dbname';
语句相关
#1. 监控SQL执行的频率
select db,exec_count,query from sys.statement_analysis order by exec_count desc;
#2. 监控使用了排序的SQL
select db,exec_count,first_seen,last_seen,query
from sys.statements_with_sorting limit 1;
#3. 监控使用了临时表或者磁盘临时表的SQL
select db,exec_count,tmp_tables,tmp_disk_tables,query
from sys.statement_analysis where tmp_tables>0 or tmp_disk_tables >0 order by (tmp_tables+tmp_disk_tables) desc;
IO相关
#1. 查看消耗磁盘IO的文件
select file,avg_read,avg_write,avg_read+avg_write as avg_io
from sys.io_global_by_file_by_bytes order by avg_read limit 10;
Innodb 相关
#1. 行锁阻塞情况
select * from sys.innodb_lock_waits;
索引优化和查询优化
索引失效的几种情况
- 不满足最佳左前缀原则
where
条件后使用计算、函数、类型转换- 范围条件右边索引失效(指的是联合索引中的范围列)
- 使用了
select *
(会引起回表) - 字段不允许为空,则
is null
和is not null
都失效;若字段允许为空,则is null
走ref
类型的索引,而is not null
走range
类型的索引。 like
查询左边有%
OR
前后存在非索引的列- 使用
not in
- 使用
!=
、<
、>
回表问题和索引覆盖
在 InnoDB
中,索引分为聚簇索引和普通索引。
- 聚簇索引的叶子节点存储的是完整的行记录
- 普通索引的叶子节点存储的是主键
可见,InnoDB
的聚簇索引负责存储完整的行数据,而普通索引只能帮助找到主键。因此,InnoDB
必须也只能有一个聚簇索引。
- 如果表定义的主键,那么主键就是聚簇索引
- 如果表没有定义主键,那么第一个
not null
的unique
列就是聚簇索引 - 否则,
InnoDB
会创建一个隐藏的row-id
作为聚簇索引
回表
利用普通索引查询到某条数据的主键后,又返回到聚簇索引,重新定位该数据。
回表查询的性能比扫一遍索引树低。
索引覆盖
如果一个索引包含(覆盖)了所需要查询的字段的值,那么就称为索引覆盖
在 InnoDB 中,普通索引树的叶子节点存储的都是主键+索引列值。
为了避免回表,可以对需要查询的数据建立联合索引。
连接查询优化
连接查询原理