Indices

Les indices (index) sont la fonctionnalité la plus puissante d'Isar. De nombreuses bases de données embarquées proposent des index "normaux" (voire aucun), mais Isar dispose également d'index composés et à entrées multiples. Il est essentiel de comprendre le fonctionnement des index pour optimiser les performances des requêtes. Isar vous permet de choisir l'index que vous voulez utiliser et comment vous voulez l'utiliser. Nous allons commencer par une introduction rapide à ce que sont les index.

Que sont les indices?

Lorsqu'une collection n'est pas indexée, l'ordre des lignes ne pourra probablement pas être discerné par la requête comme étant optimisé de quelconques manières, et votre requête devra donc rechercher les objets de façon linéaire. En d'autres termes, la requête devra parcourir chaque objet pour trouver ceux qui correspondent aux conditions. Comme vous pouvez l'imaginer, cela peut prendre du temps. La recherche dans chaque objet n'est pas très efficace.

Par exemple, cette collection Product est entièrement non ordonnée.

@collection
class Product {
  Id? id;

  late String name;

  late int price;
}

Données:

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

Une requête qui tente de trouver tous les produits dont le prix est supérieur à 30 € doit parcourir les neuf rangées. Ce n'est pas un problème pour neuf lignes, mais cela peut le devenir pour 100 000 lignes.

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

Pour améliorer les performances de cette requête, nous indexons la propriété price. Un index est comme une table de recherche triée:

@collection
class Product {
  Id? id;

  late String name;

  @Index()
  late int price;
}

Index généré:

priceid
29
34
125
151
253
307
552
606
6508

Maintenant, la requête peut être exécutée beaucoup plus rapidement. L'exécuteur peut directement sauter aux trois dernières lignes d'index et trouver les objets correspondants par leur id.

Triage

Autre point intéressant: les index peuvent effectuer des tris très rapides. Les requêtes triées sont coûteuses, car la base de données doit charger tous les résultats en mémoire avant de les trier. Même si vous spécifiez un décalage ou une limite, ils sont appliqués après le tri.

Imaginons que nous voulions trouver les quatre produits les moins chers. Nous pourrions utiliser la requête suivante:

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

Dans cet exemple, la base de données devrait charger tous (!) les objets, les trier par prix et renvoyer les quatre produits dont le prix est le plus bas.

Comme vous pouvez probablement l'imaginer, cette opération peut être réalisée de manière beaucoup plus efficace avec l'index précédent. La base de données prend les quatre premières lignes de l'index et renvoie les objets correspondants puisqu'ils sont déjà dans le bon ordre.

Pour utiliser l'index pour le tri, nous devons écrire la requête comme suit:

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

La clause where .anyX() indique à Isar d'utiliser un index uniquement pour le tri. Nous pouvons également utiliser une clause where comme .priceGreaterThan() et obtenir des résultats triés.

Indices uniques

Un index unique garantit que l'index ne contient pas de valeurs en double. Il peut être composé d'une ou plusieurs propriétés. Si un index unique a une propriété, les valeurs de cette propriété seront uniques. Si l'index unique a plus d'une propriété, la combinaison des valeurs dans ces propriétés est unique.

@collection
class User {
  Id? id;

  @Index(unique: true)
  late String username;

  late int age;
}

Toute tentative d'insertion ou de mise à jour de données dans l'index unique qui provoque un doublon entraînera une erreur:

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

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

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

// Essayons d'insérer un utilisateur avec le même nom d'utilisateur
await isar.users.put(user2); // -> error: unique constraint violated
print(await isar.user.where().findAll());
// > [{id: 1, username: 'user1', age: 25}]

Remplacement d'indices

Il n'est parfois pas préférable d'envoyer une erreur si une contrainte unique n'est pas respectée. Au lieu de cela, nous pouvons vouloir remplacer l'objet existant par le nouvel objet. Pour cela, il suffit de mettre la propriété replace de l'index à true.

@collection
class User {
  Id? id;

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

Maintenant, lorsque nous essayons d'insérer un utilisateur avec un nom déjà existant, Isar va remplacer l'utilisateur existant par le nouveau.

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

Les indices de remplacement génèrent également des méthodes putBy() qui nous permettent de mettre à jour les objets au lieu de les remplacer. L'identifiant existant est réutilisé, et les liens sont toujours présents.

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

// L'utilisateur n'existe pas, donc c'est la même chose que 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}]

Comme nous pouvons le constater, l'identifiant du premier utilisateur inséré est réutilisé.

Index insensibles à la casse

Tous les index sur les propriétés String et List<String> sont sensibles à la casse par défaut. Si nous voulons créer un index insensible à la casse, nous pouvons utiliser l'option caseSensitive:

@collection
class Person {
  Id? id;

  @Index(caseSensitive: false)
  late String name;

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

Type d'indice

Il existe différents types d'index. La plupart du temps, nous voudrons utiliser un index IndexType.value, mais les index de hachage sont plus efficaces.

Index value

Les index de valeurs sont le type par défaut et le seul autorisé pour toutes les propriétés qui ne contiennent pas de chaînes de caractères ou de listes. Les valeurs des propriétés sont utilisées pour construire l'index. Dans le cas des listes, ce sont les éléments de la liste qui sont utilisés. Il s'agit du type d'index le plus flexible mais aussi le plus gourmand en espace parmi les trois types d'index.

Conseil

Utilisez IndexType.value pour les types primitifs, les chaînes de caractères lorsque vous avez besoin de clauses startsWith() et les listes si vous voulez rechercher des éléments individuels.

Index hash

Les chaînes de caractères et les listes peuvent être hachées pour réduire de manière significative le stockage requis par l'index. L'inconvénient des index de hachage est qu'ils ne peuvent pas être utilisés pour les scans de préfixe (clauses where startsWith).

Conseil

Utilisez IndexType.hash pour les chaînes de caractères et les listes si vous n'avez pas besoin des clauses where startsWith et elementEqualTo.

Index hashElements

Les listes de chaînes peuvent être hachées dans leur ensemble (à l'aide de IndexType.hash), ou les éléments de la liste peuvent être hachés séparément (à l'aide de IndexType.hashElements), créant ainsi un index à entrées multiples avec des éléments hachés.

Conseil

Utilisez IndexType.hashElements pour les List<String> où vous avez besoin de clauses where elementEqualTo.

Indices composés

Un index composite est un index sur plusieurs propriétés. Isar nous permet de créer des index composites sur un maximum de trois propriétés.

Les index composés sont également connus sous le nom d'index à colonnes multiples.

Il est probablement préférable de commencer par un exemple. Nous créons une collection de personnes et définissons un index composé sur les propriétés âge et nom:

@collection
class Person {
  Id? id;

  late String name;

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

  late String hometown;
}

Données:

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

Index généré:

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

L'indice composé généré contient toutes les personnes triées par leur âge et leur nom.

Les index composés sont parfaits si nous souhaitons créer des requêtes efficaces triées par plusieurs propriétés. Ils permettent également d'utiliser des clauses where avancées avec plusieurs propriétés :

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

La dernière propriété d'un index composé supporte également des conditions telles que startsWith() ou lessThan() :

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

Indices à entrées multiples

Si nous indexons une liste en utilisant IndexType.value, Isar va automatiquement créer un index à entrées multiples, et chaque élément de la liste est indexé vers l'objet. Cela fonctionne pour tous les types de listes.

Les applications pratiques des index à entrées multiples comprennent l'indexation d'une liste de balises ou la création d'un index en texte intégral.

@collection
class Product {
  Id? id;

  late String description;

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

Isar.splitWords() divise une chaîne de caractères en mots selon la spécification Unicode Annex #29open in new window, ce qui fait qu'il fonctionne correctement pour presque toutes les langues.

Data:

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]

Les entrées comportant des mots en double n'apparaissent qu'une seule fois dans l'index.

Index généré:

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

Cet index peut maintenant être utilisé pour les clauses de préfixe (ou d'égalité) des mots individuels de la description.

Conseil

Au lieu de stocker les mots directement, vous pouvez également envisager d'utiliser le résultat d'un algorithme phonétiqueopen in new window comme Soundexopen in new window.