Индексы в Dexie.js определяют структуру доступа к данным и напрямую влияют на производительность запросов в IndexedDB. Неправильный выбор индексов приводит либо к избыточному расходу памяти и замедлению записи, либо к деградации чтения и необходимости полного перебора таблиц.
Dexie.js выступает тонким слоем над IndexedDB и наследует его модель индексации. Каждая таблица может иметь один первичный ключ и набор вторичных индексов. Индекс создаёт дополнительную структуру данных, позволяющую выполнять выборки не через полный перебор, а через B-tree поиск.
Каждый индекс дублирует часть данных таблицы в отсортированном виде. Это означает:
Баланс между этими факторами и определяет архитектуру индексов.
Dexie.js поддерживает несколько форм индексации, каждая из которых решает отдельный класс задач.
Обычный индекс создаётся для одного поля:
db.version(1).stores({
users: '++id, email, age'
});
В этом случае email и age индексируются
независимо. Такие индексы подходят для фильтрации и сортировки по
отдельным атрибутам.
Особенность заключается в том, что каждый индекс оптимизирован под одиночный ключ, а комбинирование условий происходит уже на уровне выполнения запроса.
Составной индекс объединяет несколько полей:
db.version(2).stores({
orders: '++id, [userId+createdAt]'
});
Такой индекс полезен при запросах, где одновременно используются несколько условий:
db.orders
.where('[userId+createdAt]')
.between([1, 0], [1, Date.now()])
Порядок полей в составном индексе критически важен. Индекс
[userId+createdAt] эффективен для фильтрации по
userId, а затем по диапазону createdAt.
Обратный порядок делает такие запросы невозможными или
неэффективными.
MultiEntry индекс применяется к массивам:
db.version(3).stores({
posts: '++id, tags',
}).upgrade(tx => {
tx.table('posts').toCollection().modify(post => {
post.tags = post.tags || [];
});
});
Индекс может быть объявлен как multiEntry:
db.version(4).stores({
posts: '++id, tags',
}).upgrade(tx => {
db.posts.schema.indexes.forEach(i => i.multi = true);
});
Идея заключается в том, что каждый элемент массива становится отдельной точкой индексации. Это позволяет эффективно выполнять запросы по тегам, категориям и другим множественным признакам.
Проектирование индексов требует анализа характера запросов, а не структуры данных.
Индексы должны соответствовать наиболее частым операциям чтения. Поля, которые используются редко, не должны индексироваться без необходимости, поскольку каждая запись будет дороже.
Кардинальность определяет количество уникальных значений:
При низкой кардинальности индекс часто возвращает большие наборы данных, что снижает его ценность.
Если запросы часто используют несколько полей одновременно, предпочтительнее составной индекс вместо нескольких одиночных.
Пример неэффективной схемы:
users: '++id, country, city'
Запрос:
db.users.where('country').equals('KZ')
.and(user => user.city === 'Karaganda')
Такой подход приводит к дополнительной фильтрации после выборки.
Оптимизированный вариант:
users: '++id, [country+city]'
Порядок в compound индексах определяет возможность использования диапазонов.
Правило выбора:
Пример:
[status+createdAt]
Позволяет выполнять:
status = 'active' AND createdAt BETWEEN ...status = 'active' AND createdAt > ...Но не позволяет эффективно выполнять:
createdAt BETWEEN ... без указания
status.Каждый индекс увеличивает стоимость операций записи. Избыточность возникает, когда индекс покрывает поля, которые редко используются в запросах.
Типичный антипример:
users: '++id, email, phone, age, country, city, gender, createdAt'
Такая схема приводит к:
Индексирование должно быть минимально достаточным.
Dexie.js не поддерживает классические covering indexes как SQL, но эффект достигается за счёт структуры запроса. Если индекс содержит все поля, используемые в фильтрации и сортировке, чтение выполняется без обращения к основной таблице.
Пример:
db.orders.where('[userId+status+createdAt]').between(...)
При правильной структуре индекса большинство операций выполняется на уровне B-tree без дополнительных lookups.
IndexedDB использует индексы для сортировки результатов. Если запрос
требует orderBy, но индекс отсутствует, происходит полный
перебор.
Пример:
db.users.orderBy('createdAt')
Оптимальный вариант — наличие индекса:
users: '++id, createdAt'
Сортировка по неиндексированному полю приводит к загрузке всей коллекции в память.
Dexie.js не поддерживает произвольные SQL-подобные планы выполнения. Это означает, что структура индексов должна заранее учитывать все возможные формы запросов.
Типичная проблема:
В таких случаях применяется компромисс:
Изменение индексов требует увеличения версии базы:
db.version(5).stores({
users: '++id, [country+city], createdAt'
});
При изменении структуры:
Переиндексация является дорогостоящей операцией и особенно заметна при больших объёмах данных.
Наиболее распространённые ошибки:
Каждая из этих ошибок приводит либо к избыточной нагрузке на запись, либо к деградации чтения.
Рациональный подход строится вокруг анализа запросов:
Эта модель приводит к схеме, где каждый индекс обслуживает конкретный класс запросов, а не абстрактную структуру данных.
Dexie.js и IndexedDB в целом оптимизированы под сценарии, где чтение преобладает над записью. Индексы усиливают этот дисбаланс: чтение становится быстрее, запись — дороже.
Оптимальная конфигурация индексов всегда определяется профилем нагрузки: