Индексирование в Azure Cosmos DB — обзор

ПРИМЕНИМО К: API SQL

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" }
        ]
    }

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

Предыдущий элемент, представленный в виде дерева

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

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

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

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

  • /locations/0/country: "Германия"
  • /locations/0/city: "Берлин"
  • /locations/1/country: "Франция"
  • /locations/1/city: "Париж"
  • /headquarters/country: "Бельгия"
  • /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. Чтобы использовать этот тип индекса, задайте его с помощью свойства "kind": "Range" при настройке политики индексации. Сведения о настройке пространственных индексов см. в разделе с примерами политики пространственной индексации.

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

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

  • Запросы 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'`

Предикат запроса (фильтрация по элементам, где любое расположение имеет значение "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 выполняется в индексе. Время поиска по индексу и цена за единицу для сканирования индекса увеличивается по мере увеличения количества элементов пути. Иными словами, чем больше возможных уникальных значений, которые механизму запросов необходимо сканировать, тем выше время задержки и оплата в единицах запроса, участвующие в выполнении полного сканирования индекса.

Например, рассмотрим два свойства: город и страна. Количество элементов города — 5000, а количество элементов страны — 200. Ниже приведены два примера запросов, каждая из которых содержит системную функцию Contains, которая выполняет полное сканирование индекса для свойства town. Первый запрос, скорее всего, будет использовать больше единиц запросов, чем второй, так как кратность элементов, содержащих "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 должен зависеть только от индекса, поэтому если есть вероятность, что критерий фильтра возвратит ложные положительные совпадения, обработчик запросов будет выполнять полную проверку.

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

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

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