Запросы с inAnyRange()

Dexie.js предоставляет высокоуровневый интерфейс для работы с IndexedDB, включая расширенные возможности индексированных выборок. Одним из таких механизмов является inAnyRange(), позволяющий выполнять запросы по нескольким диапазонам значений в рамках одного индекса, избегая необходимости объединять результаты нескольких отдельных запросов на стороне приложения.

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


Синтаксис inAnyRange()

Базовая форма использования:

db.table.where(index).inAnyRange(ranges)

Где:

  • index — индексированное поле или составной индекс
  • ranges — массив диапазонов

Каждый диапазон задаётся как пара границ:

[
  [lower1, upper1],
  [lower2, upper2],
  ...
]

Также возможны дополнительные параметры границ:

  • включительность/исключительность границ
  • использование Dexie.Range для более точного контроля

Принцип работы

IndexedDB хранит данные в B-tree структуре по индексам. inAnyRange() выполняет несколько диапазонных сканов по одному индексу и объединяет результаты на уровне движка Dexie.

Логика работы:

  1. Каждый диапазон превращается в отдельный индексный проход
  2. Результаты объединяются в единый поток
  3. Dexie устраняет дубликаты (если они возможны в рамках индекса)
  4. Итерация выполняется лениво, без полной загрузки всех данных в память

Простые диапазоны

Пример таблицы:

db.version(1).stores({
  logs: "++id, timestamp, level"
});

Запрос по нескольким диапазонам времени:

db.logs
  .where("timestamp")
  .inAnyRange([
    [1700000000000, 1700003600000],
    [1700010000000, 1700013600000]
  ])
  .toArray();

В этом случае выбираются записи из двух временных окон.


Использование с Dexie.Range

Более гибкий способ задания диапазонов:

import Dexie fr om "dexie";

db.logs
  .wh ere("timestamp")
  .inAnyRange([
    Dexie.range(1700000000000, 1700003600000),
    Dexie.range(1700010000000, 1700013600000).excludeUpperBound()
  ])
  .toArray();

Возможности Dexie.Range:

  • включение/исключение границ
  • открытые диапазоны (below, above)
  • точные интервалы

Работа с составными индексами

При использовании compound index:

db.version(1).stores({
  orders: "[userId+createdAt], status"
});

Запрос:

db.orders
  .where("[userId+createdAt]")
  .inAnyRange([
    [ [1, 1700000000000], [1, 1700100000000] ],
    [ [2, 1700000000000], [2, 1700100000000] ]
  ])
  .toArray();

Каждый диапазон задаётся как пара составных ключей.

Важный момент: порядок полей в индексе строго влияет на корректность диапазона.


Отличие от anyOf()

inAnyRange() и anyOf() решают похожие задачи, но работают по-разному:

anyOf()

  • точечные совпадения
  • список значений
  • не поддерживает диапазоны
db.users.where("id").anyOf([1, 5, 9])

inAnyRange()

  • диапазонные запросы
  • несколько интервалов
  • поддержка сложных ключей
db.users.where("age").inAnyRange([
  [18, 25],
  [40, 60]
])

Отличие от between()

between() ограничен одним диапазоном:

db.users.where("age").between(18, 30)

inAnyRange() расширяет модель до множества диапазонов:

db.users.where("age").inAnyRange([
  [18, 30],
  [50, 65]
])

Оптимизация выполнения

При работе с inAnyRange() важно учитывать поведение IndexedDB:

1. Количество диапазонов

Каждый диапазон = отдельный индексный проход. Большое количество диапазонов может увеличивать время выполнения.

Рекомендуется:

  • объединять пересекающиеся диапазоны
  • избегать дробления без необходимости

2. Сортировка диапазонов

Хотя Dexie нормализует входные данные, предварительная сортировка диапазонов может улучшить предсказуемость выполнения:

ranges.sort((a, b) => a[0] - b[0]);

3. Использование ограниченных индексов

Запросы быстрее при использовании узких индексов:

  • timestamp быстрее, чем JSON поле
  • составные индексы быстрее фильтрации в памяти

Поведение с дубликатами

При пересечении диапазонов возможны повторные совпадения. Dexie выполняет дедупликацию на уровне результата, но поведение зависит от структуры индекса.

Пример пересечения:

[
  [10, 50],
  [40, 80]
]

Значения 40–50 встречаются в обоих диапазонах, но итоговый результат возвращается без дублирования.


Асинхронная обработка

Как и большинство операций Dexie, inAnyRange() возвращает Promise или Collection:

const result = await db.events
  .where("timestamp")
  .inAnyRange([
    [t1, t2],
    [t3, t4]
  ]);

Ленивая итерация возможна:

db.events
  .where("timestamp")
  .inAnyRange([...])
  .each(event => {
    // обработка по мере поступления
  });

Фильтрация после диапазонов

После inAnyRange() допустимо добавлять дополнительные фильтры:

db.logs
  .where("timestamp")
  .inAnyRange([
    [a, b],
    [c, d]
  ])
  .and(log => log.level === "error")
  .toArray();

Важно: фильтр .and() выполняется в памяти, а не в индексе.


Ограничения

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

  • работает только с индексированными полями
  • не поддерживает неиндексированные фильтры на уровне БД
  • количество диапазонов влияет на производительность
  • сложные составные ключи требуют строгого соответствия структуре индекса
  • не заменяет полноценные логические OR-запросы для всех случаев

Практические сценарии применения

1. Логи по нескольким временным окнам

Разделение по периодам активности системы:

[
  ночной_период,
  дневной_период,
  вечерний_период
]

2. Фильтрация по нескольким диапазонам идентификаторов

db.records.where("id").inAnyRange([
  [100, 199],
  [500, 599]
]);

3. Сегментация данных пользователей

Например, возрастные группы:

[
  [18, 25],
  [30, 40],
  [50, 65]
]

Поведение при больших объёмах данных

При больших наборах данных ключевыми факторами становятся:

  • плотность индекса
  • распределение значений
  • количество диапазонов
  • наличие пересечений

В некоторых случаях inAnyRange() может быть заменён на:

  • разбиение таблицы
  • предрасчёт сегментов
  • материализованные индексы

Взаимодействие с транзакциями

inAnyRange() полностью поддерживается внутри транзакций Dexie:

db.transaction("r", db.logs, async () => {
  return db.logs
    .where("timestamp")
    .inAnyRange([[a, b], [c, d]])
    .toArray();
});

Это обеспечивает консистентность данных в рамках операции чтения.