TinyDb 索引链路由 3 层组成:

  1. IndexScanner:扫描实体特性并自动建索引。

  2. IndexManager:集合级索引生命周期管理。

  3. BTreeIndex / DiskBTree:底层 B+ 树存储与检索。

1. 自动索引从哪里来

DocumentCollection<T> 构造时会调用:

CreateAutoIndexes();

内部就是 IndexScanner.ScanAndCreateIndexes(engine, typeof(T), collectionName),默认会做:

  1. 创建主键索引(_id,唯一)。

  2. 扫描属性上的 [Index]

  3. 扫描类上的 [CompositeIndex]

2. 特性如何映射到索引

[Entity("users")]
[CompositeIndex("idx_age_city", "Age", "City", Unique = false)]
public partial class User
{
    [Id]
    public ObjectId Id { get; set; } = ObjectId.NewObjectId();

    [Index(Unique = true)]
    public string Email { get; set; } = string.Empty;

    [Index]
    public int Age { get; set; }

    [Index]
    public string City { get; set; } = string.Empty;
}

注意:字段名会经过 camelCase 兼容转换,避免序列化字段名不一致。

3. IndexManager 的职责

IndexManager 主要负责:

  • CreateIndex / DropIndex

  • IndexExists / GetIndex

  • GetBestIndex(给优化器选索引)

  • 在文档变更时同步索引:

  • InsertDocument(oldDoc)

  • UpdateDocument(oldDoc, newDoc)

  • DeleteDocument(doc)

更新时它会先删旧键再插新键;若唯一索引冲突,会尝试回滚旧键,保证索引一致性。

4. BTreeIndex 的核心能力

BTreeIndex 提供:

  • Insert/ Delete

  • FindExact

  • FindRange / FindRangeReverse

  • GetAll / GetAllReverse

  • Validate / GetStatistics

并通过读写锁保证并发安全。

5. 唯一索引冲突处理

IsUnique=true 且键已存在,Insert 返回失败并在上层抛异常。


业务侧应把它当“约束失败”处理:

try
{
    users.Insert(new User { Email = "dup@example.com" });
}
catch (InvalidOperationException ex)
{
    // 唯一约束冲突
    Console.WriteLine(ex.Message);
}

6. 查询如何利用索引

QueryOptimizer 会根据条件字段和索引统计选择策略:

  • 主键等值 -> PrimaryKeyLookup

  • 唯一索引全等值 -> IndexSeek

  • 普通索引或范围 -> IndexScan

  • 无可用索引 -> FullTableScan

所以“索引是否存在”决定了执行计划是否降级为全表扫描。

7. 索引设计建议(按优先级)

  1. 主键外,先建唯一业务键(如 EmailOrderNo)。

  2. 高频过滤字段建立单列索引。

  3. 高频组合查询建立复合索引,字段顺序按过滤前缀排序。

  4. 不要盲目给所有字段建索引,写入成本会增加。

8. 常见陷阱

  1. 复合索引顺序错:命中率下降。

  2. 更新高频字段都在索引里:写放大明显。

  3. 小表过度索引:收益不大,复杂度增加。

9. 小结

TinyDb 索引系统的关键价值在于:

  • 自动扫描特性降低接入成本。

  • IndexManager 集中维护索引一致性。

  • B+ 树支持等值、范围、顺序遍历,能覆盖大多数业务查询。

下一篇进入 AOT 与源码生成,解释为什么 [Entity] 是核心约束。