SpringBoot + MySQL 索引失效监控 + 执行计划分析:慢查询自动识别索引未命中原因
前言
在数据库应用中,索引是提高查询性能的关键。然而,即使创建了索引,在某些情况下索引也可能会失效,导致查询性能下降。索引失效是数据库性能问题的常见原因之一,特别是在大型应用中。
想象一下这样的场景:你的应用在生产环境中突然出现性能瓶颈,响应时间变长,数据库服务器CPU使用率飙升。通过查看慢查询日志,你发现有大量的慢查询语句。进一步分析这些慢查询,你发现它们本应该使用索引,但实际上却没有使用,导致了全表扫描。
如何快速识别和解决索引失效问题? 本文将详细介绍如何在 Spring Boot 中实现 MySQL 索引失效监控和执行计划分析,帮助你自动识别慢查询中索引未命中的原因。
一、核心概念
1.1 索引的工作原理
索引是一种数据结构,用于快速查找数据库表中的数据。在 MySQL 中,最常用的索引类型是 B+ 树索引。B+ 树是一种平衡树结构,它允许在 O(log n) 的时间复杂度内进行查找、插入和删除操作。
1.2 索引失效的原因
索引失效是指查询语句本应该使用索引,但实际上却没有使用,导致了全表扫描。常见的索引失效原因包括:
- 索引列参与计算:当索引列参与计算时,索引会失效
- 索引列使用函数:当索引列使用函数时,索引会失效
- 索引列使用 LIKE 操作符:当 LIKE 操作符的通配符在开头时,索引会失效
- 索引列使用 OR 操作符:当使用 OR 操作符连接的列中有一个没有索引时,索引会失效
- 索引列使用不等于操作符:当使用 != 或 <> 操作符时,索引会失效
- 索引列使用 IS NULL 或 IS NOT NULL:当使用 IS NULL 或 IS NOT NULL 时,索引可能会失效
- 数据类型不匹配:当查询条件的数据类型与索引列的数据类型不匹配时,索引会失效
- 统计信息过时:当 MySQL 的统计信息过时时,可能会导致索引选择错误
1.3 执行计划
执行计划是 MySQL 优化器生成的查询执行方案,它显示了 MySQL 如何执行查询语句。执行计划包含了查询的各个步骤,如表的访问方式、连接方式、索引使用情况等。
1.4 慢查询
慢查询是指执行时间超过指定阈值的查询语句。MySQL 可以通过配置 long_query_time 参数来定义慢查询的阈值,默认值为 10 秒。
1.5 索引监控
索引监控是指监控索引的使用情况,包括索引的命中率、扫描行数、使用频率等。通过索引监控,可以发现索引的使用问题,如索引失效、索引使用不当等。
二、技术方案
2.1 架构设计
MySQL 索引失效监控和执行计划分析的架构设计主要包括以下几个部分:
- 数据层:MySQL 数据库,存储业务数据和慢查询日志
- 服务层:Spring Boot 应用,负责业务逻辑和索引监控
- 监控层:监控 MySQL 慢查询和索引使用情况
- 分析层:分析执行计划,识别索引失效原因
- 告警层:当发现索引失效时,触发告警通知
2.2 技术选型
- Spring Boot:作为基础框架,提供依赖注入、配置管理等功能
- Spring Data JPA:用于操作数据库
- MySQL:作为数据库存储
- MyBatis:用于执行原生 SQL 查询
- Spring Boot Actuator:用于暴露监控端点
- Prometheus:用于监控系统指标
- Grafana:用于可视化监控数据
- 企业微信/钉钉:用于发送告警通知
- Druid:用于数据库连接池和 SQL 监控
2.3 核心流程
- 慢查询收集:收集 MySQL 慢查询日志
- 执行计划获取:对慢查询语句执行 EXPLAIN 命令,获取执行计划
- 索引失效分析:分析执行计划,识别索引失效的原因
- 告警触发:当发现索引失效时,触发告警通知
- 优化建议:根据分析结果,提供索引优化建议
三、Spring Boot MySQL 索引失效监控实现
3.1 依赖配置
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.11</version>
</dependency>
<!-- Spring Boot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Micrometer -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3.2 配置文件
spring:
datasource:
url: jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
filters: stat,wall,log4j
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
use-global-data-source-stat: true
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate.format_sql: true
# 监控配置
management:
endpoints:
web:
exposure:
include: health,info,prometheus
# 慢查询监控配置
mysql:
slow-query:
enabled: true
threshold: 1000 # 慢查询阈值,单位毫秒
check-interval: 60000 # 检查间隔,单位毫秒
alert-enabled: true # 是否启用告警
notify-url: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your_key" # 企业微信告警地址
# Prometheus 配置
micrometer:
prometheus:
enabled: true
3.3 慢查询监控服务
@Service
@Slf4j
public class SlowQueryMonitor {
@Autowired
private DataSource dataSource;
@Autowired
private SlowQueryAnalyzer slowQueryAnalyzer;
@Value("${mysql.slow-query.threshold:1000}")
private long threshold;
@Value("${mysql.slow-query.alert-enabled:true}")
private boolean alertEnabled;
@Value("${mysql.slow-query.notify-url}")
private String notifyUrl;
/**
* 监控慢查询
*/
public void monitorSlowQueries() {
log.info("开始监控慢查询");
try {
// 获取慢查询日志
List<SlowQuery> slowQueries = getSlowQueries();
if (slowQueries.isEmpty()) {
log.info("没有发现慢查询");
return;
}
for (SlowQuery slowQuery : slowQueries) {
log.info("发现慢查询: {}", slowQuery.getSql());
// 分析执行计划
ExecutionPlan executionPlan = slowQueryAnalyzer.analyze(slowQuery.getSql());
// 检查索引是否失效
if (executionPlan.isIndexNotUsed()) {
log.warn("索引失效: {}", slowQuery.getSql());
// 触发告警
if (alertEnabled) {
sendAlert(slowQuery, executionPlan);
}
}
}
} catch (Exception e) {
log.error("监控慢查询失败", e);
}
log.info("慢查询监控完成");
}
/**
* 获取慢查询日志
*/
private List<SlowQuery> getSlowQueries() throws SQLException {
List<SlowQuery> slowQueries = new ArrayList<>();
try (Connection connection = dataSource.getConnection()) {
// 执行 SHOW PROCESSLIST 命令
try (Statement statement = connection.createStatement()) {
try (ResultSet resultSet = statement.executeQuery("SHOW PROCESSLIST")) {
while (resultSet.next()) {
String command = resultSet.getString("Command");
String state = resultSet.getString("State");
long time = resultSet.getLong("Time");
String info = resultSet.getString("Info");
// 过滤慢查询
if ("Query".equals(command) && time > threshold / 1000 && info != null) {
SlowQuery slowQuery = new SlowQuery();
slowQuery.setSql(info);
slowQuery.setExecutionTime(time * 1000); // 转换为毫秒
slowQueries.add(slowQuery);
}
}
}
}
}
return slowQueries;
}
/**
* 发送告警
*/
private void sendAlert(SlowQuery slowQuery, ExecutionPlan executionPlan) {
try {
// 构建告警消息
String message = String.format("【MySQL 索引失效告警】\nSQL: %s\n执行时间: %d ms\n索引使用情况: %s\n可能原因: %s",
slowQuery.getSql(), slowQuery.getExecutionTime(),
executionPlan.isIndexUsed() ? "使用索引" : "未使用索引",
executionPlan.getIndexNotUsedReason());
// 发送告警
if (StringUtils.isNotBlank(notifyUrl)) {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(notifyUrl))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"msgtype\":\"text\",\"text\":{\"content\":\"" + message + "\"}}"))
.build();
client.send(request, HttpResponse.BodyHandlers.ofString());
log.info("告警发送成功");
} else {
log.info("告警地址未配置,仅记录日志: {}", message);
}
} catch (Exception e) {
log.error("发送告警失败", e);
}
}
}
3.4 执行计划分析服务
@Service
@Slf4j
public class SlowQueryAnalyzer {
@Autowired
private DataSource dataSource;
/**
* 分析慢查询
*/
public ExecutionPlan analyze(String sql) {
log.info("开始分析 SQL: {}", sql);
ExecutionPlan executionPlan = new ExecutionPlan();
try {
// 执行 EXPLAIN 命令
List<ExplainResult> explainResults = explain(sql);
if (explainResults.isEmpty()) {
log.warn("执行 EXPLAIN 命令失败");
executionPlan.setIndexUsed(false);
executionPlan.setIndexNotUsedReason("执行 EXPLAIN 命令失败");
return executionPlan;
}
// 分析执行计划
for (ExplainResult result : explainResults) {
String type = result.getType();
String key = result.getKey();
String possibleKeys = result.getPossibleKeys();
// 检查是否使用索引
if ("ALL".equals(type) && (key == null || key.isEmpty())) {
executionPlan.setIndexUsed(false);
executionPlan.setIndexNotUsedReason(analyzeIndexNotUsedReason(sql, possibleKeys));
break;
} else {
executionPlan.setIndexUsed(true);
executionPlan.setIndexName(key);
}
}
} catch (Exception e) {
log.error("分析执行计划失败", e);
executionPlan.setIndexUsed(false);
executionPlan.setIndexNotUsedReason("分析执行计划失败: " + e.getMessage());
}
log.info("SQL 分析完成: 索引使用={}", executionPlan.isIndexUsed());
return executionPlan;
}
/**
* 执行 EXPLAIN 命令
*/
private List<ExplainResult> explain(String sql) throws SQLException {
List<ExplainResult> results = new ArrayList<>();
try (Connection connection = dataSource.getConnection()) {
// 构建 EXPLAIN 命令
String explainSql = "EXPLAIN " + sql;
// 执行 EXPLAIN 命令
try (Statement statement = connection.createStatement()) {
try (ResultSet resultSet = statement.executeQuery(explainSql)) {
while (resultSet.next()) {
ExplainResult result = new ExplainResult();
result.setId(resultSet.getInt("id"));
result.setSelectType(resultSet.getString("select_type"));
result.setTable(resultSet.getString("table"));
result.setType(resultSet.getString("type"));
result.setPossibleKeys(resultSet.getString("possible_keys"));
result.setKey(resultSet.getString("key"));
result.setKeyLen(resultSet.getString("key_len"));
result.setRef(resultSet.getString("ref"));
result.setRows(resultSet.getLong("rows"));
result.setExtra(resultSet.getString("Extra"));
results.add(result);
}
}
}
}
return results;
}
/**
* 分析索引失效原因
*/
private String analyzeIndexNotUsedReason(String sql, String possibleKeys) {
// 检查是否使用了函数
if (sql.matches(".*\b(LEFT|RIGHT|SUBSTRING|CONCAT|UPPER|LOWER|DATE|YEAR|MONTH|DAY|HOUR|MINUTE|SECOND)\b.*")) {
return "索引列使用了函数";
}
// 检查是否使用了 LIKE 操作符
if (sql.matches(".*LIKE\s+['\"].*%.*['\"].*")) {
return "LIKE 操作符的通配符在开头";
}
// 检查是否使用了 OR 操作符
if (sql.matches(".*OR.*")) {
return "使用了 OR 操作符,可能导致索引失效";
}
// 检查是否使用了不等于操作符
if (sql.matches(".*(!=|<>)\s+.*")) {
return "使用了不等于操作符";
}
// 检查是否使用了 IS NULL 或 IS NOT NULL
if (sql.matches(".*IS\s+(NULL|NOT\s+NULL).*")) {
return "使用了 IS NULL 或 IS NOT NULL";
}
// 检查是否使用了范围查询
if (sql.matches(".*(>|<|>=|<=|BETWEEN).*")) {
return "使用了范围查询,可能导致索引失效";
}
// 检查是否没有可用的索引
if (possibleKeys == null || possibleKeys.isEmpty()) {
return "没有可用的索引";
}
return "未知原因";
}
}
3.5 定时任务
@Component
public class SlowQueryCheckTask {
@Autowired
private SlowQueryMonitor slowQueryMonitor;
@Value("${mysql.slow-query.check-interval:60000}")
private long checkInterval;
@PostConstruct
public void init() {
// 启动定时任务
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
executorService.scheduleAtFixedRate(() -> {
try {
slowQueryMonitor.monitorSlowQueries();
} catch (Exception e) {
e.printStackTrace();
}
}, 0, checkInterval, TimeUnit.MILLISECONDS);
}
}
3.6 监控指标
@Component
public class SlowQueryMetrics {
@Autowired
private MeterRegistry meterRegistry;
private Counter slowQueryCounter;
private Counter indexNotUsedCounter;
@PostConstruct
public void init() {
// 初始化慢查询计数器
slowQueryCounter = Counter.builder("mysql.slow.query.count")
.description("MySQL 慢查询数量")
.tag("type", "slow_query")
.register(meterRegistry);
// 初始化索引未使用计数器
indexNotUsedCounter = Counter.builder("mysql.index.not.used.count")
.description("MySQL 索引未使用数量")
.tag("type", "index_not_used")
.register(meterRegistry);
}
/**
* 增加慢查询计数
*/
public void incrementSlowQueryCount() {
slowQueryCounter.increment();
}
/**
* 增加索引未使用计数
*/
public void incrementIndexNotUsedCount() {
indexNotUsedCounter.increment();
}
/**
* 记录慢查询执行时间
*/
public void recordSlowQueryExecutionTime(long executionTime) {
DistributionSummary.builder("mysql.slow.query.execution.time")
.description("MySQL 慢查询执行时间")
.register(meterRegistry)
.record(executionTime);
}
}
四、执行计划分析实现
4.1 执行计划结果类
@Data
public class ExplainResult {
private int id;
private String selectType;
private String table;
private String type;
private String possibleKeys;
private String key;
private String keyLen;
private String ref;
private long rows;
private String extra;
}
4.2 执行计划分析结果类
@Data
public class ExecutionPlan {
private boolean indexUsed;
private String indexName;
private String indexNotUsedReason;
private List<ExplainResult> explainResults;
/**
* 检查是否未使用索引
*/
public boolean isIndexNotUsed() {
return !indexUsed;
}
}
4.3 慢查询类
@Data
public class SlowQuery {
private String sql;
private long executionTime;
private LocalDateTime timestamp;
public SlowQuery() {
this.timestamp = LocalDateTime.now();
}
}
五、Spring Boot 完整实现
5.1 项目结构
mysql-index-monitor-demo/
├── src/
│ ├── main/
│ │ ├── java/com/example/mysql/ # 源代码
│ │ │ ├── config/ # 配置类
│ │ │ ├── service/ # 服务类
│ │ │ ├── entity/ # 实体类
│ │ │ ├── task/ # 定时任务
│ │ │ └── MysqlIndexMonitorApplication.java # 应用入口
│ │ └── resources/ # 配置文件
│ └── test/ # 测试代码
└── pom.xml # Maven 依赖
5.2 核心配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
filters: stat,wall,log4j
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
use-global-data-source-stat: true
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate.format_sql: true
# 监控配置
management:
endpoints:
web:
exposure:
include: health,info,prometheus
# 慢查询监控配置
mysql:
slow-query:
enabled: true
threshold: 1000 # 慢查询阈值,单位毫秒
check-interval: 60000 # 检查间隔,单位毫秒
alert-enabled: true # 是否启用告警
notify-url: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your_key" # 企业微信告警地址
# Prometheus 配置
micrometer:
prometheus:
enabled: true
5.3 核心代码
5.3.1 数据源配置
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return DruidDataSourceBuilder.create().build();
}
}
5.3.2 慢查询监控服务
@Service
@Slf4j
public class SlowQueryMonitor {
@Autowired
private DataSource dataSource;
@Autowired
private SlowQueryAnalyzer slowQueryAnalyzer;
@Autowired
private SlowQueryMetrics slowQueryMetrics;
@Value("${mysql.slow-query.threshold:1000}")
private long threshold;
@Value("${mysql.slow-query.alert-enabled:true}")
private boolean alertEnabled;
@Value("${mysql.slow-query.notify-url}")
private String notifyUrl;
/**
* 监控慢查询
*/
public void monitorSlowQueries() {
log.info("开始监控慢查询");
try {
// 获取慢查询日志
List<SlowQuery> slowQueries = getSlowQueries();
if (slowQueries.isEmpty()) {
log.info("没有发现慢查询");
return;
}
for (SlowQuery slowQuery : slowQueries) {
log.info("发现慢查询: {}", slowQuery.getSql());
// 增加慢查询计数
slowQueryMetrics.incrementSlowQueryCount();
// 记录慢查询执行时间
slowQueryMetrics.recordSlowQueryExecutionTime(slowQuery.getExecutionTime());
// 分析执行计划
ExecutionPlan executionPlan = slowQueryAnalyzer.analyze(slowQuery.getSql());
// 检查索引是否失效
if (executionPlan.isIndexNotUsed()) {
log.warn("索引失效: {}", slowQuery.getSql());
// 增加索引未使用计数
slowQueryMetrics.incrementIndexNotUsedCount();
// 触发告警
if (alertEnabled) {
sendAlert(slowQuery, executionPlan);
}
}
}
} catch (Exception e) {
log.error("监控慢查询失败", e);
}
log.info("慢查询监控完成");
}
// 其他方法...
}
5.3.3 执行计划分析服务
@Service
@Slf4j
public class SlowQueryAnalyzer {
@Autowired
private DataSource dataSource;
/**
* 分析慢查询
*/
public ExecutionPlan analyze(String sql) {
log.info("开始分析 SQL: {}", sql);
ExecutionPlan executionPlan = new ExecutionPlan();
try {
// 执行 EXPLAIN 命令
List<ExplainResult> explainResults = explain(sql);
executionPlan.setExplainResults(explainResults);
if (explainResults.isEmpty()) {
log.warn("执行 EXPLAIN 命令失败");
executionPlan.setIndexUsed(false);
executionPlan.setIndexNotUsedReason("执行 EXPLAIN 命令失败");
return executionPlan;
}
// 分析执行计划
for (ExplainResult result : explainResults) {
String type = result.getType();
String key = result.getKey();
String possibleKeys = result.getPossibleKeys();
// 检查是否使用索引
if ("ALL".equals(type) && (key == null || key.isEmpty())) {
executionPlan.setIndexUsed(false);
executionPlan.setIndexNotUsedReason(analyzeIndexNotUsedReason(sql, possibleKeys));
break;
} else {
executionPlan.setIndexUsed(true);
executionPlan.setIndexName(key);
}
}
} catch (Exception e) {
log.error("分析执行计划失败", e);
executionPlan.setIndexUsed(false);
executionPlan.setIndexNotUsedReason("分析执行计划失败: " + e.getMessage());
}
log.info("SQL 分析完成: 索引使用={}", executionPlan.isIndexUsed());
return executionPlan;
}
// 其他方法...
}
5.3.4 定时任务
@Component
public class SlowQueryCheckTask {
@Autowired
private SlowQueryMonitor slowQueryMonitor;
@Value("${mysql.slow-query.check-interval:60000}")
private long checkInterval;
@PostConstruct
public void init() {
// 启动定时任务
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
executorService.scheduleAtFixedRate(() -> {
try {
slowQueryMonitor.monitorSlowQueries();
} catch (Exception e) {
e.printStackTrace();
}
}, 0, checkInterval, TimeUnit.MILLISECONDS);
}
}
六、最佳实践
6.1 索引设计最佳实践
原则:
- 选择合适的索引列:选择经常用于查询条件、排序和分组的列作为索引列
- 遵循最左前缀原则:创建复合索引时,按照查询频率和 cardinality 排序
- 避免过度索引:不要创建过多的索引,会增加写操作的开销
- 定期维护索引:定期重建索引,保持索引的效率
建议:
- 对于频繁查询的列,创建索引
- 对于复合查询,创建复合索引
- 对于大表,考虑使用覆盖索引
- 定期分析表的统计信息
6.2 慢查询优化最佳实践
原则:
- 避免全表扫描:尽量使用索引,避免全表扫描
- 优化查询语句:简化查询语句,减少不必要的字段和表
- 使用分页:对于大量数据的查询,使用分页
- 避免复杂的 JOIN:减少 JOIN 的数量和复杂度
建议:
- 使用 EXPLAIN 分析查询执行计划
- 优化查询语句,避免索引失效
- 合理使用缓存,减少数据库查询
- 定期清理慢查询日志
6.3 监控告警最佳实践
原则:
- 实时监控:实时监控慢查询和索引使用情况
- 阈值合理:设置合理的慢查询阈值
- 多渠道告警:使用多种告警渠道,确保告警及时送达
- 自动分析:自动分析索引失效的原因,提供优化建议
建议:
- 使用 Prometheus + Grafana 监控慢查询和索引使用情况
- 设置两级告警阈值:预警阈值和处理阈值
- 使用企业微信、钉钉等渠道发送告警
- 定期生成慢查询分析报告
6.4 执行计划分析最佳实践
原则:
- 定期分析:定期分析慢查询的执行计划
- 重点关注:重点关注 type 为 ALL 的查询
- 索引使用:关注 key 列,确保使用了合适的索引
- 扫描行数:关注 rows 列,尽量减少扫描的行数
建议:
- 对于 type 为 ALL 的查询,分析是否可以使用索引
- 对于 key 为 NULL 的查询,分析是否缺少索引
- 对于 rows 较大的查询,分析是否可以优化
- 定期比较执行计划的变化
七、总结
MySQL 索引失效是数据库性能问题的常见原因之一,特别是在大型应用中。通过本文的实现方案,开发者可以构建一个功能强大的 MySQL 索引监控系统,帮助团队更好地管理数据库性能,避免索引失效带来的性能问题。
互动话题:
- 你在实际项目中遇到过索引失效的问题吗?是如何解决的?
- 你认为索引设计的最佳实践是什么?
- 你有使用过类似的监控工具或方案吗?
欢迎在评论区留言讨论!更多技术文章,欢迎关注公众号:服务端技术精选
标题:SpringBoot + MySQL 索引失效监控 + 执行计划分析:慢查询自动识别索引未命中原因
作者:jiangyi
地址:http://jiangyi.space/articles/2026/04/11/1775880899603.html
公众号:服务端技术精选
- 前言
- 一、核心概念
- 1.1 索引的工作原理
- 1.2 索引失效的原因
- 1.3 执行计划
- 1.4 慢查询
- 1.5 索引监控
- 二、技术方案
- 2.1 架构设计
- 2.2 技术选型
- 2.3 核心流程
- 三、Spring Boot MySQL 索引失效监控实现
- 3.1 依赖配置
- 3.2 配置文件
- 3.3 慢查询监控服务
- 3.4 执行计划分析服务
- 3.5 定时任务
- 3.6 监控指标
- 四、执行计划分析实现
- 4.1 执行计划结果类
- 4.2 执行计划分析结果类
- 4.3 慢查询类
- 五、Spring Boot 完整实现
- 5.1 项目结构
- 5.2 核心配置
- 5.3 核心代码
- 5.3.1 数据源配置
- 5.3.2 慢查询监控服务
- 5.3.3 执行计划分析服务
- 5.3.4 定时任务
- 六、最佳实践
- 6.1 索引设计最佳实践
- 6.2 慢查询优化最佳实践
- 6.3 监控告警最佳实践
- 6.4 执行计划分析最佳实践
- 七、总结
评论