# 执行计划：修复捕获输出查询 500（大 JSON 列排序爆内存）

> 生成时间：2026-08-24
> 关联文档：`_问题处理_捕获输出查询500与大JSON列排序爆内存.md`
> 状态：✅ 已完成并验证

---

## 问题概述

`GET /api/performance/outputs` 与 `GET /api/performance/tasks/{task_id}/outputs` 在查询含大 JSON `value` 列（1.28MB，4033 个 token）的记录时，`ORDER BY created_at` 导致 MySQL filesort 将整行载入 sort buffer，报 `Out of sort memory`（1038）→ 接口 500。

**根因**：SQLAlchemy `select(Model).order_by(...)` 会 SELECT 全部列（含大 `value` 列）参与排序，行宽超过 `sort_buffer_size`。

---

## 执行步骤

### Step 1: 修改列表端点为两段式查询

**文件**：`backend/app/routers/performance_output.py`

**改动**：`list_outputs()` 与 `get_task_outputs()` 两个端点，将

```python
query.order_by(desc(created_at)).offset(...).limit(...)   # 整行进 sort buffer
result.scalars().all()
```

改为两段式：

```python
# 1) 轻量排序：只取 id 列（value 大列不进 sort buffer）
id_rows = await db.execute(
    select(PerformanceTaskOutput.id)
    .where(同过滤条件)
    .order_by(desc(PerformanceTaskOutput.created_at))
    .offset((page - 1) * page_size)
    .limit(page_size)
)
ids = [r[0] for r in id_rows]

# 2) 无任务时直接返回空列表；否则按 id 回查整行
items = []
if ids:
    rows = await db.execute(
        select(PerformanceTaskOutput).where(PerformanceTaskOutput.id.in_(ids))
    )
    # 按第一段排序还原分页顺序
    by_id = {o.id: o for o in rows.scalars().all()}
    items = [by_id[i] for i in ids if i in by_id]
```

**验收标准**：
- 生成 SQL 的 ORDER BY 子查询只含 `id` 列（可开启 SQL echo 日志确认）
- 返回结果顺序、分页与修复前一致
- 本地 SQLite 跑通（无 task_id 过滤 / 有过滤 / key 过滤 / 空 ids 四种场景）

### Step 2: 本地回归验证

**验收标准**：
- `python -c "import app.routers.performance_output"` 无语法错误
- 本地起服务后 `GET /api/performance/outputs` 返回 200

### Step 3: 上传到 5.60 并重启

```bash
scp backend/app/routers/performance_output.py ubains@192.168.5.60:/data/third_party/plat-auto-test/backend/app/routers/performance_output.py
ssh ubains@192.168.5.60 "cd /data/third_party/plat-auto-test/deploy && docker compose restart app"
```

**验收标准**：容器正常启动，`/health` 200

### Step 4: 服务器真实大列数据验证

```bash
# 原复现路径（修复前 500）
curl "http://192.168.5.60/api/performance/outputs?task_id=perf_0fbcb792dcc143fd80b42cb405a75176"
curl "http://192.168.5.60/api/performance/tasks/perf_0fbcb792dcc143fd80b42cb405a75176/outputs"
```

**验收标准**：
- 两个接口均 200
- 返回 `items[0].value` 含 4033 个 token（数据完整）

---

## 改动清单

| 文件 | 类型 | 说明 |
|------|------|------|
| `backend/app/routers/performance_output.py` | **MODIFY** | `list_outputs` / `get_task_outputs` 两段式查询（轻量列排序 + id 回查） |
| `Docs/.../_问题处理_捕获输出查询500与大JSON列排序爆内存.md` | **NEW** | 问题处理文档 |
| `Docs/.../_执行计划_修复捕获输出查询500与大JSON列排序.md` | **NEW** | 本执行计划 |

**无需改动**：
- `models/performance_output.py`（表结构不变）
- `schemas/performance_output.py`（响应结构不变）
- 前端代码（接口契约不变）
- MySQL 配置（不调 sort_buffer_size，治本不治标）

---

## 验证结果

| 验证项 | 结果 | 说明 |
|--------|------|------|
| 本地语法/导入检查 | ✅ | 模块导入无错误 |
| 5.60 `GET /outputs?task_id=...` | ✅ 200 | 原 500，返回 1 行 tokens 输出 |
| 5.60 `GET /tasks/{id}/outputs` | ✅ 200 | 原 500 |
| 大列数据完整 | ✅ | value 数组 4033 个 JWT token |

---

## 教训

1. 含 JSON 大列的表做 `ORDER BY` 时，先 SELECT 轻量列拿 id，再按 id 回查整行——排序成本与行宽解耦。
2. 本地 SQLite 不会暴露 MySQL filesort 的内存限制，列表接口上线前要想"最大那一行有多大"。
3. 修复闭环：改代码 → 上传 → 重启 → 用触发过 500 的真实数据原路径复测。

---

*本文档由 Claude Code 生成*
