Перейти к содержанию

COLLECT

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

Оператор COLLECT удаляет все локальные переменные в текущей области видимости. После COLLECT доступны только переменные, введенные самим COLLECT.

Синтаксис

Существует несколько вариантов синтаксиса для операций COLLECT:

COLLECT variableName = expression
COLLECT variableName = expression INTO groupsVariable
COLLECT variableName = expression INTO groupsVariable = projectionExpression
COLLECT variableName = expression INTO groupsVariable KEEP keepVariable
COLLECT variableName = expression WITH COUNT INTO countVariable
COLLECT variableName = expression AGGREGATE variableName = aggregateExpression
COLLECT variableName = expression AGGREGATE variableName = aggregateExpression INTO groupsVariable
COLLECT AGGREGATE variableName = aggregateExpression
COLLECT AGGREGATE variableName = aggregateExpression INTO groupsVariable
COLLECT WITH COUNT INTO countVariable

Все варианты могут опционально заканчиваться предложением OPTIONS { ... }.

Синтаксис группировки

Первая синтаксическая форма COLLECT группирует результат только по определенным групповым критериям, указанным в выражении. Для дальнейшей обработки результатов, полученных с помощью COLLECT, вводится новая переменная (заданная variableName). Эта переменная содержит значение группы.

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

1
2
3
4
5
FOR u IN users
  COLLECT city = u.city
  RETURN {
    "city" : city
  }

Вторая форма делает то же самое, что и первая, но дополнительно вводит переменную (заданную groupsVariable), которая содержит все элементы, попавшие в группу. Это работает следующим образом: Переменная groupsVariable - это массив, содержащий столько элементов, сколько их в группе. Каждый член этого массива представляет собой объект JSON, в котором значение каждой переменной, определенной в запросе AQL, привязано к соответствующему атрибуту. Обратите внимание, что при этом учитываются все переменные, определенные до оператора COLLECT, но не те, которые находятся на верхнем уровне (вне любого FOR), если только оператор COLLECT сам не находится на верхнем уровне, в этом случае учитываются все переменные. Кроме того, обратите внимание, что оптимизатор может перемещать операторы LET из операторов FOR для повышения производительности.

1
2
3
4
5
6
FOR u IN users
  COLLECT city = u.city INTO groups
  RETURN {
    "city" : city,
    "usersInCity" : groups
  }

В приведенном выше примере массив users будет сгруппирован по атрибуту city. В результате будет получен новый массив документов, с одним элементом для каждого отдельного значения u.city. Элементы исходного массива (здесь: users) для каждого города доступны в переменной groups. Это происходит благодаря предложению INTO.

COLLECT также позволяет указать несколько групповых критериев. Отдельные групповые критерии могут быть разделены запятыми:

1
2
3
4
5
6
7
FOR u IN users
  COLLECT country = u.country, city = u.city INTO groups
  RETURN {
    "country" : country,
    "city" : city,
    "usersInCity" : groups
  }

В приведенном выше примере массив users сгруппирован сначала по стране, а затем по городу, и для каждой отдельной комбинации страны и города будут возвращены пользователи.

Отказ от устаревших переменных

Третья форма COLLECT позволяет переписать содержимое groupsVariable с помощью произвольной projectionExpression:

1
2
3
4
5
6
7
FOR u IN users
  COLLECT country = u.country, city = u.city INTO groups = u.name
  RETURN {
    "country" : country,
    "city" : city,
    "userNames" : groups
  }

В приведенном выше примере только projectionExpression является u.name. Поэтому только этот атрибут копируется в groupsVariable для каждого документа. Это, вероятно, гораздо эффективнее, чем копирование всех переменных из области видимости в groupsVariable, как это произошло бы без projectionExpression.

Выражение, следующее за INTO, также может быть использовано для произвольных вычислений:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
FOR u IN users
  COLLECT country = u.country, city = u.city INTO groups = {
    "name" : u.name,
    "isActive" : u.status == "active"
  }
  RETURN {
    "country" : country,
    "city" : city,
    "usersInCity" : groups
  }

COLLECT также предоставляет необязательное условие KEEP, которое можно использовать для управления тем, какие переменные будут скопированы в переменную, созданную INTO. Если условие KEEP не указано, все переменные из области видимости будут скопированы в качестве податрибутов в groupsVariable. Это безопасно, но может отрицательно сказаться на производительности, если в области видимости много переменных или переменные содержат большие объемы данных.

В следующем примере переменные, которые копируются в groupsVariable, ограничиваются только name. Переменные u и someCalculation, также присутствующие в области видимости, не будут скопированы в groupsVariable, поскольку они не указаны в предложении KEEP:

1
2
3
4
5
6
7
8
FOR u IN users
  LET name = u.name
  LET someCalculation = u.value1 + u.value2
  COLLECT city = u.city INTO groups KEEP name
  RETURN {
    "city" : city,
    "userNames" : groups[*].name
  }

KEEP действует только в сочетании с INTO. В предложении KEEP можно использовать только правильные имена переменных. KEEP поддерживает указание нескольких имен переменных.

Вычисление длины группы

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

В простейшей форме возвращается только количество элементов, попавших в COLLECT:

1
2
3
FOR u IN users
  COLLECT WITH COUNT INTO length
  RETURN length

Вышеприведенный вариант эквивалентен, но менее эффективен, чем:

1
RETURN LENGTH(users)

Предложение WITH COUNT также можно использовать для эффективного подсчета количества элементов в каждой группе:

1
2
3
4
5
6
FOR u IN users
  COLLECT age = u.age WITH COUNT INTO length
  RETURN {
    "age" : age,
    "count" : length
  }

Предложение WITH COUNT можно использовать только вместе с предложением INTO.

Агрегация

Оператор COLLECT можно использовать для агрегирования данных по группам. Чтобы определить только длину группы, можно использовать вариант WITH COUNT INTO оператора COLLECT, как описано ранее.

Для других агрегаций можно запустить агрегатные функции на результатах COLLECT:

1
2
3
4
5
6
7
FOR u IN users
  COLLECT ageGroup = FLOOR(u.age / 5) * 5 INTO g
  RETURN {
    "ageGroup" : ageGroup,
    "minAge" : MIN(g[*].u.age),
    "maxAge" : MAX(g[*].u.age)
  }

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

Специальный вариант AGGREGATE функции COLLECT позволяет создавать агрегатные значения постепенно во время операции сбора и поэтому часто является более эффективным.

При использовании варианта AGGREGATE приведенный выше запрос становится:

1
2
3
4
5
6
7
8
FOR u IN users
  COLLECT ageGroup = FLOOR(u.age / 5) * 5
  AGGREGATE minAge = MIN(u.age), maxAge = MAX(u.age)
  RETURN {
    ageGroup,
    minAge,
    maxAge
  }

Ключевое слово AGGREGATE может использоваться только после ключевого слова COLLECT. Если оно используется, то должно следовать непосредственно за объявлением ключей группировки. Если ключи группировки не используются, оно должно следовать непосредственно за ключевым словом COLLECT:

1
2
3
4
5
6
FOR u IN users
  COLLECT AGGREGATE minAge = MIN(u.age), maxAge = MAX(u.age)
  RETURN {
    minAge,
    maxAge
  }

В правой части каждого присваивания AGGREGATE допускаются только определенные выражения:

  • на верхнем уровне агрегированное выражение должно быть вызовом одной из поддерживаемых функций агрегирования:

  • LENGTH() / COUNT()

  • MIN()
  • MAX()
  • SUM()
  • AVERAGE() / AVG()
  • STDDEV_POPULATION() / STDDEV()
  • STDDEV_SAMPLE()
  • VARIANCE_POPULATION() / VARIANCE()
  • VARIANCE_SAMPLE()
  • UNIQUE()
  • SORTED_UNIQUE()
  • COUNT_DISTINCT() / COUNT_UNIQUE()
  • BIT_AND()
  • BIT_OR()
  • BIT_XOR()

  • агрегированное выражение не должно ссылаться на переменные, введенные самим COLLECT.

COLLECT против RETURN DISTINCT

Для того чтобы сделать набор результатов уникальным, можно использовать либо COLLECT, либо RETURN DISTINCT.

1
2
FOR u IN users
  RETURN DISTINCT u.age
1
2
3
FOR u IN users
  COLLECT age = u.age
  RETURN age

За кулисами оба варианта создают CollectNode. Однако они используют разные реализации COLLECT, которые имеют разные свойства:

  • RETURN DISTINCT сохраняет порядок результатов, но он ограничен одним значением.

  • COLLECT изменяет порядок результатов (отсортированный или неопределенный), но поддерживает несколько значений и является более гибким, чем RETURN DISTINCT.

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

Параметры COLLECT

method

Существует два варианта COLLECT, которые оптимизатор может выбрать: вариант sorted и вариант hash. Опция method может быть использована в операторе COLLECT, чтобы сообщить оптимизатору о предпочтительном методе, "sorted" или "hash".

1
COLLECT ... OPTIONS { method: "sorted" }

Если метод не указан пользователем, то оптимизатор создаст план, использующий метод sorted, и дополнительный план, использующий метод hash, если оператор COLLECT удовлетворяет его требованиям.

Если метод явно установлен в sorted, то оптимизатор всегда будет использовать sorted вариант COLLECT и даже не создаст план с использованием hash варианта. Если он явно установлен в hash, то оптимизатор будет создавать план с использованием метода hash только если оператор COLLECT соответствует требованиям. Не все операторы COLLECT могут использовать метод hash, в частности, операторы с клаузулой INTO не подходят. Если оператор COLLECT соответствует требованиям, то будет только один план, использующий метод hash. В противном случае оптимизатор по умолчанию будет использовать метод sorted.

Метод sorted требует, чтобы его входные данные были отсортированы по групповым критериям, указанным в предложении COLLECT. Для обеспечения корректности результата оптимизатор автоматически вставит в запрос операцию SORT перед оператором COLLECT. В дальнейшем оптимизатор может отказаться от этой операции SORT, если для групповых критериев имеется отсортированный индекс.

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

Если порядок сортировки в COLLECT не имеет значения для пользователя, добавление дополнительной инструкции SORT null после COLLECT позволит оптимизатору полностью удалить сортировку:

1
2
3
4
FOR u IN users
  COLLECT age = u.age
  SORT null  /* note: will be optimized away */
  RETURN age

Какой вариант COLLECT используется оптимизатором, если явно не задан предпочтительный метод, зависит от оценок затрат оптимизатора. Созданные планы с различными вариантами COLLECT будут проходить через обычный конвейер оптимизации. В итоге оптимизатор, как обычно, выберет план с наименьшей оценкой общей стоимости.

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

Если в критериях группы нет отсортированного индекса, то предварительная сортировка, требуемая вариантом sorted, может оказаться дорогостоящей. В этом случае, скорее всего, оптимизатор предпочтет hash вариант COLLECT, который не требует сортировки входных данных.

Какой вариант COLLECT будет использоваться на самом деле, можно выяснить, посмотрев на план выполнения запроса, в частности, на комментарий CollectNode:

1
2
3
4
5
6
7
8
Execution plan:
 Id   NodeType                  Est.   Comment
  1   SingletonNode                1   * ROOT
  2   EnumerateCollectionNode      5     - FOR doc IN coll   /* full collection scan, projections: `name` */
  3   CalculationNode              5       - LET #2 = doc.`name`   /* attribute expression */   /* collections used: doc : coll */
  4   CollectNode                  5       - COLLECT name = #2   /* hash */
  6   SortNode                     5       - SORT name ASC   /* sorting strategy: standard */
  5   ReturnNode                   5       - RETURN name

Комментарии