Indizes

Indizes sind Isars mächtigstes Feature. Viele eingebettete Datenbanken bieten "normale" Indizes (wenn überhaupt), aber Isar hat auch Komposit- und Mehrfach-Indizes. Zu verstehen, wie Indizes funktionieren ist grundlegend um die Abfrageleistung zu optimieren. Isar lässt dich wählen welchen Index du verwenden möchtest und wie du ihn benutzen willst. Wir beginnen mit einer schnellen Einführung was Indizes sind.

Was sind Indizes?

Wenn eine Collection nicht indiziert ist, wird die Reihenfolge der Zeilen von der Abfrage aus sicherlich nicht als in irgendeiner Weise optimiert erkennbar sein. Daher muss die Abfrage linear alle Objekte durchsuchen. In anderen Worten, die Abfrage muss alle Objekte durchsuchen, um diejenigen zu finden, die zu den Bedingungen passen. Wie du dir bestimmt vorstellen kannst, kann das seine Zeit dauern. Durch jedes einzelne Objekt zu gucken ist nicht sehr effizient.

Zum Beispiel ist diese Product-Collection komplett unsortiert.

@collection
class Product {
  late int id;

  late String name;

  late int price;
}

Daten:

idnameprice
1Book15
2Table55
3Chair25
4Pencil3
5Lightbulb12
6Carpet60
7Pillow30
8Computer650
9Soap2

Eine Abfrage, die versucht alle Produkte zu finden, die mehr als 30€ kosten, muss alle neun Zeilen durchsuchen. Das ist kein Problem für nur neun Zeilen, aber könnte ein Problem für 100k Zeilen werden.

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

Um die Leistung dieser Abfrage zu verbessern, indizieren wir die Eigenschaft price. Ein Index ist wie eine sortierte Nachschlagetabelle.

@collection
class Product {
  late int id;

  late String name;

  @Index()
  late int price;
}

Generierter Index:

priceid
29
34
125
151
253
307
552
606
6508

Jetzt kann die Abfrage deutlich schneller durchgeführt werden. Es kann direkt zu den letzten drei Indexzeilen gesprungen werden und die entsprechenden Objekte anhand ihrer ID gefunden werden.

Sortierung

Eine andere coole Sache: Indizes können superschnell sortieren. Sortierte Abfragen sind kostenintensiv, weil die Datenbank alle Ergebnisse in den Speicher laden muss, bevor sie sortiert werden. Sogar wenn du einen Offset oder eine Limitierung angibst, werden diese erst nach dem Sortieren angewandt.

Stell dir vor, wir wollten die vier günstigsten Produkte finden. Wir könnten die folgende Abfrage verwenden:

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

In diesem Beispiel müsste die Datenbank alle (!) Objekte laden, sie nach dem Preis sortieren und die vier Produkte mit dem niedrigsten Preis zurückgeben.

Wie du dir vermutlich vorstellen kannst, kann das mit dem vorherigen Index sehr viel effizienter gemacht werden. Die Datenbank nimmt die ersten vier Zeilen des Indexes und gibt die zugehörigen Objekte zurück, da sie schon in der korrekten Reihenfolge sind.

Um einen Index zum Sortieren zu verwenden würden wir die Abfrage so schreiben:

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

Die .anyX() Where-Klausel teilt Isar mit, einen Index nur zum Sortieren zu verwenden. Du kannst also eine Where-Klausel wie .priceGreaterThan() benutzen und sortierte Ergenisse erhalten.

Eindeutige Indizes

Ein eindeutiger Index stellt sicher, dass der Index keine doppelten Werte enthält. Er kann aus einem oder mehreren Eigenschaften bestehen. Wenn ein eindeutiger Index eine Eigenschaft hat, sind die Werte dieser Eigenschaft eindeutig. Wenn ein eindeutiger Index mehr als eine Eigenschaft hat, dann ist die Kombination der Werte dieser Eigenschaften eindeutig.

@collection
class User {
  late int id;

  @Index(unique: true)
  late String username;

  late int age;
}

Jeder Versuch Daten in einen eindeutigen Index einzufügen oder zu aktualisieren, die ein Dukplikat verursachen würden, resultieren in einem Fehler:

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

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

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

// Versucht einen Benutzer mit dem gleichen Benutzernamen einzufügen
await isar.users.put(user2); // -> Fehler: Eindeutigkeitsbeschränkung verletzt
print(await isar.user.where().findAll());
// > [{id: 1, username: 'user1', age: 25}]

Indizes ersetzen

Manchmal ist es nicht von Vorteil einen Fehler zu verursachen, wenn eine Eindeutigkeitsbeschränkung verletzt wird. Stattdessen möchtest du vielleicht das vorhandene Objekt mit dem Neuen ersetzen. Das kann erreicht werden, indem die Eigenschaft replace des Indexes auf true gesetzt wird.

@collection
class User {
  late int id;

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

Jetzt, wenn wir versuchen einen Benutzer mit einem vorhandenen Benutzernamen einzufügen, wird Isar den Vorhandenen mit dem neuen Benutzer ersetzen.

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}]

Ersetzbare Indizes generieren auch putBy()-Methoden, die es dir ermöglichen Objekte zu aktualisieren statt sie zu ersetzen. Die vorhandene ID wird wiederverwendet und Links bleiben erhalten.

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

// Nutzer existiert nicht, also ist es das gleiche wie 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}]

Wie du sehen kannst, wird die ID des zuerst eingefügten Benutzers wiederverwendet.

Indizes ohne Berücksichtigung auf Groß-/Kleinschreibung

Alle Indizes auf String- und List<String>-Eigenschaften beachten standardmäßig die Groß-/Kleinschreibung. Wenn du einen Index erstellen willst, der die Groß-/Kleinschreibung nicht berücksichtigt, kannst du die caseSensitive-Option verwenden:

@collection
class Person {
  late int id;

  @Index(caseSensitive: false)
  late String name;

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

Index-Typen

Es gibt verschiedene Typen von Indizes. Meistens wirst du einen IndexType.value-Index verwenden wollen, aber Hash-Indizes sind effizienter.

Wert-Index

Wert-Indizes sind der Standardtyp und der Einzige, der für alle Eigenschaften erlaubt ist, die nicht Strings oder Listen enthalten. Eigenschaftswerte werden verwendet, um den Index zu erstellen. Im Fall von Listen, werden die Elemente der Liste verwendet. Es ist der flexibelste, aber auch platzraubendste der drei Index-Typen.

Tipp

Benutze IndexType.value für Primitives, Strings, wenn du startsWith()-Where-Klauseln brauchst, und Listen, wenn du nach einzelnen Elementen suchst.

Hash-Index

Strings und Listen können gehasht werden um den für den Index benötigten Speicher drastisch zu verringern. Der Nachteil eines Hash-Indexes ist, dass sie nicht für Präfixsuchen (startsWith()-Where-Klauseln) verwendet werden können.

Tipp

Verwende IndexType.hash für Strings und Listen, wenn du die startsWith- und elementEqualTo-Where-Klauseln nicht benötigst.

HashElements-Index

Stringlisten können als Ganzes gehasht werden (indem man IndexType.hash verwendet) oder die Elemente der Liste können seperat gehasht werden (indem man IndexType.hashElements nutzt) wodurch ein Mehreintragsindex mit gehashten Elementen erzeugt wird.

Tipp

Nutze IndexType.hashElements für List<String> bei denen du elementEqualTo-Where-Klauseln benötigst.

Komposit-Indizes

Ein Komposit-Index ist ein Index auf mehrere Eigenschaften. Isar erlaubt es dir zusammengesetzte Indizes mit bis zu drei Eigenschaften zu erstellen.

Komposit-Indizes sind auch als Mehr-Spalten-Indizes bekannt.

Es ist vermutlich am besten mit einem Beispiel zu starten. Wir erstellen eine Personen-Collection und definieren einen zusammengesetzten Index auf die Alters- und Namenseigenschaften:

@collection
class Person {
  late int id;

  late String name;

  @Index(composite: [CompositeIndex('name')])
  late int age;

  late String hometown;
}

Daten:

idnameagehometown
1Daniel20Berlin
2Anne20Paris
3Carl24San Diego
4Simon24Munich
5David20New York
6Carl24London
7Audrey30Prague
8Anne24Paris

Generierter Index:

agenameid
20Anne2
20Daniel1
20David5
24Anne8
24Carl3
24Carl6
24Simon4
30Audrey7

Der generierte zusammengesetzte Index enthält alle Personen sortiert nach ihrem Alter und ihrem Namen.

Komposit-Indizes sind super, wenn du effiziente Abfragen, sortiert nach mehreren Eigenschaften, stellen willst. Sie erlauben auch anspruchsvolle Where-Klauseln mit mehreren Eigenschaften:

final result = await isar.where()
  .ageNameEqualTo(24, 'Carl')
  .hometownProperty()
  .findAll() // -> ['San Diego', 'London']

Die letzte Eigenschaft eines zusammengesetzten Index unterstützt auch Bedingungen wie startsWith() oder lessThan():

final result = await isar.where()
  .ageEqualToNameStartsWith(20, 'Da')
  .findAll() // -> [Daniel, David]

Mehrfach-Indizes

Wenn du eine Liste mit IndexType.value indizierst, wird Isar automatische einen Mehrfach-Index erzeugen und jeder Eintrag in der Liste wird mit dem Objekt indiziert. Das funktioniert für alle Listentypen.

Zu sinnvollen Anwendungen für Mehrfach-Indizes zählen das Indizieren einer Liste an Tags oder einen Volltext-Index zu erstellen.

@collection
class Product {
  late int id;

  late String description;

  @Index(type: IndexType.value, caseSensitive: false)
  List<String> get descriptionWords => Isar.splitWords(description);
}

Isar.splitWords() trennt einen String nach der Unicode Annex #29open in new window-Spezifikation in Worte, sodass es für fast alle Sprachen richtig funktioniert.

Daten:

iddescriptiondescriptionWords
1comfortable blue t-shirt[comfortable, blue, t-shirt]
2comfortable, red pullover!!![comfortable, red, pullover]
3plain red t-shirt[plain, red, t-shirt]
4red necktie (super red)[red, necktie, super, red]

Einträge mit doppelten Worten tauchen nur einmal im Index auf.

Generierter Index:

descriptionWordsid
comfortable[1, 2]
blue1
necktie4
plain3
pullover2
red[2, 3, 4]
super4
t-shirt[1, 3]

Dieser Index kann nun für (Gleichheits- oder) Präfix-Where-Klauseln der individuellen Worte der Beschreibung verwendet werden.

Tipp

Statt Worte direkt zu speichern kannst du auch in Betracht ziehen das Ergebnis einer Phonetischen Sucheopen in new window wie von dem Algorithmus Soundexopen in new window zu verwenden.