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 索引失效监控和执行计划分析的架构设计主要包括以下几个部分:

  1. 数据层:MySQL 数据库,存储业务数据和慢查询日志
  2. 服务层:Spring Boot 应用,负责业务逻辑和索引监控
  3. 监控层:监控 MySQL 慢查询和索引使用情况
  4. 分析层:分析执行计划,识别索引失效原因
  5. 告警层:当发现索引失效时,触发告警通知

2.2 技术选型

  • Spring Boot:作为基础框架,提供依赖注入、配置管理等功能
  • Spring Data JPA:用于操作数据库
  • MySQL:作为数据库存储
  • MyBatis:用于执行原生 SQL 查询
  • Spring Boot Actuator:用于暴露监控端点
  • Prometheus:用于监控系统指标
  • Grafana:用于可视化监控数据
  • 企业微信/钉钉:用于发送告警通知
  • Druid:用于数据库连接池和 SQL 监控

2.3 核心流程

  1. 慢查询收集:收集 MySQL 慢查询日志
  2. 执行计划获取:对慢查询语句执行 EXPLAIN 命令,获取执行计划
  3. 索引失效分析:分析执行计划,识别索引失效的原因
  4. 告警触发:当发现索引失效时,触发告警通知
  5. 优化建议:根据分析结果,提供索引优化建议

三、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 索引监控系统,帮助团队更好地管理数据库性能,避免索引失效带来的性能问题。

互动话题

  1. 你在实际项目中遇到过索引失效的问题吗?是如何解决的?
  2. 你认为索引设计的最佳实践是什么?
  3. 你有使用过类似的监控工具或方案吗?

欢迎在评论区留言讨论!更多技术文章,欢迎关注公众号:服务端技术精选


标题:SpringBoot + MySQL 索引失效监控 + 执行计划分析:慢查询自动识别索引未命中原因
作者:jiangyi
地址:http://jiangyi.space/articles/2026/04/11/1775880899603.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消