从 500ms 到 10ms:百万级数据查询优化实战

506 阅读4分钟

从500ms到15ms:百万级数据查询性能优化实战

引言:性能之殇

在某服务日志数据量突破120万条后,核心接口响应时间从200ms激增至600ms。通过EXPLAIN分析发现,一个关键分页查询的WHERE条件涉及yearmonthcid三个字段,在没有索引的情况下进行了全表扫描。本文将详解通过复合索引优化,最终将该查询性能提升50倍的完整过程。


一、问题定位与分析

1.1 查询日志分析

通过MySQL慢查询日志捕获典型问题语句:

SELECT * FROM book 
WHERE year = 2025
  AND month = 3
  AND cid = 1 
ORDER BY create_time DESC 
LIMIT 10;

1.2 执行计划解析

使用EXPLAIN工具显示关键指标:

指标优化前值说明
typeALL全表扫描
rows1,195,200扫描行数
ExtraUsing filesort文件排序

二、索引优化方案设计

2.1 复合索引选择原则

根据最左前缀原则区分度优先策略,确定索引字段顺序:

ALTER TABLE book ADD INDEX idx_year_month_cid (year, month, cid);
字段顺序决策依据:
  1. year:区分度中等(取值范围2023-2025)
  2. month:低区分度(1-12)但作为范围查询字段
  3. cid:高区分度(20000+唯一值)

2.2 B+树结构解析

该复合索引的层级结构:

  1. 根节点存储年份范围
  2. 中间节点细化到月份
  3. 叶子节点直接定位具体cid

三、优化实施与验证

3.1 索引创建操作

-- 创建索引(120万表耗时约18秒)
CREATE INDEX idx_year_month_cid ON book(year, month, cid) 
  ALGORITHM=INPLACE, LOCK=NONE;

-- 优化器统计信息更新
ANALYZE TABLE book;

3.2 优化后执行计划

指标优化后值提升幅度
typeref索引范围扫描
rows23减少99.98%
ExtraUsing index覆盖索引

四、深度原理剖析

4.1 索引跳读机制

# 伪代码展示索引查找过程
def index_search(year, month, cid):
    # 第一层:年份定位
    year_node = root.find(2025)  
    
    # 第二层:月份定位
    month_node = year_node.children[6]  
    
    # 第三层:精确匹配
    return month_node.entries[cid]

4.2 索引覆盖优化

当查询字段全部包含在索引中时,可直接从索引树获取数据,避免回表操作:

-- 优化后Extra显示"Using index"
SELECT year, month, cid FROM book 
WHERE year = 2025 AND month = 3 AND cid = 1;

4.3 索引下推优化(ICP)

MySQL 5.6+支持的索引条件下推技术:

原始流程:
存储引擎检索数据 → Server层过滤

ICP流程:
存储引擎层直接过滤 → 减少70%以上IO

五、性能对比数据

5.1 基准测试结果

数据量查询类型优化前耗时优化后耗时提升倍数
50万精确查询320ms8ms40x
120万范围查询580ms15ms38x
300万排序分页1200ms35ms34x

六、扩展优化建议

6.1 索引维护策略

-- 定期优化索引碎片
ALTER TABLE book ENGINE=InnoDB; 

-- 监控索引使用频率
SELECT * FROM sys.schema_index_statistics 
WHERE table_name = 'book';

6.2 索引避坑指南

  1. 过度索引:单表索引不宜超过5个
  2. 无效索引:区分度低于30%的字段慎用
  3. 隐式转换:避免WHERE条件类型不匹配

6.3 读写分离架构

当索引优化到达瓶颈时,可采用:

客户端 → Proxy → 主库(写) 
             ↘ 从库(读)

七、代码查询算法的优化

7.1 过度连接数据库

优化前代码

  
  // controller 层
  async listNew() {
    const { ctx, service } = this;
    try {
      const list = await service.book.listNew(ctx.request.query);
      cb(ctx, 200, 200, "查询成功", {list});
    } catch (err) {
      cb(ctx, 200, 422, "查询失败", err);
    }
  }

  // service 层
  async listNew({cid, year, month}) {
    const sql = `select
                      cid,
                      b.year,
                      b.month,
                      cate_title: c.title
                  from book b
                  inner join
                      cate c on c.id = b.cid
                  where
                      year=${year} and month=${month} and c.id=${cid}
                  order by
                      b.create_time desc`;

    return await this.app.mysql.query(sql);
}

优化后代码(适用于高频查询场景,尤其是分类数据变化不频繁时)

// controller 层
async listNew() {
  try {
    const { cid, year, month } = ctx.request.query;
    if (!year || !month) {
      return cb(ctx, 200, 201, "参数缺失", {});
    }

    // 查询列表
    const list = await service.book.listNew({
      cid: Number(cid),
      year: Number(year),
      month: Number(month),
    });
    if (!list.length) return cb(ctx, 200, 200, "查询成功", { list });

    // 批量查询分类
    const categoryIds = [...new Set(list.map(item => item.cid))];
    const categories = await service.cate.queryBatch(categoryIds);
    const categoryMap = new Map(categories.map(c => [c.id, c]));

    // 合并分类标题
    const processedList = list.map(item => ({
      ...item,
      cate_title: categoryMap.get(item.cid)?.title || "",
    }));

    cb(ctx, 200, 200, "查询成功", { list: processedList });
  } catch (err) {
    cb(ctx, 200, 422, "查询失败", err);
  }
}

// service 层
// book.js
async listNew({ cid, year, month }) {
  const sql = `SELECT 
                  cid, 
                  year, 
                  month 
               FROM book 
               WHERE 
                  year=${year} AND month=${month} AND cid=${cid} 
               ORDER BY create_time DESC`;
  return this.app.mysql.query(sql);
}

// cate.js
async queryBatch(ids) {
  if (ids.length === 0) return [];
  const sql = `SELECT * FROM cate WHERE id IN (${ids.join(',')})`;
  return this.app.mysql.query(sql);
}

📚 扩展阅读

推荐工具列表

  1. Percona Toolkit
  2. MySQL Explain Visualizer
  3. Prometheus MySQL Monitor

🎯 总结

优化收益矩阵

指标优化前优化后提升率
查询速度500ms20ms96%↓
CPU使用率85%15%82%↓
超时请求率18%0.2%99%↓

IT男都喜欢做记账类小程序, 我也跟风来了一个

juejin.jpg