MySQL 索引底层完整详解|磁盘Page|B+树推导|聚簇非聚簇索引|索引SQL操作

1. 没有索引,可能会有什么问题

1.1 索引简介

索引:提高数据库的性能,索引是物美价廉的东西。不用加内存,不用改程序,不用调SQL,只要执行正确的create index,查询速度就可能提高成百上千倍。

天下没有免费的午餐,查询速度的提高是以插入、更新、删除的速度为代价的,这些写操作,增加了大量的I/O。

索引价值:提高海量数据的检索速度

1.2 常见索引分类

  1. 主键索引(primary key)

  2. 唯一索引(unique)

  3. 普通索引(index)

  4. 全文索引(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 磁盘硬件结构名词

  1. 磁盘:盘片,存放数据

  2. 磁道:磁盘表面一圈一圈同心圆

  3. 扇区:磁道被切割成小段,硬件最小读写单元,默认512字节,新式硬盘4K扇区

  4. 磁头:读写盘片数据

  5. 柱面:多盘磁盘,半径完全相等的一组磁道,构成柱面

定位扇区:磁头(盘面)+柱面(磁道)+扇区编号,即可定位一块磁盘扇区

2.3 操作系统不直接按扇区读写

  1. 如果操作系统直接按512字节扇区交互,代码强绑定硬件,硬盘更换系统就要修改

  2. 单次IO读写512字节数据量太小,多次IO访问,效率极低

操作系统读写磁盘基本单位:数据块(4KB)

2.4 随机访问与连续访问

  1. 随机访问:两次IO扇区地址不连续,磁头需要大幅度移动,寻道耗时长,速度慢

  2. 连续访问:两次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. 建立共识(底层原理)

  1. MySQL的数据,以page(16KB)单位保存在磁盘

  2. MySQL增删改查,先找到对应page页

  3. 任何计算、查询操作,必须先把磁盘page加载到内存,CPU只能操作内存数据

  4. 修改完成后,内存数据按照一定策略,刷新回磁盘,完成持久化

  5. MySQL在内存开辟一大块Buffer Pool缓冲池,缓存磁盘页,减少磁盘IO

  6. 优化数据库核心思路:尽可能减少磁盘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页

  1. 一页大小固定16KB

  2. Page内部存储多条有序数据记录

  3. Page自带page_prev、page_next指针,多个Page组成双向链表

缺点:多页双向链表查找数据,依旧需要一页一页加载磁盘,大量IO,查询慢

5.4 页内目录(页内索引)

在Page内部增加目录项(key+偏移指针)

不需要遍历页内全部数据,直接通过目录快速定位记录,优化单页内部查询速度。

5.5 多页场景,引入页目录(B+树雏形)

数据量变大,需要很多Page存储,Page之间是双向链表,跨页查找依旧很慢。

解决方案:单独拿出一类Page,只存下级Page最小主键值+Page指针(目录页)

  1. 普通页:存储真实用户数据

  2. 目录页:只存【最小主键值 + 下级页指针】,不存完整数据

  3. 目录页还可以再向上增加一层顶层目录页

多层目录页 + 底层数据页,就构成 B+树

5.6 B+树复盘总结

  1. Page分为目录页(索引节点)、数据页(叶子节点)

  2. 目录页只存放下级Page最小key值,不存放完整行数据

  3. 查询自上而下检索,只加载少量目录页到内存,极大减少磁盘IO次数,提升查询速度

5.7 为什么InnoDB选择B+树,不用其他数据结构

  1. 链表:只能线性遍历,查询效率极低O(N)

  2. 普通BST二叉搜索树:极端场景退化成链表

  3. AVL、红黑树:二叉树层级太深,树高很高,磁盘IO次数多

  4. Hash索引:等值查询很快O(1),无法范围查找、排序

MySQL InnoDB默认索引结构:B+树

5.8 B树 VS B+树核心区别

  1. B树:每一层节点既存key,又存完整数据

  2. 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+树叶子节点存储主键值,不是完整数据

  1. 通过二级索引查到主键id

  2. 使用主键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. 一张表最多1个主键索引

  2. 查询效率极高

  3. 主键列不能重复、不能NULL

  4. 主键优先使用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);

唯一索引特点:

  1. 一张表可以有多个唯一索引

  2. 查询速度快

  3. 该字段不能出现重复值

  4. 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);

普通索引特点:

  1. 一张表可以创建多个普通索引,开发最常用

  2. 字段允许重复,允许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 删除索引

  1. 删除主键索引
sql 复制代码
alter table 表名 drop primary key;
  1. 删除普通/唯一索引(通过索引名删除)
sql 复制代码
alter table user10 drop index idx_name;
  1. drop index简写语法
sql 复制代码
drop index name on user8;

6.7 索引创建设计原则

  1. 频繁作为where查询条件的字段,适合建立索引

  2. 唯一性太差(性别、状态只有2‑3种值),不适合单独建索引

  3. 更新非常频繁的字段,谨慎建索引,写操作会变慢

  4. 不会出现在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 优缺点

✅优点

  1. 一次索引可以对多列条件加速查询,减少回表次数

  2. 可以触发索引覆盖,不需要回表拿数据

❌缺点

  1. 字段越多,索引体积越大,占用磁盘Page更多

  2. DML(增删改)维护索引开销更大

  3. 顺序极其关键,顺序写错,索引直接失效

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 优缺点

✅优点

  1. 减少一次B+树查找,磁盘IO减半

  2. 性能提升非常明显,优化首选手段

❌缺点

  1. 需要增加联合索引,索引体积变大

  2. 索引不能无限加长,会拖慢增删改

6.8.3.5 面试标准答案

覆盖索引:查询列全部包含在索引中,无需回表读取聚簇索引的数据,减少IO开销,提升查询性能,执行计划Extra显示Using index。

配套面试简答题汇总

  1. Q:复合索引是多棵索引吗?

A:不是,是一棵B+树,多列排序。

  1. Q:最左匹配是where书写顺序吗?

A:不是,优化器自动调整条件顺序,不能缺少索引最左侧字段。

  1. Q:什么情况下后面索引失效?

A:等值条件之后出现范围查询,范围右侧字段无法使用索引。

  1. Q:覆盖索引怎么看有没有生效?

A:explain执行计划Extra列出现 Using index。

  1. Q:覆盖索引为什么快?

A:避免回表,减少一次B+树磁盘IO读取。

相关推荐
leisoo80971 小时前
涨停板次日表现因子怎么挖掘本地化Python全流程实战
大数据·人工智能·python
Mico181 小时前
Percona Toolkit 3.5.7 完整实战笔记(从入门到精通)
mysql
众人皆醒我独醉1 小时前
GPU 集群 IaC:Terraform + Ansible 部署自动化
面试·ansible·gpu
用户3610588626122 小时前
SparkSQL 数据源与底层架构深度剖析
大数据·spark
小白勇闯网安圈2 小时前
Django Form 组件详解:从表单生成到 ModelForm 数据校验
数据库·django·sqlite
我命由我123452 小时前
商圈六大配套
学习·职场和发展·求职招聘·职场发展·产品经理·学习方法·零售
sbjdhjd2 小时前
企业站 SQL 注入实战:PCRE 正则回溯绕过关键词 WAF 完整实战 | 进阶02
sql·安全·web安全·网络安全·ai·开源·php
Dr.kangder2 小时前
嵌入式面试总结(二十二)——指针
java·面试·职场和发展·架构·嵌入式
梦想不只是梦与想2 小时前
MySQL 数据库(二):数据类型
数据库·mysql·数据类型