1. 没有索引,可能会有什么问题
1.1 索引简介
索引:提高数据库的性能,索引是物美价廉的东西。不用加内存,不用改程序,不用调SQL,只要执行正确的create index,查询速度就可能提高成百上千倍。
天下没有免费的午餐,查询速度的提高是以插入、更新、删除的速度为代价的,这些写操作,增加了大量的I/O。
索引价值:提高海量数据的检索速度
1.2 常见索引分类
-
主键索引(primary key)
-
唯一索引(unique)
-
普通索引(index)
-
全文索引(fulltext):解决中文索引问题
1.3 案例演示:无索引海量数据查询
1.3.1 构建8000000条测试数据
构建海量表数据需要有差异性,使用存储过程来创建
sql
--产生随机字符串
delimiter $$
create function rand_string(n INT)
returns varchar(255)
begin
declare chars_str varchar(100) default
'abcdefghijklmnopqrstuvwxyzABCDEFHIJKLMNOPQRSTUVWXYZ';
declare return_str varchar(255) default '';
declare i int default 0;
while i < n do
set return_str=concat(return_str,substring(chars_str,floor(1+rand()*52),1));
set i = i + 1;
end while;
return return_str;
end $$
delimiter ;
--产生随机数字
delimiter $$
create function rand_num()
returns int(5)
begin
declare i int default 0;
set i = floor(10+rand()*500);
return i;
end $$
delimiter ;
--创建存储过程,向雇员表添加海量数据
delimiter $$
create procedure insert_emp(in start int(10),in max_num int(10))
begin
declare i int default 0;
set autocommit = 0;
repeat
set i = i + 1;
insert into EMP values ((start+i)
,rand_string(6),'SALES',0001,curdate(),2000,400,rand_num());
until i = max_num
end repeat;
commit;
end $$
delimiter ;
-- 执行存储过程,添加8000000条记录
call insert_emp(100001, 8000000);
1.3.2 无索引查询
sql
select * from EMP where empno=998877;
耗时4.93秒,单机单人查询就很慢;公网环境1000人并发查询,极易卡死。
1.3.3 创建索引优化查询
sql
alter table EMP add index(empno);
再次查询
sql
select * from EMP where empno=123456;
查询耗时大幅降低
2. 认识磁盘
2.1 MySQL与存储
MySQL给用户提供存储服务,数据全部存放在磁盘。磁盘属于机械设备,读写效率远低于电子元件,减少磁盘IO是MySQL性能优化的核心。
2.2 磁盘硬件结构名词
-
磁盘:盘片,存放数据
-
磁道:磁盘表面一圈一圈同心圆
-
扇区:磁道被切割成小段,硬件最小读写单元,默认512字节,新式硬盘4K扇区
-
磁头:读写盘片数据
-
柱面:多盘磁盘,半径完全相等的一组磁道,构成柱面
定位扇区:磁头(盘面)+柱面(磁道)+扇区编号,即可定位一块磁盘扇区
2.3 操作系统不直接按扇区读写
-
如果操作系统直接按512字节扇区交互,代码强绑定硬件,硬盘更换系统就要修改
-
单次IO读写512字节数据量太小,多次IO访问,效率极低
操作系统读写磁盘基本单位:数据块(4KB)
2.4 随机访问与连续访问
-
随机访问:两次IO扇区地址不连续,磁头需要大幅度移动,寻道耗时长,速度慢
-
连续访问:两次IO扇区地址连续,磁头几乎不用移动,读写速度快
IO效率瓶颈:不在于单次读写大小,而在于IO次数
3. MySQL与磁盘交互基本单位
InnoDB引擎和磁盘IO交互,基本单位是16KB,称为Page(页)
sql
SHOW GLOBAL STATUS LIKE 'innodb_page_size';
--输出16384,16*1024=16384字节=16KB
磁盘硬件最小单元512字节,MySQL按16KB一页和磁盘交换数据,和操作系统page区分开。
4. 建立共识(底层原理)
-
MySQL的数据,以page(16KB)单位保存在磁盘
-
MySQL增删改查,先找到对应page页
-
任何计算、查询操作,必须先把磁盘page加载到内存,CPU只能操作内存数据
-
修改完成后,内存数据按照一定策略,刷新回磁盘,完成持久化
-
MySQL在内存开辟一大块Buffer Pool缓冲池,缓存磁盘页,减少磁盘IO
-
优化数据库核心思路:尽可能减少磁盘IO次数
5. 索引的理解(B+树完整推导)
5.1 建立测试表
sql
create table if not exists user (
id int primary key, --主键,自动创建主键索引
age int not null,
name varchar(16) not null
);
插入无序主键数据
sql
insert into user (id, age, name) values(3, 18, '杨过');
insert into user (id, age, name) values(4, 16, '小龙女');
insert into user (id, age, name) values(2, 26, '黄蓉');
insert into user (id, age, name) values(5, 36, '郭靖');
insert into user (id, age, name) values(1, 56, '欧阳锋');
查询结果:数据自动按照主键id有序排列
sql
select * from user;
|----|-----|------|
| id | age | name |
| 1 | 56 | 欧阳锋 |
| 2 | 26 | 黄蓉 |
| 3 | 18 | 杨过 |
| 4 | 16 | 小龙女 |
| 5 | 36 | 郭靖 |
5.2 为什么IO交互单位是Page(16KB)
假设一条一条读取记录,查id=5,需要5次磁盘IO。
如果多条记录放在同一个16KB Page中,第一次IO把整页加载到Buffer Pool,后续查询同页其他数据,直接读内存,不再产生IO。
局部性原理:本次读取的数据,很大概率下次还会访问。一次性加载整页,大幅降低IO次数。
5.3 理解单个Page页
-
一页大小固定16KB
-
Page内部存储多条有序数据记录
-
Page自带page_prev、page_next指针,多个Page组成双向链表
缺点:多页双向链表查找数据,依旧需要一页一页加载磁盘,大量IO,查询慢
5.4 页内目录(页内索引)
在Page内部增加目录项(key+偏移指针)
不需要遍历页内全部数据,直接通过目录快速定位记录,优化单页内部查询速度。
5.5 多页场景,引入页目录(B+树雏形)
数据量变大,需要很多Page存储,Page之间是双向链表,跨页查找依旧很慢。
解决方案:单独拿出一类Page,只存下级Page最小主键值+Page指针(目录页)
-
普通页:存储真实用户数据
-
目录页:只存【最小主键值 + 下级页指针】,不存完整数据
-
目录页还可以再向上增加一层顶层目录页
多层目录页 + 底层数据页,就构成 B+树
5.6 B+树复盘总结
-
Page分为目录页(索引节点)、数据页(叶子节点)
-
目录页只存放下级Page最小key值,不存放完整行数据
-
查询自上而下检索,只加载少量目录页到内存,极大减少磁盘IO次数,提升查询速度
5.7 为什么InnoDB选择B+树,不用其他数据结构
-
链表:只能线性遍历,查询效率极低O(N)
-
普通BST二叉搜索树:极端场景退化成链表
-
AVL、红黑树:二叉树层级太深,树高很高,磁盘IO次数多
-
Hash索引:等值查询很快O(1),无法范围查找、排序
MySQL InnoDB默认索引结构:B+树
5.8 B树 VS B+树核心区别
-
B树:每一层节点既存key,又存完整数据
-
B+树:
非叶子(目录页)只存key+指针,不存数据,单页可以放下更多key,树高度更矮,IO更少
全部真实数据只存在最下层叶子节点
所有叶子节点使用双向链表串联,范围查询、排序极强
5.9 聚簇索引 VS 非聚簇索引
5.9.1 非聚簇索引(MyISAM引擎)
• 索引页 和 数据页完全分开两个文件
• B+树叶子节点,不存完整数据,只存数据行磁盘地址
• 查询到索引后,再根据地址去读取真实数据
文件:.frm表结构、.MYD真实数据、.MYI索引数据
sql
create table mtest(
id int primary key,
name varchar(11) not null
)engine=MyISAM;
5.9.2 聚簇索引(InnoDB引擎,MySQL默认)
• 索引和数据存放在同一个ibd文件
• B+树叶子节点直接存放完整一行用户数据
• 主键B+树直接拿到全部数据,不需要二次寻址
文件:.frm表结构、.ibd索引+数据
sql
create table itest(
id int primary key,
name varchar(11) not null
)engine=InnoDB;
5.9.3 InnoDB普通索引(二级索引/辅助索引)
普通索引B+树叶子节点存储主键值,不是完整数据
-
通过二级索引查到主键id
-
使用主键id再去主键聚簇索引B+树查询完整数据
这个二次查找过程,叫做回表查询
二级索引不存储完整行数据,节省大量磁盘空间
6. 索引SQL操作大全
6.1 创建主键索引 primary key
方式1:建表直接指定主键
sql
create table user1(id int primary key, name varchar(30));
方式2:建表末尾声明主键
sql
create table user2(id int, name varchar(30), primary key(id));
方式3:建表完成后追加主键索引
sql
create table user3(id int, name varchar(30));
alter table user3 add primary key(id);
主键索引特点:
-
一张表最多1个主键索引
-
查询效率极高
-
主键列不能重复、不能NULL
-
主键优先使用int类型
6.2 创建唯一索引 unique
方式1:字段后直接加unique
sql
create table user4(id int primary key, name varchar(30) unique);
方式2:表末尾声明唯一索引
sql
create table user5(id int primary key, name varchar(30), unique(name));
方式3:建表后新增唯一索引
sql
create table user6(id int primary key, name varchar(30));
alter table user6 add unique(name);
唯一索引特点:
-
一张表可以有多个唯一索引
-
查询速度快
-
该字段不能出现重复值
-
unique允许NULL(可以多个null),not null+unique等价主键
6.3 创建普通索引 index
方式1:建表末尾直接声明
sql
create table user8(
id int primary key,
name varchar(20),
email varchar(30),
index(name)
);
方式2:alter追加普通索引
sql
create table user9(id int primary key, name varchar(20), email varchar(30));
alter table user9 add index(name);
方式3:create index指定索引名创建
sql
create table user10(id int primary key, name varchar(20), email varchar(30));
create index idx_name on user10(name);
普通索引特点:
-
一张表可以创建多个普通索引,开发最常用
-
字段允许重复,允许NULL
6.4 全文索引 fulltext
注意:MyISAM支持全文索引,默认只支持英文,中文需要第三方分词插件
sql
CREATE TABLE articles (
id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
title VARCHAR(200),
body TEXT,
FULLTEXT (title,body)
)engine=MyISAM;
INSERT INTO articles (title,body) VALUES
('MySQL Tutorial','DBMS stands for Database ...'),
('How To Use MySQL well','After you went through a ...'),
('Optimizing MySQL','In this tutorial we will show ...');
❌模糊查询like '%xxx%'不走全文索引
sql
select * from articles where body like '%database%';
✅全文索引标准查询语法
sql
SELECT * FROM articles
WHERE MATCH (title,body) AGAINST ('database');
可以使用explain查看执行计划,确认type为fulltext代表命中索引
6.5 查询索引
写法1
sql
show keys from 表名;
写法2
sql
show index from 表名;
写法3(简略)
sql
desc 表名;
输出关键字段说明
• Non_unique:0=唯一索引,1=允许重复索引
• Key_name:索引名称
• Column_name:建立索引的列
• Index_type:BTREE(B+树索引)
6.6 删除索引
- 删除主键索引
sql
alter table 表名 drop primary key;
- 删除普通/唯一索引(通过索引名删除)
sql
alter table user10 drop index idx_name;
- drop index简写语法
sql
drop index name on user8;
6.7 索引创建设计原则
-
频繁作为where查询条件的字段,适合建立索引
-
唯一性太差(性别、状态只有2‑3种值),不适合单独建索引
-
更新非常频繁的字段,谨慎建索引,写操作会变慢
-
不会出现在where条件的字段,不要创建索引
6.8 拓展概念(自行拓展学习)
6.8.1 复合索引(联合索引)
6.8.1.1 定义
复合索引,也叫联合索引:一个索引包含多个字段,把多个列合在一起创建一颗B+树索引。
语法
sql
-- 给 col1,col2,col3 创建复合索引
create index idx_col1_col2_col3 on table_name(col1,col2,col3);
索引排序规则:先按第1列排序,第一列相同再按第2列排序,第二列相同再按第3列排序。
物理上,这只是一棵B+树,不是多棵索引合并。
6.8.1.2 存储结构理解
叶子节点里每条索引项:(col1值,col2值,col3值,主键id)
排序优先级:col1 > col2 > col3
例:索引(a,b,c)
先全部按a排;a相等 → 按b排;b相等 → 按c排。
6.8.1.3 优缺点
✅优点
-
一次索引可以对多列条件加速查询,减少回表次数
-
可以触发索引覆盖,不需要回表拿数据
❌缺点
-
字段越多,索引体积越大,占用磁盘Page更多
-
DML(增删改)维护索引开销更大
-
顺序极其关键,顺序写错,索引直接失效
6.8.1.4 创建原则
• 区分度高的字段放最左边
• 查询高频字段靠左
• 范围查询字段尽量放最后
6.8.2 最左匹配原则(最左前缀原则)
6.8.2.1 核心定义
复合索引(a,b,c),查询条件必须匹配索引最左侧的前缀,索引才会生效。
索引顺序:a → b → c
生效前缀组合:
(a)
(a,b)
(a,b,c)
只要左边断了,后面字段索引失效
6.8.2.2 举例 索引 idx(a,b,c)
✅可以走索引
sql
where a=?
where a=? and b=?
where a=? and b=? and c=?
where a=? and c=? --a生效,c不走索引
❌完全不走索引
sql
where b=?
where c=?
where b=? and c=?
重点:顺序不看where书写顺序,看优化器会自动调整条件顺序,但是不能缺少最左列
6.8.2.3 范围查询断裂
一旦某一列出现 > < between like前缀模糊,该字段右边所有列索引失效
索引(a,b,c)
sql
where a=1 and b>2 and c=3
-- a、b走索引,c完全用不到索引!!
原因:b是范围,b的值不再有序,后面c无法有序快速查找。
6.8.2.4 口诀
等值放前,范围靠后;左边不能丢,一断全作废
6.8.3 索引覆盖(覆盖索引)
6.8.3.1 定义
查询所需要的全部列数据,在索引叶子节点上就能直接拿到,不需要回到主键索引读取完整行数据,这个现象叫索引覆盖。
省去【回表】操作,极大减少磁盘IO,查询速度极快。
不需要回表 = 覆盖索引命中
6.8.3.2 回表是什么(对比理解)
普通二级索引叶子节点:存【索引字段 + 主键ID】
查到主键ID之后,拿着ID去聚簇索引B+树再查一次完整行数据 → 回表,两次B+树查找。
覆盖索引:select要查的字段全部就在二级索引叶子里,不用拿主键再查一次,一次B+树搞定。
6.8.3.3 实操示例
表 user(id, name,age,phone)
普通单列索引:idx_name(name)
sql
-- 需要回表:name索引只能拿到id,age不在索引里,要回表
select age from user where name='张三';
-- 建立联合索引 idx_name_age(name,age)
create index idx_name_age on user(name,age);
-- ✅触发覆盖索引
select age from user where name='张三';
--索引叶子:name+age+id,age直接读出,不用回表
extra字段查看执行计划:Using index → 代表命中覆盖索引
6.8.3.4 优缺点
✅优点
-
减少一次B+树查找,磁盘IO减半
-
性能提升非常明显,优化首选手段
❌缺点
-
需要增加联合索引,索引体积变大
-
索引不能无限加长,会拖慢增删改
6.8.3.5 面试标准答案
覆盖索引:查询列全部包含在索引中,无需回表读取聚簇索引的数据,减少IO开销,提升查询性能,执行计划Extra显示Using index。
配套面试简答题汇总
- Q:复合索引是多棵索引吗?
A:不是,是一棵B+树,多列排序。
- Q:最左匹配是where书写顺序吗?
A:不是,优化器自动调整条件顺序,不能缺少索引最左侧字段。
- Q:什么情况下后面索引失效?
A:等值条件之后出现范围查询,范围右侧字段无法使用索引。
- Q:覆盖索引怎么看有没有生效?
A:explain执行计划Extra列出现 Using index。
- Q:覆盖索引为什么快?
A:避免回表,减少一次B+树磁盘IO读取。