Сортировка: sortBy() и reverse()

Основы сортировки в Dexie.js

В Dexie.js сортировка данных выполняется поверх индексированных запросов IndexedDB и базируется на порядке значений в индексах. В отличие от SQL-подобных систем, где сортировка может применяться к произвольным полям через ORDER BY, в Dexie.js эффективная сортировка достигается за счёт использования индексов или постобработки результатов.

Ключевые инструменты сортировки:

  • sortBy() — извлекает данные, отсортированные по указанному полю
  • reverse() — инвертирует порядок уже отсортированного набора данных

Эти методы применяются к объекту Collection, который формируется после вызова where() или toCollection().


Метод sortBy(): базовая сортировка по индексу

sortBy() используется для получения массива записей, отсортированных по конкретному полю. Важно понимать, что этот метод требует, чтобы поле было индексировано, иначе Dexie.js выполнит полное сканирование таблицы.

Сигнатура:

table.sortBy(index)

Возвращаемое значение:

  • Promise<Array> — массив объектов, отсортированных по значению указанного индекса

Принцип работы sortBy()

При вызове sortBy() Dexie.js:

  1. Проверяет наличие индекса для указанного поля
  2. Использует B-tree структуру IndexedDB для быстрого обхода
  3. Возвращает отсортированный массив без дополнительной сортировки в JavaScript (если индекс существует)

Если индекс отсутствует:

  • происходит полное чтение таблицы
  • сортировка выполняется в памяти

Пример использования sortBy()

db.users.toArray()
db.users.orderBy('age').toArray()
db.users.orderBy('age').sortBy('age')

Более корректный и типичный вариант:

db.users.orderBy('age').toArray()

или:

db.users.toCollection().sortBy('age')

Сортировка по возрастанию

По умолчанию sortBy() всегда возвращает данные в порядке возрастания:

const users = await db.users.toCollection().sortBy('age')

Результат:

  • 18
  • 25
  • 30
  • 45

Ограничения sortBy()

Несмотря на удобство, метод имеет ряд ограничений:

  • невозможно комбинировать с фильтрацией внутри индекса (используются отдельные операции where())
  • отсутствует поддержка сложных выражений (например, вычисляемых полей)
  • всегда возвращает массив целиком, без ленивой итерации
  • не поддерживает пагинацию напрямую

Производительность sortBy()

При наличии индекса:

  • операция выполняется быстро
  • используется внутренний порядок IndexedDB

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

  • происходит полное сканирование таблицы
  • сортировка выполняется через Array.sort()
  • сложность возрастает до O(n log n)

Метод reverse(): инверсия порядка

reverse() применяется к коллекции и изменяет направление сортировки на обратное. Он не сортирует данные самостоятельно, а лишь инвертирует уже заданный порядок.

Сигнатура:

collection.reverse()

Возвращает:

  • тот же Collection, но с изменённым направлением обхода

Принцип работы reverse()

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

  • forward — стандартное направление (ASC)
  • reverse — обратное направление (DESC)

Метод влияет только на итерацию, а не на данные.


Пример использования reverse()

const users = await db.users
    .orderBy('age')
    .reverse()
    .toArray()

Результат:

  • 45
  • 30
  • 25
  • 18

Комбинирование reverse() с where()

const result = await db.orders
    .where('status')
    .equals('completed')
    .reverse()
    .toArray()

Здесь:

  • сначала фильтрация по индексу status
  • затем инверсия порядка результатов

Отличие reverse() от sortBy()

sortBy():

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

reverse():

  • не сортирует данные
  • меняет направление уже существующего порядка
  • работает только в рамках Collection

Типичные сценарии использования

1. Получение самых новых записей

const latest = await db.logs
    .orderBy('timestamp')
    .reverse()
    .limit(10)
    .toArray()

2. Топ-N значений по убыванию

const topScores = await db.players
    .orderBy('score')
    .reverse()
    .limit(5)
    .toArray()

3. Сортировка без индекса (неэффективный вариант)

const users = await db.users.toCollection().sortBy('name')

Особенности поведения при цепочках вызовов

Dexie.js применяет сортировку и направление в строгом порядке:

  1. where() формирует выборку
  2. orderBy() задаёт базовую сортировку
  3. reverse() инвертирует порядок
  4. limit() ограничивает результат
  5. toArray() материализует коллекцию

Любое изменение порядка шагов может привести к другому результату.


Влияние индексов на сортировку

Индексы играют ключевую роль:

db.version(1).stores({
    users: '++id, age, name'
})

При наличии индекса age:

db.users.orderBy('age').reverse()

Dexie использует индекс напрямую, избегая дополнительной сортировки.


Частые ошибки при использовании

  • использование sortBy() на неиндексированном поле без понимания стоимости операции
  • ожидание, что reverse() изменяет данные (он меняет только порядок обхода)
  • попытка комбинировать sortBy() и reverse() как взаимозаменяемые операции
  • игнорирование индексов при проектировании схемы

Сравнение поведения в памяти и через IndexedDB

При индексе:

  • сортировка происходит на уровне базы
  • минимальная нагрузка на JavaScript

Без индекса:

  • все данные загружаются в память
  • сортировка выполняется через Array.prototype.sort()
  • возможны задержки при больших объёмах данных

Использование с ограничением выборки

const recent = await db.events
    .orderBy('date')
    .reverse()
    .limit(20)
    .toArray()

Такой подход часто используется для:

  • логов
  • сообщений
  • транзакций
  • истории действий

Поведение при равных значениях

Если несколько записей имеют одинаковое значение индекса:

  • порядок между ними не гарантируется
  • зависит от внутреннего порядка хранения IndexedDB

Для стабильной сортировки используется составной индекс:

db.version(1).stores({
    users: '++id, age, name'
})

и дополнительная логика в приложении.