TinyDb 查询链路可以分成 4 层:
Queryable<T>接收 LINQ 表达式。QueryShapeExtractor提取可下推条件。QueryOptimizer生成执行计划。QueryExecutor按策略执行(主键查找/索引扫描/全表扫描)。
1. 入口:Query() 与 IQueryable
DocumentCollection<T>.Query() 返回 new Queryable<T>(_queryExecutor, _name)。
后续 LINQ 调用会由 QueryProvider.Execute 转给 QueryPipeline.Execute。
2. QueryPipeline 做了什么
QueryPipeline.Execute 的核心流程:
提取
QueryShape(Where/Order/Skip/Take 等结构)。调
executor.ExecuteShaped(...)执行数据库侧可下推部分。对剩余表达式执行 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
优化器核心决策顺序:
无条件 ->
FullTableScan主键等值 ->
PrimaryKeyLookup有索引可用 ->
IndexScan唯一索引且全字段等值 -> 升级
IndexSeek否则
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. 查询调优建议
高频等值查询字段加唯一或普通索引。
多条件查询尽量让条件前缀匹配复合索引字段顺序。
分页必须配排序,且排序字段最好有索引支持。
对复杂表达式,先做业务层预计算,降低解析负担。
9. 小结
TinyDb 查询并不是“纯 LINQ to Objects”,而是“尽可能下推 + 必要时回退”的混合执行模型。
这一模型的优势是:
对常见查询有索引优化路径。
对复杂查询仍能保证功能正确。
在 AOT 场景下保持可用与可控。
下一篇进入索引系统:索引如何自动创建、如何维护、如何影响查询计划。
发表评论