[官文翻译]Futter超快数据库Isar - 概念 - 索引(上)

438 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第15天,点击查看活动详情


Isar:用于 Futter 可跨平台的超快数据库

官方文档:Home | Isar Database

pub:isar | Dart Package (flutter-io.cn)

本文翻译自:Indexes | Isar Database

译时版本:isar: 3.0.2


索引

索引是 Isar 最强大的特性。多数嵌入式的数据库都提供了 “常规的” 索引(如果有的话)。 但 Isar 还有组合索引和多实体索引。 理解索引的工作机制对于优化查询性能很有必要。Isar 可以让你选择你想使用的索引和选择如何使用它。最开始我们先快速介绍下索引是什么。

索引是什么?

当集合未被索引时,数据行的排序可能不会被任何方式优化的查询识别,因此查询需要线性检索整个对象。换句话说,查询必须查询每个对象来找到匹配条件的那个。正如你可以想像到的那样,这会花费很长时间。查看每个单独的对象不会很高效。

例如,该 Product 集合完全没有排序。

@Collection()
class Product {
  int? id;

  late String name;

  late int price;
}

数据:

idname(名称)price(价格)
1Book15
2Table55
3Chair25
4Pencil3
5Lightbulb12
6Carpet60
7Pillow30
8Computer650
9Soap2

一个尝试查询所有价值超过 €30 的所有商品需要查询完所有9条记录。 对于9条记录来说这没什么问题,但是对于10万条记录就成问题了。

final expensiveProducts = await isar.products..filter()
  .priceGreaterThan(30)
  .findAll();

要改进该查询的性能,我们对 price 属性添加索引。索引就像是排序好的查询表:

@Collection()
class Product {
  int? id;

  late String name;

  @Index()
  late int price;
}

生成索引:

价格ID
29
34
125
151
253
307
552
606
6508

现在执行查询会快很多。 执行器可以直接跳到索引后的最后三行,然后通过 ID 找到相应的对象。

排序

另外一个很酷的索引是超快排序。 排序后的查询成本很高因为数据库需要在排序前把所有结果加载到内存。 即使你指定了偏移和限定,因为它们是在排序之后起作用。

让我们想像一下,我们想找到 4 个最便宜的商品。 我们可以使用以下查询:

final cheapest = await isar.products.filter()
  .sortByPrice()
  .limit(4)
  .findAll();

该示例中,数据库需要加载所有的对象,然后按照价格排序然后根据最低价格返回 4 个商品。

正如你能想像得到的,这可以通过前面说的索引实现高效查询。 数据库会先拿到索引的前 4 条记录,然后返回相应的对象,因为它们已经是正确地排好序了。

要使用索引用于排序,我们可以如下编写查询:

final cheapestFast = await isar.products.where()
  .anyPrice()
  .limit(4)
  .findAll();

.anyX() Where 子句告诉 Isar 使用仅为排序使用索引。 也可以使用如 .priceGreaterThan() 的 Where 子句,它也会获取排好序的结果。

唯一索引

唯一索引确保索引中不包含任何重复值。 它可能由一个或多个属性构成。 如果唯一索引有一个属性,该属性的值将是唯一的。 唯一索引包含多个属性的情况中,这些属性值的组合是唯一的。

@Collection()
class User {
  int? id;

  @Index(unique: true)
  late String username;
}

任何尝试插入或修改唯一索引造成值重复的话,都会导致错误。 这有一个选项,可替换现有实体而不是失败:

final user1 = User()
  ..id = 1
  ..username = 'user1'
  ..age = 25;

await isar.users.put(user1); // -> ok

final user2 = User()
  ..id = 2;
  ..username = 'user1'
  ..age = 30;

// 尝试插入有相同 username 的 user
await isar.users.put(user2); // -> 错误:唯一约束违反
print(await isar.user.where().findAll()); // -> [user1]

替换索引

有时候,如果违反了唯一索引并不想抛出错误。 取而代之的是用新的索引代替现有的对象。 这可以通过把索引的 replace 属性设置为 true 来实现。

@collection
class User {
  Id? id;

  @Index(unique: true, replace: true)
  late String username;
}

现在当我们试着插入已有用户名的用户时, 现有的用户会被替换为新的。

final user1 = User()
  ..id = 1
  ..username = 'user1'
  ..age = 25;

await isar.users.put(user1);
print(await isar.user.where().findAll());
// > [{id: 1, username: 'user1', age: 25}]

final user2 = User()
  ..id = 2;
  ..username = 'user1'
  ..age = 30;

await isar.users.put(user2);
print(await isar.user.where().findAll());
// > [{id: 2, username: 'user1' age: 30}]

替换索引也会生成 putBy() 方法,这样可以更新对象而不是替换它们。 现有的 id 会被重新使用,链接仍然会被填充。

final user1 = User()
  ..id = 1
  ..username = 'user1'
  ..age = 25;

// user 不存在,所以这和 put() 是同样的。
await isar.users.putByUsername(user1); 
await isar.user.where().findAll(); // -> [{id: 1, username: 'user1', age: 25}]

final user2 = User()
  ..id = 2;
  ..username = 'user1'
  ..age = 30;

await isar.users.put(user2);
await isar.user.where().findAll(); // -> [{id: 1, username: 'user1' age: 30}]

正如所见,第一个插入的 user 的 id 被重新使用了。

大小写敏感索引

默认情况下,String 和 List<String> 属性的所有索引都是大小写敏感的。 这意味着索引中只包含作为索引值的属性值完全相同的实体。 如果想要创建大小写不敏感的索引,可以使用 caseSensitive 选项:

@Collection()
class Person {
  int? id;

  @Index(caseSensitive: false)
  late String name;

  @Index(caseSensitive: false)
  late List<String> tags;
}