Метод where() с составным индексом через [field1+field2]

Принцип составного индекса в IndexedDB

Составной индекс в Dexie.js основан на возможностях IndexedDB хранить ключи, состоящие из нескольких полей. Такой индекс формируется как единая лексикографическая структура, где значения сравниваются последовательно: сначала первое поле, затем второе и далее по порядку.

В Dexie.js составной индекс объявляется через синтаксис:

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

Выражение [userId+createdAt] создаёт индекс, в котором:

  • userId — первичный компонент сортировки
  • createdAt — вторичный компонент сортировки

Такой индекс позволяет эффективно выполнять запросы, где фильтрация идёт по комбинации двух полей одновременно.


Логика работы [field1+field2]

Составной индекс в Dexie.js не является простым объединением значений в строку. Внутри IndexedDB используется упорядоченная структура ключей:

[userId, createdAt]

Сравнение выполняется лексикографически:

  1. Сначала сравнивается userId
  2. Если userId совпадает — сравнивается createdAt

Это означает, что индекс оптимален для запросов, где:

  • фиксирован userId
  • или используется диапазон по createdAt внутри одного userId

Использование where() с составным индексом

Метод where() позволяет обращаться к составному индексу как к единому ключу.

db.orders
  .where('[userId+createdAt]')
  .equals([123, 1700000000000])
  .toArray();

Здесь важно, что:

  • передаётся массив значений в том же порядке, что и в индексе
  • соответствие должно быть точным при использовании equals()

Структура запроса equals()

Формат значения для equals() строго соответствует структуре индекса:

.where('[field1+field2]')
.equals([value1, value2])

Пример:

db.orders
  .where('[userId+createdAt]')
  .equals([42, 1710000000000])
  .toArray();

Dexie преобразует это в IndexedDB range с точным совпадением обоих компонентов.


Диапазонные запросы с составными индексами

Составной индекс особенно полезен для диапазонов, но с ограничением: диапазонность корректно работает только начиная с последнего заданного префикса.

Диапазон по второму полю при фиксированном первом

db.orders
  .where('[userId+createdAt]')
  .between(
    [42, 1700000000000],
    [42, 1710000000000]
  )
  .toArray();

В этом случае:

  • userId фиксирован
  • диапазон применяется только к createdAt

Это основной сценарий использования составных индексов.


Ограничения составных индексов

Составной индекс имеет строгие ограничения, связанные с моделью IndexedDB:

1. Нельзя пропускать первый компонент

Запрос:

.where('[userId+createdAt]').equals([null, 1700000000000])

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


2. Диапазоны работают слева направо

Корректные варианты:

  • [userId, *range on createdAt*] — допустимо
  • [*range on userId*, createdAt] — допустимо, но ограниченно
  • [*range on both*] — возможно, но менее предсказуемо

3. Сортировка всегда лексикографическая

Составной индекс не поддерживает независимую сортировку по второму полю без фиксации первого.


Отличие between(), above(), below()

Dexie.js предоставляет несколько методов для работы с составными индексами:

between()

Используется для диапазона:

.where('[userId+createdAt]')
.between([10, 0], [10, Infinity])

above()

.where('[userId+createdAt]')
.above([10, 1700000000000])

below()

.where('[userId+createdAt]')
.below([10, 1710000000000])

Во всех случаях массив интерпретируется как единый ключ сравнения.


Особенности сортировки результатов

Результаты, полученные через составной индекс, уже отсортированы в порядке индекса:

userId ASC → createdAt ASC

Это означает:

  • дополнительный orderBy() не требуется
  • сортировка гарантирована IndexedDB

Использование compound index в реальных структурах

Сценарий: заказы пользователя

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

Запрос последних заказов пользователя:

db.orders
  .where('[userId+createdAt]')
  .between(
    [42, 0],
    [42, Date.now()]
  )
  .reverse()
  .toArray();

Композиция нескольких составных индексов

Dexie.js позволяет определять несколько составных индексов для одной таблицы:

stores({
  logs: "++id, level, module, timestamp, [module+timestamp], [level+timestamp]"
});

Каждый индекс оптимизирует свой сценарий:

  • [module+timestamp] — поиск логов модуля
  • [level+timestamp] — фильтрация по уровню и времени

Поведение equals() при частичном совпадении

Составной индекс требует полного совпадения структуры ключа:

.where('[userId+createdAt]').equals([42])

такой вызов некорректен, так как отсутствует второй компонент.


Оптимизация запросов через правильный порядок полей

Порядок полей в [field1+field2] критически важен:

  • поле с наибольшей селективностью должно быть первым
  • поле для диапазонов — вторым

Пример неэффективного индекса:

[createdAt+userId]

Проблема:

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

Правильный вариант:

[userId+createdAt]

Работа с undefined и null значениями

IndexedDB и Dexie.js различают:

  • undefined — отсутствующее значение
  • null — явное значение

В составных индексах:

  • null допустим как часть ключа
  • undefined обычно исключается из индексации

Поведение with continuation cursors

При обходе через each() или toArray() составной индекс используется как cursor:

db.orders
  .where('[userId+createdAt]')
  .above([42, 0])
  .each(order => {
    console.log(order);
  });

Cursor перемещается по лексикографическому порядку:

(userId, createdAt)

Типичные ошибки при работе с where(‘[field1+field2]’)

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

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

Если составной индекс не объявлен:

.where('[userId+createdAt]')

Dexie.js выбрасывает ошибку, так как IndexedDB не может выполнить запрос без предварительно созданного индекса.


Влияние на производительность

Составные индексы значительно уменьшают необходимость:

  • полного сканирования таблицы
  • последующей фильтрации в JavaScript

Вместо этого IndexedDB выполняет:

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

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