401 lines
12 KiB
Markdown
401 lines
12 KiB
Markdown
|
|
# 数据库设计和查询效率问题分析报告
|
|||
|
|
|
|||
|
|
## 报告时间
|
|||
|
|
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函数
|
|||
|
|
|