Isar Plus

Indexes

Optimize query performance with powerful indexing strategies

Indexes

Indexes are Isar's most powerful feature for query optimization. Learn how to use single, composite, and multi-entry indexes effectively.

Understanding indexes is essential to optimize query performance!

What are Indexes?

Without indexes, queries must scan through every object linearly. With indexes, queries can jump directly to the relevant data.

Example Without Index

@collection
class Product {
  Id? id;
  late String name;
  late int price;
}

Unindexed Data:

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

To find products > €30, Isar must check all 9 rows:

final expensive = await isar.products.where()
  .priceGreaterThan(30)
  .findAll();

With Index

Add an index to the price field:

@collection
class Product {
  Id? id;
  late String name;
  
  @Index()
  late int price;
}

Generated Index (sorted):

priceid
29
34
125
151
253
307
552
606
6508

Now the query jumps directly to the relevant rows! ⚡

Creating Indexes

Single Property Index

@collection
class User {
  Id? id;

  @Index()
  late String email;

  @Index(type: IndexType.value)
  late String username;
}

Default - Stores the actual value. Supports all where clauses.

@Index(type: IndexType.value)
late String email;

// Supports: equalTo, between, startsWith, etc.
await isar.users.where()
  .emailStartsWith('john')
  .findAll();

Stores hash of value. Only supports equality checks. Uses less space.

@Index(type: IndexType.hash)
late String email;

// Only supports: equalTo
await isar.users.where()
  .emailEqualTo('john@example.com')
  .findAll();

For Lists only. Hashes each element individually.

@Index(type: IndexType.hashElements)
late List<String> tags;

await isar.posts.where()
  .tagsElementEqualTo('flutter')
  .findAll();

Composite Indexes

Index multiple properties together for complex queries:

@collection
class Person {
  Id? id;

  late String firstName;

  @Index(composite: ['firstName'])
  late String lastName;

  late int age;
}
// Uses composite index efficiently
final people = await isar.persons
  .where()
  .lastNameFirstNameEqualTo('Doe', 'John')
  .findAll();

Composite indexes can also use only the first property: .lastNameEqualTo('Doe')

Multi-Property Composite

@collection
@Index(composite: ['lastName', 'age'])
class Person {
  Id? id;
  late String firstName;
  late String lastName;
  late int age;
}
// All of these use the composite index:
.firstNameEqualTo('John')
.firstNameLastNameEqualTo('John', 'Doe')
.firstNameLastNameAgeEqualTo('John', 'Doe', 25)

Multi-Entry Indexes

Create indexes for list elements:

@collection
class Post {
  Id? id;

  late String title;

  @Index(type: IndexType.value)
  late List<String> tags;
}
// Fast lookup by any tag
final flutterPosts = await isar.posts
  .where()
  .tagsElementEqualTo('flutter')
  .findAll();

Multi-entry indexes can significantly increase database size for lists with many elements.

Unique Indexes

Enforce uniqueness constraints:

@collection
class User {
  Id? id;

  @Index(unique: true)
  late String username;

  late int age;
}
await isar.writeAsync((isar) async {
  final user1 = User()
    ..id = 1
    ..username = 'john_doe'
    ..age = 25;
  await isar.users.put(user1); // ✅ inserted

  final user2 = User()
    ..id = 2
    ..username = 'john_doe'
    ..age = 30;
  await isar.users.put(user2); // ✅ overwrites the previous row

  final current = await isar.users
      .where()
      .usernameEqualTo('john_doe')
      .findFirst();
  print(current);
  // => {id: 2, username: john_doe, age: 30}
});

Unique indexes always replace

v4 keeps the most recent write for a unique key combination. If you need to reject duplicates instead of overwriting, query first and throw your own error:

await isar.writeAsync((isar) async {
  final exists = await isar.users
      .where()
      .usernameEqualTo('john_doe')
      .findFirst();
  if (exists != null) {
    throw StateError('username already taken');
  }

  final user = User()
    ..id = 42
    ..username = 'john_doe'
    ..age = 30;

  await isar.users.put(user);
});

Case Sensitivity

Control case sensitivity for string indexes:

@collection
class User {
  Id? id;

  @Index(caseSensitive: false)
  late String email;
}
// Both find the same user
await isar.users.where().emailEqualTo('JOHN@example.com').findAll();
await isar.users.where().emailEqualTo('john@example.com').findAll();

Case-insensitive indexes take slightly more space but provide flexible queries.

Index for Sorting

Indexes provide super-fast sorting:

@collection
class Product {
  Id? id;

  late String name;

  @Index()
  late int price;
}
// ❌ Slow - loads all, then sorts
final cheapest = await isar.products
  .where()
  .sortByPrice()
  .limit(4)
  .findAll();
// ✅ Fast - uses sorted index
final cheapest = await isar.products
  .where()
  .anyPrice()
  .limit(4)
  .findAll();

Using indexed sorting avoids loading and sorting all results in memory!

Where Clauses

Use indexes with where clauses for maximum performance:

@collection
class Product {
  Id? id;

  late String name;

  @Index()
  late int price;
}
// Fast - uses index
final products = await isar.products
  .where()
  .priceBetween(10, 100)
  .findAll();

// Fast - index + sort
final sorted = await isar.products
  .where()
  .anyPrice()
  .limit(10)
  .findAll();

// Fast - index + filter
final filtered = await isar.products
  .where()
  .priceGreaterThan(50)
  .where()
  .nameStartsWith('iPhone')
  .findAll();

Index Types Comparison

TypeSizeWhere ClausesUse Case
IndexType.valueLargeAllFull text search, ranges
IndexType.hashSmallEquality onlyUnique constraints, lookups
IndexType.hashElementsMediumList elementsTag systems, categories

Best Practices

Choose the Right Properties

Index properties used frequently in where clauses:

@collection
class User {
  Id? id;

  @Index() // Frequently queried
  late String email;

  late String name; // Not indexed - rarely queried alone
}

Don't Over-Index

Each index increases write time and storage:

// ❌ Too many indexes
@collection
class User {
  @Index() Id? id; // Id is already indexed!
  @Index() late String email;
  @Index() late String name;
  @Index() late String phone;
  @Index() late int age;
}

// ✅ Index only what you query
@collection
class User {
  Id? id;
  @Index(unique: true) late String email;
  late String name;
  late String phone;
  late int age;
}

Use Composite Indexes Wisely

// ✅ Good - queries firstName + lastName together
  @Index(composite: ['lastName'])
late String firstName;

// ❌ Bad - separate queries
@Index()
late String firstName;
@Index()
late String lastName;

Profile Your Queries

Use Isar Inspector to analyze query performance:

// Enable inspector in debug mode
final isar = Isar.open(
  schemas: [UserSchema],
  inspector: true, // Open inspector
);

Index Limitations

  • Only the first 1024 bytes of strings are indexed
  • Maximum of 3 properties in composite indexes (on web)
  • Indexes increase write operation time
  • Indexes consume additional storage

Next Steps

Last Update