Ограничение выборки: limit() и offset()

Работа с ограничением выборки в Dexie.js основана на механизмах курсорной навигации IndexedDB и позволяет управлять количеством возвращаемых записей, а также смещением начала выборки. Эти операции особенно важны при реализации пагинации, постраничного вывода данных и оптимизации запросов к локальной базе.

Dexie.js предоставляет два ключевых метода для этих целей: limit() и offset(). Они применяются к объекту запроса (Collection) и могут комбинироваться с фильтрацией, сортировкой и индексированными выборками.


Базовая структура работы с выборкой

Любая операция ограничения выборки в Dexie.js начинается с формирования коллекции:

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

После получения коллекции можно применять методы ограничения:

  • limit(n) — ограничивает количество возвращаемых записей
  • offset(n) — пропускает первые n записей

Метод limit(): ограничение количества результатов

Метод limit() задаёт верхнюю границу количества элементов, которые будут возвращены из коллекции. Он работает на уровне курсора и останавливает итерацию после достижения указанного числа записей.

Синтаксис

collection.limit(n)

где n — максимальное количество записей.

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

db.users
  .orderBy('age')
  .limit(10)
  .toArray()
  .then(users => {
    console.log(users);
  });

В данном случае будет возвращено не более 10 пользователей, отсортированных по возрасту.


Особенности работы limit()

limit() не изменяет исходные данные, а влияет только на выборку.

Ключевые особенности:

  • применяется после сортировки (orderBy)
  • работает с индексированными и неиндексированными запросами
  • прекращает обход курсора сразу после достижения лимита
  • может использоваться совместно с where() и filter()

Применение limit() с фильтрацией

db.users
  .where('role')
  .equals('admin')
  .limit(5)
  .toArray()

В этом случае сначала происходит поиск по индексу role, затем ограничение результата до 5 записей.


Метод offset(): смещение выборки

Метод offset() задаёт количество первых записей, которые необходимо пропустить перед началом возврата результата.

Синтаксис

collection.offset(n)

где n — количество пропускаемых элементов.

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

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

Результат будет содержать всех пользователей, начиная с 11-й записи в отсортированном списке.


Поведение offset() на уровне IndexedDB

В отличие от SQL-систем, где OFFSET может быть относительно дешёвым при индексах, в IndexedDB реализация Dexie.js использует курсорную прокрутку. Это означает:

  • пропуск происходит путём последовательного прохода по записям
  • производительность падает при больших значениях offset
  • фактическое время выполнения зависит от размера пропуска

Комбинирование offset() и limit()

Наиболее типичный сценарий — пагинация:

const pageSize = 20;
const pageNumber = 3;

db.users
  .orderBy('age')
  .offset(pageNumber * pageSize)
  .limit(pageSize)
  .toArray()

Логика:

  • пропустить pageNumber * pageSize записей
  • вернуть следующую порцию pageSize

Важный порядок применения методов

Порядок вызова методов имеет значение, так как Dexie.js строит цепочку обработки курсора:

collection.offset(10).limit(5)

и

collection.limit(5).offset(10)

могут давать одинаковый результат в простых случаях, но логически правильной и предсказуемой является последовательность:

orderBy(...).offset(...).limit(...)

Ограничения при использовании offset()

offset() имеет фундаментальные ограничения, связанные с природой IndexedDB:

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

Поэтому при больших наборах данных использование offset становится узким местом.


Оптимизация пагинации вместо offset()

В Dexie.js предпочтительнее использовать курсорную пагинацию через where() и ключи.

Пример пагинации по ключу

db.users
  .where('age')
  .above(lastSeenAge)
  .limit(20)
  .toArray()

Такой подход позволяет:

  • избежать полного прохода через offset
  • использовать индекс для перехода
  • повысить производительность

limit() и offset() внутри транзакций

При работе внутри транзакции:

db.transaction('r', db.users, async () => {
  const result = await db.users
    .orderBy('age')
    .offset(5)
    .limit(10)
    .toArray();

  return result;
});

поведение остаётся идентичным, но важно учитывать:

  • транзакция удерживает блокировку на время итерации курсора
  • большие offset увеличивают длительность транзакции
  • возможны блокировки других операций чтения/записи

Влияние сортировки на limit() и offset()

Без orderBy() поведение может стать недетерминированным:

db.users
  .limit(10)
  .toArray()

В этом случае порядок зависит от внутреннего порядка хранения IndexedDB, который не гарантируется.

Сортировка фиксирует последовательность:

db.users
  .orderBy('id')
  .offset(10)
  .limit(10)

Использование с compound queries

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

db.orders
  .orderBy('[userId+date]')
  .offset(20)
  .limit(10)
  .toArray()

Dexie.js применяет ограничения уже после построения упорядоченного курсора по композитному ключу.


Поведение при пустых выборках

Если результат фильтрации пустой:

db.users
  .where('age')
  .above(1000)
  .offset(10)
  .limit(10)
  .toArray()

то:

  • offset() не выполняет обход
  • limit() не имеет эффекта
  • результатом является пустой массив

Практические особенности взаимодействия limit и offset

  • limit(0) всегда возвращает пустой результат
  • offset(0) эквивалентен отсутствию смещения
  • комбинация offset + limit формирует окно выборки
  • при цепочке фильтров сначала выполняется narrowing через where()/filter(), затем ограничение

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

Часто встречаются следующие ошибки:

1. Использование offset без orderBy

db.users.offset(10).limit(5)

Результат нестабилен и не гарантирует воспроизводимости.

2. Попытка использовать offset как прямой доступ по индексу

IndexedDB не поддерживает произвольный доступ по позиции.

3. Большие offset в UI-пагинации

При больших таблицах это приводит к деградации производительности.


Роль limit() и offset() в архитектуре запросов Dexie.js

Эти методы не являются самостоятельными механизмами извлечения данных. Они выступают как слой управления курсором, который накладывается поверх:

  • индексированных выборок
  • фильтрации через where()
  • сортировки через orderBy()
  • кастомной логики через filter()

Их основная функция — ограничение объёма данных, проходящих через цепочку обработки, а не оптимизация доступа к произвольным участкам данных.