docs+chore(projects): 索引 EXPLAIN 分析 + 修正过时注释

- BizOrgMapper: selectSponsorOrgOptions/selectExecutorOrgOptions 注释
  写 "走 idx_org_user_type_name", EXPLAIN 实际命中 idx_org_type_status
  (优化器小数据下选小索引), 改为实测索引 + Using where 描述
- manager_projects.md §9.3: biz_project_assign 唯一约束状态标 
  (SHOW INDEX 实测有 uk_project_unit, doc 老版本误标 )
- manager_projects.md §9.5: 新增索引分析章节, 记录 8 个关键查询
  EXPLAIN 命中 + 已加/评估后未加的索引决策

DB 实际改动 (已落库, 无 .sql 文件):
- ADD INDEX idx_assign_exec_user_project (exec_user_id, project_id)
  on biz_project_assign — 服务机机构 EXISTS+IN covering 优化
- DROP 临时测试的 idx_org_type_id — 优化器未采纳, 33 行数据量不值
This commit is contained in:
郭庆泰
2026-08-20 22:39:34 +08:00
parent 2b69ef1c92
commit be1c973366
2 changed files with 41 additions and 3 deletions
+37 -1
View File
@@ -377,7 +377,43 @@ CREATE TABLE `biz_project` (
- ✅ biz_project: uk_project_no — 项目编号唯一,合理 - ✅ biz_project: uk_project_no — 项目编号唯一,合理
- ✅ biz_project_rating: uk_rating (project_id, rater_id, rater_role) — 一人评一次,合理 - ✅ biz_project_rating: uk_rating (project_id, rater_id, rater_role) — 一人评一次,合理
- biz_project_assign: **缺唯一约束 (project_id, execution_unit_id)** — 可能重复分配同一执行方 - biz_project_assign: uk_project_unit (project_id, execution_unit_id) — 同一项目同一执行方只能分配一次
### 9.5 索引分析与 EXPLAIN (2026-08-20)
数据规模: biz_project 12 行 / biz_org 33 行 / biz_project_assign 12 行 / sys_user 47 行 (测试数据)
| # | 查询 | 命中索引 | 备注 |
|---|---|---|---|
| Q1 | `selectSponsorOrgOptions` | `idx_org_type_status` (2 列) | 优化器在小数据量倾向小索引, `org_name LIKE` 走 Using where 二次过滤, `ORDER BY org_id DESC` 走 filesort |
| Q2 | `selectExecutorOrgOptions` | `idx_org_type_status` + `PRIMARY` sys_user | 同上, `parent_user_id IS NULL` 走 Using where |
| Q3 | `selectList` 主项目 + `sponsor_admin_user_id IN` | `idx_sponsor_admin_user_id` range + Using index | ✅ 完美 covering, 无需回表 |
| Q4 | `selectList` 主项目 + `EXISTS(exec_user_id IN)` | `idx_assign_exec_user_project` + LooseScan | ✅ 完美 covering (新加索引, 见下) |
| Q5 | `selectList` 主项目 + sponsor IN + 多 LEFT JOIN + GROUP_CONCAT 子查询 | `idx_project_form` Backward scan + 3 个 LEFT JOIN 全 eq_ref/Using index + `uk_project_unit` GROUP_CONCAT | ✅ 全部走索引 |
| Q6 | `selectList` 主项目 + 单独 exec EXISTS (无其他过滤) | `idx_assign_exec_user_project` range + Start temporary | ⚠️ 优化器会 material 子查询, 数据量小可接受 |
| Q7 | selectList 同时 sponsor IN + exec EXISTS | `idx_project_form` 主表 + `idx_assign_exec_user_project` FirstMatch 子查询 | ✅ 组合最优, semi-join |
| Q8 | selectList 仅 projectForm + isFinished | `index_merge(intersect(idx_project_form, idx_is_finished))` | ✅ MySQL 8 自动交集 |
**已加索引** (2026-08-20):
```sql
ALTER TABLE biz_project_assign ADD INDEX idx_assign_exec_user_project (exec_user_id, project_id);
```
理由: Q4/Q6/Q7 (服务机机构 EXISTS+IN) 原命中 `idx_assign_exec_user(exec_user_id)` 单列索引,
需回表读 project_id; 新加复合索引直接 covering, EXPLAIN 显示 `Using index` + `LooseScan`/`range`, 无需回表。
**评估后未加的索引** (记录备查):
- `biz_org (org_type, org_id, org_name)` — 用于消除 options 查询的 filesort。
评估: 强制 hint 后确实变 `Backward index scan` 无 filesort,
但优化器在 33 行数据下仍选 `idx_org_type_status` (2 列更小, 估算成本更低)。
决策: 数据量小不需要, **DROP**, 等生产数据增长到 1k+ 行再 EVAL。
- `biz_org (org_type, status, is_published)` 等 — 无明显 QPS 压力场景, 不加。
**过时注释修正** (2026-08-20):
- `BizOrgMapper.xml` 旧注释写 "走 idx_org_user_type_name 索引", EXPLAIN 实际命中 `idx_org_type_status`, 已修正为实测命中索引 + Using where 描述
### 9.4 业务闭环检查 ### 9.4 业务闭环检查
@@ -104,7 +104,8 @@
<!-- <!--
支持方下拉选项: JOIN sys_user 取主账号 user_name (供分配弹窗缓存 biz_project.sponsor_admin_user_name 用) 支持方下拉选项: JOIN sys_user 取主账号 user_name (供分配弹窗缓存 biz_project.sponsor_admin_user_name 用)
返回 Map: userId / orgName / userName 返回 Map: userId / orgName / userName
走 idx_org_user_type_name 索引 (user_id, org_type, org_name) EXPLAIN 实际命中: idx_org_type_status (org_type,status) (优化器在数据量小时倾向小索引),
org_name LIKE 走 Using where 二次过滤, ORDER BY org_id DESC 走 filesort
过滤条件: orgName 模糊匹配 (主用) / userId 精确匹配 (拉回已选项) 过滤条件: orgName 模糊匹配 (主用) / userId 精确匹配 (拉回已选项)
--> -->
<select id="selectSponsorOrgOptions" parameterType="BizOrg" resultType="java.util.LinkedHashMap"> <select id="selectSponsorOrgOptions" parameterType="BizOrg" resultType="java.util.LinkedHashMap">
@@ -124,7 +125,8 @@
<!-- <!--
执行方下拉选项: JOIN sys_user 取 MAIN 账号 user_name (parent_user_id IS NULL 限定主账号) 执行方下拉选项: JOIN sys_user 取 MAIN 账号 user_name (parent_user_id IS NULL 限定主账号)
返回 Map: userId / orgName / userName 返回 Map: userId / orgName / userName
走 idx_org_user_type_name 索引 (user_id, org_type, org_name) EXPLAIN 实际命中: idx_org_type_status (org_type,status) (优化器在数据量小时倾向小索引),
sys_user 走 PRIMARY eq_ref, parent_user_id IS NULL 走 Using where
关键: 只查 MAIN 账号 (parent_user_id IS NULL), 过滤掉同一公司下的普通员工子账号 关键: 只查 MAIN 账号 (parent_user_id IS NULL), 过滤掉同一公司下的普通员工子账号
value=userId (MAIN 账号 sys_user.user_id, 直接写 biz_project_assign.exec_user_id) value=userId (MAIN 账号 sys_user.user_id, 直接写 biz_project_assign.exec_user_id)
label=orgName (执行单位名称, 不带 user_name 避免人名/昵称混淆) label=orgName (执行单位名称, 不带 user_name 避免人名/昵称混淆)