Files
ai-contract-platform/project-docs/V2-database-postgresql-explain-baseline-2026-06-24.md
yuqei a3bb78e06b
Security Quality Gate / npm audit, Bandit, pip-audit, detect-secrets (pull_request) Failing after 16m35s
release: freeze V2 release candidate
2026-07-12 15:32:24 +08:00

155 lines
7.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AI 合同平台 V2 PostgreSQL EXPLAIN ANALYZE 基线
日期:2026-06-24
## 结论
本轮完成 DB-5 第二阶段:在 192.168.3.90 的 V2 smoke Docker 环境中,对真实 PostgreSQL/pgvector 数据库执行测试数据造数、`ANALYZE`、`EXPLAIN ANALYZE` 和 warm-cache 复测。
整体结论:
- 合同主列表、文件仓库按合同查询、审查 worker 队列、合同审计时间线、AI Trace、AI 质量看板、知识库列表、知识库 worker 队列整体可用。
- 当前最大性能风险不在普通列表,而在“统一搜索/审查搜索”这类跨表、多字段、`OR + ILIKE '%keyword%'` 查询。
- AI 向量检索的 HNSW 索引已可用,但当前代码形态使用 `embedding <=> query_vector`,不会命中本轮建立的表达式 HNSW 索引;需要把代码查询表达式调整为 `embedding::vector(64) <=> query_vector::vector(64)` 或在后续 schema 中把 `embedding` 固定为 `vector(64)`。
- 本轮只是 DB-5 基线,不等同于最终生产压测;后续仍需 DB-6 的安全、备份、RLS 和慢查询监控。
## 测试环境
- Server:`192.168.3.90`
- Compose project:`yuqei-contract-v2-smoke`
- Contract DB:`yuqei_contract`
- AI DB:`yuqei_ai`
- PostgreSQL image:`pgvector/pgvector:pg16`
- 验证目录:`project-docs/qa-artifacts/v2-database-explain-baseline/2026-06-24T18-45-59`
## 测试数据量
合同库:
- `contracts`:12,000
- `contract_files`:18,000
- `review_tasks`:16,000
- `approval_timeline_items`:36,000
- `contract_audit_events`:48,000
AI 库:
- `ai_run_audits`:50,000
- `knowledge_documents`:10,000
- `knowledge_chunks`:60,000
- `knowledge_ingestion_jobs`:20,000
- `ai_quality_improvement_actions`:8,000
- 另有模型配置、Prompt 变更、重建索引记录、worker 执行记录等辅助数据。
所有测试数据都使用 `perf-` 前缀,后续可以重复清理和重建。
## 关键结果
### 合同库
| 查询 | warm-cache 执行时间 | 计划 | 结论 |
| --- | ---: | --- | --- |
| 合同列表按状态 | 0.233 ms | `ix_contracts_updated_at` | 通过,但规划器未选 `ix_contracts_status_updated_id` |
| 合同状态计数 | 0.692 ms | `ix_contracts_status` index only | 通过 |
| 合同标题/编号/相对方关键词 | 1.598 ms | `ix_contracts_updated_at` + filter | 当前量级可接受 |
| 合同正文统一搜索 | 1574.955 ms | Seq Scan | 需优化 |
| 文件仓库按合同 | 0.113 ms | `ix_contract_files_contract_uploaded_id` | 通过 |
| 文件仓库关键词跨合同 | 60.220 ms | uploaded_at index + nested loop | 中风险 |
| 审查 worker 领取 | 0.053 ms | `ix_review_tasks_created_at` | 通过,但未选组合索引 |
| 审查任务统一搜索 | 472.446 ms | Seq Scan + Hash Join | 需优化 |
| 合同审计时间线 | 0.110 ms | `ix_contract_audit_events_contract_id` | 通过 |
| 审批时间线 | 0.090 ms | `ix_approval_timeline_items_contract_id` | 通过 |
合同库主要问题:
1. 合同正文统一搜索走全表扫描,原因是跨多个字段的 `OR + ILIKE`,并且包含 `body_content` 大文本字段。
2. 审查任务搜索走 `review_tasks` 和 `contracts` 的全量 hash join,后续数据量扩大后会明显拖慢统一搜索页。
3. 时间线和 worker 查询虽然很快,但规划器优先使用单列索引;这不是当前瓶颈,但后续可以逐步减少重复单列索引,让组合索引更明确。
### AI 库
| 查询 | warm-cache 执行时间 | 计划 | 结论 |
| --- | ---: | --- | --- |
| AI Trace 最新列表 | 0.168 ms | `ix_ai_run_audits_created_at` | 通过 |
| AI Trace 按 operation | 0.215 ms | created_at index + filter | 通过 |
| AI 质量看板 14 天窗口 | 8.959 ms | `ix_ai_run_audits_created_at` | 通过 |
| AI Trace 按 provider/model | 0.336 ms | `ix_ai_run_audits_provider_model_created` | 通过 |
| AI Trace 问题关键词 | 2.263 ms | created_at index + filter | 当前量级可接受 |
| 质量改进项待处理队列 | 0.283 ms | updated_at index + filter | 通过 |
| 质量改进项按 owner/status | 0.120 ms | `ix_quality_actions_owner_status_updated` | 通过 |
| 知识库文档按 base/status | 0.248 ms | `ix_knowledge_documents_kb_status_created` | 通过 |
| 知识库关键词检索 | 0.296 ms | `ix_knowledge_chunks_kb_created` + filter | 当前量级可接受 |
| 当前代码形态向量检索 | 18.221 ms | bitmap + sort | 可用但未命中 HNSW |
| HNSW 表达式形态向量检索 | 1.051 ms | `ix_knowledge_chunks_embedding_hnsw` | 通过 |
| 知识导入 worker 领取 | 0.045 ms | updated_at index + filter | 通过 |
| 知识导入列表按 base/status | 0.043 ms | `ix_ingestion_jobs_kb_status_updated` index only | 通过 |
| worker 执行时间线 | 0.256 ms | started_at index + filter | 通过 |
AI 库主要问题:
1. HNSW 索引有效,但当前 repository 中 `_search_knowledge_chunks_with_pgvector` 的 SQL 形态没有使用表达式 cast,因此没有命中 `ix_knowledge_chunks_embedding_hnsw`。
2. 当前向量检索在 60,000 chunk、单知识库 3,000 chunk 规模下仍可接受;如果知识库扩大到百万 chunk,必须让查询命中 HNSW 或改成固定维度 `vector(64)` 列。
3. AI Trace 和质量看板目前表现良好,短期不用优先继续加索引。
## 优先优化项
### P0:统一搜索改造
当前合同正文搜索和审查任务搜索是最大风险。
建议下一轮做:
- 合同搜索从多字段 `OR + ILIKE` 改为 PostgreSQL `tsvector` 搜索列或独立 search document 表。
- 把 `contracts`、`contract_files`、`review_tasks` 的搜索统一写入 `contract_search_documents`:
- `id`
- `source_type`
- `source_id`
- `title`
- `summary`
- `search_text`
- `status`
- `updated_at`
- `search_vector`
- 对 `search_vector` 建 GIN 索引。
- 统一搜索页先查 search document,再按结果类型回表取详情。
这样可以避免跨表 OR 和大文本全表扫描。
### P1:pgvector 查询形态改造
当前 HNSW 索引已经存在并可用,但代码未命中。
建议:
- 短期:把 `_search_knowledge_chunks_with_pgvector` 的 SQL 改为 `embedding::vector(64) <=> CAST(:query_vector AS vector(64))`。
- 中期:固定 `knowledge_chunks.embedding` 为 `vector(64)` 或按 embedding dimension 拆表/分区,避免表达式索引长期存在。
- 长期:按 `knowledge_base_id` 做分区或加检索候选召回策略,避免全局 HNSW 先召回再过滤知识库导致返回不足。
### P2:索引策略整理
本轮发现一些查询仍选旧单列索引。
短期不需要马上删除旧索引,因为当前性能可接受;但后续 DB-2/DB-3 清洁模型落地后,应统一整理:
- 保留确实被查询使用的组合索引。
- 删除重复、低收益单列索引。
- 让迁移和模型中的 `index=True` 不再随意生成单列索引。
## 本轮产物
- `project-docs/qa-artifacts/v2-database-explain-baseline/2026-06-24T18-45-59/seed-contract.sql`
- `project-docs/qa-artifacts/v2-database-explain-baseline/2026-06-24T18-45-59/seed-ai.sql`
- `project-docs/qa-artifacts/v2-database-explain-baseline/2026-06-24T18-45-59/explain-contract.sql`
- `project-docs/qa-artifacts/v2-database-explain-baseline/2026-06-24T18-45-59/explain-ai.sql`
- `project-docs/qa-artifacts/v2-database-explain-baseline/2026-06-24T18-45-59/explain-contract.log`
- `project-docs/qa-artifacts/v2-database-explain-baseline/2026-06-24T18-45-59/explain-contract-warm.log`
- `project-docs/qa-artifacts/v2-database-explain-baseline/2026-06-24T18-45-59/explain-ai.log`
- `project-docs/qa-artifacts/v2-database-explain-baseline/2026-06-24T18-45-59/explain-ai-warm.log`
- `project-docs/qa-artifacts/v2-database-explain-baseline/2026-06-24T18-45-59/explain-summary.tsv`
## 下一步
建议继续 DB-5 第三阶段:实现统一搜索专用表或 tsvector 搜索列,先解决合同正文搜索和审查任务搜索的全表扫描问题。
之后再做 pgvector 查询形态改造,让线上法律问答和合同审查的 RAG 检索真正稳定命中 HNSW。