Составные индексы в Dexie.js представляют собой механизм индексирования, при котором в качестве ключа используется комбинация нескольких полей объекта. Такая конструкция позволяет эффективно выполнять запросы по нескольким атрибутам одновременно, избегая необходимости сканирования всей таблицы и существенно ускоряя выборки при сложной фильтрации.
В основе Dexie.js лежит IndexedDB, где составные индексы реализованы
через строковую декларацию вида [field1+field2+field3].
Порядок полей в такой конструкции имеет принципиальное значение: он
определяет лексикографический порядок сортировки и правила использования
индекса при запросах.
Составной индекс задаётся внутри схемы версии базы данных:
const db = new Dexie("app-db");
db.version(1).stores({
users: "++id, firstName, lastName, age, [firstName+lastName]"
});
Здесь индекс [firstName+lastName] создаёт
комбинированный ключ, по которому можно выполнять быстрые выборки.
Допускается также использование составного индекса как первичного ключа:
db.version(1).stores({
users: "[firstName+lastName], age"
});
В этом случае комбинация firstName + lastName становится
уникальным идентификатором записи, а отдельного id уже
нет.
Составной индекс хранит значения как упорядоченную пару (или кортеж), которая сравнивается лексикографически:
[firstName, lastName]
Сравнение происходит поэтапно:
firstNamelastNameЭто влияет на:
Пример упорядочивания:
("Alex", "Brown")
("Alex", "Cooper")
("Bob", "Adams")
("Bob", "Smith")
Dexie.js предоставляет API where для работы с индексами.
Для составных индексов используется массив значений:
db.users
.where("[firstName+lastName]")
.equals(["Alex", "Brown"])
.toArray();
Этот запрос использует индекс напрямую и выполняется за O(log n), избегая полного перебора таблицы.
Составной индекс особенно полезен при строгом совпадении нескольких полей:
db.users
.where("[firstName+lastName]")
.equals(["John", "Doe"])
.first();
Такой подход заменяет комбинацию:
where(firstName).equals(...)lastNameчто значительно эффективнее.
Составные индексы поддерживают диапазоны, но только с соблюдением правил лексикографического порядка.
Пример:
db.users
.where("[firstName+lastName]")
.between(
["A", ""],
["B", ""],
true,
true
)
.toArray();
Этот запрос выбирает все записи, где firstName находится
в диапазоне от “A” до “B”.
Важно понимать, что диапазон корректно работает только по левому префиксу индекса.
Одно из ключевых ограничений составных индексов заключается в том, что запросы эффективно работают только начиная с первого поля индекса.
Индекс:
[firstName+lastName+age]
Поддерживаемые сценарии:
Неподдерживаемые или неэффективные:
Это связано с тем, что структура индекса физически отсортирована по первому полю.
Dexie позволяет сортировать данные по составному индексу:
db.users
.orderBy("[firstName+lastName]")
.toArray();
Результат будет отсортирован сначала по firstName, затем
по lastName.
Если требуется обратный порядок:
db.users
.orderBy("[firstName+lastName]")
.reverse()
.toArray();
Составные индексы не поддерживают произвольные частичные фильтры по правым полям без указания левых.
Например, нельзя эффективно выполнить:
db.users
.where("[firstName+lastName]")
.equals(["", "Smith"]);
Потому что firstName должен участвовать в поиске.
В таких случаях применяются альтернативные подходы:
[lastName].filter() (с потерей
производительности)Если индекс используется как уникальный, Dexie гарантирует отсутствие дубликатов комбинации значений:
db.version(1).stores({
users: "++id, [email+country]"
});
Теперь невозможно добавить два объекта с одинаковыми
email и country.
Данные добавляются обычным способом:
await db.users.add({
firstName: "Alex",
lastName: "Brown",
age: 32
});
Dexie автоматически формирует ключ индекса
[firstName+lastName] из объекта.
При изменении любого поля, входящего в составной индекс, запись автоматически переиндексируется:
await db.users.update(1, {
firstName: "Alexander"
});
В этом случае:
Частые обновления индексируемых полей могут влиять на производительность.
Использование составных индексов даёт значительный выигрыш при:
Однако избыточное индексирование приводит к:
Баланс между количеством индексов и скоростью операций является ключевым фактором проектирования схемы.
Dexie позволяет объявлять несколько комбинированных индексов:
db.version(1).stores({
users: "++id, firstName, lastName, age, [firstName+lastName], [lastName+age]"
});
Это даёт гибкость запросов:
При этом каждый индекс поддерживает свою B-tree структуру.
Составные индексы хорошо сочетаются с цепочками Dexie:
db.users
.where("[firstName+lastName]")
.between(["A", ""], ["M", "zzz"])
.filter(user => user.age > 18)
.limit(50)
.toArray();
Здесь:
.filter() применяется уже к уменьшенному набору
данныхАльтернативой составному индексу является создание двух отдельных индексов:
db.version(1).stores({
users: "++id, firstName, lastName"
});
Однако запрос:
db.users
.where("firstName").equals("Alex")
.and(u => u.lastName === "Brown")
будет менее эффективным, чем:
.where("[firstName+lastName]").equals(["Alex", "Brown"])
Причина в том, что составной индекс сразу использует готовую сортированную структуру.
Часто встречающиеся проблемы:
.filter() вместо корректного индексаЭти ошибки приводят к деградации производительности и росту времени выборки.
Составной индекс оправдан, если:
Если же поля используются независимо, предпочтительнее отдельные индексы или комбинация нескольких простых запросов.