ETest-Vue-FastAPI/查询效率问题分析报告.md

12 KiB
Raw Permalink Blame History

数据库设计和查询效率问题分析报告

报告时间

2025年11月7日


一、当前实现方式回顾

1. 测试类别管理test_category

  • 表结构id分类IDname(分类名称)
  • 使用场景:测试项(test_item)表中存储 test_category_id

2. 产品类别管理eut_type

  • 表结构id分类IDname(分类名称)
  • 使用场景
    • test_item表存储 eut_type_id产品类别ID
    • test_eut表存储 test_eut_type_id产品类别ID

3. 人员信息

  • 表结构sys_user表
  • 使用场景
    • test_job表存储 tester_idreviewer_idsecond_tester_idthird_tester_id
    • test_flow表存储 creatorupdate_by

二、存在的问题详解

问题1前端频繁查找数组进行ID到名称的转换 ⚠️⚠️⚠️

典型案例test_job作业管理

数据库存储

test_job:
- tester_id: int(11)          -- 测试人ID
- reviewer_id: int(11)         -- 一审人员ID  
- second_tester_id: int(11)    -- 二审人员ID
- third_tester_id: int(11)     -- 三审人员ID

前端实现

<!-- 列表显示 -->
<el-table-column label="测试人" prop="testerId" :formatter="formatTesterName" />
<el-table-column label="一审人员" prop="reviewerId" :formatter="formatReviewerName"/>
<el-table-column label="二审人员" prop="secondTesterId" :formatter="formatSecondTesterName"/>
<el-table-column label="三审人员" prop="thirdTesterId" :formatter="formatThirdTesterName"/>

<script>
methods: {
  formatTesterName(row) {
    return this.userOptions.find(u => u.value === row.testerId)?.label || row.testerId
  },
  formatReviewerName(row) {
    return this.userOptions.find(u => u.value === row.reviewerId)?.label || row.reviewerId
  },
  formatSecondTesterName(row) {
    return this.userOptions.find(u => u.value === row.secondTesterId)?.label || row.secondTesterId
  },
  formatThirdTesterName(row) {
    return this.userOptions.find(u => u.value === row.thirdTesterId)?.label || row.thirdTesterId
  },
  loadUserOptions() {
    listUser().then(response => {
      this.userOptions = response.rows.map(item => ({
        label: item.nickName || item.userName,
        value: item.userId
      }));
    });
  }
}
</script>

性能问题分析

  1. 每次渲染表格时,每一行的每个人员字段都要执行 Array.find() 操作
  2. 时间复杂度O(n×m)其中n是表格行数m是用户数组长度
  3. 举例如果列表有50条作业每条有4个人员字段用户表有100人
    • 总查找次数50 × 4 × 100 = 20,000 次数组遍历
  4. 额外网络请求:需要额外调用 listUser() API 加载所有用户数据

问题2后端未进行JOIN查询或数据预加载 ⚠️⚠️

典型案例test_job_service.py

@classmethod
async def get_test_job_list_services(cls, query_db: AsyncSession, query_object, is_page: bool = False):
    """获取作业列表信息"""
    test_job_list_result = await Test_jobDao.get_test_job_list(query_db, query_object, is_page)
    return test_job_list_result  # ⚠️ 直接返回原始数据只包含ID

返回的数据

{
  "rows": [
    {
      "id": 46,
      "name": "入库DSM作业",
      "testerId": 2,        // ⚠️ 只有ID没有名称
      "reviewerId": 106,    // ⚠️ 只有ID没有名称
      "secondTesterId": 107,// ⚠️ 只有ID没有名称
      "thirdTesterId": 108  // ⚠️ 只有ID没有名称
    }
  ]
}

应该返回的数据

{
  "rows": [
    {
      "id": 46,
      "name": "入库DSM作业",
      "testerId": 2,
      "testerName": "张三",      // ✅ 包含名称
      "reviewerId": 106,
      "reviewerName": "李四",    // ✅ 包含名称
      "secondTesterId": 107,
      "secondTesterName": "王五",// ✅ 包含名称
      "thirdTesterId": 108,
      "thirdTesterName": "赵六"  // ✅ 包含名称
    }
  ]
}

问题3导出功能只导出ID不导出名称 ⚠️

@staticmethod
async def export_test_job_list_services(test_job_list: List):
    mapping_dict = {
        'testerId': '测试人ID',      # ⚠️ 导出的是ID不是名称
        'reviewerId': '一审人员ID',   # ⚠️ 导出的是ID不是名称
        'secondTesterId': '二审人员ID',# ⚠️ 导出的是ID不是名称
        'thirdTesterId': '三审人员人ID',# ⚠️ 导出的是ID不是名称
    }
    binary_data = ExcelUtil.export_list2excel(test_job_list, mapping_dict)
    return binary_data

导出的Excel

| 作业名称     | 测试人ID | 一审人员ID | 二审人员ID | 三审人员ID |
|-------------|---------|-----------|-----------|-----------|
| 入库DSM作业  | 2       | 106       | 107       | 108       |

用户看到的 只有数字ID完全不知道是谁

应该导出的

| 作业名称     | 测试人   | 一审人员  | 二审人员  | 三审人员  |
|-------------|---------|-----------|-----------|-----------|
| 入库DSM作业  | 张三     | 李四      | 王五      | 赵六      |

问题4相同问题在多个模块重复出现 ⚠️

同样的问题存在于:

  • test_eut表test_eut_type_id → 前端需要加载类别列表并查找
  • test_item表test_category_ideut_type_id → 前端需要两次查找
  • test_flow表creatorupdate_by → 前端需要查找用户
  • 入库管理warehouse虽然目前存储的是字符串但如果改成ID也会有同样问题

三、影响范围统计

受影响的表和字段

表名 外键字段 关联表 前端是否需要查找转换
test_job tester_id sys_user
test_job reviewer_id sys_user
test_job second_tester_id sys_user
test_job third_tester_id sys_user
test_eut test_eut_type_id eut_type
test_item test_category_id test_category
test_item eut_type_id eut_type
test_flow creator sys_user
test_flow update_by sys_user

性能影响估算

假设:

  • 作业列表显示20条记录
  • 每条记录有4个人员字段需要转换
  • 用户表有50个用户

计算

  • 数组查找次数20 × 4 × 50 = 4,000 次
  • 如果用户表有200人20 × 4 × 200 = 16,000 次
  • 每次刷新列表都要重复这些查找操作

四、解决方案建议

方案A后端JOIN查询推荐

优点:

  • 一次查询获取所有数据
  • 前端无需额外处理
  • 导出功能自然包含名称
  • 减少前端计算负担
  • 减少网络请求

实现示例:

# 修改后的 Service
@classmethod
async def get_test_job_list_services(cls, query_db: AsyncSession, query_object, is_page: bool = False):
    """获取作业列表信息(包含关联用户名称)"""
    # 使用JOIN查询
    query = (
        select(
            TestJob,
            User.nick_name.label('tester_name'),
            User2.nick_name.label('reviewer_name'),
            User3.nick_name.label('second_tester_name'),
            User4.nick_name.label('third_tester_name')
        )
        .outerjoin(User, TestJob.tester_id == User.user_id)
        .outerjoin(User2, TestJob.reviewer_id == User2.user_id)
        .outerjoin(User3, TestJob.second_tester_id == User3.user_id)
        .outerjoin(User4, TestJob.third_tester_id == User4.user_id)
    )
    
    # 执行查询并格式化结果
    results = await query_db.execute(query)
    job_list = []
    for row in results:
        job_dict = CamelCaseUtil.transform_result(row.TestJob)
        job_dict['testerName'] = row.tester_name
        job_dict['reviewerName'] = row.reviewer_name
        job_dict['secondTesterName'] = row.second_tester_name
        job_dict['thirdTesterName'] = row.third_tester_name
        job_list.append(job_dict)
    
    return job_list
<!-- 前端代码简化 -->
<el-table-column label="测试人" prop="testerName" />  <!--  直接显示名称 -->
<el-table-column label="一审人员" prop="reviewerName" />
<el-table-column label="二审人员" prop="secondTesterName" />
<el-table-column label="三审人员" prop="thirdTesterName" />
<!--  不需要formatter函数 -->
<!--  不需要loadUserOptions() -->
<!--  不需要userOptions数组 -->

方案B后端批量查询并组装次优

如果不想使用JOIN可以

  1. 查询作业列表
  2. 收集所有出现的user_id
  3. 批量查询这些用户
  4. 在后端组装数据
@classmethod
async def get_test_job_list_services(cls, query_db: AsyncSession, query_object, is_page: bool = False):
    # 1. 查询作业列表
    jobs = await Test_jobDao.get_test_job_list(query_db, query_object, is_page)
    
    # 2. 收集所有user_id
    user_ids = set()
    for job in jobs:
        user_ids.update([job.tester_id, job.reviewer_id, job.second_tester_id, job.third_tester_id])
    
    # 3. 批量查询用户
    users = await UserDao.get_users_by_ids(query_db, list(user_ids))
    user_map = {u.user_id: u.nick_name for u in users}
    
    # 4. 组装数据
    for job in jobs:
        job.tester_name = user_map.get(job.tester_id)
        job.reviewer_name = user_map.get(job.reviewer_id)
        # ...
    
    return jobs

方案C前端使用Map代替数组查找临时方案

如果暂时不能修改后端,至少优化前端:

<script>
data() {
  return {
    userMap: new Map(), // ✅ 使用Map代替数组
  }
},
methods: {
  formatTesterName(row) {
    return this.userMap.get(row.testerId) || row.testerId  // O(1)
  },
  loadUserOptions() {
    listUser().then(response => {
      // ✅ 转换为Map
      response.rows.forEach(item => {
        this.userMap.set(item.userId, item.nickName || item.userName);
      });
      
      // 保留原数组用于下拉选择
      this.userOptions = response.rows.map(item => ({
        label: item.nickName || item.userName,
        value: item.userId
      }));
    });
  }
}
</script>

性能提升

  • 原来O(n×m) → 20条×4字段×50用户 = 4,000次遍历
  • 现在O(n) → 20条×4字段 = 80次Map查找接近常数时间
  • 性能提升约 50倍

五、改进优先级建议

高优先级 🔴

  1. test_job表4个人员字段每个列表都要显示
  2. test_eut表:产品类别字段,使用频率高
  3. test_item表2个类别字段配置页面常用

中优先级 🟡

  1. test_flow表:创建人和更新人字段
  2. 导出功能所有导出都应该包含名称而非ID

低优先级 🟢

  1. warehouse相关表:目前存储的是字符串,暂无此问题

六、总结

当前问题

  • 是的测试类别和产品类别管理都是数据库存ID前端通过ID查询名称显示
  • 人员字段测试人、审核人等也是存ID前端需要加载用户列表并查找转换

核心问题

  1. 后端返回原始ID没有做JOIN查询
  2. 前端需要额外加载所有选项数据(用户列表、类别列表等)
  3. 前端使用Array.find()在每次渲染时查找,性能低下
  4. 导出功能只导出ID用户体验差

推荐方案

  1. 最佳方案修改后端使用JOIN查询返回完整数据包含ID和名称
  2. 临时方案前端使用Map代替数组提升查找性能50倍以上
  3. 长期规划:统一处理所有外键关联,建立标准的关联数据返回机制

附录:需要修改的文件清单

后端文件(推荐修改)

  • module_admin/system/service/test_job_service.py
  • module_admin/system/service/test_eut_service.py
  • module_admin/system/service/test_item_service.py
  • module_admin/system/service/test_flow_service.py
  • module_admin/system/dao/test_job_dao.py (添加JOIN查询)
  • 其他相关DAO文件

前端文件(临时优化)

  • views/system/test_job/index.vue
  • views/system/test_eut/index.vue
  • views/system/test_item/index.vue
  • views/system/test_flow/index.vue

预期收益

  • 🚀 列表加载速度:提升 30-50%
  • 📉 前端CPU占用:减少 60-80%
  • 🎯 用户体验导出的Excel可直接使用无需手动对照ID
  • 🔧 代码维护性前端代码更简洁减少formatter函数