MySQL 索引底层彻底吃透:B + 树原理、聚簇索引机制与全场景优化指南

简介: 本文深入剖析MySQL InnoDB索引底层原理:从B+树为何成为最优选,到聚簇/二级索引机制、回表与覆盖索引;详解最左前缀、索引失效10大场景及根因;并给出分页优化、联合索引设计、ICP等生产级实战方案,助你真正知其所以然。

你写的SQL为什么总是慢?为什么建了索引还是全表扫描?为什么分页查询越往后越慢?90%以上的MySQL性能问题,根源都在于对索引底层原理的理解不到位。本文从数据结构本质出发,彻底讲透MySQL InnoDB索引的底层逻辑、聚簇索引核心机制、索引失效的底层原因,以及生产环境可落地的优化方案,让你不仅知其然,更知其所以然。


一、索引的本质:到底什么是索引?

索引的本质是帮助MySQL高效获取数据的排好序的数据结构,核心作用是减少磁盘IO次数,把随机IO转化为顺序IO。

很多人存在误区:把索引等同于表中的字段。实际上,索引是基于数据表的字段构建的排序数据结构,存储在磁盘的ibd文件中,其核心价值是通过排序结构,避免全表扫描,把O(n)的查询复杂度降低到O(logn)级别。


二、为什么MySQL InnoDB最终选择了B+树?

数据库的核心瓶颈是磁盘IO,所有数据结构的选型,最终都是为了减少IO次数。我们逐一对比主流数据结构的缺陷,就能明白B+树的不可替代性。

2.1 为什么不用二叉搜索树?

二叉搜索树的核心特性:左子树所有节点值小于父节点,右子树所有节点值大于父节点。 致命缺陷:极端场景下(比如顺序插入数据)会退化成单向链表,树高等于数据量。比如100万条数据,树高可达100万,查询需要100万次IO,完全无法满足性能要求。

2.2 为什么不用平衡二叉树/红黑树?

平衡二叉树(AVL)通过旋转保证左右子树高度差不超过1,红黑树通过弱平衡规则保证最长路径不超过最短路径的2倍,解决了二叉搜索树的退化问题。 核心缺陷:树高依然过高。即使是100万条数据,红黑树的树高也接近20,意味着一次查询需要20次磁盘IO,性能无法满足高并发场景。同时二叉树每个节点只能存储一个键值,完全浪费了磁盘预读(每次IO读取一页16KB数据)的特性。

2.3 为什么不用B树(平衡多路查找树)?

B树是多路平衡树,每个节点可以存储多个键值和数据,树高大幅降低。但它依然存在无法解决的缺陷:

  1. 非叶子节点存储了完整数据,导致每个节点能存储的索引键数量大幅减少,树高依然高于B+树;
  2. 范围查询需要多次中序遍历,跨节点IO次数多,不适合排序、分页等高频场景;
  3. 查询性能不稳定,部分查询在非叶子节点就能返回,部分需要遍历到叶子节点,不利于优化器稳定预估成本。

2.4 B+树的核心特性与绝对优势

B+树是专为数据库存储引擎设计的B树变种,也是InnoDB索引的底层数据结构,其核心特性完美适配数据库的查询场景:

  1. 非叶子节点只存索引键,不存数据:所有完整数据都存储在叶子节点,非叶子节点能存储的索引键数量大幅提升,树高极低。
  2. 叶子节点是有序双向链表:所有叶子节点按索引键升序排列,通过双向链表串联,范围查询、排序、分页操作只需遍历链表即可,性能极高。
  3. 所有查询最终都落到叶子节点:查询路径长度固定,性能稳定,便于优化器做成本预估。

B+树树高计算(核心干货)

InnoDB中,每个数据页的大小固定为16KB(操作系统页大小的整数倍,保证原子IO):

  • 非叶子节点:存储bigint类型主键(8字节)+ 子节点指针(6字节),单个键值对仅占14字节;
  • 单个非叶子节点可存储的索引数量:16KB / 14B ≈ 1170个;
  • 树高2层:可存储1170 * 16 = 18720行数据(按单行数据1KB计算);
  • 树高3层:可存储1170 * 1170 * 16 = 21902400行数据,也就是2千多万行数据,仅需3次IO即可完成查询,性能碾压其他数据结构。


三、InnoDB核心:聚簇索引与二级索引的底层机制

InnoDB是索引组织表,整张表的数据就是按照聚簇索引的B+树组织的,这是它和MyISAM引擎最核心的区别,也是所有索引优化的基础。

3.1 什么是聚簇索引?

聚簇索引是按照每张表的主键构建的B+树,其叶子节点直接存储了整张表的整行数据。聚簇索引既是索引,也是数据的存储方式,数据和主键索引是完全绑定在一起的。

聚簇索引的核心规则:

  1. 每张表有且只有一个聚簇索引;
  2. 若显式定义了主键,InnoDB会将主键索引作为聚簇索引;
  3. 若未定义主键,InnoDB会选择第一个唯一非空索引作为聚簇索引;
  4. 若既无主键也无唯一非空索引,InnoDB会隐式创建一个6字节的ROWID作为聚簇索引。

3.2 二级索引(辅助索引)的底层结构

二级索引是基于非主键字段构建的B+树,其核心特性和聚簇索引完全不同:

  • 非叶子节点仅存储索引键值;
  • 叶子节点不存储整行数据,仅存储对应的主键值

这里有一个核心设计问题:为什么二级索引叶子节点存主键值,而不是数据行的物理地址? 原因是聚簇索引会发生页分裂,数据行的物理地址会发生变化。如果二级索引存储物理地址,每次页分裂都需要更新所有二级索引的地址,维护成本极高。而存储主键值,页分裂不会影响二级索引,只需通过主键到聚簇索引中查找即可,这是InnoDB索引设计的核心巧思。

3.3 回表查询的完整流程

回表是InnoDB中最核心的概念:当二级索引无法覆盖查询所需的所有列时,需要通过二级索引叶子节点中的主键值,到聚簇索引中查找整行数据的过程,这个额外的IO操作就是回表,也是性能损耗的核心来源。

我们通过具体表结构和SQL示例,完整还原回表过程:

CREATE TABLE `sys_user` (
 `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
 `user_name` varchar(64) NOT NULL COMMENT '用户名',
 `age` int NOT NULL COMMENT '年龄',
 `gender` tinyint NOT NULL COMMENT '性别 1-男 2-女',
 `phone` varchar(11) NOT NULL COMMENT '手机号',
 `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
 PRIMARY KEY (`id`),
 UNIQUE KEY `uk_phone` (`phone`),
 KEY `idx_name_age` (`user_name`,`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';

执行SQL:

SELECT * FROM sys_user WHERE user_name = '张三' AND age = 20;

完整执行流程:

  1. 遍历二级索引idx_name_age的B+树,找到user_name='张三'age=20的叶子节点,获取对应的主键id=1001;
  2. 遍历聚簇索引的B+树,通过主键id=1001找到对应的整行数据,返回给客户端。

3.4 聚簇索引的主键设计黄金法则

  1. 必须显式定义主键:避免InnoDB隐式创建无法控制的ROWID,同时主键可用于查询和关联优化;
  2. 优先使用自增有序主键:推荐使用bigint auto_increment,有序插入只会在最后一个数据页追加写入,不会触发页分裂,顺序IO性能极高;
  3. 禁止使用UUID/无序字符串作为主键:无序插入会随机写入数据页中间位置,频繁触发页分裂,导致数据页碎片化,写入性能暴跌;同时UUID占用空间大,会导致所有二级索引体积膨胀,增加查询IO次数;
  4. 主键长度尽量小:二级索引叶子节点都会存储主键值,主键越短,二级索引体积越小,IO效率越高。

四、索引生效的核心规则与失效场景底层解析

很多人背了无数索引失效规则,却依然写不好SQL,核心原因是没有理解规则背后的底层逻辑。所有索引失效的本质,都是无法利用索引的排序结构,只能全表扫描。

4.1 最左前缀原则:索引生效的核心底层逻辑

最左前缀原则是联合索引的核心规则:MySQL联合索引会按照索引定义的字段顺序,从左到右匹配查询条件,遇到范围查询(>、<、between、like前缀匹配)就会停止匹配。

底层原因:联合索引的B+树是按照索引定义的字段顺序排序的——先按第一个字段排序,第一个字段值相同的情况下,再按第二个字段排序,以此类推。只有保证左边的字段是精确匹配,右边的字段才能利用索引的有序性。

我们以联合索引idx_name_age_create_time(user_name,age,create_time)为例,完整说明生效与失效场景:

生效场景

  1. 全值匹配:WHERE user_name = '张三' AND age = 20 AND create_time = '2024-01-01',三个字段全部生效,可通过explainkey_len字段验证;
  2. 匹配最左N个字段:WHERE user_name = '张三' AND age = 20,前两个字段生效;
  3. 匹配最左前缀:WHERE user_name = '张三',第一个字段生效;
  4. 匹配最左字段范围查询:WHERE user_name BETWEEN '张三' AND '李四',第一个字段生效。

失效场景

  1. 不匹配最左前缀:WHERE age = 20 AND create_time = '2024-01-01',完全无法使用索引;
  2. 范围查询之后的字段:WHERE user_name = '张三' AND age > 20 AND create_time = '2024-01-01',仅user_nameage生效,create_time无法生效,因为范围查询后停止匹配;
  3. 跳过中间字段:WHERE user_name = '张三' AND create_time = '2024-01-01',仅user_name生效,create_time无法生效。

4.2 索引失效的10大场景与底层原因

所有场景均基于MySQL 8.0验证,每个场景都附带可执行SQL与底层原理解析。

1. 对索引字段使用函数操作

-- 索引失效
EXPLAIN SELECT * FROM sys_user WHERE DATE(create_time) = '2024-01-01';
-- 索引生效
EXPLAIN SELECT * FROM sys_user WHERE create_time BETWEEN '2024-01-01 00:00:00' AND '2024-01-01 23:59:59';

底层原因:索引存储的是字段的原始值,不是函数计算后的值。对索引字段使用函数后,MySQL无法匹配索引的排序结构,只能全表扫描。

2. 隐式类型转换

-- phone为varchar类型,查询条件为数字,触发隐式类型转换,索引失效
EXPLAIN SELECT * FROM sys_user WHERE phone = 13800138000;
-- 类型匹配,索引生效
EXPLAIN SELECT * FROM sys_user WHERE phone = '13800138000';

底层原因:当查询条件的字段类型和索引字段类型不一致时,MySQL会对索引字段做隐式类型转换,相当于对索引字段使用了函数,导致索引失效。 注意:若索引字段为int类型,查询条件为varchar类型,MySQL会对查询条件做转换,不会对索引字段操作,索引不会失效。

3. 模糊查询以%开头

-- %开头,无法匹配前缀,索引失效
EXPLAIN SELECT * FROM sys_user WHERE user_name LIKE '%张三';
-- %结尾,前缀匹配,索引生效
EXPLAIN SELECT * FROM sys_user WHERE user_name LIKE '张三%';

底层原因:联合索引是按字段前缀有序排列的,%开头的模糊查询无法匹配前缀,只能全表扫描。若需前后模糊匹配,推荐使用MySQL全文索引,而非like语句。

4. 使用OR连接非索引字段

-- gender无索引,OR连接后索引失效
EXPLAIN SELECT * FROM sys_user WHERE user_name = '张三' OR gender = 1;
-- 两个字段均有索引,触发索引合并,索引生效
EXPLAIN SELECT * FROM sys_user WHERE user_name = '张三' OR phone = '13800138000';

底层原因:OR连接的条件中,只要有一个字段没有索引,MySQL就无法通过索引匹配所有条件,只能全表扫描。

5. 索引字段参与算术运算

-- 对索引字段做算术运算,索引失效
EXPLAIN SELECT * FROM sys_user WHERE age + 1 = 21;
-- 索引生效
EXPLAIN SELECT * FROM sys_user WHERE age = 20;

底层原因:和函数操作一致,对索引字段做算术运算后,无法匹配索引的原始值,导致索引失效。

6. join关联字段类型/字符集不一致

两张表join时,若关联字段的类型、字符集不一致,会触发隐式类型转换,导致索引失效,这是生产环境中最常见的隐藏坑。

7. 索引区分度过低

区分度计算公式:COUNT(DISTINCT 字段)/COUNT(*),值越接近1,区分度越高。 比如gender字段只有1、2两个值,区分度极低,MySQL认为全表扫描比索引查询成本更低,不会使用索引。

8. MySQL优化器选错索引

当表中有多个索引时,MySQL优化器会基于成本预估选择索引,可能出现选错索引的情况。可通过force index强制指定索引,但需谨慎使用。

9. 使用不等于(!=、<>)、NOT IN、IS NOT NULL

这类操作不是绝对失效,但绝大多数场景下,MySQL会认为全表扫描成本更低,放弃使用索引。

10. 冗余与无效的索引设计

比如已有联合索引idx_a_b(a,b),再单独建索引idx_a(a),属于冗余索引,不会提升查询性能,只会增加写入负担。


五、生产级索引优化实战指南

所有优化方案均基于底层原理设计,可直接落地到生产环境,解决高频性能问题。

5.1 覆盖索引:彻底消除回表

覆盖索引是性能优化的第一利器:查询的所有列,都包含在索引中,无需回表查询,性能可提升数十倍。

-- 回表查询,需到聚簇索引获取完整数据
EXPLAIN SELECT * FROM sys_user WHERE user_name = '张三' AND age = 20;
-- 覆盖索引,无需回表,Extra字段显示Using index
EXPLAIN SELECT id,user_name,age FROM sys_user WHERE user_name = '张三' AND age = 20;

底层原理:二级索引idx_name_age的叶子节点已经包含了user_name、age、主键id,查询的列全部在索引中,可直接返回,无需回表。Using index是覆盖索引的核心标志。

生产最佳实践:

  1. 禁止使用SELECT *,只查询业务需要的字段,更容易实现覆盖索引;
  2. 设计联合索引时,将业务需要返回的字段加入索引,实现覆盖索引。

5.2 联合索引设计黄金法则

  1. 等值查询优先:将等值查询的字段放在联合索引前面,范围查询的字段放在后面;
  2. 高区分度优先:区分度高的字段放在前面,可快速过滤掉大部分数据;
  3. 高频查询优先:将频繁查询的字段放在前面,让更多查询能复用索引;
  4. 覆盖索引优先:将需要返回的字段加入索引,避免回表。

示例:业务高频查询WHERE user_name = ? AND age > ?,需要返回user_name、age、phone。 设计联合索引:idx_name_age_phone(user_name,age,phone),符合最左前缀原则,等值查询字段在前,范围查询在后,同时实现覆盖索引,无需回表。

5.3 大偏移量分页查询优化

传统分页SELECT * FROM sys_user LIMIT 100000,20在偏移量极大时性能极差,因为MySQL需要扫描100020行数据,再丢弃前100000行。

优化方案1:主键覆盖优化

SELECT * FROM sys_user
WHERE id >= (SELECT id FROM sys_user ORDER BY id LIMIT 100000,1)
LIMIT 20;

原理:子查询通过覆盖索引快速找到偏移量对应的主键id,再通过主键范围查询,仅需扫描20行数据,性能提升上百倍。

优化方案2:游标分页(推荐用于APP下拉加载)

SELECT * FROM sys_user
WHERE id > 100000
ORDER BY id LIMIT 20;

原理:记录上一页最后一条数据的主键id,下一页通过id > 上一页id查询,完全避免大偏移量,性能稳定。

5.4 Order By/Group By优化

核心原则:利用索引的有序性,避免Using filesort文件排序。

示例:联合索引idx_name_age(user_name,age)

-- 利用索引有序性,无文件排序
EXPLAIN SELECT user_name,age FROM sys_user WHERE user_name = '张三' ORDER BY age;
-- 符合索引顺序,无文件排序
EXPLAIN SELECT user_name,age FROM sys_user ORDER BY user_name,age;
-- 顺序相反,触发文件排序
EXPLAIN SELECT user_name,age FROM sys_user ORDER BY age,user_name;

MySQL 8.0支持降序索引,可解决正反序排序的文件排序问题:

-- 创建降序索引
CREATE INDEX idx_name_asc_age_desc ON sys_user(user_name ASC, age DESC);
-- 不再触发文件排序
EXPLAIN SELECT user_name,age FROM sys_user ORDER BY user_name ASC, age DESC;

5.5 索引条件下推(ICP)优化

ICP是MySQL 5.6引入的优化特性,默认开启,核心作用是在存储引擎层遍历索引时,直接对索引中包含的字段进行过滤,减少回表次数和IO次数。

示例:联合索引idx_name_age(user_name,age)

SELECT * FROM sys_user WHERE user_name LIKE '张%' AND age = 20;

  • 无ICP:存储引擎通过user_name LIKE '张%'找到所有匹配的索引,全部回表,再在server层过滤age=20的记录;
  • 有ICP:存储引擎遍历索引时,直接过滤age=20的记录,仅对符合条件的记录回表,大幅减少回表次数。

Using index condition是ICP开启的核心标志。


六、索引设计最佳实践与避坑指南

  1. 控制索引数量:单表索引数量控制在5个以内,过多的索引会导致插入、更新、删除时需要同步维护多个B+树,写入性能暴跌,同时占用大量磁盘空间。
  2. 小表无需建索引:数据量小于1000行的表,全表扫描成本低于索引查询,无需建索引。
  3. 低区分度字段不单独建索引:性别、状态等枚举值少的字段,单独建索引无意义,可和其他字段组成联合索引。
  4. 避免冗余索引:已有联合索引idx_a_b(a,b),无需再建idx_a(a),联合索引已支持最左前缀匹配。
  5. 更新频繁的字段少建索引:字段更新时需要同步更新索引,频繁更新会导致索引维护成本极高。
  6. 定期清理无用索引:通过sys.schema_unused_indexes查看数据库启动后未使用的索引,定期清理,减少数据库负担。
  7. 批量导入数据优化:导入大量数据前,可先关闭非唯一索引的更新,导入完成后再重建,大幅提升导入性能。

七、Java业务层索引优化实战代码

核心依赖配置

<parent>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-parent</artifactId>
   <version>3.2.4</version>
   <relativePath/>
</parent>
<dependencies>
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
   <dependency>
       <groupId>com.baomidou</groupId>
       <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
       <version>3.5.6</version>
   </dependency>
   <dependency>
       <groupId>com.mysql</groupId>
       <artifactId>mysql-connector-j</artifactId>
       <scope>runtime</scope>
   </dependency>
   <dependency>
       <groupId>org.projectlombok</groupId>
       <artifactId>lombok</artifactId>
       <version>1.18.32</version>
       <scope>provided</scope>
   </dependency>
   <dependency>
       <groupId>org.springdoc</groupId>
       <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
       <version>2.5.0</version>
   </dependency>
   <dependency>
       <groupId>com.alibaba.fastjson2</groupId>
       <artifactId>fastjson2</artifactId>
       <version>2.0.49</version>
   </dependency>
   <dependency>
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
       <version>32.1.3-jre</version>
   </dependency>
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-jdbc</artifactId>
   </dependency>
</dependencies>

实体类定义

package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.time.LocalDateTime;

/**
* 用户实体类
*
* @author ken
*/

@Data
@TableName("sys_user")
@Schema(description = "用户信息实体")
public class SysUser {

   @TableId(type = IdType.AUTO)
   @Schema(description = "主键ID", example = "1")
   private Long id;

   @Schema(description = "用户名", example = "张三")
   private String userName;

   @Schema(description = "年龄", example = "20")
   private Integer age;

   @Schema(description = "性别 1-男 2-女", example = "1")
   private Integer gender;

   @Schema(description = "手机号", example = "13800138000")
   private String phone;

   @Schema(description = "创建时间")
   private LocalDateTime createTime;

   @Schema(description = "更新时间")
   private LocalDateTime updateTime;
}

Mapper层定义

package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.SysUser;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
* 用户Mapper接口
*
* @author ken
*/

public interface SysUserMapper extends BaseMapper<SysUser> {
   /**
    * 游标分页查询用户列表
    *
    * @param startId 起始主键ID
    * @param limit 分页条数
    * @return 用户列表
    */

   List<SysUser> selectPageByCursor(@Param("startId") Long startId, @Param("limit") Integer limit);
}

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jam.demo.mapper.SysUserMapper">
   <select id="selectPageByCursor" resultType="com.jam.demo.entity.SysUser">
       SELECT id, user_name, age, phone FROM sys_user
       WHERE id > #{startId}
       ORDER BY id ASC
       LIMIT #{limit}
   </select>
</mapper>

VO类定义

package com.jam.demo.vo;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

/**
* 用户查询VO
*
* @author ken
*/

@Data
@Schema(description = "用户查询参数")
public class UserQueryVO {
   @Schema(description = "上一页最后一条数据的主键ID", example = "100000")
   private Long lastId;

   @Schema(description = "每页条数", example = "20")
   private Integer pageSize;
}

package com.jam.demo.vo;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

/**
* 用户响应VO
*
* @author ken
*/

@Data
@Schema(description = "用户响应信息")
public class UserRespVO {
   @Schema(description = "主键ID", example = "1")
   private Long id;

   @Schema(description = "用户名", example = "张三")
   private String userName;

   @Schema(description = "年龄", example = "20")
   private Integer age;

   @Schema(description = "手机号", example = "13800138000")
   private String phone;
}

Service层定义

package com.jam.demo.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.demo.entity.SysUser;
import com.jam.demo.vo.UserQueryVO;
import com.jam.demo.vo.UserRespVO;

import java.util.List;

/**
* 用户服务接口
*
* @author ken
*/

public interface SysUserService extends IService<SysUser> {
   /**
    * 根据用户名和年龄查询用户信息
    *
    * @param userName 用户名
    * @param age 年龄
    * @return 用户响应VO
    */

   UserRespVO getUserByNameAndAge(String userName, Integer age);

   /**
    * 游标分页查询用户列表
    *
    * @param queryVO 查询参数
    * @return 用户列表
    */

   List<UserRespVO> getUserListByCursor(UserQueryVO queryVO);

   /**
    * 批量新增用户
    *
    * @param userList 用户列表
    * @return 新增结果
    */

   Boolean batchAddUser(List<SysUser> userList);
}

package com.jam.demo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.collect.Lists;
import com.jam.demo.entity.SysUser;
import com.jam.demo.mapper.SysUserMapper;
import com.jam.demo.service.SysUserService;
import com.jam.demo.vo.UserQueryVO;
import com.jam.demo.vo.UserRespVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import jakarta.annotation.Resource;
import java.util.List;

/**
* 用户服务实现类
*
* @author ken
*/

@Slf4j
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {

   @Resource
   private SysUserMapper sysUserMapper;

   @Resource
   private TransactionTemplate transactionTemplate;

   @Override
   public UserRespVO getUserByNameAndAge(String userName, Integer age) {
       if (!StringUtils.hasText(userName)) {
           log.warn("用户名不能为空");
           return null;
       }
       if (ObjectUtils.isEmpty(age)) {
           log.warn("年龄不能为空");
           return null;
       }
       LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<SysUser>()
               .select(SysUser::getId, SysUser::getUserName, SysUser::getAge, SysUser::getPhone)
               .eq(SysUser::getUserName, userName)
               .eq(SysUser::getAge, age);
       SysUser sysUser = sysUserMapper.selectOne(queryWrapper);
       if (ObjectUtils.isEmpty(sysUser)) {
           log.info("未查询到用户信息,userName:{}, age:{}", userName, age);
           return null;
       }
       UserRespVO respVO = new UserRespVO();
       BeanUtils.copyProperties(sysUser, respVO);
       return respVO;
   }

   @Override
   public List<UserRespVO> getUserListByCursor(UserQueryVO queryVO) {
       if (ObjectUtils.isEmpty(queryVO)) {
           return Lists.newArrayList();
       }
       Long startId = ObjectUtils.isEmpty(queryVO.getLastId()) ? 0L : queryVO.getLastId();
       Integer limit = ObjectUtils.isEmpty(queryVO.getPageSize()) ? 20 : queryVO.getPageSize();
       List<SysUser> userList = sysUserMapper.selectPageByCursor(startId, limit);
       if (CollectionUtils.isEmpty(userList)) {
           return Lists.newArrayList();
       }
       List<UserRespVO> respList = Lists.newArrayListWithCapacity(userList.size());
       for (SysUser user : userList) {
           UserRespVO respVO = new UserRespVO();
           BeanUtils.copyProperties(user, respVO);
           respList.add(respVO);
       }
       return respList;
   }

   @Override
   public Boolean batchAddUser(List<SysUser> userList) {
       if (CollectionUtils.isEmpty(userList)) {
           return Boolean.FALSE;
       }
       return transactionTemplate.execute(new TransactionCallback<Boolean>() {
           @Override
           public Boolean doInTransaction(TransactionStatus status) {
               try {
                   boolean saveResult = saveBatch(userList, 1000);
                   if (!saveResult) {
                       status.setRollbackOnly();
                       log.error("批量新增用户失败");
                       return Boolean.FALSE;
                   }
                   log.info("批量新增用户成功,数量:{}", userList.size());
                   return Boolean.TRUE;
               } catch (Exception e) {
                   status.setRollbackOnly();
                   log.error("批量新增用户异常", e);
                   return Boolean.FALSE;
               }
           }
       });
   }
}

Controller层定义

package com.jam.demo.controller;

import com.jam.demo.entity.SysUser;
import com.jam.demo.service.SysUserService;
import com.jam.demo.vo.UserQueryVO;
import com.jam.demo.vo.UserRespVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;

import jakarta.annotation.Resource;
import java.util.List;

/**
* 用户控制器
*
* @author ken
*/

@RestController
@RequestMapping("/user")
@Tag(name = "用户管理", description = "用户信息相关接口")
public class SysUserController {

   @Resource
   private SysUserService sysUserService;

   @GetMapping("/getByNameAndAge")
   @Operation(summary = "根据用户名和年龄查询用户", description = "使用覆盖索引优化,避免回表")
   public UserRespVO getUserByNameAndAge(
           @Parameter(description = "用户名", required = true)
@RequestParam String userName,
           @Parameter(description = "年龄", required = true) @RequestParam Integer age) {
       return sysUserService.getUserByNameAndAge(userName, age);
   }

   @PostMapping("/listByCursor")
   @Operation(summary = "游标分页查询用户列表", description = "优化大偏移量分页查询性能")
   public List<UserRespVO> getUserListByCursor(@RequestBody UserQueryVO queryVO) {
       return sysUserService.getUserListByCursor(queryVO);
   }

   @PostMapping("/batchAdd")
   @Operation(summary = "批量新增用户", description = "批量插入数据,优化索引写入性能")
   public Boolean batchAddUser(@RequestBody List<SysUser> userList) {
       if (CollectionUtils.isEmpty(userList)) {
           return Boolean.FALSE;
       }
       return sysUserService.batchAddUser(userList);
   }
}


写在最后

MySQL索引优化的核心,从来不是死记硬背各种规则,而是彻底理解底层的B+树结构、聚簇索引的组织方式,以及MySQL优化器的执行逻辑。所有的优化方案,都是基于底层原理推导出来的。只有从根上搞懂了这些,才能面对任何复杂的SQL场景,都能写出高性能的查询语句,解决生产环境的各种性能问题。

目录
相关文章
|
11天前
|
人工智能 安全 Linux
【OpenClaw保姆级图文教程】阿里云/本地部署集成模型Ollama/Qwen3.5/百炼 API 步骤流程及避坑指南
2026年,AI代理工具的部署逻辑已从“单一云端依赖”转向“云端+本地双轨模式”。OpenClaw(曾用名Clawdbot)作为开源AI代理框架,既支持对接阿里云百炼等云端免费API,也能通过Ollama部署本地大模型,完美解决两类核心需求:一是担心云端API泄露核心数据的隐私安全诉求;二是频繁调用导致token消耗过高的成本控制需求。
5571 13
|
19天前
|
人工智能 JavaScript Ubuntu
5分钟上手龙虾AI!OpenClaw部署(阿里云+本地)+ 免费多模型配置保姆级教程(MiniMax、Claude、阿里云百炼)
OpenClaw(昵称“龙虾AI”)作为2026年热门的开源个人AI助手,由PSPDFKit创始人Peter Steinberger开发,核心优势在于“真正执行任务”——不仅能聊天互动,还能自动处理邮件、管理日程、订机票、写代码等,且所有数据本地处理,隐私完全可控。它支持接入MiniMax、Claude、GPT等多类大模型,兼容微信、Telegram、飞书等主流聊天工具,搭配100+可扩展技能,成为兼顾实用性与隐私性的AI工具首选。
22159 118

热门文章

最新文章