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

401 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 数据库设计和查询效率问题分析报告
## 报告时间
2025年11月7日
---
## 一、当前实现方式回顾
### 1. 测试类别管理test_category
- **表结构**`id`分类ID、`name`(分类名称)
- **使用场景**:测试项(test_item)表中存储 `test_category_id`
### 2. 产品类别管理eut_type
- **表结构**`id`分类ID、`name`(分类名称)
- **使用场景**
- test_item表存储 `eut_type_id`产品类别ID
- test_eut表存储 `test_eut_type_id`产品类别ID
### 3. 人员信息
- **表结构**sys_user表
- **使用场景**
- test_job表存储 `tester_id`、`reviewer_id`、`second_tester_id`、`third_tester_id`
- test_flow表存储 `creator`、`update_by`
---
## 二、存在的问题详解
### 问题1前端频繁查找数组进行ID到名称的转换 ⚠️⚠️⚠️
#### 典型案例test_job作业管理
**数据库存储**
```sql
test_job:
- tester_id: int(11) -- 测试人ID
- reviewer_id: int(11) -- 一审人员ID
- second_tester_id: int(11) -- 二审人员ID
- third_tester_id: int(11) -- 三审人员ID
```
**前端实现**
```vue
<!-- 列表显示 -->
<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
```python
@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
```
**返回的数据**
```json
{
"rows": [
{
"id": 46,
"name": "入库DSM作业",
"testerId": 2, // ⚠️ 只有ID没有名称
"reviewerId": 106, // ⚠️ 只有ID没有名称
"secondTesterId": 107,// ⚠️ 只有ID没有名称
"thirdTesterId": 108 // ⚠️ 只有ID没有名称
}
]
}
```
**应该返回的数据**
```json
{
"rows": [
{
"id": 46,
"name": "入库DSM作业",
"testerId": 2,
"testerName": "张三", // ✅ 包含名称
"reviewerId": 106,
"reviewerName": "李四", // ✅ 包含名称
"secondTesterId": 107,
"secondTesterName": "王五",// ✅ 包含名称
"thirdTesterId": 108,
"thirdTesterName": "赵六" // ✅ 包含名称
}
]
}
```
---
### 问题3导出功能只导出ID不导出名称 ⚠️
```python
@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_id`、`eut_type_id` → 前端需要两次查找
- **test_flow表**`creator`、`update_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查询推荐 ⭐⭐⭐⭐⭐
#### 优点:
- ✅ 一次查询获取所有数据
- ✅ 前端无需额外处理
- ✅ 导出功能自然包含名称
- ✅ 减少前端计算负担
- ✅ 减少网络请求
#### 实现示例:
```python
# 修改后的 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
```
```vue
<!-- 前端代码简化 -->
<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. 在后端组装数据
```python
@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代替数组查找临时方案 ⭐⭐⭐
如果暂时不能修改后端,至少优化前端:
```vue
<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个类别字段配置页面常用
### 中优先级 🟡
4. **test_flow表**:创建人和更新人字段
5. **导出功能**所有导出都应该包含名称而非ID
### 低优先级 🟢
6. **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函数