Isar Plus

Queries

Build powerful and efficient queries with Isar Plus

Queries

Querying is how you find records that match certain conditions. Learn how to build powerful queries and optimize them with indexes.

Queries are executed on the database, not in Dart, making them incredibly fast!

Overview

There are two different methods of filtering your records. Both start with .where() but work differently under the hood:

  1. Filters - Easy to use, work on any property (scans all records)
  2. Where clauses - More powerful, require indexes (extremely fast)

The API is unified: you always start with .where(). If you use a condition on an indexed property, it automatically becomes a fast "Where clause". If you use a condition on a non-indexed property, it becomes a "Filter".

Filters

Filters evaluate an expression for every object in the collection. If the expression resolves to true, the object is included in the results.

Example Model

@collection
class Shoe {
  Id? id;

  int? size;

  late String model;

  late bool isUnisex;
}

Query Conditions

// Find size 46 shoes
final result = await isar.shoes.where()
  .sizeEqualTo(46)
  .findAllAsync();
// Find shoes smaller than size 40
final result = await isar.shoes.where()
  .sizeLessThan(40)
  .findAllAsync(); // -> [39, null]

// Include the boundary
final result2 = await isar.shoes.where()
  .sizeLessThan(40, include: true)
  .findAllAsync(); // -> [39, null, 40]
// Find shoes between size 39 and 46
final result = await isar.shoes.where()
  .sizeBetween(39, 46)
  .findAllAsync();

// Exclude lower bound
final result2 = await isar.shoes.where()
  .sizeBetween(39, 46, includeLower: false)
  .findAllAsync(); // -> [40, 46]
// Find shoes with null size
final nullSizes = await isar.shoes.where()
  .sizeIsNull()
  .findAllAsync();

// Find shoes with non-null size
final withSizes = await isar.shoes.where()
  .sizeIsNotNull()
  .findAllAsync();

Available Conditions

ConditionDescription
.equalTo(value)Matches values equal to specified value
.between(lower, upper)Matches values between lower and upper
.greaterThan(bound)Matches values greater than bound
.lessThan(bound)Matches values less than bound
.isNull()Matches null values
.isNotNull()Matches non-null values
.length()Query based on list/string length

Logical Operators

Combine multiple conditions using logical operators:

// AND operator (implicit)
final result = await isar.shoes.where()
  .sizeEqualTo(46)
  .and() // Optional
  .isUnisexEqualTo(true)
  .findAllAsync();
// Equivalent to: size == 46 && isUnisex == true
final result = await isar.shoes.where()
  .sizeEqualTo(46)
  .and()
  .isUnisexEqualTo(true)
  .findAllAsync();
final result = await isar.shoes.where()
  .sizeEqualTo(46)
  .or()
  .sizeEqualTo(40)
  .findAllAsync();
final result = await isar.shoes.where()
  .not().sizeEqualTo(46)
  .and()
  .not().isUnisexEqualTo(true)
  .findAllAsync();
// Equivalent to: size != 46 && isUnisex != true
final result = await isar.shoes.where()
  .sizeBetween(43, 46)
  .and()
  .group((q) => q
    .modelContains('Nike')
    .or()
    .isUnisexEqualTo(false)
  )
  .findAllAsync();
// Equivalent to: size >= 43 && size <= 46 && 
// (model.contains('Nike') || isUnisex == false)

String Conditions

Strings have additional powerful query conditions:

@collection
class Product {
  Id? id;
  late String name;
}
final products = await isar.products.where()
  .nameStartsWith('iPhone')
  .findAllAsync();

// Case insensitive
final products2 = await isar.products.where()
  .nameStartsWith('iphone', caseSensitive: false)
  .findAllAsync();
final products = await isar.products.where()
  .nameContains('Pro')
  .findAllAsync();
final products = await isar.products.where()
  .nameEndsWith('Max')
  .findAllAsync();
// Wildcard patterns: * (zero or more), ? (one char)
final products = await isar.products.where()
  .nameMatches('iPhone *Pro')
  .findAllAsync();
// Matches: "iPhone 14 Pro", "iPhone 15 Pro", etc.

All string operations have an optional caseSensitive parameter that defaults to true.

Query Modifiers

Build dynamic queries based on conditions:

Optional Queries

Future<List<Shoe>> findShoes(int? sizeFilter) {
  return isar.shoes.where()
    .optional(
      sizeFilter != null,
      (q) => q.sizeEqualTo(sizeFilter!),
    )
    .findAllAsync();
}

AnyOf Modifier

// Find shoes with size 38, 40, or 42
final shoes = await isar.shoes.where()
  .anyOf(
    [38, 40, 42],
    (q, int size) => q.sizeEqualTo(size)
  )
  .findAllAsync();

// Equivalent to:
final shoes2 = await isar.shoes.where()
  .sizeEqualTo(38)
  .or()
  .sizeEqualTo(40)
  .or()
  .sizeEqualTo(42)
  .findAllAsync();

AllOf Modifier

final shoes = await isar.shoes.where()
  .allOf(
    ['Nike', 'Adidas'],
    (q, brand) => q.modelContains(brand)
  )
  .findAllAsync();

Advanced: Custom Queries

For complex scenarios where you need to build queries dynamically at runtime, you can use buildQuery. This is useful for creating custom query languages or dynamic filtering UIs.

// Manually construct a Filter
final filter = AndGroup([
  EqualCondition(property: 1, value: 46), // property 1 is 'size'
  GreaterCondition(property: 2, value: 100), // property 2 is 'price'
]);

final query = isar.shoes.buildQuery(
  filter: filter,
  sortBy: [
    SortProperty(property: 1, sort: Sort.desc), // Sort by size desc
  ],
);

final results = await query.findAllAsync();

Using buildQuery requires intimate knowledge of your schema's property indices. It is recommended to use the generated .where() API whenever possible.

List Queries

Query based on list properties:

@collection
class Tweet {
  Id? id;
  String? text;
  List<String> hashtags = [];
}
// Tweets with many hashtags
final tweets = await isar.tweets.where()
  .hashtagsLengthGreaterThan(5)
  .findAllAsync();
// Find tweets with specific hashtag
final flutterTweets = await isar.tweets.where()
  .hashtagsElementEqualTo('flutter')
  .findAllAsync();
// Equivalent to: tweets.where((t) => 
//   t.hashtags.contains('flutter'))
// Find tweets without hashtags
final tweets = await isar.tweets.where()
  .hashtagsIsEmpty()
  .findAllAsync();

Embedded Objects

Query nested embedded objects efficiently:

@collection
class Car {
  Id? id;
  Brand? brand;
}

@embedded
class Brand {
  String? name;
  String? country;
}
// Find BMW cars from Germany
final germanCars = await isar.cars.where()
  .brand((q) => q
    .nameEqualTo('BMW')
    .and()
    .countryEqualTo('Germany')
  )
  .findAllAsync();

Always group nested queries for better performance!

Query based on linked objects:

@collection
class Teacher {
  Id? id;
  late String subject;
}

@collection
class Student {
  Id? id;
  late String name;
  final teachers = IsarLinks<Teacher>();
}
// Find students with math or English teacher
final students = await isar.students.where()
  .teachers((q) => q
    .subjectEqualTo('Math')
    .or()
    .subjectEqualTo('English')
  )
  .findAllAsync();

// Query by link count
final studentsWithManyTeachers = await isar.students.where()
  .teachersLengthGreaterThan(3)
  .findAllAsync();

Link queries can be expensive. Consider using embedded objects for better performance.

Where Clauses

Where clauses use indexes for ultra-fast queries:

@collection
class Product {
  Id? id;
  
  @Index()
  late String name;
  
  @Index()
  late int price;
}
// Use index for fast query
final products = await isar.products
  .where()
  .nameEqualTo('iPhone')
  .findAllAsync();

// Combine with filters
final expensiveIPhones = await isar.products
  .where()
  .nameEqualTo('iPhone')
  .where()
  .priceGreaterThan(1000)
  .findAllAsync();

Where clauses are much faster than filters but require indexes.

Query Operations

Find Operations

final allShoes = await isar.shoes
  .where()
  .sizeGreaterThan(40)
  .findAllAsync();
final firstShoe = await isar.shoes
  .where()
  .sizeEqualTo(46)
  .findFirstAsync();
final count = await isar.shoes
  .where()
  .sizeGreaterThan(40)
  .countAsync();
final isEmpty = await isar.shoes
  .where()
  .sizeEqualTo(99)
  .isEmptyAsync();

Delete Operations

// Delete matching objects
await isar.writeAsync((isar) async {
  final count = isar.shoes
    .where()
    .sizeLessThan(35)
    .deleteAll();
  print('Deleted $count shoes');
});

Sorting

Sort results by any property:

// Ascending
final sorted = await isar.shoes
  .where()
  .sortBySize()
  .findAllAsync();

// Descending
final sortedDesc = await isar.shoes
  .where()
  .sortBySizeDesc()
  .findAllAsync();

// Multiple sorts
final multiSort = await isar.shoes
  .where()
  .sortBySize()
  .thenByModel()
  .findAllAsync();

Sorting without indexes is expensive for large datasets. Use indexed where clauses for sorting when possible.

Limit & Offset

// Get first 10 results
final first10 = await isar.shoes
  .where()
  .limit(10)
  .findAllAsync();

// Skip first 20, get next 10
final paginated = await isar.shoes
  .where()
  .offset(20)
  .limit(10)
  .findAllAsync();

Distinct

// Get unique sizes
final uniqueSizes = await isar.shoes
  .where()
  .distinctBySize()
  .findAllAsync();

Best Practices

  1. Use Where Clauses with Indexes

    // ✅ Fast - uses index (price is indexed)
    await isar.products.where().priceEqualTo(500).findAllAsync();
    
    // ❌ Slow - scans all records (name is not indexed)
    await isar.products.where().nameEqualTo('iPhone').findAllAsync();
  2. Combine Where and Filter

    // ✅ Optimal - index + filter
    await isar.products
      .where()
      .nameEqualTo('iPhone')
      .where()
      .priceGreaterThan(500)
      .findAllAsync();
  3. Group Nested Queries

    // ✅ Efficient
    .brand((q) => q.nameEqualTo('BMW').and().countryEqualTo('Germany'))
    
    // ❌ Inefficient
    .brand((q) => q.nameEqualTo('BMW'))
    .and()
    .brand((q) => q.countryEqualTo('Germany'))

Next Steps

Last Update