В IndexedDB, на котором построена Dexie.js, составные (compound) ключи представляют собой упорядоченные наборы значений, объединённые в один индекс. Dexie.js расширяет эту модель, позволяя выполнять эффективные диапазонные запросы по нескольким полям одновременно, используя лексикографический порядок сравнения.
Схема таблицы с составным индексом задаётся через строку вида:
db.version(1).stores({
orders: '++id, [customerId+createdAt], status'
});
Индекс [customerId+createdAt] означает, что значения
двух полей объединяются в один упорядоченный ключ, где сравнение
происходит сначала по customerId, а затем по
createdAt.
Составные ключи сравниваются по принципу лексикографического порядка, аналогично сортировке строк:
Пример ключей:
[1, "2024-01-01"]
[1, "2024-02-01"]
[2, "2024-01-01"]
Порядок будет строго следовать первому элементу, а внутри него — второму.
Это поведение критично для диапазонных запросов, так как границы диапазона должны учитывать структуру ключа.
Dexie.js позволяет выполнять запросы через where() с
указанием составного индекса:
db.orders
.where('[customerId+createdAt]')
.between([1, '2024-01-01'], [1, '2024-12-31'])
.toArray();
Здесь диапазон ограничен строго внутри одного
customerId, а второе поле используется как временной
диапазон.
Такой запрос эквивалентен:
customerId = 1createdAtНаиболее эффективная стратегия работы с compound индексами заключается в фиксации первого элемента:
db.orders
.where('[customerId+createdAt]')
.between(
[5, Dexie.minKey],
[5, Dexie.maxKey]
)
.toArray();
Здесь используются специальные границы:
Dexie.minKey — минимально возможное значениеDexie.maxKey — максимально возможное значениеЭто позволяет выбрать все записи конкретного клиента независимо от даты.
Метод between() работает не только с полными значениями,
но и с частично ограниченными диапазонами:
db.orders
.where('[customerId+createdAt]')
.between([1, '2024-01-01'], [10, '2024-01-31'])
.toArray();
В этом случае диапазон включает:
customerId от 1 до 10Однако важно учитывать: сравнение происходит по всему составному ключу, а не по отдельным полям независимо.
Dexie.js предоставляет методы above(),
below(), aboveOrEqual(),
belowOrEqual():
db.orders
.where('[customerId+createdAt]')
.above([5, '2024-01-01'])
.toArray();
Для составных ключей это означает:
Пример результата:
[5, '2024-01-02'] попадёт[5, '2023-12-31'] не попадёт[6, '2020-01-01'] попадёт даже если дата меньше, потому
что первый элемент большеЭто поведение часто становится источником логических ошибок при неправильной интерпретации диапазонов.
Наиболее распространённый паттерн — фиксация префикса и диапазон по второму полю:
const start = [userId, new Date('2024-01-01')];
const end = [userId, new Date('2024-02-01')];
db.orders
.where('[userId+createdAt]')
.between(start, end)
.toArray();
Такая структура позволяет:
Составные ключи в Dexie.js используют строгое сравнение типов, но порядок чисел и строк различается:
Проблема возникает при смешанных типах:
[1, "2"]
[1, "10"]
Строковое сравнение даст неожиданный результат:
"10" < "2"
Поэтому при проектировании диапазонов важно нормализовать типы:
Date или timestampNumber"02",
"10")Date-объекты в IndexedDB сравниваются как числовые timestamps:
db.events.where('[userId+date]')
.between([1, new Date('2024-01-01')], [1, new Date('2024-01-31')])
Это делает диапазонные запросы по времени стабильными и предсказуемыми.
Однако важно избегать:
Специальные значения позволяют расширять диапазоны до границ ключевого пространства:
db.orders
.where('[customerId+createdAt]')
.between(
[5, Dexie.minKey],
[5, Dexie.maxKey]
)
Поведение:
Dexie.minKey включает все возможные значения второго
элементаDexie.maxKey завершает диапазон максимально поздним
значениемЭто используется для:
Составные ключи позволяют реализовать стабильную пагинацию:
db.orders
.where('[customerId+createdAt]')
.between([5, lastSeenDate], [5, Dexie.maxKey])
.limit(50)
Такой подход гарантирует:
Несмотря на гибкость, существуют ограничения:
Индекс [a+b] не эквивалентен [b+a]. Запросы
по второму полю без первого становятся неэффективными или
невозможными:
.where('[customerId+createdAt]') // эффективно
.where('createdAt') // другой индекс нужен
Запрос вида:
createdAtне использует [customerId+createdAt] индекс.
Например:
[1, 100]
[2, 1]
Второй элемент не определяет глобальный порядок без учёта первого.
Для повышения эффективности:
Пример оптимальной схемы:
'userId, [userId+createdAt]'
db.logs.where('[userId+timestamp]')
.between(
[userId, startDate],
[userId, endDate]
)
db.logs.where('[userId+timestamp]')
.between(
[userId, Dexie.minKey],
[userId, Dexie.maxKey]
)
db.logs.where('[userId+timestamp]')
.above([userId, lastSeen])
Результаты всегда возвращаются в порядке составного ключа:
customerId ASC → createdAt ASC
Это гарантирует: