MySQL 本机压测分析
想看看本机上MySQL的性能怎么样,做了个测试
MySQL 本机压测分析报告
1. 测试背景
本次测试在本地 MySQL 实例上进行,主要针对表 tb_sku
(之前博客有表的相关信息,是一张1000w的表MySQL深分页优化及试验)的查询性能。测试目标是分析:
- 不同并发数对查询吞吐(QPS)的影响
- 每次查询 ID 数量对性能的影响
- 查询字段复杂度(
id
、id,name
、*
)对延迟和吞吐的影响 - 请求耗时分布情况(平均/最大/最小)
压测总请求数固定为 100 次,每个组合重复执行。
2. 测试参数
参数 | 说明 |
---|---|
并发数 | 10、60、110、160、210 |
每次查询 ID 数 | 10、110、210、310、410、510 |
查询字段 | id 、id,name 、* |
总请求数 | 100 次 |
3. 性能指标
- 总请求数:每个组合执行请求总数
- 总命中行数:每次查询返回的总行数(没什么用的,跑完发现只是个数学问题)
- 总耗时(ms):该组合完成所有请求的总耗时
- QPS:每秒处理请求数
- 平均请求耗时(ms):每次请求的平均耗时
- 最大请求耗时(ms):单次请求耗时最高值
- 最小请求耗时(ms):单次请求耗时最低值
4. 测试结果概览
4.1 并发 10
每次查询 ID 数 | 字段 | 总耗时(ms) | QPS | 平均(ms) | 最大(ms) | 最小(ms) |
---|---|---|---|---|---|---|
10 | id | 35.01 | 2856.43 | 3.44 | 21.90 | 0.51 |
10 | id+name | 71.43 | 1399.92 | 6.97 | 24.26 | 1.78 |
10 | * | 29.42 | 3399.52 | 2.84 | 4.23 | 1.06 |
110 | id | 71.47 | 1399.19 | 6.90 | 12.13 | 3.83 |
110 | id+name | 263.25 | 379.86 | 26.07 | 30.12 | 21.26 |
110 | * | 272.07 | 367.55 | 26.84 | 30.88 | 23.41 |
… | … | … | … | … | … | … |
数据很多就不全放上来了
观察:
- 查询字段越复杂,平均请求耗时增加明显,QPS 降低
- 小 ID 数量查询时耗时最小,QPS 最大
4.2 并发 60
每次查询 ID 数 | 字段 | 总耗时(ms) | QPS | 平均(ms) | 最大(ms) | 最小(ms) |
---|---|---|---|---|---|---|
10 | id | 28.51 | 3507.33 | 10.99 | 26.10 | 1.01 |
10 | id+name | 20.44 | 4891.63 | 8.73 | 16.95 | 2.24 |
10 | * | 22.67 | 4410.38 | 8.46 | 19.96 | 2.26 |
110 | id | 45.51 | 2197.54 | 22.93 | 44.90 | 7.40 |
… | … | … | … | … | … | … |
观察:
- 并发增加到 60,QPS 提升显著
- 大 ID 查询组合(如 210、310、410、510)耗时随字段复杂度大幅增加
- 平均请求耗时更能反映查询效率,极值差异较大
4.3 并发 110、160、210
- 高并发下,QPS 达到 5000+,平均请求耗时维持在 6–30ms 级别
- 查询字段复杂度对性能影响明显:
*
查询最大请求耗时最高 - 大 ID 数量查询组合在高并发下容易出现最大耗时峰值(>200ms)
一些合乎理论的情况
4.4 分析图
命中率是一个无用的指标,忽略掉
5. 性能分析结论
- 查询字段复杂度影响大:
- 简单字段
id
:吞吐最高、平均耗时最低 - 完整字段
*
:吞吐最低、耗时最高
- 简单字段
- 并发提升有效:
- QPS 随并发线性增加,但到一定程度(>160)后,最大请求耗时增长明显,说明存在 IO 或锁竞争
- 查询 ID 数量影响:
- 小 ID 数量查询更快,QPS 更高
- 大量 ID 查询时,平均请求耗时与最大耗时增幅明显
- 请求耗时分布:
- 平均耗时较稳定
- 最大耗时波动大,说明部分请求受复杂查询或数据库缓存策略影响
6. 总结与实践建议
通过本次 MySQL 压测试验,可以得到以下结论:
- 查询字段复杂度影响显著
- 试验中,查询简单字段(如
id
)的 QPS 明显高于查询完整字段(*
)。 - 理论上,查询字段越多,数据库需要读取更多行数据和列数据,IO 压力增加,同时返回结果集越大,网络传输耗时也增加。
- 实践建议:仅查询必要字段,避免
SELECT *
,尤其是在高并发场景下。
- 试验中,查询简单字段(如
- 并发提升与吞吐关系
- 随着并发数增加,QPS 线性提升,但最大请求耗时增幅明显。
- 理论上,MySQL 的连接和查询线程有限,过高并发会引发线程竞争和锁等待,导致部分请求耗时增加。
- 实践建议:根据硬件和实例配置合理设置并发上限,避免单机 MySQL 出现请求堆积。对于业务量突增场景,可考虑读写分离或数据库水平拆分。
- 查询 ID 数量与延迟关系
- 小批量查询(ID 数少)平均请求耗时低,QPS 高;大批量查询(ID 数多)耗时和最大请求耗时明显增加。(这次都是in语法,没有比较join或临时表的方式,所以下面的理论和本次实践没什么关系,不过写出来下次再试验比较一下)
- 现象就是
IN
中字段超过200个,速度就慢了不少。
- 理论上,大批量查询生成较长的
IN
条件,会增加解析和执行成本,同时导致返回结果集大,消耗更多内存和网络带宽。 - 实践建议:大批量查询应拆分为多次小批量请求或使用临时表/JOIN 方式,避免单次请求过重。
7 附录:代码
7.1 Golang
package bigtableimport ("fmt""os""sync""testing""time"
)// 生成压测组合参数(连续、细粒度)
func generateTestCombos() (concurrencyList, idsPerQueryList []int, fieldsList []string) {// 并发数:10,20,30,...,200for c := 10; c <= 210; c += 50 {concurrencyList = append(concurrencyList, c)}// 每次查询ID数:10,20,30,...,500for n := 10; n <= 510; n += 100 {idsPerQueryList = append(idsPerQueryList, n)}// 查询字段保持原来的组合fieldsList = []string{"id", "id,name", "*"}return
}// 写 CSV 头
func writeCSVHeader(file *os.File) {file.WriteString("并发数,每次查询ID数,查询字段,总请求数,总命中行数,命中率(%),总耗时(ms),QPS,平均请求耗时(ms),最大请求耗时(ms),最小请求耗时(ms)\n")
}// 执行压测并输出 CSV
func TestSkuQueryQPSDetailed(t *testing.T) {db := openDB()defer db.Close()// 总请求数totalRequests := 100maxID := 10000000// 获取组合参数concurrencyList, idsPerQueryList, fieldsList := generateTestCombos()// 创建 CSV 文件file, err := os.Create("sku_qps_detailed.csv")if err != nil {t.Fatal(err)}defer file.Close()writeCSVHeader(file)for _, concurrency := range concurrencyList {for _, idsPerQuery := range idsPerQueryList {for _, fields := range fieldsList {var wg sync.WaitGroupvar mu sync.Mutexvar totalCount intrequestTimes := make([]float64, 0, totalRequests)start := time.Now()sem := make(chan struct{}, concurrency)for i := 0; i < totalRequests; i++ {wg.Add(1)sem <- struct{}{}go func() {defer wg.Done()defer func() { <-sem }()reqStart := time.Now()ids := randomIDs(idsPerQuery, maxID)inParams := ""for i, id := range ids {if i > 0 {inParams += ","}inParams += fmt.Sprintf("%d", id)}query := fmt.Sprintf("SELECT %s FROM tb_sku WHERE id IN (%s)", fields, inParams)rows, err := db.Query(query)if err != nil {t.Error(err)return}defer rows.Close()var count intfor rows.Next() {count++}reqDuration := time.Since(reqStart).Seconds() * 1000 // msmu.Lock()totalCount += countrequestTimes = append(requestTimes, reqDuration)mu.Unlock()}()}wg.Wait()duration := time.Since(start)qps := float64(totalRequests) / duration.Seconds()hitRate := float64(totalCount) * 100 / float64(totalRequests*idsPerQuery)// 单次请求耗时统计var minReq, maxReq, sumReq float64minReq = 1e9for _, r := range requestTimes {sumReq += rif r > maxReq {maxReq = r}if r < minReq {minReq = r}}avgReq := sumReq / float64(len(requestTimes))// 写 CSVif fields == "id,name" {fields = "id+name"}file.WriteString(fmt.Sprintf("%d,%d,%s,%d,%d,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f\n",concurrency, idsPerQuery, fields,totalRequests, totalCount, hitRate,duration.Seconds()*1000, qps, avgReq, maxReq, minReq,))// 控制台日志t.Logf("并发: %d, 每次查询ID数: %d, 字段: %s | 总请求数: %d, 总命中行数: %d, 命中率: %.2f%%, 总耗时: %.2fms, QPS: %.2f, 平均: %.2fms, 最大: %.2fms, 最小: %.2fms",concurrency, idsPerQuery, fields, totalRequests, totalCount, hitRate, duration.Seconds()*1000, qps, avgReq, maxReq, minReq,)}}}
}
7.2 Python
先安装依赖
pip install matplotlib pandas seaborn
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
import numpy as np
import os# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False# 从CSV文件读取数据
def load_data_from_csv(filename='sku_qps_detail.csv'):
# def load_data_from_csv(filename='sku_qps_result.csv'):"""从CSV文件加载性能测试数据"""if not os.path.exists(filename):raise FileNotFoundError(f"CSV文件 {filename} 不存在")df = pd.read_csv(filename)print(f"成功加载数据,共 {len(df)} 行记录")print(f"数据列: {list(df.columns)}")print("\n数据预览:")print(df.head())return df# 创建多子图分析
def create_performance_analysis(df):"""基于数据框创建性能分析图表"""fig, axes = plt.subplots(2, 3, figsize=(18, 12))fig.suptitle('MySQL查询性能分析报告 (基于CSV数据)', fontsize=16, fontweight='bold')# 1. QPS vs 并发数和查询ID数 (3D曲面图)try:ax1 = fig.add_subplot(231, projection='3d')# 按查询字段分组,选择id字段的数据df_id = df[df['查询字段'] == 'id']x = df_id['并发数']y = df_id['每次查询ID数']z = df_id['QPS']ax1.plot_trisurf(x, y, z, cmap='viridis', alpha=0.8)ax1.set_xlabel('并发数')ax1.set_ylabel('每次查询ID数')ax1.set_zlabel('QPS')ax1.set_title('QPS分布(3D) - 仅查询id字段')except Exception as e:print(f"3D图表创建失败: {e}")# 2. QPS热力图 - 按查询字段分别显示ax2 = axes[0, 1]# 选择特定查询字段的数据pivot_qps = df.pivot_table(values='QPS', index='并发数', columns='每次查询ID数', aggfunc='mean')sns.heatmap(pivot_qps, annot=True, fmt='.0f', cmap='YlOrRd', ax=ax2)ax2.set_title('QPS热力图 (所有查询字段平均)')ax2.set_xlabel('每次查询ID数')ax2.set_ylabel('并发数')# 3. 查询字段性能对比ax3 = axes[0, 2]field_performance = df.groupby('查询字段')['QPS'].mean().sort_values(ascending=False)bars = ax3.bar(range(len(field_performance)), field_performance.values, color=['skyblue', 'lightgreen', 'lightcoral'])ax3.set_xlabel('查询字段')ax3.set_ylabel('平均QPS')ax3.set_title('不同查询字段性能对比')ax3.set_xticks(range(len(field_performance)))ax3.set_xticklabels(field_performance.index, rotation=45)# 在柱状图上显示数值for i, bar in enumerate(bars):height = bar.get_height()ax3.text(bar.get_x() + bar.get_width()/2., height,f'{height:.0f}', ha='center', va='bottom')# 4. QPS折线图 - 按查询字段分别显示ax4 = axes[1, 0]for field in df['查询字段'].unique():for concurrency in df['并发数'].unique():subset = df[(df['查询字段'] == field) & (df['并发数'] == concurrency)]if not subset.empty:ax4.plot(subset['每次查询ID数'], subset['QPS'], marker='o', linewidth=2, label=f'{field}-并发{concurrency}')ax4.set_xlabel('每次查询ID数')ax4.set_ylabel('QPS')ax4.set_title('QPS vs 查询ID数 (按字段和并发)')ax4.legend(bbox_to_anchor=(1.05, 1), loc='upper left')ax4.grid(True, alpha=0.3)# 5. 响应时间分析ax5 = axes[1, 1]time_metrics = ['平均请求耗时(ms)', '最大请求耗时(ms)', '最小请求耗时(ms)']time_data = df.groupby('查询字段')[time_metrics].mean()x = np.arange(len(time_data))width = 0.25for i, metric in enumerate(time_metrics):ax5.bar(x + i*width, time_data[metric], width, label=metric)ax5.set_xlabel('查询字段')ax5.set_ylabel('耗时(ms)')ax5.set_title('响应时间分析')ax5.set_xticks(x + width)ax5.set_xticklabels(time_data.index)ax5.legend()# 6. 命中率分析ax6 = axes[1, 2]hit_rate_pivot = df.pivot_table(values='命中率(%)', index='并发数', columns='每次查询ID数', aggfunc='mean')sns.heatmap(hit_rate_pivot, annot=True, fmt='.1f', cmap='Blues', ax=ax6)ax6.set_title('命中率分析(%)')ax6.set_xlabel('每次查询ID数')ax6.set_ylabel('并发数')# 调整布局plt.tight_layout()return fig# 生成详细分析图表
def create_detailed_analysis(df):"""创建详细的分析图表"""fig, axes = plt.subplots(2, 2, figsize=(15, 12))fig.suptitle('MySQL查询性能详细分析', fontsize=16, fontweight='bold')# 子图1: 查询字段性能对比(详细)ax1 = axes[0, 0]field_concurrency_perf = df.pivot_table(values='QPS', index='查询字段', columns='并发数', aggfunc='mean')field_concurrency_perf.plot(kind='bar', ax=ax1)ax1.set_ylabel('QPS')ax1.set_title('不同查询字段在不同并发下的QPS')ax1.legend(title='并发数', bbox_to_anchor=(1.05, 1), loc='upper left')ax1.grid(True, alpha=0.3)# 子图2: 响应时间分布ax2 = axes[0, 1]time_columns = ['平均请求耗时(ms)', '最大请求耗时(ms)', '最小请求耗时(ms)']time_stats = df.groupby('查询字段')[time_columns].mean()time_stats.plot(kind='bar', ax=ax2)ax2.set_ylabel('耗时(ms)')ax2.set_title('响应时间统计')ax2.legend(bbox_to_anchor=(1.05, 1), loc='upper left')ax2.grid(True, alpha=0.3)# 子图3: 命中率分析ax3 = axes[1, 0]hit_rate_by_field = df.groupby(['查询字段', '每次查询ID数'])['命中率(%)'].mean().unstack()hit_rate_by_field.plot(kind='bar', ax=ax3)ax3.set_ylabel('命中率(%)')ax3.set_title('不同查询字段和ID数的命中率')ax3.legend(title='每次查询ID数', bbox_to_anchor=(1.05, 1), loc='upper left')ax3.grid(True, alpha=0.3)# 子图4: 性能优化建议ax4 = axes[1, 1]ax4.axis('off')# 计算最佳配置best_qps = df.loc[df['QPS'].idxmax()]best_response = df.loc[df['平均请求耗时(ms)'].idxmin()]best_hit_rate = df.loc[df['命中率(%)'].idxmax()]recommendations = [f"🔥 最高QPS配置:",f" 字段: {best_qps['查询字段']}",f" 并发: {best_qps['并发数']}, ID数: {best_qps['每次查询ID数']}",f" QPS: {best_qps['QPS']:.0f}","",f"⚡ 最快响应配置:",f" 字段: {best_response['查询字段']}",f" 平均耗时: {best_response['平均请求耗时(ms)']:.2f}ms","",f"🎯 最高命中率:",f" 命中率: {best_hit_rate['命中率(%)']:.1f}%",f" 字段: {best_hit_rate['查询字段']}","",f"📊 数据统计:",f" 总测试数: {len(df)}",f" 平均QPS: {df['QPS'].mean():.0f}",f" 平均命中率: {df['命中率(%)'].mean():.1f}%"]for i, rec in enumerate(recommendations):ax4.text(0.05, 0.95 - i*0.05, rec, fontsize=10, transform=ax4.transAxes, verticalalignment='top')plt.tight_layout()return fig# 生成性能洞察报告
def generate_insights(df):"""生成性能洞察报告"""print("=" * 60)print("MySQL查询性能测试关键洞察:")print("=" * 60)# 基本统计print(f"测试配置总数: {len(df)}")print(f"查询字段类型: {df['查询字段'].unique().tolist()}")print(f"并发数范围: {df['并发数'].min()} - {df['并发数'].max()}")print(f"每次查询ID数范围: {df['每次查询ID数'].min()} - {df['每次查询ID数'].max()}")# 按查询字段分析print("\n1. 按查询字段性能分析:")field_stats = df.groupby('查询字段').agg({'QPS': ['mean', 'max', 'min'],'命中率(%)': 'mean','平均请求耗时(ms)': 'mean'}).round(2)print(field_stats)# 找出最佳配置best_qps = df.loc[df['QPS'].idxmax()]best_response = df.loc[df['平均请求耗时(ms)'].idxmin()]best_hit_rate = df.loc[df['命中率(%)'].idxmax()]print(f"\n2. 最佳性能配置:")print(f" 🚀 最高QPS: {best_qps['QPS']:.0f}")print(f" 配置: {best_qps['查询字段']}, 并发{best_qps['并发数']}, {best_qps['每次查询ID数']}个ID")print(f" ⚡ 最快响应: {best_response['平均请求耗时(ms)']:.2f}ms")print(f" 配置: {best_response['查询字段']}, 并发{best_response['并发数']}, {best_response['每次查询ID数']}个ID")print(f" 🎯 最高命中率: {best_hit_rate['命中率(%)']:.1f}%")print(f" 配置: {best_hit_rate['查询字段']}, 并发{best_hit_rate['并发数']}, {best_hit_rate['每次查询ID数']}个ID")# 性能趋势分析print(f"\n3. 性能趋势:")for field in df['查询字段'].unique():field_data = df[df['查询字段'] == field]qps_vs_ids = field_data['每次查询ID数'].corr(field_data['QPS'])if qps_vs_ids < -0.3:trend = "下降"elif qps_vs_ids > 0.3:trend = "上升"else:trend = "稳定"print(f" {field}: ID数增加 → QPS {trend} (相关性: {qps_vs_ids:.2f})")# 优化建议print(f"\n4. 优化建议:")if best_qps['查询字段'] == 'id':print(" ✅ 仅查询id字段可获得最高QPS")else:print(" ✅ 多字段查询在特定场景下表现良好")if df['QPS'].max() > 3000:print(" 🎉 性能优秀: 达到3000+ QPS")elif df['QPS'].max() > 1000:print(" 👍 性能良好")else:print(" ⚠️ 性能有待优化")# 主函数
def main():try:# 从CSV文件加载数据df = load_data_from_csv('sku_qps_detailed.csv')# 生成性能洞察generate_insights(df)# 创建主分析图表fig1 = create_performance_analysis(df)plt.savefig('mysql_performance_analysis_new.png', dpi=300, bbox_inches='tight')print("\n✅ 主分析图表已保存: mysql_performance_analysis_new.png")# 创建详细分析图表fig2 = create_detailed_analysis(df)plt.savefig('mysql_detailed_analysis_new.png', dpi=300, bbox_inches='tight')print("✅ 详细分析图表已保存: mysql_detailed_analysis_new.png")# 显示图表plt.show()except FileNotFoundError as e:print(f"❌ 错误: {e}")print("请确保 sku_qps_detailed.csv 文件在当前目录下")except Exception as e:print(f"❌ 处理数据时出错: {e}")import tracebacktraceback.print_exc()if __name__ == "__main__":main()