Общие сведения об индексировании в Azure Cosmos DB

ПРИМЕНИМО К: Nosql Mongodb Кассандра Гремлин Таблица

Azure Cosmos DB — это база данных без использования схем, которая позволяет выполнять итерацию приложения, не отвлекаясь на управление схемами или индексами. По умолчанию Azure Cosmos DB автоматически индексирует каждое свойство всех элементов в контейнере. Разработчикам не нужно определять схемы или настраивать вторичные индексы.

В статье описан способы индексирования данных в Azure Cosmos DB и использования индексов для повышения производительности запросов. Рекомендуется ознакомиться с этим разделом, прежде чем изучать настройку политик индексирования.

От элементов к деревьям

Каждый раз, когда элемент сохраняется в контейнере, его содержимое проецируется как документ JSON, а затем преобразуется в представление в виде дерева. Это преобразование означает, что каждое свойство этого элемента будет представлено в виде узла в дереве. Псевдоузел корня создается как родительский элемент для всех свойств элемента первого уровня. Листовые узлы содержат фактические скалярные значения, перенесенные элементом.

Рассмотрим этот элемент в качестве примера:

{
  "locations": [
    { "country": "Germany", "city": "Berlin" },
    { "country": "France", "city": "Paris" }
  ],
  "headquarters": { "country": "Belgium", "employees": 250 },
  "exports": [
    { "city": "Moscow" },
    { "city": "Athens" }
  ]
}

Это дерево представляет пример элемента JSON:

Схема предыдущего элемента JSON, представленного в виде дерева.

Обратите внимание, как массивы кодируются в дереве: каждая запись в массиве получает промежуточный узел с меткой индекса этой записи в массиве (0, 1 и т. д.).

От деревьев к путям к свойствам

Причина, по которой Azure Cosmos DB преобразует элементы в деревья, заключается в том, что это позволяет системе ссылаться на свойства, используя их пути в этих деревьях. Чтобы получить путь к свойству, мы можем перемещаться по дереву с корневого узла к этому свойстве и объединять метки каждого пройденного узла.

Ниже приведены пути к каждому свойству из примера элемента, описанного выше.

  • /locations/0/country: "Germany"
  • /locations/0/city: "Берлин"
  • /locations/1/country: "France"
  • /locations/1/city: "Париж"
  • /headquarters/country: "Belgium"
  • /headquarters/employees: 250
  • /exports/0/city: "Москва"
  • /exports/1/city: "Афины"

Azure Cosmos DB эффективно индексирует путь к каждому свойству и соответствующее ему значение при записи элемента.

Типы индексов

В настоящее время Azure Cosmos DB поддерживает три типа индексов. Такие типы индексов можно настроить при определении политики индексации.

Индекс диапазона

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

  • Запросы на равенство:

    SELECT * FROM container c WHERE c.property = 'value'
    
    SELECT * FROM c WHERE c.property IN ("value1", "value2", "value3")
    
  • совпадение для элемента массива

    SELECT * FROM c WHERE ARRAY_CONTAINS(c.tags, "tag1")
    
  • Запросы к диапазону:

    SELECT * FROM container c WHERE c.property > 'value'
    

    Примечание

    (для >, <, >=, <=, !=)

  • Проверка наличия свойства:

    SELECT * FROM c WHERE IS_DEFINED(c.property)
    
  • Строковые системные функции:

    SELECT * FROM c WHERE CONTAINS(c.property, "value")
    
    SELECT * FROM c WHERE STRINGEQUALS(c.property, "value")
    
  • Запросы ORDER BY:

    SELECT * FROM container c ORDER BY c.property
    
  • Запросы JOIN:

    SELECT child FROM container c JOIN child IN c.properties WHERE child = 'value'
    

Индексы диапазона можно использовать для скалярных значений (строка или число). Политика индексирования по умолчанию, задаваемая для только что созданных контейнеров, применяет диапазонные индексы для любых строк или чисел. Сведения о настройке индексов диапазонов см. в разделе с примерами политики диапазонной индексации.

Примечание

Предложение ORDER BY, которое упорядочивается по одному свойству, всегда требует индекс диапазона, и если у пути, на который оно ссылается, его нет, оно завершится ошибкой. Аналогичным образом запросу ORDER BY, который упорядочивается по нескольким свойствам, всегда требуется составной индекс.

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

Пространственные индексы позволяют выполнять эффективные запросы к геопространственным объектам, таким как точки, линии и многоугольники. В этих запросах используются ключевые слова ST_DISTANCE, ST_WITHIN и ST_INTERSECTS. Ниже приведены некоторые примеры, которые используют пространственные индексы:

  • Геопространственные запросы расстояний:

    SELECT * FROM container c WHERE ST_DISTANCE(c.property, { "type": "Point", "coordinates": [0.0, 10.0] }) < 40
    
  • Геопространственные запросы включения:

    SELECT * FROM container c WHERE ST_WITHIN(c.property, {"type": "Point", "coordinates": [0.0, 10.0] })
    
  • Геопространственные запросы пересечения:

    SELECT * FROM c WHERE ST_INTERSECTS(c.property, { 'type':'Polygon', 'coordinates': [[ [31.8, -5], [32, -5], [31.8, -5] ]]  })  
    

Пространственные индексы можно использовать для правильно отформатированных объектов GeoJSON. В настоящее время поддерживаются Points, LineStrings, Polygons и MultiPolygons. Сведения о настройке пространственных индексов см. в разделе с примерами политики пространственной индексации.

Составные индексы

Составные индексы повышают эффективность при выполнении операций с несколькими полями. Составные индексы используются в таких случаях:

  • Запросы ORDER BY к нескольким свойствам:

    SELECT * FROM container c ORDER BY c.property1, c.property2
    
  • Запросы с фильтром и ORDER BY. Эти запросы могут использовать составной индекс, если свойство фильтра добавлено в предложение ORDER BY.

    SELECT * FROM container c WHERE c.property1 = 'value' ORDER BY c.property1, c.property2
    
  • Запросы с фильтром для двух или более свойств, в которых по крайней мере одно свойство является фильтром проверки на равенство

    SELECT * FROM container c WHERE c.property1 = 'value' AND c.property2 > 'value'
    

Пока один предикат фильтра использует один из типов индекса, обработчик запросов сначала оценивает это перед сканированием остальных. Например, вас есть SQL-запрос SELECT * FROM c WHERE c.firstName = "Andrew" and CONTAINS(c.lastName, "Liu")

  • Приведенный выше запрос сначала фильтрует записи со значением firstName = "Andrew", используя индекс. Затем он передает все записи firstName = "Andrew" через последующий конвейер для вычисления предиката фильтра CONTAINS.

  • Вы можете ускорить запросы и избежать полного сканирования контейнеров при использовании функций, выполняющих полное сканирование, например CONTAINS. Вы можете добавить дополнительные предикаты фильтров, которые используют индекс для ускорения этих запросов. Порядок предложений фильтра не важен. Обработчик запросов определяет, какие предикаты являются более выборочными, и выполняет запрос соответствующим образом.

Сведения о настройке составных индексов см. в разделе с примерами политики составной индексации.

Использование индексов

Существует пять способов, с помощью которых механизм запросов может оценивать фильтры запросов, отсортированные от наиболее эффективных до наименее эффективных.

  • Поиск в индексе
  • Точная проверка индекса
  • Расширенная проверка индекса
  • Полная проверка индекса
  • Полная проверка

При индексировании путей свойств обработчик запросов автоматически использует индекс как можно эффективнее. Помимо индексирования новых путей свойств, для оптимизации того, как запросы используют индекс, не нужно настраивать что-либо еще. Оплата за единицу запроса — это сочетание оплаты за единицу использования индекса и оплаты за единицу при загрузке элементов.

Ниже приведена таблица, в которую перечислены различные способы использования индексов в Azure Cosmos DB:

Тип поиска индекса Описание Распространенные примеры Плата за единицу использования индекса Плата за единицы запросов за загрузку элементов из хранилища транзакционных данных
Поиск в индексе Чтение только необходимых индексированных значений и загрузка только совпадающих элементов из транзакционного хранилища данных Фильтры равенства, IN Константа для фильтра равенства Увеличивается на основе числа элементов в результатах запроса
Точная проверка индекса Двоичный поиск индексированных значений и загрузка только совпадающих элементов из транзакционного хранилища данных Сравнения диапазонов (>, <, <=, или >=), StartsWith При сравнении с поиском по индексу немного увеличивается в зависимости от количества элементов индексированных свойств Увеличивается на основе числа элементов в результатах запроса
Расширенная проверка индекса Оптимизированный поиск (но менее эффективный, чем двоичный поиск) индексированных значений и загрузка только совпадающих элементов из транзакционного хранилища данных StartsWith (без учета регистра), StringEquals (без учета регистра) Немного увеличивается в зависимости от количества элементов индексированных свойств Увеличивается на основе числа элементов в результатах запроса
Полная проверка индекса Чтение конкретного набора индексированных значений и загрузка только совпадающих элементов из транзакционного хранилища данных Contains, EndsWith, RegexMatch, LIKE Увеличивается линейно в зависимости от количества элементов индексированных свойств Увеличивается на основе числа элементов в результатах запроса
Полная проверка Загрузка всех элементов из хранилища транзакционных данных Upper, Lower Н/Д Увеличивается на основе числа элементов в контейнере

При написании запросов следует использовать предикаты фильтров, которые используют индекс как можно эффективнее. Например, если вариант StartsWith или Contains подходит для вашего варианта использования, следует выбрать StartsWith вариант, так как он выполняет точное сканирование индекса, а не полное сканирование индекса.

Данные об использовании индекса

В этом разделе мы рассмотрим дополнительные сведения о том, как запросы используют индексы. Этот уровень детализации не требуется для начала работы с Azure Cosmos DB, но он подробно описан для любопытных пользователей. Мы ссылаемся на пример элемента, доступного ранее в этом документе:

Элементы для примера.

{
  "id": 1,
  "locations": [
    { "country": "Germany", "city": "Berlin" },
    { "country": "France", "city": "Paris" }
  ],
  "headquarters": { "country": "Belgium", "employees": 250 },
  "exports": [
    { "city": "Moscow" },
    { "city": "Athens" }
  ]
}
{
  "id": 2,
  "locations": [
    { "country": "Ireland", "city": "Dublin" }
  ],
  "headquarters": { "country": "Belgium", "employees": 200 },
  "exports": [
    { "city": "Moscow" },
    { "city": "Athens" },
    { "city": "London" }
  ]
}

Azure Cosmos DB использует инвертированный индекс. Индекс работает путем сопоставления каждого пути JSON с набором элементов, содержащих это значение. Сопоставление идентификаторов элементов представлено на множестве разных страниц индекса для контейнера. Ниже приведен пример схемы инвертированного индекса для контейнера, который включает два примера элементов:

Путь Значение Список идентификаторов элементов
/locations/0/country Германия 1
/locations/0/country Ирландия 2
/locations/0/city Берлин 1
/locations/0/city Дублин 2
/locations/1/country Франция 1
/locations/1/city Париж 1
/headquarters/country Бельгия 1, 2
/headquarters/employees 200 2
/headquarters/employees 250 1

Инвертированный индекс имеет два важных атрибута.

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

Обработчик запросов может использовать инвертированный индекс четырьмя разными способами.

Поиск в индексе

Обратите внимание на следующий запрос:

SELECT location
FROM location IN company.locations
WHERE location.country = 'France'`

Предикат запроса (фильтрация по элементам, где в любом расположении имеется "Франция" в качестве страны или региона) будет соответствовать пути, выделенному на этой схеме:

Схема обхода (поиска), соответствующего определенному пути в дереве.

Поскольку этот запрос имеет фильтр равенства, после обхода этого дерева можно быстро определить страницы индекса, содержащие результаты запроса. В этом случае обработчик запросов будет считывать страницы индекса, содержащие элемент 1. Поиск по индексу является наиболее эффективным способом использования индекса. При поиске по индексу мы считываем только необходимые страницы индекса и загружаем только элементы в результатах запроса. Таким образом, время уточняющего запроса индекса и единица оплаты единицы поиска по индексу чрезвычайно низкие, независимо от общего объема данных.

Точная проверка индекса

Обратите внимание на следующий запрос:

SELECT *
FROM company
WHERE company.headquarters.employees > 200

Предикат запроса (фильтрация по элементам, в которых имеется более 200 сотрудников) можно оценить с помощью точного просмотра индекса пути headquarters/employees. При выполнении точной проверки индекса обработчик запросов начинает с двоичного поиска по отдельному набору возможных значений, чтобы найти расположение значения 200 для пути headquarters/employees. Так как значения для каждого пути сортируются в возрастающем порядке, обработчику запросов достаточно выполнить двоичный поиск. После того как обработчик запросов обнаружит значение 200, он начнет считывать все оставшиеся страницы индекса (в направлении возрастания).

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

Расширенная проверка индекса

Обратите внимание на следующий запрос:

SELECT *
FROM company
WHERE STARTSWITH(company.headquarters.country, "United", true)

Предикат запроса (фильтрация по элементам с штаб-квартирой в расположении, начинающемся с без учета регистра "United") можно оценить с помощью расширенного headquarters/country сканирования индекса пути. Операции, которые выполняют расширенное сканирование индекса, имеют оптимизацию, которая помогает избежать необходимости сканировать каждую страницу индекса, но немного дороже, чем двоичный поиск, осуществляемый при точном поиске по индексу.

Например, при оценке StartsWithбез учета регистра подсистема запросов проверяет индекс на наличие различных возможных сочетаний значений в верхнем и нижнем регистре. Такая оптимизация позволяет обработчику запросов избежать чтения большинства страниц индекса. Различные системные функции имеют разные оптимизации, которые они могут использовать, чтобы избежать чтения каждой страницы индекса, поэтому они широко классифицируются как расширенные проверки индекса.

Полная проверка индекса

Обратите внимание на следующий запрос:

SELECT *
FROM company
WHERE CONTAINS(company.headquarters.country, "United")

Предикат запроса (фильтрация по элементам, штаб-квартире которых находится в расположении, содержащ "United") можно оценить с помощью сканирования headquarters/country индекса пути. В отличие от точного сканирования индекса, полное сканирование индекса всегда проверяет отдельный набор возможных значений, чтобы определить страницы индекса, на которых есть результаты. В этом случае Contains выполняется в индексе. Время поиска по индексу и цена за единицу для сканирования индекса увеличивается по мере увеличения количества элементов пути. Иными словами, чем больше возможных уникальных значений, которые механизму запросов необходимо сканировать, тем выше время задержки и оплата в единицах запроса, участвующие в выполнении полного сканирования индекса.

Например, рассмотрим два свойства: town и country. Кратность города составляет 5000, а кратность country — 200. Ниже приведены два примера запросов, каждая из которых содержит системную функцию Contains, которая выполняет полное сканирование индекса для свойства town. Первый запрос использует больше ЕЗ, чем второй запрос, так как кратность города выше , чем country.

SELECT *
FROM c
WHERE CONTAINS(c.town, "Red", false)
SELECT *
FROM c
WHERE CONTAINS(c.country, "States", false)

Полная проверка

В некоторых случаях обработчик запросов может не вычислить фильтр запроса с помощью индекса. В этом случае обработчику запросов необходимо загрузить все элементы из транзакционного хранилища, чтобы оценить фильтр запросов. При полной проверке индекс не используется, а плата за единицу запроса увеличивается линейно с общим размером данных. К счастью, операции, требующие полной проверки, являются редкими.

Запросы со сложными выражениями фильтра

В предыдущих примерах мы рассматривали только запросы с простыми выражениями фильтра (например, запросы только с одним фильтром равенства или диапазоном). В реальности большинство запросов имеют гораздо более сложные критерии фильтра.

Обратите внимание на следующий запрос:

SELECT *
FROM company
WHERE company.headquarters.employees = 200 AND CONTAINS(company.headquarters.country, "United")

Для выполнения этого запроса обработчик запросов должен выполнить поиск по индексу для headquarters/employees и полный просмотр индекса для headquarters/country. Обработчик запросов имеет внутренние эвристики, используемые для вычисления выражения фильтра запросов как можно быстрее. В этом случае механизму запросов не нужно считывать ненужные страницы индекса, сначала выполнив поиск по индексу. Если, например, фильтру равенства соответствует только 50 элементов, обработчику запросов потребуется выполнить оценку Contains только на страницах индекса, содержащих эти 50 элементов. Полное сканирование индекса всего контейнера не потребуется.

Использование индексов для скалярных агрегатных функций

Запросы с агрегатными функциями должны зависеть только от индекса, чтобы его можно было использовать.

В некоторых случаях индекс может возвращать ложные положительные результаты. Например, при вычислении Contains на индексе количество совпадений в индексе может превышать число результатов запроса. Модуль запросов загружает все совпадения индекса, вычисляет фильтр для загруженных элементов и возвращает только правильные результаты.

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

Например, рассмотрим следующий запрос:

SELECT *
FROM company
WHERE CONTAINS(company.headquarters.country, "United")

Системная Contains функция может возвращать некоторые ложноположительные совпадения, поэтому подсистеме запросов необходимо проверить, соответствует ли каждый загруженный элемент выражению фильтра. В этом примере обработчику запросов может потребоваться загрузить только несколько дополнительных элементов, поэтому влияние на использование индекса и затраты на единицы запроса являются минимальными.

Однако запросы с агрегатными функциями должны зависеть только от индекса, чтобы его можно было использовать. Например, рассмотрим следующий запрос с агрегатной функцией Count.

SELECT COUNT(1)
FROM company
WHERE CONTAINS(company.headquarters.country, "United")

Как и в первом примере, системная функция Contains может вернуть несколько ложных положительных совпадений. Однако в отличие от запроса SELECT *, запрос Count не может вычислить критерий фильтра загруженных элементов для проверки всех совпадений индекса. Запрос Count должен полагаться исключительно на индекс, поэтому если есть вероятность, что выражение фильтра возвращает ложноположительные совпадения, обработчик запросов прибегнет к полной проверке.

Запросы со следующими агрегатными функциями должны зависеть только от индекса, поэтому для оценки некоторых системных функций требуется полная проверка.

Дальнейшие действия

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