MySQL数据库:索引

文章目录

MySQL索引入门保姆级指南:从"全表扫描坐牢"到"B+树飙车"

家人们,写SQL的时候有没有过这种绝望:一张表几百万行数据,一个select下去,屏幕转了五秒才出结果,你以为是电脑卡了,其实是你的SQL正在做全表扫描------相当于从图书馆第一排书架开始,一本一本翻找你要的那本书。

而解决这个问题的"数据库性能外挂",就是索引 。不用加内存、不用改业务逻辑,只要一句create index,查询速度直接起飞。但天下没有免费的午餐,索引的水远比你想的深。今天咱们就从磁盘底层讲到语法实操,从踩坑避坑到调优工具,把MySQL索引扒得明明白白。

一、没索引的痛:800万行数据实测教你做人

先给大家看个可复现的"大型翻车现场"。我们从零开始造一张800万行的员工表,直观感受索引的威力。

完整实验脚本(直接复制就能跑)

先建员工表,再创建两个辅助函数生成随机数据,最后用存储过程批量灌入数据。

复制代码
-- 1. 建员工表 emp
create table emp (
    empno int,
    ename varchar(255),
    job varchar(255),
    mgr int,
    hiredate date,
    sal decimal(10,2),
    comm decimal(10,2),
    deptno int
);

-- 2. 创建随机字符串函数(生成随机姓名)
delimiter $$
create function rand_string(n int) returns varchar(255)
begin 
  declare chars_str varchar(100) default 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
  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 ;

-- 3. 创建随机数字函数(生成随机部门编号)
delimiter $$
create function rand_num() returns int(5)
begin 
  declare i int default 0;
  set i = floor(10+rand()*500);
  return i;
end $$
delimiter ;

-- 4. 创建批量插入存储过程 insert_emp
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), 'SALESMAN', 0001, curdate(), 2000, 400, rand_num()
    );
  until i = max_num end repeat;
  commit; -- 一次性提交,大幅减少磁盘IO
end $$
delimiter ;

-- 5. 调用存储过程,灌入800万条数据
-- 配置低的同学可以改成10万,效果一样明显
call insert_emp(100001, 8000000);

无索引 vs 有索引 性能对比

复制代码
-- 无索引:全表扫描,本地测试耗时约4.93秒
select * from emp where empno = 998877;

-- 创建普通索引
alter table emp add index(empno);

-- 有索引:走B+树,耗时直接降到毫秒级
select * from emp where empno = 123456;

这就是索引的本质------用少量的存储空间,换取海量的查询效率提升 ,经典的「空间换时间」算法思想。但索引不是银弹,它会降低写入速度(增删改都要同步更新索引),所以核心在于:给高频查询字段建索引,给高频写入字段慎建索引。

二、先搞懂底层:数据库性能瓶颈为啥是IO?

很多人学索引只记语法,不理解底层,这就像学开车不懂发动机原理,迟早要开沟里。索引的一切设计,都是为了对付一个"慢家伙"------磁盘。

1. 磁盘到底有多慢?

磁盘是个机械玩意儿,靠磁头在盘片上转来转去读数据。你可以把它想象成老式黑胶唱片:

  • 盘片:圆形的唱片,数据存在上面一圈圈的磁道上
  • 磁道:盘片上的同心圆,最外面是0磁道
  • 扇区:每个磁道被切成一小块一小块,每个扇区默认512字节(新硬盘多为4K)
  • 柱面:多盘片硬盘中,同半径的所有磁道加起来叫柱面

磁头要读某个扇区,得先移动到对应磁道(寻道时间),再等盘片转到对应位置(旋转延迟)。机械运动的速度,比电子运算慢了几百万倍,这就是数据库性能的命门。

2. 随机IO vs 连续IO:性能天差地别

  • 随机IO:两次读的扇区不挨着,磁头要来回跳,巨慢
  • 连续IO:顺着磁道连续读,磁头不用大动,速度能快几百倍

数据库优化的核心,本质就是想尽一切办法把随机IO变成连续IO,并且减少IO的总次数。

3. 三层视角:扇区 → 块 → page

这里要纠正一个初学者误区:MySQL的16KB page是应用层逻辑概念,磁盘根本不认。

层级 单位 负责方 说明
硬件层 扇区(512字节/4KB) 磁盘 磁盘最小读写单位,只认二进制
操作系统内核层 块(4KB) OS内核 文件系统最小读写单位,管理磁盘
MySQL应用层 页(16KB) MySQL服务进程 InnoDB自己定义的IO单位,B+树节点大小

MySQL运行在操作系统的应用层(用户态) ,它不能直接操作磁盘硬件,必须调用内核接口。为了减少IO次数,InnoDB一次就从磁盘读16KB的整页数据到内存缓冲池(buffer pool)。根据局部性原理------你查了某条数据,大概率接下来还要查它附近的数据,一次加载一整页,后面直接在内存里找,不用再碰磁盘。

一句话总结:IO次数才是性能的瓶颈,而不是单次IO读的数据量。

三、B+树:MySQL索引的灵魂结构

理解了页和IO,我们就能一步步推导出,为什么MySQL最终选择了B+树作为索引结构。

1. 从链表到B+树的进化之路

阶段1:单页链表------纯纯坐牢

最开始数据在页里就是一条链表,要找某条数据得从头逐条遍历。一页几百条数据还好,要是有1000页,就得遍历1000次,全表扫描就是这么来的。

阶段2:页内加目录------每页的"页眉"

就像书本每页有页眉,我们在页开头加个页内目录,记录"第N条数据的主键值是多少"。找数据先查目录定位大概位置,不用逐条遍历。

阶段3:多页加页目录------整本书的目录

数据多了页也多了,页之间也是链表,跨页查找还是要遍历。那我们再给所有页建一个高层目录,每个目录项存"某一页的最小主键值 + 页地址"。找数据先查高层目录,定位到具体哪一页,再去页里找。

阶段4:多层目录叠buff------B+树诞生!

如果页特别多,高层目录也会很大,那就再给高层目录建目录......一层一层往上叠,直到最顶层只有一个目录页。

没错,这棵"多层目录树"就是传说中的B+树:

  • 非叶子节点(目录页):只存主键值 + 下一层页的指针,不存真实数据
  • 叶子节点(数据页):存完整的用户数据,并且用双向链表连起来

2. 为啥偏偏是B+树?其他数据结构不行吗?

初学者最爱问:二叉树、红黑树、哈希表不香吗?为啥非得整个B+树?咱们一个个拉出来对比:

数据结构 核心问题
链表 O(n)线性查找,数据量大了直接坐牢
二叉搜索树 容易退化成歪脖子链表(按顺序插入直接变线性结构)
红黑树/AVL树 虽然平衡,但毕竟是二叉,每个节点最多2个子节点。100万数据树高就有20层,意味着要做20次IO,太慢了
哈希表 单个查询O(1)巨快,但范围查询直接废了。比如查"工资2000-5000的员工",哈希表根本没法搞
B树 每个节点都存完整数据,导致一个节点存不了多少key,树变高,IO次数变多;而且范围查询要来回遍历

而B+树就是为磁盘IO量身定做的"矮胖学霸":

  • ✅ 非叶子节点只存key和指针,一个16KB的页能存上千个key,树特别矮。千万级数据树高也就3-4层,查一条数据只要3-4次IO,直接秒杀二叉树
  • ✅ 叶子节点用双向链表串联 ,范围查询只要找到起点,顺着链表往后走就行,完美支持between、>、<这类操作
  • ✅ 数据都在叶子节点,每次查询都要走到叶子,查询性能稳定

四、聚簇索引 vs 非聚簇索引:InnoDB和MyISAM的核心差异

同样是B+树,InnoDB和MyISAM的实现却完全不一样,这就是初学者最容易懵的「聚簇索引」和「非聚簇索引」,也是面试高频考点。

1. MyISAM的非聚簇索引:索引和数据分家

MyISAM的索引叶子节点,存的不是数据本身,而是数据的磁盘地址。索引文件和数据文件是分开的:

  • .MYD文件:存真实数据
  • .MYI文件:存索引

就像图书馆的卡片目录:卡片上写着书的位置,你查到位置后得自己跑去书架拿书。索引和数据是两码事,所以叫非聚簇索引。

2. InnoDB的聚簇索引:索引就是数据

InnoDB就狠了,它直接把完整的用户数据存在主键索引的叶子节点里。也就是说,主键索引的叶子节点,就是整张表的所有数据。索引和数据是一体的,所以叫聚簇索引。

你可以理解成按拼音排序的新华字典:正文本身就是按拼音排好的,你找到拼音目录直接就能翻到对应的字,不用再跑别的地方找。

💡 关键结论:InnoDB表里,主键就是聚簇索引,聚簇索引就是整张表的数据本身。所以一张表只能有一个聚簇索引。

3. InnoDB普通索引的坑:回表查询

一个表只能有一个主键,也就只能有一个聚簇索引。那我们给其他字段建索引(普通索引/辅助索引)怎么办?

InnoDB的普通索引叶子节点,只存主键值,不存完整数据。

比如给name字段建索引,你查name='杨过'的时候:

  1. 先在name索引上找到对应的主键值(比如id=3)
  2. 再拿着id=3去主键索引里,找到完整的行数据

这个"查完普通索引,再去主键索引查一遍"的过程,就叫回表查询。

为啥不直接把数据存在普通索引里?废话,一个表可以建十几个索引,每个索引都存一份完整数据,那空间不得炸了?

✨ 优化技巧:有一种优化叫索引覆盖 ------如果你查的字段刚好都在索引里,就不用回表了。比如select name from user where name='杨过',直接从name索引就能拿到name值,效率拉满。

五、【语法大全】各类键与索引实操

先澄清一个90%初学者都会搞混的概念:

键(key)≠ 索引(index)

键是数据完整性约束 (主键、唯一键、外键),用来保证数据不瞎填、不重复、不孤儿;

索引是查询加速数据结构(B+树),用来让查询变快。

只不过:主键、唯一键创建时,数据库会自动帮你建对应的索引;外键是纯约束,不会自动建索引。

下面把所有常用的键和索引,从创建、特点到删除,一次性给全,全部采用MySQL小写规范写法。

5.1 主键约束 + 主键索引

主键是每一行数据的「身份证号」,InnoDB强制用主键构建聚簇索引,整张表的数据就挂在主键B+树上。

核心特点
  • 一张表最多1个主键
  • 主键值不能重复、不能为null
  • InnoDB自动创建聚簇索引,查询主键不需要回表,速度最快
  • 主键建议用自增整数,不要用UUID、业务字段
三种创建方式
复制代码
-- 方式1:建表时直接在字段后声明(最常用)
create table user1(
    id int primary key auto_increment, -- 自增主键
    name varchar(30) not null
);

-- 方式2:建表末尾声明(适合复合主键)
create table user2(
    id int auto_increment,
    name varchar(30) not null,
    primary key(id) -- 指定主键
);

-- 方式3:表已建好,后期追加主键
create table user3(
    id int not null,
    name varchar(30) not null
);
alter table user3 add primary key(id);
alter table user3 modify id int auto_increment; -- 顺便设置自增
复合主键(多个字段联合当主键)

只有全部字段合起来不重复才行,单个字段可以重复。

复制代码
-- 学生成绩表:学号+课程号联合作为主键
create table score(
    student_id int,
    course_id int,
    score int,
    primary key(student_id, course_id)
);
删除主键

⚠️ 坑:如果主键列是自增的,不能直接删,要先去掉自增属性。

复制代码
-- 先去掉自增
alter table user1 modify id int;
-- 再删除主键
alter table user1 drop primary key;

5.2 唯一约束 + 唯一索引

唯一约束保证字段值不重复,和主键的区别是:允许为null,且一张表可以有多个。

核心特点
  • 一张表可以有多个唯一索引
  • 字段值不能重复,但允许多个null值(因为null≠null)
  • 数据库自动创建同名唯一索引,查询效率接近主键
  • 如果唯一列设为not null,效果等价于主键
三种创建方式
复制代码
-- 方式1:字段后直接声明
create table user4(
    id int primary key auto_increment,
    phone varchar(20) unique -- 手机号唯一
);

-- 方式2:建表末尾声明
create table user5(
    id int primary key auto_increment,
    phone varchar(20),
    id_card varchar(18),
    unique(phone), -- 手机号唯一
    unique(id_card) -- 身份证号唯一
);

-- 方式3:建表后追加
create table user6(
    id int primary key auto_increment,
    email varchar(50)
);
alter table user6 add unique(email);
-- 或者指定索引名
create unique index idx_email on user6(email);
删除唯一索引
复制代码
alter table user6 drop index email;
-- 或者
drop index idx_email on user6;

5.3 普通索引(最常用)

普通索引就是单纯用来加速查询的"工具人",没有任何约束,允许重复值和null值,是开发中用得最多的索引。

核心特点
  • 一张表可以建多个
  • 允许字段值重复、允许为null
  • 只负责加速查询,不做数据校验
  • 叶子节点存主键值,查询需要回表
三种创建方式
复制代码
-- 方式1:建表时直接声明
create table user7(
    id int primary key auto_increment,
    name varchar(20) not null,
    age int,
    index idx_name(name) -- 给name建普通索引
);

-- 方式2:alter table追加
create table user8(
    id int primary key auto_increment,
    name varchar(20) not null,
    age int
);
alter table user8 add index idx_age(age);

-- 方式3:create index创建(推荐,可自定义索引名)
create index idx_name_age on user8(name, age);

📌 开发规范:索引名建议统一格式 idx_表名_字段名,比如 idx_user_name,方便维护。

5.4 联合索引(复合索引)

多个字段组合成一个索引,是性能优化的核心武器,一个好的联合索引往往顶好几个单列索引。

创建语法
复制代码
-- 给 name、age、job 三个字段建联合索引
create table user9(
    id int primary key auto_increment,
    name varchar(20),
    age int,
    job varchar(20),
    index idx_name_age_job(name, age, job) -- 字段顺序非常重要
);

⚠️ 重中之重:联合索引的字段顺序直接决定能不能命中索引,遵循最左匹配原则,详见下一章。

5.5 全文索引

专门用来对大段文本进行关键词检索,比如文章内容、商品描述。

核心特点
  • 原生对中文支持很差,中文全文检索推荐用Elasticsearch
  • MySQL 5.6+ InnoDB也开始支持全文索引,但中文场景仍不推荐
  • 不能用like走全文索引,必须用专用语法
创建与使用
复制代码
-- 创建全文索引
create table articles (
    id int unsigned auto_increment not null primary key,
    title varchar(200),
    body text,
    fulltext (title, body) -- 给标题和正文建全文索引
) engine=innodb;

-- 插入测试数据
insert into articles (title, body) values
('mysql tutorial', 'dbms stands for data base management system'),
('how to use mysql well', 'after you went through this tutorial you will master mysql'),
('optimizing mysql', 'in this tutorial we will show you how to optimize mysql');

-- ❌ 错误用法:like不走全文索引,全表扫描
select * from articles where body like '%database%';

-- ✅ 正确用法:match against 语法
select * from articles where match (title, body) against ('database');

5.6 外键约束(不是索引,但总有人搞混)

很多人把外键当索引,其实外键是跨表的数据完整性约束,用来保证「子表的某一列,必须引用父表主键中已存在的值」,防止出现孤儿数据。

核心特点
  • 作用是约束数据一致性,不是加速查询
  • 外键字段不会自动建索引,需要手动建
  • 实际互联网开发中很少用外键(影响写入性能、分库分表不支持),一般靠业务代码保证一致性
创建与使用示例
复制代码
-- 父表:部门表
create table dept(
    deptno int primary key,
    dname varchar(20) not null
);

-- 子表:员工表,deptno是外键,引用部门表的主键
create table emp(
    empno int primary key auto_increment,
    ename varchar(20) not null,
    deptno int,
    -- 定义外键约束
    foreign key (deptno) references dept(deptno)
);

-- 插入部门数据
insert into dept values (10, '研发部'), (20, '销售部');

-- ✅ 正常插入:部门10存在
insert into emp(ename, deptno) values ('张三', 10);

-- ❌ 报错:部门99不存在,外键约束阻止插入
insert into emp(ename, deptno) values ('李四', 99);
删除外键
复制代码
-- 先查外键名(一般自动生成,比如 emp_ibfk_1)
show create table emp;

-- 删除外键约束
alter table emp drop foreign key emp_ibfk_1;

💡 补充:虽然外键不是索引,但联表查询时外键字段通常都会手动建普通索引,用来加速join。

5.7 查看索引的三种方式

复制代码
-- 方式1:最详细,看索引名、字段、类型、是否唯一
show keys from user;
show index from user;

-- 方式2:简略看表结构,能看到主键、唯一键
desc user;

-- 方式3:看建表语句,所有索引和约束都在里面
show create table user;

六、联合索引核心:最左匹配原则

联合索引是索引优化的重中之重,而最左匹配原则就是它的灵魂。

1. 什么是最左匹配原则

联合索引会按照字段从左到右的顺序构建B+树,查询时必须从最左边的字段开始匹配,不能跳过中间字段。就像查字典:你可以按拼音首字母查,也可以按首字母+第二个字母查,但不能跳过首字母直接按第二个字母查。

举个例子,我们有联合索引 idx_name_age_job(name, age, job):

✅ 能命中索引的查询:

复制代码
-- 匹配最左列name
select * from user where name = '杨过';

-- 从左到右连续匹配 name + age
select * from user where name = '杨过' and age = 18;

-- 全字段匹配
select * from user where name = '杨过' and age = 18 and job = '侠客';

❌ 不能命中(或部分命中)的查询:

复制代码
-- 跳过最左的name,完全不走索引
select * from user where age = 18;

-- 跳过中间的age,只命中name部分
select * from user where name = '杨过' and job = '侠客';

-- 最左列完全没出现,全表扫描
select * from user where job = '侠客';

2. 联合索引字段怎么排?

核心原则:区分度高的字段放左边,频繁查询的字段放左边 。

比如用户表,name的区分度比gender高,就把name放左边。

七、踩坑预警:索引失效的7大经典场景

很多人建了索引还是慢,大概率是索引失效了。这7个场景是重灾区,记下来少走90%的弯路。

1. like左模糊查询

复制代码
-- ❌ 失效:%在左边,无法利用有序索引
select * from user where name like '%杨过%';

-- ✅ 生效:右模糊查询
select * from user where name like '杨过%';

2. 索引列上用函数/运算

复制代码
-- ❌ 失效:对索引列做了函数操作,破坏有序性
select * from emp where year(hiredate) = 2024;

-- ✅ 生效:改成范围查询,保留索引有序性
select * from emp where hiredate >= '2024-01-01' and hiredate < '2025-01-01';

3. 隐式类型转换

复制代码
-- phone字段是varchar类型,用数字查会触发隐式转换,索引失效
-- ❌ 失效
select * from user where phone = 13800138000;

-- ✅ 生效:用字符串匹配
select * from user where phone = '13800138000';

4. or连接非索引列

只要有一个列没索引,整个查询就不走索引。

复制代码
-- ❌ 失效:sal没有索引
select * from emp where ename = '张三' or sal = 5000;

5. 负向查询

!=、<>、not in、not exists这类负向查询,大概率会导致索引失效。

复制代码
-- 大概率全表扫描
select * from user where id != 10;

6. 联合索引不满足最左匹配

上面讲过,跳过左边字段就失效。

7. 数据量太少

表就几百行数据,MySQL优化器觉得全表扫描比走索引还快,直接放弃索引。

八、神器加持:用explain检查索引是否生效

索引建没建对、走没走,别猜,用explain工具看。这是SQL调优的第一神器。

基本用法

复制代码
explain select * from user where name = '杨过';

输出示例与重点字段解读

字段 含义 怎么看
type 访问类型 性能从好到差:system > const > eq_ref > ref > range > index > all 看到all就是全表扫描,赶紧优化
possible_keys 可能用到的索引 候选索引列表
key 实际用到的索引 显示null就是没用到索引
rows 预估扫描行数 数值越大越慢,索引就是用来减小这个数的
extra 额外信息 using index:索引覆盖,不用回表,性能极佳 using where:需要回表过滤 using filesort:文件排序,需要优化 using temporary:用了临时表,严重优化

九、索引创建黄金法则

最后总结几条实战准则,照着做基本不会出大错:

  1. 频繁出现在where、join、order by、group by中的字段建索引
  2. 区分度太差的字段别单独建索引(比如性别、状态只有几个值)
  3. 优先建联合索引,少建单列索引,一个联合索引往往顶好几个单列索引
  4. 更新特别频繁的字段慎建索引,写入开销会很大
  5. 主键尽量用自增整数,别用UUID、业务字段当主键
  6. 索引不是越多越好,一张表索引建议控制在5个以内
  7. 长字符串可以建前缀索引,不用给整个字段建索引

总结

最后咱们用几句话把索引的核心串起来:

  • 索引的本质是空间换时间 ,核心目标是减少磁盘IO次数
  • MySQL InnoDB用16KB的页做IO单位,靠B+树结构让树保持矮胖,大幅降低IO
  • InnoDB主键是聚簇索引 ,数据直接存在叶子节点;普通索引只存主键,查询需要回表
  • 联合索引遵循最左匹配原则,字段顺序很重要
  • 键是约束,索引是结构,别把外键当索引
  • 索引不是万能的,有很多失效场景,用explain验证才靠谱

当然,索引的学问远不止这些,比如索引下推、页分裂、自适应哈希索引......但把这篇吃透,你已经超过了80%的初学者。

看完这篇,再也别写全表扫描的SQL啦!

相关推荐
shaibdoio2 小时前
瞬维AI落地经验:AI Agent工具调用准确率怎么提
数据库·人工智能·oracle
NPE~2 小时前
CMDB科普:配置管理数据库——运维相关
运维·数据库·科普·cmdb·配置管理数据库
Highcharts.js2 小时前
动态数据可视化架构:Highcharts + PHP + MySQL 从数据库到数据渲染
数据库·mysql·php·开发文档·实时数据·highcharts·图表开发
余槐i2 小时前
WAL 下写并发仍是 1:SQLite 单写者模型实测与绕开方案
数据库·python·性能优化·sqlite·wal
阳光九叶草LXGZXJ10 小时前
达梦数据库-报错-15-列【XXX】长度超出定义
linux·运维·数据库·sql·学习
字节渡客11 小时前
Redis键明明过期了,业务还在读到旧数据
数据库·redis·spring
OnlineProxy13 小时前
亚马逊多账号运营:如何在规避“关联封号”风险的同时实现电商规模化扩张
服务器·数据库·redis
辻弋20115 小时前
五年前的旅行视频糊成马赛克?Video2X用Real-ESRGAN逐帧重建细节,但只支持Windows、集显用户建议直接放弃
服务器·数据库·windows·游戏引擎·电脑
可乐鸡翅yeah_16 小时前
业务中 M3U8 水印相关坑,硬水印和动态水印区别
前端·网络·数据库·ffmpeg·m3u8在线