Фильтрация через where()

Dexie.js представляет собой тонкий, но мощный слой над IndexedDB, упрощающий работу с локальными базами данных в браузере. Центральным инструментом выборки данных в Dexie является метод where(), который позволяет выполнять индексированные запросы, фильтрацию по диапазонам, точные совпадения и комбинированные условия через цепочки операторов.

Метод where() применяется к таблице (Table) и всегда опирается на индексированное поле. Это ключевая особенность: в отличие от filter(), который выполняет постфильтрацию в памяти, where() использует индексы IndexedDB и работает значительно быстрее.

Базовый синтаксис:

db.table.where('fieldName')

После выбора поля возвращается объект запроса, поддерживающий дальнейшую фильтрацию через методы:

  • equals()
  • above(), below()
  • between()
  • startsWith()
  • anyOf(), noneOf()

Требование индексации

where() работает только с индексированными полями. Это означает, что схема базы данных должна явно объявлять индекс:

const db = new Dexie('AppDB');

db.version(1).stores({
  users: '++id, name, age, city'
});

Здесь name, age и city могут использоваться в where().

Попытка выполнить where() по неиндексированному полю приведёт к ошибке выполнения.

Точное совпадение equals()

Самый базовый вариант фильтрации — поиск по точному совпадению:

db.users.where('city').equals('Almaty').toArray();

Такой запрос вернёт всех пользователей, у которых поле city строго равно "Almaty".

Диапазонные запросы: above, below, between

Dexie поддерживает эффективные диапазонные операции, которые особенно полезны для числовых и датированных данных.

above()

db.users.where('age').above(18).toArray();

Возвращает всех пользователей старше 18 лет.

below()

db.users.where('age').below(30).toArray();

Возвращает пользователей младше 30 лет.

between()

db.users.where('age').between(18, 30).toArray();

Метод between() позволяет задать диапазон. По умолчанию границы включительные/исключительные могут быть настроены:

db.users.where('age').between(18, 30, true, false);

Параметры:

  • первый boolean — включать нижнюю границу
  • второй boolean — включать верхнюю границу

Работа со строками: startsWith и префиксные запросы

Для текстовых полей часто требуется поиск по началу строки:

db.users.where('name').startsWith('A').toArray();

Такой запрос использует лексикографический порядок индекса и возвращает всех пользователей, чьи имена начинаются на “A”.

Это особенно эффективно для автодополнения и поиска по мере ввода.

Множественные значения: anyOf и noneOf

anyOf()

Позволяет выбрать записи, соответствующие любому значению из набора:

db.users.where('city').anyOf(['Almaty', 'Astana', 'Shymkent']).toArray();

Это аналог SQL-оператора IN.

noneOf()

Исключает набор значений:

db.users.where('city').noneOf(['Almaty', 'Astana']).toArray();

Такой подход полезен для фильтрации запрещённых или исключённых категорий.

Использование compound index с where()

Dexie поддерживает составные индексы, которые позволяют выполнять фильтрацию по нескольким полям одновременно.

Пример схемы:

db.version(1).stores({
  orders: '++id, [userId+status], createdAt'
});

Запрос:

db.orders.where('[userId+status]').equals([5, 'pending']).toArray();

Составной индекс требует строгого соответствия порядку полей.

Ограничения и важные особенности

Метод where() имеет несколько архитектурных ограничений, связанных с IndexedDB:

1. Только индексированные поля

Любое поле, не добавленное в схему индексов, не может использоваться.

2. Нет произвольных функций фильтрации

Нельзя применять callback-функции напрямую внутри where(). Для этого используется filter(), но он менее производителен.

3. Чувствительность к типам данных

IndexedDB различает типы строго:

  • 1 !== "1"
  • даты сравниваются как объекты Date

Комбинирование where() с сортировкой

После фильтрации можно применять сортировку:

db.users.where('age').above(18).sortBy('age');

Однако сортировка возможна только по текущему индексу или после преобразования в массив.

Более гибкий вариант:

db.users.where('age').above(18).toArray().then(arr => {
  arr.sort((a, b) => a.age - b.age);
});

Итерация через each()

Для обработки больших наборов данных без загрузки всего массива в память используется each():

db.users.where('age').above(18).each(user => {
  console.log(user.name);
});

Это потоковая обработка, полезная при больших объёмах данных.

Оптимизация запросов where()

Эффективность where() зависит от правильного проектирования индексов.

Рекомендации по структуре:

  • Индексировать поля, используемые в фильтрации
  • Использовать compound indexes для частых комбинаций условий
  • Избегать filter() для больших таблиц
  • Предпочитать startsWith() вместо частичного поиска через строки

Различие между where() и filter()

Ключевое различие:

  • where() — индексный доступ, быстрый
  • filter() — перебор всех записей, медленный

Пример filter():

db.users.filter(user => user.age > 18 && user.city === 'Almaty')

Этот подход эквивалентен, но не использует индексы.

Цепочки условий внутри where()

Некоторые методы можно комбинировать в цепочке:

db.users
  .where('age')
  .above(18)
  .and(user => user.city === 'Almaty')
  .toArray();

Однако and() частично уводит выполнение в память, снижая производительность.

Практика построения запросов

При проектировании запросов важно учитывать:

  • какие поля чаще всего участвуют в фильтрации
  • какие комбинации условий повторяются
  • какие диапазоны используются (даты, числовые значения)

Хорошо спроектированный where()-запрос превращается в O(log n)-подобную операцию благодаря индексам, тогда как неиндексированные подходы деградируют до O(n).

Поведение при пустых результатах

Если запрос не находит совпадений, возвращается пустой массив, а не ошибка:

db.users.where('city').equals('Nowhere').toArray()
// []

Это упрощает обработку результатов без необходимости try/catch.