5.5 Query builder
学习目标
完成本节后,你将能够:
- 理解为什么 ClickHouse 查询必须集中到 query builder。
- 说出
EventsQueryBuilder的字段、field set、filter、排序、表选择和 split query 机制。 - 修改列表字段或 Public API 字段时知道要同步哪些 contract。
5.5.1 先给结论
Query builder 是 ClickHouse 查询的安全出口。它不是为了少写 SQL,而是为了把这些系统规则集中起来:
- tenant isolation;
- field mapping;
- field set;
- parameterized query;
- FilterState lowering;
- ClickHouse primary key friendly order;
events_core/events_full表选择;- 大字段延迟读取;
- trace_id hash optimization;
- CTE/JOIN 结构;
- query output alias 稳定。
如果每个页面手写 SQL,这些规则一定会分叉。
5.5.2 EventsQueryBuilder 的结构
源码:packages/shared/src/server/queries/clickhouse-sql/event-query-builder.ts
5.5.3 字段映射:领域名到 SQL 表达式
EVENTS_FIELDS 把应用里的字段名映射成 ClickHouse expression,例如:
| field key | SQL 语义 |
|---|---|
id | e.span_id as id |
traceId | e.trace_id as "trace_id" |
projectId | e.project_id as "project_id" |
metadata | mapFromArrays(...) as metadata |
latency | date_diff('millisecond', e.start_time, e.end_time) |
timeToFirstToken | date_diff('millisecond', e.start_time, e.completion_start_time) |
input / output | 大字段,只有需要时读取。 |
字段映射的价值是让 UI、API、export、eval 使用同一套字段别名和含义。
5.5.4 Field set:性能 contract
FIELD_SETS 定义常见查询场景需要哪些字段。
| field set | 用途 |
|---|---|
base | 列表常用 observation 字段。 |
baseWithoutTools | 列表不需要完整 tool payload 时使用。 |
calculated | latency、time to first token 等计算字段。 |
io | input/output。 |
metadata | metadata map。 |
tools | tool definitions/calls/names。 |
core / basic / usage / prompt | Public API v2 field groups。 |
export | CSV/JSON export 所需字段。 |
eval | evaluation 相关字段。 |
experimentItems | experiment item 视图字段。 |
field set 是性能 contract:调用方必须明确自己要读什么,不能默认 SELECT *。
5.5.5 自动 project filter
BaseEventsQueryBuilder 在 build query 时默认把 project filter 放到 WHERE 开头:
e.project_id = {projectId: String}只有显式传 NoProjectId 才会 opt out。
这个设计把多租户隔离做成默认行为。手写 SQL 最容易漏的就是这一步。
5.5.6 Filter lowering 和优化
applyFilters(filterList) 会把 FilterState lowering 之后的条件放进 WHERE。
events table 还做了一个重要优化:当筛选条件是 trace_id = ... 时,query builder 会加:
xxHash32(trace_id) = xxHash32({traceIdXxHash: String})这配合 events table 的排序/主键结构,帮助 ClickHouse 更快缩小扫描范围。
5.5.7 排序优化
当调用方按 start_time 排序,orderByColumns 会补:
ORDER BY e.project_id, toStartOfMinute(e.start_time), e.start_time原因是 events table 的 primary key 以 project_id 和 start time bucket 为核心。排序不是 UI 小细节,它会影响 ClickHouse 是否能有效利用数据布局。
5.5.8 表选择:events_core vs events_full
EventsQueryBuilder 默认读 events_core:
- 列表;
- filter options;
- count;
- 常见聚合;
- 轻量字段。
当需要完整 input/output 或 metadata expansion 时,needsFullTable() 会切到 events_full:
selectIO(truncated = false);selectMetadataExpanded(...);forceFullTable()。
这就是 v4 events 的轻重字段策略:默认轻读,必要时 full read。
5.5.9 Split query
buildEventsFullTableSplitQuery 解决的是“既要高效过滤,又要 full payload”的问题。
它先在 events_core 中得到候选行,再从 events_full 取这些行的 I/O/metadata,最后 LEFT ANY JOIN 合并,避免 full table 直接承担复杂过滤和排序成本。
5.5.10 修改字段的同步面
| 改动 | 同步位置 |
|---|---|
| 新增 ClickHouse 列 | migration、dev table、insert/read record schema。 |
| worker 写新列 | IngestionService、record validation、ClickhouseWriter。 |
| query 返回新字段 | EVENTS_FIELDS、FIELD_SETS、repository mapper。 |
| UI table 展示 | column definition、sorting/filter config、client domain converter。 |
| Search Bar 支持 | field registry、grammar completions、adapter、reverse adapter。 |
| Public API 暴露 | public API field groups、Zod response、Fern。 |
| Export 使用 | export field set、CSV/JSON mapping。 |
5.5.11 自检清单
- 是否通过 query builder 而不是手写 SQL?
- 是否自动带 project filter?
- 是否选择了最小 field set?
- 列表是否避免 input/output/metadata 大字段?
- 需要 full payload 时是否先缩小候选集?
- 排序是否和表主键友好?
- 新字段是否同步 write path、read path、API/UI contract?