第 9 / 15 章 约 8 分钟
数据库设计与查询优化
学习数据查询、索引、聚合和证据组织。
内容同步:2026年10月25日 · 文档版本 6
这个模块在 Agent 里的位置
这个模块负责 Agent 的数据读取链路,决定它能以什么成本、什么延迟、什么权限读到什么数据。
它影响:
- Tool 的能力上限。
- 查询延迟。
- 上下文质量。
- 多租户隔离。
- 证据可回放性。
- 系统成本。
一个 Agent 再聪明,如果数据库和查询设计只能全表扫描,也做不出可用的产品。
先记住几个概念
| 概念 | 含义 | 为什么重要 |
|---|---|---|
| Raw event | append-only 原始事实 | 审计和回放 |
| Rollup | 小时 / 天级聚合 | TopN 和趋势 |
| Evidence window | 证据窗口 | 还原上下文 |
| Partition | 按时间分区 | 控制扫描范围 |
| Tenant-first index | 租户优先索引 | 隔离和过滤 |
| Query pushdown | 排序、聚合、limit 下推 | 避免应用层扫全量 |
| Coverage | 数据完整性状态 | 防止把缺数据当成零 |
从一个真实问题开始:Agent 问了一个“看起来简单”的问题
用户问:
过去 30 天风险最高的 10 个实体是谁?
如果直接查 raw 表:
select *
from raw_event
where tenant_id = 7
order by occurred_at desc;
在以下规模下:
- 每天 1 亿条事件
- 30 天保留,约 30 亿行
- 50 万实体
- 2000 租户
会发生什么?
- 没有时间边界,可能扫 30 亿行。
- 没有实体过滤。
- 没有 topK。
- 没有聚合下推。
- 返回不可预算。
- Agent 上下文被 raw row 淹没。
正确问题不是“模型怎么查”,而是数据库和查询层应该为 Agent 提供什么查询原语。
三层数据结构
一个面向 Agent 的数据查询层通常分三层:
| 层 | 表 | 用途 |
|---|---|---|
| 原始层 | raw_event | append-only 事实 |
| 聚合层 | risk_event_rollup_hourly | TopN / 趋势 |
| 证据层 | evidence_window | 还原上下文和引用 |
Agent 的查询顺序应该是:
先查聚合层
-> 拿到实体 ID 和排名
-> 再按 ID 查证据窗口
-> 必要时才按 ID 精查 raw event
不要让 Agent 从 raw event 开始推理。
原始事件表
create table raw_event (
id bigint,
tenant_id bigint not null,
session_id bigint not null,
event_type text not null,
entity_id bigint,
payload jsonb not null,
occurred_at timestamptz not null,
primary key (id, occurred_at)
) partition by range (occurred_at);
索引:
create index raw_event_query_idx
on raw_event (tenant_id, occurred_at, entity_id, event_type);
设计要点:
- 按时间分区,避免全表扫描。
- 索引以 tenant 开头。
- raw event 只追加,不原地修改。
- payload 保留原始事实,但不直接给模型。
小时聚合表
create table risk_event_rollup_hourly (
tenant_id bigint not null,
entity_id bigint not null,
event_hour timestamp not null,
event_count bigint not null,
risk_score numeric not null,
primary key (tenant_id, entity_id, event_hour)
);
create index risk_rollup_time_idx
on risk_event_rollup_hourly (tenant_id, event_hour, entity_id);
TopN 查询:
select entity_id,
sum(event_count) as event_count,
sum(risk_score) as risk_score
from risk_event_rollup_hourly
where tenant_id = $1
and event_hour >= $2
and event_hour < $3
group by entity_id
order by risk_score desc
limit 10;
这条查询的关键点:
- tenant 由服务端注入。
- 时间范围必填。
- group by 和 order by 在数据库完成。
- limit 在数据库完成。
不要把 50 万实体拉回应用层排序。
证据窗口表
create table evidence_window (
id bigint primary key,
tenant_id bigint not null,
session_id bigint not null,
start_ts timestamptz not null,
end_ts timestamptz not null,
event_ids bigint[] not null,
summary text not null
);
create index evidence_window_session_idx
on evidence_window (tenant_id, session_id, start_ts desc);
用途:
- 给模型返回 evidenceWindowIds。
- 输出校验引用是否存在。
- 需要回放时按窗口还原事件。
- 避免把 eventIds 全量塞进上下文。
Tool 只暴露业务原语,不暴露 SQL
推荐的工具:
| Tool | 作用 |
|---|---|
| query_top_risk_entities | TopN 聚合 |
| query_evidence_windows | 证据窗口 |
| query_metric_timeseries | 趋势 |
| get_raw_event_by_id | 精查单条事实 |
不要提供:
{
"name": "execute_sql",
"input": {
"sql": "select * from ..."
}
}
原因:
- 模型不理解 schema 语义。
- 容易生成全表扫描。
- 权限无法收口。
- 输出不可预算。
- 审计困难。
返回结果必须带 coverage
一个 TopN 查询返回:
{
"ok": true,
"query": {
"tenantId": 7,
"startTs": 1790000000000,
"endTs": 1790086400000,
"groupBy": "entity",
"topK": 10,
"order": "risk_score_desc"
},
"resolvedRange": {
"startTs": 1790000000000,
"endTs": 1790086400000,
"source": "risk_event_rollup_hourly"
},
"coverage": {
"partitionComplete": true,
"sampleCount": 182345,
"partialWindow": false
},
"entities": [
{
"entityId": "entity-9182",
"riskScore": 87.4,
"eventCount": 12345,
"evidenceWindowIds": ["win-1001", "win-1002"]
}
],
"totalEntities": 487320,
"truncated": true,
"nextCursor": "entity-cursor-9183"
}
coverage 的价值是防止模型把“没查到”当成“没有风险”。
常见工程错误
| 错误 | 实际后果 | 正确做法 |
|---|---|---|
| 直接查 raw 表 | 延迟和成本失控 | 先查 rollup |
| 无时间分区 | 30 亿行扫描 | 按天分区 |
| 索引不以 tenant 开头 | 跨租户扫描 | tenant-first index |
| TopN 在应用层算 | 内存和网络浪费 | 数据库 group by / order by / limit |
| 返回 raw row | 上下文膨胀 | 聚合 + evidence refs |
| 无 coverage | 缺数据被当成零 | 返回完整性和缺失状态 |
| 暴露 SQL 工具 | 权限和成本不可控 | 只暴露业务原语 |
上线前检查
- 时间范围必填。
- tenant scope 服务端注入。
- TopN 在数据库完成。
- 聚合表覆盖高频查询。
- raw 表只允许按 ID 精查。
- 分页有 cursor。
- 返回带 coverage。
- 查询延迟和扫描量可观测。