TinyDb 查询链路可以分成 4 层:

  1. Queryable<T> 接收 LINQ 表达式。

  2. QueryShapeExtractor 提取可下推条件。

  3. QueryOptimizer 生成执行计划。

  4. QueryExecutor 按策略执行(主键查找/索引扫描/全表扫描)。

1. 入口:Query()IQueryable

DocumentCollection<T>.Query() 返回 new Queryable<T>(_queryExecutor, _name)


后续 LINQ 调用会由 QueryProvider.Execute 转给 QueryPipeline.Execute

2. QueryPipeline 做了什么

QueryPipeline.Execute 的核心流程:

  1. 提取 QueryShape(Where/Order/Skip/Take 等结构)。

  2. executor.ExecuteShaped(...) 执行数据库侧可下推部分。

  3. 对剩余表达式执行 AOT 兼容重写(ExecuteAot)。

也就是说,TinyDb 在“数据库侧执行”和“内存侧补齐”之间做了拆分。

3. 表达式解析:ExpressionParser

ExpressionParser 会把 Expression<Func<T,bool>> 转成内部 QueryExpression


它有一个很实用的设计:

  • 对不依赖参数的子表达式先尝试求值(常量折叠)。

  • 尽量用手工求值路径,降低对动态编译的依赖(AOT 友好)。

这让以下写法更稳定:

var minAge = 18;
var q = users.Query().Where(x => x.Age >= minAge);

4. 计划选择:QueryOptimizer.CreateExecutionPlan

优化器核心决策顺序:

  1. 无条件 -> FullTableScan

  2. 主键等值 -> PrimaryKeyLookup

  3. 有索引可用 -> IndexScan

  4. 唯一索引且全字段等值 -> 升级 IndexSeek

  5. 否则 FullTableScan

这意味着同样一条语义查询,因索引定义不同,会走完全不同的路径。

5. 执行器策略细节

5.1 PrimaryKeyLookup

  • 直接 _engine.FindById(...)

  • 若事务中有 overlay,优先 overlay 文档。

5.2 IndexSeek / IndexScan

  • IndexManager 拿索引。

  • 根据键或范围取候选 _id

  • 再按条件做二次过滤。

5.3 FullTableScan

  • _engine.FindAllRawWithPredicateInfo(...)

  • 尝试用原始 BSON 扫描谓词下推。

  • 对“无法完全确定”的记录做后置过滤。

6. 谓词下推与 Raw 扫描

DataPageAccess 支持扫描原始 BSON 并结合 ScanPredicate 尝试快速判定。


好处:

  • 减少完整反序列化次数。

  • 对简单比较(如数值范围、等值)性能更好。

当某条记录“需要后置过滤”时,执行器仍会完整反序列化并运行表达式求值,保证结果正确性。

7. 事务可见性:overlay 机制

查询执行时会把当前事务中的未提交操作叠加到结果:

  • 已删除:过滤掉。

  • 已更新:替换为新文档。

  • 新插入:补到结果集。

因此同一事务内能读到自己的写入(Read Your Writes)。

8. 查询调优建议

  1. 高频等值查询字段加唯一或普通索引。

  2. 多条件查询尽量让条件前缀匹配复合索引字段顺序。

  3. 分页必须配排序,且排序字段最好有索引支持。

  4. 对复杂表达式,先做业务层预计算,降低解析负担。

9. 小结

TinyDb 查询并不是“纯 LINQ to Objects”,而是“尽可能下推 + 必要时回退”的混合执行模型。


这一模型的优势是:

  • 对常见查询有索引优化路径。

  • 对复杂查询仍能保证功能正确。

  • 在 AOT 场景下保持可用与可控。

下一篇进入索引系统:索引如何自动创建、如何维护、如何影响查询计划。