MySQL索引原理与性能优化

前言

在日常讨论中,数据库性能优化往往被等同于查询优化。这种说法并非完全准确,但在实际工程场景下是可以成立的。

从操作类型上看,数据库性能优化不仅涵盖查询(SELECT),也包括插入、更新和删除等写操作。只是 UPDATE 和 DELETE 虽然属于写操作 ,其性能瓶颈 通常集中在条件匹配阶段的行定位过程,因此在优化手段上与查询高度相关;而 INSERT 的优化更多发生在表结构、索引设计和写入策略等更高层面的设计上。

在实际业务系统中,查询操作通常占据绝大多数请求比例,也是最容易暴露性能瓶颈的环节,因此针对读操作进行优化往往具有最高的性价比。

在缺乏合适索引或索引无法被有效利用的情况下,查询通常会退化为全表扫描,其时间复杂度近似为 O(n)。通过引入合适的数据结构(如索引)以及优化 SQL 执行方式,可以显著降低查询成本。

本文将从索引(数据结构)、性能指标以及常见优化方法三个维度,对数据库性能优化进行系统性梳理。

索引

索引本质上是一种用于高效获取数据的有序数据结构,用于数据库快速查找指定数据。

首先介绍MySQL的体系架构图,数据库可分为四层:

  • 网络连接层:数据库服务端对外通信的入口,负责接收客户端发起的网络连接,并通过数据库通信协议完成连接建立与请求传输。
  • 数据库服务层:数据库服务器的核心,负责连接会话管理、SQL 解析与优化、执行计划生成以及部分内存结构的管理,并通过统一接口调用底层存储引擎完成数据读写。
  • 存储引擎层 :负责具体的数据读写与事务实现,管理页、索引和缓冲等数据结构,并与底层数据文件交互。该层采用可插拔的插件式设计,MySQL 中最常用的存储引擎是 InnoDB。
  • 系统文件层 :数据库实际使用的底层文件集合,包括数据文件、日志文件以及相关配置文件,用于持久化存储数据库状态与数据内容。

    索引在存储引擎层实现,不同的存储引擎使用不同的索引结构,要查看存储引擎可以使用show create table 表名

    常用的几种索引如下:
B+树 最常见的索引类型,广泛支持
Hash索引 哈希表,查找效率高,不可范围查询,不可排序
R-Tree 空间索引,用于存储地理位置
Full-text 倒排索引,用于文档的快速匹配

索引结构------B+树(多路平衡查找树,Balanced):

平衡二叉树(红黑树/AVL树)

解决问题:二叉树的顺序插入导致退化成链表,由此产生了平衡树以保证树高为O(logN)

遗留问题:但二叉树只有两个子节点,导致树高过高,查找效率低下,

B树

解决问题:B树引入更多子节点,将高度从 l o g 2 N log_2N log2N压缩到 l o g m N log_mN logmN,

遗留问题:数据分布在多个层次,查询路径不固定,范围查询需要跳层级。

B+树

核心方案:B+树将所有数据存储在叶子节点 中,将查找时间统一控制为 l o g m N log_mN logmN,同时引入指针串联数据,便于范围查找。

插入分裂:B+树的最大子结点数量称为度,n个度有n+1个指针,插入时当元素个数大于n,中间元素会自动向上分裂。

索引内容:key为索引列,value为主键或行内容,索引存储的内容后续会详细介绍(InnoDB的聚簇索引)

B+树的结构如下,:

有关B+树的详细介绍可见:B树与B+树的区别

索引分类

前面已经介绍过数据结构角度的索引分类:B+树、hash索引、R-Tree和full-text索引。

从索引字段特性角度可以分为:

索引 说明
主键索引 建立在主键PRIMARY上的索引,索引列值不为空,随表自动创建
唯一索引 建立在唯一UNIQUE上的索引,列值可为空,可有多个
普通索引 建立在普通字段上的索引
全文索引 用于查找文本中的关键字,而非比较索引中的值
复合索引 多列值组成一个索引,专门用于组合搜索

InnoDB存储引擎根据叶子节点的数据内容是否是完整数据将索引分为聚集索引和二级索引。

聚集索引

  • 叶子结点是行数据, 索引默认为主键,一张表只能有一个(查行数据必到聚集索引)
  • 不存在主键时使用第一个唯一索引,都不存在InnoDB自动生成rowid作为隐藏的聚集索引

二级索引

  • 叶子节点存储聚集索引的主键值,作为定位行数据的引用
  • 工作流程:二级索引中找到id,到聚集索引中找对应记录(回表查询)
  • 优化方向:如果需要的字段都在二级索引中则无需回表,常用查询字段可单独组成索引

回表查询的过程如下,避免回表也是索引查询性能优化的方向之一:

详细介绍可见:mysql索引分类

索引语法

功能 语法
创建索引 create (unique\fullText) index 索引名 on 表名 (列字段1,列字段2)
查看索引 show index from 表
删除索引 drop index 索引名 from 表

简单使用实例如下:

使用规则

验证索引效率

万级以上数据量的表查询时有无索引性能相差明显,没有测试数据就不演示了。

但创建索引因为要构建B+树有一定耗时,并且需要占用磁盘,本质还是空间换时间,但对企业来说,磁盘开销是最小的一环,索引的创建势在必行。

最左前缀法则

联合索引的查询应遵循该法则,即索引生效条件是联合索引的最左 N 个字段的任意连续前缀, 不包含最左侧的列或中间跳过部分列都会导致索引失效。

范围查询会导致范围右侧的索引列失效。

该法则可通过一个例子直观展示:

最左端匹配索引列,用到index1索引,ref对应constconst说明用到两个索引

sql 复制代码
// 创建索引列
create index index1 on userinfo (phone,hobby,description);
mysql> EXPLAIN select * from userinfo WHERE phone="18888888888" and hobby="看电影"\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: userinfo
   partitions: NULL
         type: ref
possible_keys: index1
          key: index1
      key_len: 1104
          ref: const,const
         rows: 1
     filtered: 100.00
        Extra: Using index condition
1 row in set, 1 warning (0.00 sec)

跳过最左索引列,使用全表扫描

sql 复制代码
mysql> EXPLAIN select * from userinfo WHERE hobby="看电影" and description="外国人"\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: userinfo
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 4
     filtered: 25.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

最左侧开始,但中间跳过一个索引列,ref索引对应const说明用到一个索引,第二个字段在索引查找结果基础上扫描:

sql 复制代码
mysql> EXPLAIN select * from userinfo WHERE phone="18888888888" and description="外国人"\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: userinfo
   partitions: NULL
         type: ref
possible_keys: index1
          key: index1
      key_len: 81
          ref: const
         rows: 1
     filtered: 25.00
        Extra: Using index condition
1 row in set, 1 warning (0.00 sec)

该法则可以简单理解为一张图:

上图表示针对column2-4的联合索引(索引顺序:(column2, column3, column4)):

  • 查询限定条件为2、3、4:索引完全覆盖条件,效率最高;
  • 查询限定条件为3、4:最左侧索引列未被包含,索引失效;
  • 查询限定条件2、4:跳过索引列3,自3以后的条件不使用索引,后续条件在2的结果中扫描检索。

补充:查询顺序不影响索引生效,范围查询也会引起索引连续中断。

索引失效情况

  1. 索引列上运算。索引列参与运算,如SELECT * FROM user WHERE age + 1 = 30,会导致索引存储的B+树信息无法被直接利用。
  2. 字符串不加引号"。MySQL 在执行查询时,两边前后数据类型不一致会先进行类型转换,字符串转为数字,但索引是根据字符串建立的,会导致索引失效。
  3. 头部模糊匹配。B + 树索引是按字段前缀排序存储的,头部使用%模糊查询会导致无法定位结点,只能全表遍历。
  4. or连接中的非索引列。or连接的多个查询条件中,只要有任意一列没有建立索引,整个查询就会放弃索引,走全表扫描。
  5. 违背最左前缀法则。上一节介绍的内容。
  6. 选择性过低,优化器主动放弃索引。索引选择性 = 索引列唯一值数量 / 表总数据量,优化器会认为全表扫描比索引查找更高效,主动放弃索引。

SQL提示

SQL 提示是开发者手动指定的指令,用于覆盖 MySQL 优化器的默认执行决策

  • 建议使用索引。SELECT * FROM 表名 USE INDEX (索引名1, 索引名2) WHERE 查询条件;
  • 忽略索引:SELECT * FROM 表名 IGNORE INDEX (索引名1, 索引名2) WHERE 查询条件;
  • 强制使用索引:SELECT * FROM 表名 FORCE INDEX (索引名1, 索引名2) WHERE 查询条件;

尽可能使用覆盖索引

二级索引以「索引列(key)- 定位标识(value,InnoDB 为主键值 / MyISAM 为 rowid)」存储,当查询字段超出索引列范围时,会触发 "先查索引再找全量数据" 的回表查询,带来额外性能开销。

覆盖索引通过将高频查询的过滤列 + 结果列全部纳入索引,让查询无需回表,仅通过扫描索引即可获取所有所需数据,是优化回表开销的核心手段,实际使用中应优先覆盖常用查询场景。

explain执行计划中的extra字段体现,using index condition表示回表查询,using index表示不需要回表,数据在索引列中都能找到。

前缀索引

有时存储的数据的文字,甚至文章,直接建立索引会导致索引太大,此时可以只根据前缀建立索引,提高效率。

操作为:create index 索引名 on 表 (列(n)),n表示前n个字符,整列选择性可通过SELECT COUNT(DISTINCT 列名) / COUNT(*) FROM 表名;计算,逐渐测试选择最接近整体选择性的n值。

n的长度由选择性确定(选择性:索引列唯一值数量 / 表总数据量),选择性越大越好,

索引设计原则

  1. 必要性:数据量大(万条以上记录),查询频繁的场景。
  2. 语句选择:常用whereorder bygroup by的语句。
  3. 列选择:选择性高的列。
  4. 长内容数据设置前缀索引。
  5. 多用联合索引和覆盖索引。
  6. 只设立必要的索引,索引过多会影响增删改的效率。
  7. 若索引不能存放NULL,用NotNULL约束,提高性能,减少判断。

性能指标

执行频率

查看语句访问频次show (session\global) status like "com_______";(七个_),判断语句执行比例,是否有必要做优化。

使用示例:

慢查询日志

慢查询日志会记录执行时间超过指定参数的语句,便于后续定位要优化的具体语句。

慢查询日志功能可通过show variables like "slow_query_log";查看是否开启。

未开启可到到配置文件中开启,Linux路径为etc/my.conf,Windows为C:\ProgramData\MySQL\MySQL Server 8.0\my.ini,通过slow_query_log=1开启慢查询日志,long_query_time=2指定慢查询阈值。

生成的日志文件Linux存在/var/lib/mysql/localhost_slow.log,Windows可通过show variables like 'slow_query_log_file'查看。

show profiles指令详情

profiles可用于分析资源消耗情况,使用方法如下:

复制代码
select @@have_profiling; // 查看功能是否打开
set (session/global) profiling=1; // 开启会话/全局详情功能
show profiles; // 查看语句耗时等资源消耗情况
show profile for query id; // 查看语句各阶段耗时,id来自show profiles

profiles执行示例如下:

可通过该指令查看其时间消耗,以便确定具体优化策略。

explain执行计划

查询执行计划的使用方式很简单,explain+sql语句即可,关键是返回字段的含义,常用关注字段为:

  • id:表示语句执行顺序,id大的先执行,一样大的从上到下依次执行;
  • select_type:语句类型;
  • type:访问类型,性能从好到差依次为:NULL(不访问表的查询)>system(一行数据)>const(单表的主键/唯一索引)>eq_ref(多表的主键/唯一索引)>ref(普通索引等值匹配)>range(索引范围查询)>index(全索引查询)>ALL(全表扫描);
  • possible_keys:可能用到的索引
  • key:实际用到的索引;
  • key_len:索引的字节数;
  • rows:预估查询到的行数;
  • filter:返回行数占读取行数的占比,越大越好。

执行计划查看示例如下:

更多有关字段详细信息介绍可见:explain关键字详解

性能优化

插入数据优化

大量数据插入时使用insert效率较低,可从以下三个方面入手:

  • 批量插入
    连接客户端并声明导入数据:mysql -u root -p --local-infile=1
    开启数据导入功能:set global local_infile = 1;
    执行load语句:load data local infile "文件名" into table 表 fields terminated by ',' lines terminated by '\n';
    一百万数据的写入对比,insert需要10分钟,load需要10s。
  • 手动事务提交
    关闭自动事务提交SET autocommit=0;
    begin;开启事务,sql执行结束后手动commit;
  • 按主键顺序插入
    InnoDB中,数据根据主键顺序组织存放,若乱序会引发页分裂和页合并,产生额外开销。

主键优化

补充介绍主键顺序插入的原理。

InnoDB存储引擎中,表的存储结构是:

表空间------段------区------页(16k)------行数据

其中行数据按主键顺序存放,乱序添加行数据会导致页分裂,增加额外开销。

页分裂:插入数据已满,从中间位置分裂为两个页,新数据插入,重新建立指针;

页合并:删除记录达到阈值(默认50%),找相邻的页,查看是否能合并。

主键的设计原则:

  • 降低主键长度:二级索引中叶子结点挂载的数据是主键,过长会增加磁盘IO。不使用UUID(无序)或自然主键(如身份证号)
  • 顺序插入数据:尽量使用自增主键。
  • 避免修改主键:主键修改会导致记录在表中重新排序,开销较大。

order by 排序

首先介绍两种排序方法:

  • filesort :指通过表索引或全表扫描,读取满足条件的数据行,在排序缓冲区sort buffer中完成排序操作,所有不通过索引直接返回排序结果的排序都叫filesort
  • index:通过有序索引顺序扫描,直接返回有序数据,无序额外排序,效率高。

把排序方法优化成index是主要方向,即经常执行order by查询的数据,按照其排序规则建立对应的索引。

优化排序方法有以下几点需要注意:

  • 前提:使用覆盖索引。
  • 多字段的升序降序要符合order by查询(B+树叶子节点结构),同时注意联合索引规则。
  • 大数据量查询filesort不可避免,可增大排序缓冲区sort_buffer_size大小以提高性能。

补充:不同顺序建立索引对B+树的影响:

group by 分组

本质是用索引快速定位分组数据,要注意满足最左前缀法则。

limit 优化

SELECT * FROM user ORDER BY id LIMIT 9990, 10;跳过前9990条数据,返回后十条,该语句有大量无效扫描开销,且可能触发回表查询,可用覆盖索引+子查询的方式优化。

sql 复制代码
SELECT * FROM user
WHERE id IN (
    -- 子查询:仅扫描主键索引,获取目标分页的id(无回表,快速)
    SELECT id FROM user ORDER BY id LIMIT 9990, 10
);

或是使用连接查询:

sql 复制代码
SELECT u.* FROM user u
JOIN (
    -- 子查询获取目标分页的id
    SELECT id FROM user ORDER BY id LIMIT 9990, 10
) AS t ON u.id = t.id
ORDER BY u.id;

count 优化

MyISAM引擎把数据表的总行数存储在磁盘上,count时直接返回,效率高。

InnoDB需要按行读取,累计计数,有计数需求的推荐使用redis等缓存单独管理维护该字段,count操作本身也有区别。

  • count(*):根据主键在服务层累加;
  • count(字段):要看是否被not null修饰,有则服务层累加,否则判空再累加
  • count(1):与count(*)几乎相同,根据行累加。

总体性能:count(*)≈count(1)>count(字段)

update优化

InnoDB的三大特性:事务、外键、行级锁。

无索引条件下进行更新时,由于无法通过索引快速定位目标记录,MySQL 需要进行全表扫描,并对扫描到的行逐一加锁,导致锁范围扩大、锁冲突增多,并发性能显著下降,其表现效果类似表锁。

为提高并发性能,应为频繁更新的字段建立合适的索引。

总结

本文从索引原理B+树开始,介绍了sql执行性能指标的查看方法,包括慢查询日志和执行计划,最后到性能优化的具体方案。

形成的认识如下:

  1. 索引的本质:索引本质上是根据索引列组织的数据结构,组织形式为B+树,方便后续检索,是空间换时间的方案。
  2. 索引的使用:由于B+树的结构特性,索引的使用需要遵循一些规则,其中最重要的是最左前缀,查询限制条件的索引列应为包含最左侧索引列的连续列。
  3. sql的性能指标:直观上可以通过慢查询日志定位耗时最长的语句,后续使用执行计划查看其索引使用情况进一步优化。
  4. sql优化:排序、分组、分页等操作的优化都是基于B+树结构进行,查询限制条件与索引越贴合,效率越高。
  5. B+树结构 :所有数据都在叶子节点,叶子节点的key是索引列,valuerowid(主键值),聚簇索引是行数据。
    该结构使得最左侧索引列直接决定B+树分支策略,根据该列进行检索效率最高,同时当二级索引不能满足查询要求时,要根据rowid到聚簇索引中二次检索,即回表查询,避免回表查询(覆盖索引)也是索引优化的主要方向之一。
相关推荐
找不到、了2 小时前
MySQL的FEDERATED存储引擎详解
数据库·mysql
小希smallxi2 小时前
Windows平台一键启动Redis脚本
数据库·windows·redis
小蒜学长2 小时前
python基于Python的医疗机构药品及耗材信息管理系统(代码+数据库+LW)
数据库·spring boot·后端·python
星光一影2 小时前
同城搭子活动组局H5系统源码-伴伴搭子系统源码
vue.js·mysql·php·uniapp
千寻技术帮2 小时前
10363_基于SSM的农机租赁管理系统
mysql·毕业设计·ssm·源码·农机租赁
xUxIAOrUIII2 小时前
【数据库原理】期末复习(初稿)
数据库·笔记
Pocker_Spades_A2 小时前
AI Ping 上线 GLM-4.7 与 MiniMax M2.1:两款国产旗舰模型免费用!
大数据·数据库·人工智能
峰顶听歌的鲸鱼2 小时前
20.MySql数据库
运维·数据库·笔记·mysql·云计算·学习方法
G_H_S_3_2 小时前
【网络运维】SQL 语言:MySQL数据库基础与管理
运维·网络·数据库·mysql