MySQL 多表查询与体系结构(完整版)
---
第一部分:多表查询
---
一、表与表之间的关系
在SQL语句中,数据表与数据表之间,如果存在关系,一般一共有3种情况:
1.1 一对一关系
比如有A、B两张表,A表中的每一条数据,在B表中有一条唯一的数据与之对应。
用户表 tb_user
| user_id(用户编号) | 账号username | 密码password |
|---|---|---|
| 001 | admin | admin888 |
| 002 | itheima | 123456 |
用户详情表 tb_user_items
| user_id(用户编号) | 真实姓名 | 年龄 | 联系方式 |
|---|---|---|---|
| 001 | 张三 | 16 | 10086 |
| 002 | 李四 | 18 | 10010 |
其中,user_id是一一对应的,我们把用户表与用户详情表之间的关系就称之为一对一关系。
1.2 一对多关系
比如有A、B两张表,A表中的每一条数据,在B表中都有多条数据与之对应,我们把这种关系就称之为一对多关系。
产品分类表
| 分类id编号 | 分类名称 |
|---|---|
| 1 | 手机 |
| 2 | 电脑 |
产品信息表
| 产品id编号 | 产品名称 | 产品价格 | 所属分类id编号 |
|---|---|---|---|
| 1 | Apple iPhone 13 | 6799.00 | 1 |
| 2 | Redmi Note 9 | 3499.00 | 1 |
我们把产品分类表与产品表之间的(分类id的对应)关系就称之为一对多关系。
1.3 多对多关系
用户表
| 用户编号 | 登录账号 | 登录密码 |
|---|---|---|
| 1 | admin | admin888 |
| 2 | itheima | 123456 |
权限表
| 权限id编号 | 权限名称 |
|---|---|
| 1 | 增加 |
| 2 | 删除 |
| 3 | 修改 |
| 4 | 查询 |
用户与权限之间是多对多关系,需要通过中间表来关联。
---
二、连接查询介绍
连接查询可以实现多个表的查询,当查询的字段数据来自不同的表就可以使用连接查询来完成。
连接查询可以分为:
- 内连接查询(也是默认连接)
- 左外连接查询
- 右外连接查询
2.1 准备数据集
-- 1. 准备数据集
-- 创建数据库并切换
create database db_itheima; -- 创建数据库db_itheima
use db_itheima; -- 切换到db_itheima数据库
-- 创建班级表classes,拥有两个字段,cls_id代表班级编号,cls_name代表班级名称
drop table if exists classes; -- 如果classes表存在则删除
create table classes( -- 创建classes表
cls_id int, -- 班级编号字段
cls_name varchar(20) -- 班级名称字段
);
-- 插入数据,ui、java、python、shell
insert into classes values
(1, 'UI'), -- 班级1:UI
(2, 'Java'), -- 班级2:Java
(3, 'Python'), -- 班级3:Python
(4, 'Shell'); -- 班级4:Shell
-- 创建学生表students,拥有id、name、age、gender值使用male和female、score、cls_id
drop table if exists students; -- 如果students表存在则删除
create table students( -- 创建students表
id int, -- 学生编号字段
name varchar(20), -- 学生姓名字段
age int, -- 学生年龄字段
gender enum('male', 'female'), -- 性别字段,只能是male或female
score decimal(11,2), -- 分数字段
cls_id int -- 班级编号字段
);
-- 插入数据,刘备属于Java,貂蝉属于UI,赵云属于Python,关羽属于Python,大乔属于UI
insert into students values
(1, '刘备', 18, 'male', 100.00, 2), -- 刘备:Java班,100分
(2, '貂蝉', 18, 'female', 99.00, 1), -- 貂蝉:UI班,99分
(3, '赵云', 18, 'male', 98.00, 3), -- 赵云:Python班,98分
(4, '关羽', 18, 'male', 96.00, 3), -- 关羽:Python班,96分
(5, '大乔', 18, 'female', 97.00, 1), -- 大乔:UI班,97分
(6, '小乔', 18, 'female', 97.00, 5); -- 小乔:cls_id=5不存在
-- 创建学生详情表students_detail
drop table if exists students_detail; -- 如果students_detail表存在则删除
create table students_detail( -- 创建students_detail表
id int primary key auto_increment, -- 主键,自动增长
stu_id int, -- 学生编号
address varchar(200) -- 地址
);
-- 插入数据
insert into students_detail (stu_id, address)
values (1,'aaaaaa'), -- 学生1的地址
(2,'bbbbbb'); -- 学生2的地址2.2 交叉连接(笛卡尔积)
-- 交叉连接也称之为笛卡尔积连接,一旦出现,就会出现数据库性能问题
select * from students cross join classes; -- 使用cross join关键字
-- 或
select * from students, classes; -- 使用逗号分隔笛卡尔积连接,没有意义,但是它是所有连接的基础。 其功能就是将表1和表2中的每一条数据进行连接。
结果:
- 字段数 = 表1字段 + 表2的字段
- 记录数 = 表1中的总数量 × 表2中的总数量(笛卡尔积)
| id | name | age | gender | score | cls_id | cls_id | cls_name |
|---|---|---|---|---|---|---|---|
| 1 | 刘备 | 18 | male | 100.00 | 2 | 1 | UI |
| 1 | 刘备 | 18 | male | 100.00 | 2 | 2 | Java |
| 1 | 刘备 | 18 | male | 100.00 | 2 | 3 | Python |
| 1 | 刘备 | 18 | male | 100.00 | 2 | 4 | Shell |
| 2 | 貂蝉 | 18 | female | 99.00 | 1 | 1 | UI |
| ... | ... | ... | ... | ... | ... | ... | ... |
共 6 × 4 = 24 条记录
---
三、内连接(INNER JOIN)
说明:查询两个表中符合条件的共有记录。
语法格式:
select 字段,... from 表1 inner join 表2 on 表1.字段1 = 表2.字段2;说明:
inner join就是内连接查询关键字
on就是连接查询条件
案例1:查询学生及其班级(所有字段)
-- 案例:求每个学生所属的班级信息(学生所有字段 + 班级名称)
select * from students inner join classes on students.cls_id = classes.cls_id;
-- 给表起别名,在后续使用
select * from students as stu inner join classes as cls on stu.cls_id = cls.cls_id;
-- 要students表中所有字段,但只要classes中的cls_name字段
select stu.*,cls.cls_name from students as stu inner join classes as cls on stu.cls_id = cls.cls_id;
-- inner关键字可以不写,默认就是,所以直接写一个join就可以了
select stu.*,cls.cls_name from students as stu join classes as cls on stu.cls_id = cls.cls_id;执行结果:
| id | name | age | gender | score | cls_id | cls_id | cls_name |
|---|---|---|---|---|---|---|---|
| 1 | 刘备 | 18 | male | 100.00 | 2 | 2 | Java |
| 2 | 貂蝉 | 18 | female | 99.00 | 1 | 1 | UI |
| 3 | 赵云 | 18 | male | 98.00 | 3 | 3 | Python |
| 4 | 关羽 | 18 | male | 96.00 | 3 | 3 | Python |
| 5 | 大乔 | 18 | female | 97.00 | 1 | 1 | UI |
小乔(cls_id=5)不匹配,不会出现。
案例2:指定字段 + WHERE 条件
-- 要students中指定的字段和classes表中指定的字段
select stu.id,stu.name,cls.cls_name from students as stu inner join classes as cls on stu.cls_id = cls.cls_id;
-- 要求student中分数在97及以上,要得到它的分类,还要指定字段
select stu.id,stu.name,stu.score,cls.cls_name from students as stu inner join classes as cls
on stu.cls_id = cls.cls_id
where stu.score>=97;执行结果:
| id | name | score | cls_name |
|---|---|---|---|
| 1 | 刘备 | 100.00 | Java |
| 2 | 貂蝉 | 99.00 | UI |
| 3 | 赵云 | 98.00 | Python |
| 5 | 大乔 | 97.00 | UI |
内连接小结
- 内连接使用
inner join .. on ..,on表示两个表的连接查询条件
- 内连接根据连接查询条件取出两个表的"交集",也就是满足关联条件的结果
- 特别注意:
join默认就是inner join
---
四、左外连接(LEFT JOIN)
说明:有一个主表概念,默认情况下,关联后会保留主表的所有记录。以左表为主根据条件查询右表数据,如果根据条件查询右表数据不存在,使用null值填充。
语法格式:
select 字段 from 表1 left join 表2 on 表1.字段1 = 表2.字段2;说明:
left join就是左连接查询关键字
on就是连接查询条件
- 表1 是左表
- 表2 是右表
左外连接示意图
商品分类表
| 编号 | 分类名称 |
|---|---|
| 1 | 手机 |
| 2 | 电脑 |
商品表
| 编号 | 商品名称 | 商品分类编号 |
|---|---|---|
| 1 | Vivo手机 | 1 |
| 2 | Xiaomi手机 | 1 |
左外连接查询:select * from 分类表(主表) left join 商品表 on 分类表.编号 = 商品表.分类编号;
| 编号 | 分类名称 | 编号 | 商品名称 | 商品分类编号 |
|---|---|---|---|---|
| 1 | 手机 | 1 | Vivo手机 | 1 |
| 1 | 手机 | 2 | Xiaomi手机 | 1 |
| 2 | 电脑 | null | null | null |
左外连接默认会保留左表,然后与右边的表进行匹配;如果匹配到,则显示右表对应的数据;匹配不到,也要显示。只不过右表的所有字段使用null进行填充!
案例:查询所有班级及其学生
-- 左外连接 主表和从表,取主表的全部和从表的交集部份,如果主表中的数据在从表中不存在,则使用null
select stu.id,stu.name,stu.score,cls.cls_name from classes as cls left join students as stu
on stu.cls_id = cls.cls_id;执行结果:
| id | name | score | cls_name |
|---|---|---|---|
| 5 | 大乔 | 97.00 | UI |
| 2 | 貂蝉 | 99.00 | UI |
| 1 | 刘备 | 100.00 | Java |
| 4 | 关羽 | 96.00 | Python |
| 3 | 赵云 | 98.00 | Python |
| NULL | NULL | NULL | Shell |
Shell班没有学生,但左连接保证左表(classes)全部显示。
左连接后过滤
-- 左连接后再进行where过滤
select stu.id,stu.name,stu.score,cls.cls_name from classes as cls left join students as stu
on stu.cls_id = cls.cls_id where id is not null;执行结果:
| id | name | score | cls_name |
|---|---|---|---|
| 5 | 大乔 | 97.00 | UI |
| 2 | 貂蝉 | 99.00 | UI |
| 1 | 刘备 | 100.00 | Java |
| 4 | 关羽 | 96.00 | Python |
| 3 | 赵云 | 98.00 | Python |
左外连接小结
- 左连接使用
left join .. on ..,on表示两个表的连接查询条件
- 左连接以左表为主根据条件查询右表数据,右表数据不存在使用null值填充
---
五、右外连接(RIGHT JOIN)
说明:以右表为主根据条件查询左表数据,如果根据条件查询左表数据不存在则使用null值填充。
语法格式:
select 字段 from 表1 right join 表2(主表) on 表1.字段1 = 表2.字段2;说明:
right join就是右连接查询关键字
on就是连接查询条件
- 表1 是左表
- 表2 是右表
案例:查询所有学生及其班级
-- 查看班级中对应的学生信息,如果某个班级没有对应的学生也要显示。(既可以用左外连接也可以用右外连接)
select stu.id,stu.name,stu.score,cls.cls_name from classes as cls right join students as stu
on stu.cls_id = cls.cls_id;执行结果:
| id | name | score | cls_name |
|---|---|---|---|
| 1 | 刘备 | 100.00 | Java |
| 2 | 貂蝉 | 99.00 | UI |
| 3 | 赵云 | 98.00 | Python |
| 4 | 关羽 | 96.00 | Python |
| 5 | 大乔 | 97.00 | UI |
| 6 | 小乔 | 97.00 | NULL |
小乔的cls_id=5没有对应班级,右连接保证右表(students)全部显示。
右外连接小结
- 右连接使用
right join .. on ..,on表示两个表的连接查询条件
- 右连接以右表为主根据条件查询左表数据
- 左表数据不存在使用null值填充
- 左表数据存在右表不存在就不展示
- 注意:左和右是相对的,如果两个表互换,左右也会反转
---
六、子查询(扩展)
6.1 子查询介绍
在一个 select 语句中,嵌入了另外一个 select 语句,那么被嵌入的 select 语句称之为子查询语句。外部那个select语句则称为主查询语句。
select * from (select * from xxx) as t;作用:子查询比较适合复杂查询以及多层级查询结构。
主查询和子查询的关系:
- 子查询是嵌入到主查询中
- 子查询是辅助主查询的,要么充当条件,要么充当数据源(数据表) => 要么出现在where,要么出现在from位置
- 子查询是可以独立存在的语句,是一条完整的
select语句
应用场景:在我们需求的基础上,如果这个需求需要通过多条SQL语句分步查询的情况,一般都需要基于子查询。
注意:子查询会全表扫描两次,大数据量下需谨慎使用。所以我们说了解就行。
案例1:WHERE 中的子查询
-- 取出大于平均分的人信息 子查询 位置 where条件处
select * from tb_score where math > (select avg(math) from tb_score);执行过程:子查询先算出平均分 84.6,主查询再过滤 math > 84.6 的记录。
| id | name | math | english |
|---|---|---|---|
| 1 | 刘备 | 90 | 85 |
| 4 | 赵云 | 95 | 88 |
| 5 | 貂蝉 | 88 | 92 |
案例2:SELECT 列表中的子查询
-- 在from取字段处
select name,math,(select max(math) as aa from tb_score) from tb_score;执行结果:
| name | math | max_math |
|---|---|---|
| 刘备 | 90 | 95 |
| 关羽 | 80 | 95 |
| 张飞 | 70 | 95 |
| 赵云 | 95 | 95 |
| 貂蝉 | 88 | 95 |
案例3:自关联查询
-- 自关联,一张表自己和自己关联起来
select a.*,b.title from areas a join areas b on a.pid=b.id where a.id=110106;---
七、连接查询对比表
| 特性 | 内连接 (INNER JOIN) | 左外连接 (LEFT JOIN) | 右外连接 (RIGHT JOIN) |
|---|---|---|---|
| 关键字 | inner join / join | left join | right join |
| 结果集 | 两表交集 | 左表全部 + 右表匹配 | 右表全部 + 左表匹配 |
| NULL 填充 | 不填充 | 右表无匹配填 NULL | 左表无匹配填 NULL |
| 语法 | on 关联条件 | on 关联条件 | on 关联条件 |
| 使用场景 | 查共有数据 | 左表全量展示 | 右表全量展示 |
---
第二部分:MySQL 体系结构
---
一、MySQL四层架构简介
面试题:MySQL底层,一条SQL语句的执行流程/原理?
答:经过四层,分别是连接层(连接池)、服务层(查询缓存、分析器、优化器、执行器)、引擎层、存储层。
| 层级 | 名称 | 主要功能 |
|---|---|---|
| 第一层 | 连接层 | 管理连接、身份认证、连接池 |
| 第二层 | 服务层 | SQL解析、优化、缓存 |
| 第三层 | 引擎层 | 数据存储和提取的实现 |
| 第四层 | 存储层 | 与操作系统交互,数据持久化 |
---
二、连接层
2.1 客户端(连接者)
MySQL的客户端可以是:
- 某个客户端软件(DataGrip)
- 不同的编程语言(Python/Java等)编写的应用程序
- 一些API的接口
-- 查看当前的连接者
show processlist;2.2 连接层核心功能
主要作用:管理和缓冲用户连接,为客户端请求做连接处理;身份认证等。
面试题:进程和线程?
| 概念 | 特点 | 适用场景 |
|---|---|---|
| 进程 | 一个应用软件,启动后往往会产生1个甚至多个进程,进程需要消耗一定的计算机资源(CPU、内存、磁盘、网络),进程之间的数据是不共享的 | CPU密集型应用(大量的计算程序,需要消耗资源) |
| 线程 | 一个进程可以产生多个线程,线程不能单独存在,必须依赖进程。所有线程共享进程资源,线程启动、停止快,资源开销小 | IO密集型应用(文件操作、网络爬虫、数据库连接) |
总结:进程是资源分配的基本单位,而线程是操作系统进行CPU调度和执行的最小单位。
2.3 连接池机制
连接层中的缓存池,就是为了优化数据库连接而设置的机制,专门用来缓存和复用已建立的连接。
主要作用:
- 减少连接开销:避免每次新建连接带来的系统负担,提升性能
- 提升响应速度:连接池里有现成的连接,随取随用,减少等待时间
- 优化资源利用:通过控制最大连接数,防止过多连接耗尽系统资源
工作流程:创建连接 → 用完放回池中 → 再次复用,保持高效循环。
2.4 连接池配置参数
-- 模糊查看wait_timeout
show variables like 'wait_timeout';执行结果:
| Variable_name | Value |
|---|---|
| wait_timeout | 28800 |
-- 查看当前mysql中系统变量
-- mysql中变量调用用 @@变量名
select @@wait_timeout;执行结果:
| @@wait_timeout |
|---|
| 28800 |
配置文件 /etc/my.cnf:
[mysqld]
max_connections=100 -- 最大连接数
thread_cache_size=4 -- 线程缓存池大小
wait_timeout=300 -- 连接闲置等待时间
interactive_timeout=300 -- 交互超时时间注意事项:
- 修改配置前先备份原文件,尤其是 SSH、Nginx、数据库和 Kubernetes 生产配置。
三、服务层
主要作用:接受用户的SQL请求,查询分析,权限处理,优化,结果缓存等。
---
四、引擎层(重要)
4.1 什么是存储引擎?
1. 存储引擎说白了就是如何管理操作数据(存储数据、如何更新、查询数据等)的一种方法和机制 2. 在MySql数据库中提供了多种存储引擎,各个存储引擎的优势各不一样,现在mysql中推荐使用Innodb 3. 用户可以根据不同需求为数据表选择不同的存储引擎,也可以根据自己需要编写自己的存储引擎 4. 甚至一个库中不同的表使用不同的存储引擎,这些都是允许的
-- 查看当前mysql实例中支持的存储引擎种类
show engines;
-- 查看当前默认的存储引擎
select @@default_storage_engine;执行结果:
| @@default_storage_engine |
|---|
| InnoDB |
4.2 MyISAM与InnoDB引擎对比表
| 特性 | MyISAM | InnoDB |
|---|---|---|
| 事务支持 | 不支持 | 支持(ACID特性) |
| 外键支持 | 不支持 | 支持 |
| 锁机制 | 表级锁 | 行级锁 |
| 适用场景 | 读多写少的应用,查询性能好 | 高并发写操作的应用 |
| 崩溃恢复 | 数据损坏风险高,无法自动恢复 | 自动恢复,数据更安全,双写(dw double write) |
| 存储结构 | 每个表有单独的文件 | 表和索引存储在表空间中 |
| 性能 | 查询性能较好,但不支持事务处理 | 支持事务处理,写操作性能较高 |
4.3 面试题:你用过哪些数据库引擎?各自特点?
答:早期MySQL5.5版本使用过MyISAM引擎,后期MySQL5.7、MySQL8.0等等都是使用InnoDB引擎,偶尔也了解MEMORY引擎。
① MyISAM引擎
- 擅长数据查询,支持较好的索引优化、数据压缩、支持表级锁以及全文索引技术
- 安全性相对于InnoDB略差一些
② InnoDB引擎
- 擅长数据安全,支持行级锁,支持事务处理,支持外键约束等等
- 强调安全性
③ Memory引擎
- 擅长数据缓存,加快数据查询
- 由于数据放置于内存,所以安全性没有MyISAM以及InnoDB好
---
五、InnoDB事务处理
5.1 应用场景:银行转账(最典型)
我的银行卡:1.00
itwu银行卡:2000.00
发生一系列操作:
① itwu发起转账,扣款1000,余额-1000
② 银行接收任务,处理(ATM)
③ 我的银行卡接收到1000,余额+1000update bank set money=money-1000 where name = "itwu"
-- 交易系统停电了
update bank set money=money+1000 where name = "我"5.2 事务操作步骤
操作步骤:事务处理配合Python/Java程序一起使用,就是把所有要执行的SQL语句当做一个整体,要么全部成功,要么全部失败。
要想完成事务操作,你的创建表时所用的存储引擎,必须且一定要为 innodb
-- 查看表的存储引擎
show create table 表名;事务三步骤:
-- ① 开启事务处理功能
start transaction;
-- ② 执行一系列的SQL语句(多条)
update bank set money=money-1000 where name = "itwu";
update bank set money=money+1000 where name = "我";
-- ③ 判断SQL语句是否全部执行成功,如果成功则提交事务 => commit; 失败,则回滚事务 => rollback;
commit;5.3 事务示例
-- 创建InnoDB引擎的表
create table bank (
id int primary key auto_increment, -- 主键,自动增长
name varchar(20), -- 姓名
money int not null default 1 -- 余额,默认为1
);
-- 查看表结构
show create table bank;
-- 插入测试数据
insert into bank (name,money) values ('itwu',2000),('我',1);
-- 代码示例:
-- 开始一个事务
start transaction;
-- 扣款
update bank set money=money-1000 where name = "itwu";
-- 收款
update bank set money=money+1000 where name = "我";
-- 如果事务中没有问题,则提交commit;如果发现有问题则可以rollback
commit;执行结果:
| id | name | money |
|---|---|---|
| 1 | itwu | 1000 |
| 2 | 我 | 1001 |
---
六、存储层(物理层)
核心作用:物理层负责与底层的操作系统交互,将数据存储到磁盘上,并确保数据的物理安全。
默认存储位置:/usr/local/mysql/data 数据目录下
工作方式:
- 将数据以物理文件的形式存储在磁盘上(如表空间文件、数据文件、日志文件等)
- 通过文件系统与操作系统进行交互,管理数据的读写、缓存、索引文件等
---
第三部分:数据引擎详解
---
一、MyISAM引擎
-- 永久修改默认存储引擎方案
-- vi /etc/my.cnf
-- [mysqld]
-- default_storage_engine=innodb
-- 在创建表时来指定存储引擎 engine
create database test5;
use test5;
-- 创建一个表并指定它的存储引擎为myisam
create table user1(id int) engine=myisam;MyISAM引擎文件结构:
| 文件 | 说明 |
|---|---|
| *.sdi | 序列化字典信息(Serialized Dictionary Information)表的元数据信息,主要用于数据字典管理;数据表结构、字段、类型等等 |
| *.MYI | INDEX索引,主要用于存放索引文件 |
| *.MYD | Data数据文件,主要用于存储数据文件 |
注意:早期MySQL5.7及以前版本,没有*.sdi文件,只有*.frm文件。早期MySQL5.7及以前版本,我们可以通过cp *.frm、*.MYI、*.MYD这三个文件来实现MyISAM引擎表的备份,MySQL8.0以后引入更多复杂的功能,导致没有办法直接copy,只能通过物理备份或逻辑备份!
---
二、InnoDB引擎
-- 创建一个表并指定它的存储引擎为innodb
-- 如果默认mysql它的引擎就为innodb,所以你可以不指定
create table user2(id int) engine=innodb;
-- 查看一下表空间大小
select @@innodb_data_file_path;执行结果:
| @@innodb_data_file_path |
|---|
| ibdata1:12M:autoextend |
InnoDB引擎文件结构:
| 文件 | 说明 |
|---|---|
| .ibd | 每个表都会有一个独立的.ibd文件,存储该表的表数据和索引 |
| ibdata1 | 用于存储全局的表空间、数据字典和事务日志等 |
表空间类型:
- 共享表空间
- 独立表空间 -- 数据所在位置 mysql8.x 默认就是开启了独立表空间 innodb_file_per_table=1|ON
- 临时表空间
表空间:MySQL的InnoDB存储引擎采用表空间来管理数据,可以把它看作是数据库内容的物理存储仓库。用这样的方式为了更好的去扩容。
-- 配置文件 /etc/my.cnf
-- innodb_file_per_table=1
-- 建议最多不要超过3个
-- innodb_data_file_path=ibdata1:12M;ibdata2:100M:autoextendredo log日志文件(ib_logfile0, ib_logfile1等):用于存储事务日志,确保数据一致性和恢复能力。
redo log配置:
# /etc/my.cnf
[mysqld]
# 每个重做日志文件的大小
innodb_log_file_size = 50M
innodb_redo_log_capacity=100M
# 重做日志组中日志文件的个数 2(默认值)
innodb_log_files_in_group = 2
# redo log 文件存放路径
innodb_log_group_home_dir = /usr/local/mysql/data注意事项:
- 修改配置前先备份原文件,尤其是 SSH、Nginx、数据库和 Kubernetes 生产配置。
第四部分:慢查询日志与索引优化
---
一、慢查询日志配置
1.1 启动慢查询日志(重点)
SQL语句:执行过程中,有快有慢,找出慢查询SQL。
作用:慢查询日志记录了所有执行时间超过设定阈值的查询(300ms、1s、5s根据不同业务场景而定),帮助你发现需要优化的慢查询SQL语句,辅助优化。
如何启用:
# /etc/my.cnf
[mysqld]
# 开启慢查询日志
slow_query_log=1
# 设置超过 1 秒的查询被记录
long_query_time=1
# 指定慢查询日志文件存放路径
slow_query_log_file=/usr/local/mysql/logs/mysql-slow.log
# 记录未使用索引的查询(可选)
log_queries_not_using_indexes=1
# 通用日志 开启会他们记录所有的sql操作,建议不要在生产环境中去开启,只要测试环境中去开启,方便调试
general_log=1
general_log_file=/usr/local/mysql/data/general.log注意事项:
- 修改配置前先备份原文件,尤其是 SSH、Nginx、数据库和 Kubernetes 生产配置。
1.2 日志内容示例
# Time: 2026-05-26T10:00:38.070872Z
# User@Host: root[root] @ localhost [] Id: 8
# 执行的多久
# Query_time: 0.419292 Lock_time: 0.000002 Rows_sent: 655360 Rows_examined: 655360
use test5;
SET timestamp=1779789637;
# 语句
select * from user2;日志字段说明:
| 字段 | 含义 |
|---|---|
| Query_time | 查询耗时(秒) |
| Lock_time | 锁等待时间 |
| Rows_sent | 返回行数 |
| Rows_examined | 扫描行数(越少越好) |
分析日志:慢查询日志可以显示查询的执行时间、锁等待时间、以及查询的详细内容。分析日志后,优化这些慢查询是提升性能的首要任务。
1.3 案例设计 - 慢查询
添加200万条数据到数据表中,做测试(不要求掌握以下,只是为了做测试)
use db_itheima;
-- 创建测试表
drop table if exists tb_people;
create table tb_people (
id int auto_increment primary key, -- 主键,自动增长
name varchar(50), -- 姓名
age int -- 年龄
);
-- 创建存储过程
drop procedure if exists insertdata;
delimiter //
create procedure insertdata()
begin
declare i int default 1; -- 声明变量i,默认值为1
start transaction; -- 开启事务
while i <= 2000000 -- 循环200万次
do
insert into tb_people (name, age)
values (concat('user_', i), floor(18 + (rand() * 42)));
set i = i + 1; -- i自增
end while;
commit; -- 提交事务
end
// delimiter ;
-- delimiter用于临时更改语句结束符(默认;),常用于定义存储过程、触发器等包含分号的复合语句。
-- 调用存储过程
call insertdata();
-- 移除自动增长,因为有自动增长的情况,主键无法移除的,因为自动增长所在字段要求必须是一个key索引
alter table tb_people
change id id int, -- 先移除自动增长
drop primary key; -- 移除主键
-- 检查数据生成结果
select count(*) from tb_people;执行结果:
| count(*) |
|---|
| 2000000 |
-- 做一个查询,然后查看效果
select * from tb_people where id = 1990000;
-- 查看性能分析
EXPLAIN select * from tb_people where id = 1990000;---
二、EXPLAIN执行计划
2.1 简介
简单来说:explain/desc执行计划就是用于分析一个SQL语句如何执行的,核心作用:帮助我们提升SQL查询效率,加快查询速度。
2.2 索引概念
Index索引:
学不认识的汉字,会去词典当中寻找
- 传统:一页一页进行查找,如果要查的数据在最后一页,整体要翻一遍 => 全表扫描
- 索引:给所有的汉字添加一个目录页(拼音、偏旁部首),以后在查找汉字的时候,不是一页一页翻,而是先查找目录页,然后在根据目录页快速定位汉字 => 走索引(索引优化)=> 快速定位我们要查找的内容
2.3 EXPLAIN命令
简单来说,EXPLAIN是用来查看数据库如何执行一个SQL查询的工具。它能告诉你数据库在执行查询时,选择了什么样的操作步骤(比如是扫描整个表还是使用索引等),并且通过这些信息你可以判断查询是否高效。
作用:假设你写了一个SQL查询来查询数据,如果查询效率低下(比如很慢),你可能想知道数据库是怎么执行的,找出瓶颈在哪。EXPLAIN就是用来帮你"拆解"查询,了解数据库是如何处理每一步的。
如何使用:
EXPLAIN SELECT * FROM orders WHERE user_id = 123;2.4 分析结果
当你在查询前加上EXPLAIN时,它会显示一个执行计划的表格,这个表格给出了执行查询时的每一个细节。
核心字段说明:
| 字段 | 说明 | 重要程度 |
|---|---|---|
| type | 连接类型,反映查询效率 | 最重要 |
| key | 实际使用的索引(NULL=没用索引) | 很重要 |
| rows | 估算扫描行数(越少越好) | 很重要 |
| possible_keys | 可能使用的索引 | 重要 |
| Extra | 额外信息 | 重要 |
详细字段说明:
| 字段 | 说明 |
|---|---|
| id | 这个数字表示查询的执行顺序。对于复杂的查询,可能会有多个查询步骤,id就帮助你理解它们的顺序 |
| select_type | 表示查询的类型,是普通查询还是复杂查询(比如涉及子查询等) |
| table | 代表查询是从哪张表中获取数据 |
| type | 查询是如何连接数据的。这个字段非常重要,它告诉你查询效率的高低 |
| possible_keys | 查询可能会使用到哪些索引。索引就像数据的"目录",使用索引可以加速数据查找 |
| key | 实际使用了哪个索引 |
| key_len | 使用的索引的长度(表示数据库选择了多长的索引) |
| ref | 显示与哪个列进行匹配来获取数据 |
| rows | 数据库估算的需要扫描的行数。越少的行数,意味着查询越高效 |
| Extra | 额外信息,比如是否使用了临时表或者文件排序等 |
2.5 type效率对比表(从高到低)
| type | 速度 | 说明 |
|---|---|---|
| const | 最快 | 主键/唯一索引等值查询 |
| ref | 快 | 普通索引等值查询 |
| range | 较快 | 索引范围查询(BETWEEN、>、<) |
| index | 较慢 | 全扫描索引树 |
| ALL | 最慢 | 全表扫描(必须优化!) |
---
三、索引类型简介
3.1 查看索引
-- 查看索引
SHOW INDEX FROM 表名; -- 详细显示所有索引
-- 等价于
show keys from 表名;
-- 第二种:直接展示
desc student; -- 简略显示,PRI/UNI/MUL 标记索引类型3.2 唯一索引 unique
-- unique 唯一索引
alter table 表名 add unique (字段);3.3 普通索引 key/index
在MySQL当中,Index的中文是索引。
- Key本意有键的意思,比如,Primary Key是主键约束
- Key也有索引的意思。比如,去掉Primary的Key,就是普通索引。因此,这两个单词在MySQL中等价
-- key/index 普通索引,这里以 name 举例
alter table student add index 索引名称 (字段);
-- 或者
create index 索引名称 on 表名 (字段);3.4 联合索引
联合索引,其实是普通索引的扩展。通常适用于加快查询多个字段的结果。
-- 联合索引:可以针对多个字段建立索引 on 数据表(字段1,字段2)
alter table tb_student add index index_name_age (name, age);
-- 或者
create index index_name_age on tb_student (name, age);---
四、加索引前后对比案例
4.1 未加索引
-- 没加索引情况
explain select * from tb_people where id = 1990000;+----+------+------+------+---------+----------+-------------+
| id | type | key | rows | filtered | Extra |
+----+------+------+------+---------+----------+-------------+
| 1 | ALL | NULL | 2M | 100.00 | Using where |
+----+------+------+------+---------+----------+-------------+4.2 添加主键索引
-- 添加主键索引
alter table tb_people add primary key(id);
-- 查看表结构
desc tb_people;
show create table tb_people;
show index from tb_people;4.3 加索引后
-- 添加索引的情况
explain select * from tb_people where id = 1990000;+----+-------+---------+------+----------+-------+
| id | type | key | rows | filtered | Extra |
+----+-------+---------+------+----------+-------+
| 1 | const | PRIMARY | 1 | 100.00 | |
+----+-------+---------+------+----------+-------+对比: type从ALL → const,rows从200万 → 1,性能提升约200万倍。
4.4 执行计划详细说明
| 字段 | 值 | 说明 |
|---|---|---|
| id | 1 | 表示这是查询的第一步 |
| select_type | SIMPLE | 表示这是一个简单查询,没有复杂的连接操作 |
| table | simple_table | 表示查询的数据表 |
| type | const | 表示使用常量值(如WHERE id = 1990000中的1990000)来匹配到了结果 |
| possible_keys | Primary | 表示数据库考虑过使用的Key |
| key | Primary | 表示数据库实际使用的主键约束来查的 |
| key_len | 4 | 表示数据库使用了该索引,占了4Bytes |
| ref | const | 表示查询近似于使用了常量时间(即查询复杂度低) |
| rows | 100 | 表示数据库预计需要扫描100行数据 |
| filtered | 100 | 表示过滤(where id = 1990000)出来的数据,100%命中了结果 |
| Extra | 没有额外的信息 | - |
---
五、key_len长度说明
| 数据类型 | 占用字节 |
|---|---|
| INT类型 | 4字节 |
| VARCHAR(n)类型 | n字节(最大长度) |
| DATE类型 | 3字节 |
| CHAR(n)类型 | n字节 |
---
六、面试题
问:在运维环境中,发现MySQL运行缓慢,如何去解决?
答:
① 从系统层面,检查系统资源使用情况:
- CPU负载:
top
- 内存占用:
free -h
- 磁盘空间使用:
df -h
- 磁盘IO:
iostat
② 从日志层面,开启慢查询日志,把查询缓慢的SQL写入到慢查询日志中,进行捕获
③ 从SQL语句层面,使用explain执行计划分析SQL执行过程,是否有走缓慢等等,查看具体慢的原因。如果是全表扫描,可以考虑引入索引(如主键索引、唯一索引、普通索引)等等实现优化操作
---
全文完