Подсчёт записей: count()

Базовое назначение метода count()

Метод count() в Dexie.js используется для получения количества записей, удовлетворяющих определённому запросу, без необходимости загружать сами данные в память. Он возвращает Promise<number>, что делает его полностью асинхронным и интегрированным с моделью работы IndexedDB.

Основная цель:

  • быстро определить размер выборки
  • избежать загрузки больших массивов данных
  • выполнять агрегирующие операции на уровне IndexedDB

count() на уровне таблицы (Table.count())

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

const db = new Dexie("AppDB");

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

const totalUsers = await db.users.count();
console.log(totalUsers);

В этом случае Dexie использует нативный механизм IndexedDB count() по индексу или по primary key, что обеспечивает минимальные накладные расходы.

Особенности:

  • работает без фильтров
  • использует внутренние индексы IndexedDB
  • не требует перебора курсором

Подсчёт через where() и индексы

При использовании индексированных запросов count() остаётся эффективным, так как IndexedDB может ограничить диапазон напрямую по индексу.

const adminsCount = await db.users
  .where("role")
  .equals("admin")
  .count();

Или диапазон:

const adultsCount = await db.users
  .where("age")
  .aboveOrEqual(18)
  .count();

В этих случаях:

  • используется индекс role или age
  • выполняется ограниченный проход по индексному диапазону
  • не происходит загрузки объектов записей

Поведение при составных индексах

При работе с compound-index Dexie использует лексикографический порядок ключей IndexedDB.

db.version(1).stores({
  orders: "++id, [status+priority], status, priority"
});

const count = await db.orders
  .where("[status+priority]")
  .between(["open", 1], ["open", 5])
  .count();

Особенности:

  • диапазон вычисляется на уровне индекса
  • эффективность сохраняется при корректной структуре индекса
  • критически важно соблюдать порядок полей

count() и filter(): полная потеря оптимизации

Использование filter() переводит выполнение в режим перебора всех записей.

const count = await db.users
  .filter(user => user.age % 2 === 0)
  .count();

Поведение:

  • IndexedDB не может использовать индекс
  • происходит полный проход по таблице
  • каждая запись проверяется вручную

Фактически это эквивалент:

  • загрузка всех записей
  • применение JS-функции
  • подсчёт результата

На больших объёмах данных это становится дорогостоящей операцией.


Отличие count() от toArray().length

Частая ошибка — использовать загрузку массива для подсчёта:

const count = (await db.users.toArray()).length;

Сравнение:

Метод Поведение Производительность
count() подсчёт на уровне IndexedDB высокая
toArray().length загрузка всех данных низкая

count() всегда предпочтителен при отсутствии необходимости работать с данными.


Поведение в цепочках запросов

count() может быть частью цепочек Dexie Query API.

const activeAdmins = await db.users
  .where("role").equals("admin")
  .and(user => user.active === true)
  .count();

Здесь важно:

  • первая часть (where) использует индекс
  • and() уже выполняется в JS
  • итоговый count() зависит от количества отфильтрованных результатов

Следствие:

  • при наличии and() оптимизация частично теряется
  • IndexedDB не может полностью предсказать результат

count() в Collection и Query

Dexie разделяет поведение:

  • Table.count() — прямой доступ к таблице
  • Collection.count() — после where()
  • Query.count() — после orderBy() или цепочек

Пример с сортировкой:

const count = await db.users
  .orderBy("age")
  .above(30)
  .count();

Даже при orderBy() используется индекс, но:

  • сортировка может добавить накладные расходы
  • диапазон всё ещё ограничивается индексом

Ограничения производительности

Метод count() может деградировать в следующих случаях:

1. filter() или and()

Полный перебор всех записей.

2. Отсутствие индекса

Если поле не индексировано:

db.users.where("nonIndexedField").equals("x").count();

Dexie вынужден сканировать всю таблицу.

3. Большие диапазоны данных

Даже индексированный count может быть дорогим при миллионах записей, если диапазон широкий.


Поведение внутри транзакций

count() выполняется внутри текущей транзакции и подчиняется её ограничениям.

db.transaction("r", db.users, async () => {
  const count = await db.users.count();
  console.log(count);
});

Особенности:

  • гарантированная консистентность данных
  • отсутствие race conditions внутри транзакции
  • блокировка соответствующего object store

Асинхронная модель выполнения

Все варианты count() возвращают Promise:

const count = await db.users.count();

или

db.users.count().then(c => {
  console.log(c);
});

Характеристики:

  • неблокирующий вызов
  • выполнение в event loop браузера
  • интеграция с async/await

Оптимизационные рекомендации на уровне архитектуры

Поведение count() напрямую зависит от схемы базы:

  • индексированные поля обеспечивают O(log n) или близкое к O(1)
  • неиндексированные поля приводят к O(n)
  • filter() всегда O(n)

Пример оптимальной схемы:

db.version(1).stores({
  logs: "++id, type, createdAt, status"
});

Подсчёты:

await db.logs.where("status").equals("error").count();
await db.logs.where("createdAt").above(Date.now() - 86400000).count();

Поведение при изменении данных во время подсчёта

Dexie использует snapshot-поведение IndexedDB транзакций:

  • данные фиксируются на момент старта транзакции
  • изменения вне транзакции не влияют на результат
  • внутри транзакции результат стабилен

Частые сценарии использования

  • подсчёт уведомлений
  • отображение количества элементов в UI
  • пагинация (расчёт total items)
  • аналитика без загрузки данных

Пример для пагинации:

const total = await db.products.where("category").equals("books").count();
const pages = Math.ceil(total / pageSize);

Различие между count() и ручной агрегацией

Dexie не предоставляет серверных агрегатов кроме count(), поэтому:

  • count() — единственная нативная агрегирующая операция
  • сложные подсчёты требуют filter() или and()
  • IndexedDB не поддерживает SQL-подобные агрегаты