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:
@@ -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 避免人名/昵称混淆)
|
||||||
|
|||||||
Reference in New Issue
Block a user