Составной индекс в 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]
Сравнение выполняется лексикографически:
userIduserId совпадает — сравнивается
createdAtЭто означает, что индекс оптимален для запросов, где:
userIdcreatedAt внутри одного
userIdМетод where() позволяет обращаться к составному индексу
как к единому ключу.
db.orders
.where('[userId+createdAt]')
.equals([123, 1700000000000])
.toArray();
Здесь важно, что:
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:
Запрос:
.where('[userId+createdAt]').equals([null, 1700000000000])
не имеет смысла в терминах индексации, так как второе поле не может быть использовано без первого.
Корректные варианты:
[userId, *range on createdAt*] — допустимо[*range on userId*, createdAt] — допустимо, но
ограниченно[*range on both*] — возможно, но менее
предсказуемоСоставной индекс не поддерживает независимую сортировку по второму полю без фиксации первого.
Dexie.js предоставляет несколько методов для работы с составными индексами:
Используется для диапазона:
.where('[userId+createdAt]')
.between([10, 0], [10, Infinity])
.where('[userId+createdAt]')
.above([10, 1700000000000])
.where('[userId+createdAt]')
.below([10, 1710000000000])
Во всех случаях массив интерпретируется как единый ключ сравнения.
Результаты, полученные через составной индекс, уже отсортированы в порядке индекса:
userId ASC → createdAt ASC
Это означает:
orderBy() не требуется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] — фильтрация по уровню и времениСоставной индекс требует полного совпадения структуры ключа:
.where('[userId+createdAt]').equals([42])
такой вызов некорректен, так как отсутствует второй компонент.
Порядок полей в [field1+field2] критически важен:
Пример неэффективного индекса:
[createdAt+userId]
Проблема:
Правильный вариант:
[userId+createdAt]
IndexedDB и Dexie.js различают:
undefined — отсутствующее значениеnull — явное значениеВ составных индексах:
null допустим как часть ключаundefined обычно исключается из индексацииПри обходе через each() или toArray()
составной индекс используется как cursor:
db.orders
.where('[userId+createdAt]')
.above([42, 0])
.each(order => {
console.log(order);
});
Cursor перемещается по лексикографическому порядку:
(userId, createdAt)
Если составной индекс не объявлен:
.where('[userId+createdAt]')
Dexie.js выбрасывает ошибку, так как IndexedDB не может выполнить запрос без предварительно созданного индекса.
Составные индексы значительно уменьшают необходимость:
Вместо этого IndexedDB выполняет:
Это особенно важно при больших объёмах записей, где без индекса сложность запроса становится линейной относительно всей таблицы.