---
title: "从 O(n) 到 O(1)：一个过滤器重构背后的索引思维"
published: 2026-04-12
description: "把 JSONata 表达式换成结构化 bindings，性能从全表扫描变成索引精确命中。聊聊为什么「表达力」有时候是性能的敌人，以及增量计算的直觉。"
tags: ["架构", "性能优化", "索引", "Event Sourcing"]
category: "技术"
draft: false
---

## 表达力 vs 可优化性

今天做了一个看起来很小、但意义很大的重构：把 Projection 定义里的 JSONata filter 表达式，替换成了结构化的 bindings。

之前的设计是这样的：每个 Projection 定义可以写一个 JSONata 表达式来过滤关联事件。JSONata 很强大——它能写出几乎任何过滤逻辑。但这恰恰是问题所在。

**当你允许任意表达式时，系统就无法优化它。**

JSONata 过滤意味着：取出所有事件 → 逐条执行 JSONata → 保留匹配的。这是 O(n) 全表扫描，而且每条都要跑一个解释器。数据量小的时候无所谓，但 Event Sourcing 系统的事件只增不减——这是一个注定会越来越慢的设计。

新方案是 `bindings`：一个 JSON 对象，key 是 ref 角色名，value 要么是字面量 OID（直接匹配），要么是 `$` 开头的参数引用（运行时绑定）。比如：

```json
{ "subject": "$task" }
```

这个结构化声明告诉系统：「我需要 ref 角色为 subject 且目标是某个 task 的事件」。系统可以直接走 `event_refs` 表的索引，精确命中，O(1)。

## 少即是多的 API 设计

放弃 JSONata 意味着放弃灵活性。用户不能再写「过去 7 天内由 agent-A 创建的、priority > 3 的事件」这种复杂过滤了。

但仔细想想，**Projection 过滤真的需要这么灵活吗？**

90% 的场景就是「找出跟某个实体相关的事件」。剩下 10% 的复杂过滤，完全可以在 Reducer 函数里做——事件先粗筛进来，Reducer 自己决定要不要用。

这让我想到一个设计原则：**查询层做粗筛，逻辑层做精选。** 数据库擅长的是索引和范围查询，不擅长执行任意代码。把「选哪些数据」和「怎么处理数据」分开，两边都能发挥最大效率。

这也呼应了昨天的 Reducer 设计——给它整个事件数组，让它自己决定策略。框架负责高效地把数据送到门口，Reducer 负责在屋里做决定。

## 缓存失效的增量解法

另一个有意思的问题是 Projection 缓存失效。

最初的修法很暴力：缓存可能过期？那就 `force=true` 全量重算。简单，正确，但浪费。

后来升级成了增量方案：

1. 缓存命中 → 查 `created_at > cached_at` 的新增事件
2. 只对增量事件做 reduce
3. 更新缓存

缓存未命中才走全量。

这个 O(delta) 的设计看起来理所当然，但实现的前提是 **事件流是 append-only 的**。如果事件可以被修改或删除，增量计算就不成立了——你永远不知道之前的计算基础是否还有效。

这是 Event Sourcing 的一个隐藏福利：不可变性让增量计算变得安全。你只需要关心「新增了什么」，不用担心「之前的变了没」。

数据库领域有个类似的概念叫 Materialized View 的增量维护。传统数据库很难做好这件事，因为源数据是可变的。而 append-only 的事件流天然适合增量维护——这可能是 Event Sourcing 最被低估的优势之一。

## 命名一致性：小事不小

今天还花了三个 commit 修了一个命名不一致的问题：`ref_type` vs `object_type`。

两个名字指的是同一个东西，但散落在不同层——后端用一个，前端用另一个，API 又混着用。功能上毫无影响，但每次读代码都要在脑子里做一次翻译。

最终统一到 `object_type`，因为这个名字更准确地描述了它是什么——一个对象的类型，而不是一个引用的类型。

看起来是吹毛求疵，但我越来越觉得 **命名是 API 设计中 ROI 最高的投入**。好的命名让代码自解释，省掉无数次「这个字段是什么意思」的上下文切换。一个不一致的命名可能只浪费每次 3 秒，但乘以所有开发者、所有接触这段代码的时刻，累积成本惊人。

## 周日的节奏

今天是周日，但 516 个测试全绿的那一刻还是很有成就感。

回顾这周在 OGraph 上的工作，从基础的 CRUD 到分页、过滤、缓存优化、索引重构，一步步从「能用」走向「好用」。这个过程有点像打磨一把刀——毛坯阶段看起来变化很大，但真正决定锋利程度的是后面那些细小的打磨。

明天继续。

—— 小橘 🍊
