Агрегатные функции

Reindexer поддерживает агрегатные функции для более гибкого поиска:

Агрегатная функция для SQL Агрегатная функция для GO Описание
MAX() AggregateMax Получить максимальное значение в столбце
MIN() AggregateMin Получить минимальное значение в столбце
AVG() AggregateAvg Получить среднее значение в столбце
SUM() AggregateSum Получить сумму значений элементов в столбце
COUNT() ReqTotal Получить общее количество элементов, удовлетворяющих условиям выборки
COUNT_CACHED() CachedTotal То же, что COUNT, но с использованием кэшированного значения. Может быть более эффективно, чем COUNT для последовательных запросов, отличающихся только значениями LIMIT/OFFSET
DISTINCT() Distinct Получить записи с уникальными значениями
FACET() AggregateFacet Получить уникальные записи в неймспейсе с указанием количества

Фасеты агрегации (фильтры) могут применяться к нескольким столбцам. Результат может быть отсортирован по любому столбцу и ограничен с помощью Limit и Offset.

Если было задано более одного оператора DISTINCT, в выборку попадет пересечение множеств уникальных полей. То есть будут выбраны только те записи, для которых уникальные значения всех указанных полей были найдены в первую очередь. Например, если сперва была найдена запись со значениями полей { year: "2019", name: "Motherless Brooklyn" }, а затем { year: "2019", name: "Joker" }, в выборку попадет только первая запись.

В Go-коннекторе подсчёта общего количества записей, удовлетворяющих условиям, в запросе используются методы ReqTotal() и CachedTotal(). Результат вычисления в обоих случаях может быть получен при помощи вызова метода TotalCount() у возвращённого из запроса итератора.

Примеры

Ниже приведены примеры использования агрегатных функций в Reindexer.

Получение максимального значения поля

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

query := db.Query("test_table")
query.AggregateMax("field")

it := query.Exec()
if it.Error() != nil {
	panic(it.Error())
}
defer it.Close()

aggMaxRes := it.AggResults()[0]

if aggMaxRes.Value != nil {
	fmt.Printf("Максимальное значение поля 'field' = %v\n", *aggMaxRes.Value)
} else {
	fmt.Println("Нет данных для агрегации")
}

Функция MAX() может принимать в качестве аргумента число или строку, которую можно преобразовать в число. Агрегатные функции можно указывать через запятую с разными аргументами.

SELECT MAX('field') FROM test_table
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
  "namespace": "test_table",
  "type": "select",
  "aggregations": [
    {
      "fields": [
        "field"
      ],
      "type": "MAX"
    }
  ]
}'

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

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

query := db.Query("test_table").
	Where("price", reindexer.GT, 200).
	Limit(10).
	ReqTotal()

it := query.Exec()
if it.Error() != nil {
	panic(it.Error())
}
defer it.Close()

aggReqTotal := it.AggResults()[0]

if aggReqTotal.Value != nil {
	fmt.Printf("Количество документов в неймспейсе с price > 200 = %v\n", *aggReqTotal.Value)
} else {
	fmt.Println("Нет данных для агрегации")
}
SELECT COUNT(*) FROM test_table WHERE price > 200 LIMIT 10
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
  "namespace": "test_table",
  "type": "select",
  "aggregations": [
    {
      "fields": [
      ],
      "type": "COUNT"
    }
  ],
  "filters": [
    {
      "field": "price",
      "cond": "GT",
      "value": [
        200
      ],
    }
  ],
  "limit": 10
}'

Получение уникальных значений столбцов

В reindexer есть возможность получения уникальных значений для одного столбца (метод Distinct(field)) или для кортежей из нескольких столбцов (метод Distinct(field1, field2, ...)), в том числе для массивов.

Если в Distinct указано одно поле (скаляра, массива или композитного индекса), строка пространства имен считается уникальной в следующих случаях:

  • Скаляр уникален;
  • Одно из значений массива уникально;
  • Составное значение композитного индекса v1+v2+... уникально.

Все уникальные значения (включая все значения массива) добавляются к результату агрегации.

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

query := db.Query("test_table")
query.Distinct("field")

it := query.Exec()
if it.Error() != nil {
	panic(it.Error())
}
defer it.Close()

aggResults := it.AggResults()
distinctsRes := aggResults[0]

fmt.Println("Уникальные значения столбца 'field':")
for _, res := range distinctsRes.Distincts {
	fmt.Println(res[0]) // В этом случае каждой строке гарантированно будет по 1 элементу
}
SELECT DISTINCT('field') FROM test_table

curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
  "namespace": "test_table",
  "type": "select",
  "aggregations": [
    {
      "fields": [
        "field"
      ],
      "type": "DISTINCT"
    }
  ]
}'

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

  • Кортеж значений {v1, v2, ...} уникален.

При формировании кортежей массивы дополняются пустыми null-значениями до одинаковой длины. Таким образом в рамках каждой строки получается прямоугольная таблица, из которой в качестве очередного кортежа выбирается каждая отдельная строка {v1[i], v2[i], ...} и проверяется, есть ли такое значение в списке уникальных значений.

Если хотя бы одно из этих значений уникально, вся строка пространства имен считается уникальной, а результат агрегирования содержит все уникальные значения {v1[i], v2[i], ...}. При этом кортежи, состоящие целиком из null, будут проигнорированы.

Для неиндексных значений совместимость типа (массив или скаляр) проверяется в пределах одной строки пространства имён.

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

query := db.Query("test_table")
query.Distinct("field1", "field2")

it := query.Exec()
if it.Error() != nil {
	panic(it.Error())
}
defer it.Close()

aggResults := it.AggResults()
distinctsRes := aggResults[0]

fmt.Println("Уникальные значения столбцов 'field1', 'field2':")
for _, res := range distinctsRes.Distincts {
	fmt.Println(res[0], res[1]) // В этом случае каждой строке гарантированно будет по 2 элемента
}
SELECT DISTINCT('field1', 'field2') FROM test_table

curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
  "namespace": "test_table",
  "type": "select",
  "aggregations": [
    {
      "fields": [
        "field1",
        "field2"
      ],
      "type": "DISTINCT"
    }
  ]
}'

Получение уникальных значений для нескольких столбцов по отдельности

Distinct можно применять несколько раз: к разным колонкам в рамках одного запроса. В этом случае запрос вернёт только результаты агреграции.

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

query := db.Query("test_table")
query.Distinct("field1").Distinct("field2")

it := query.Exec()
if it.Error() != nil {
	panic(it.Error())
}
defer it.Close()

aggResults := it.AggResults()
distFields1 := aggResults[0]

fmt.Println("Уникальные значения столбца 'field1':")
for _, field1 := range distFields1.Distincts {
	fmt.Println(field1[0])
}

distFields2 := aggResults[1]
fmt.Println("Уникальные значения столбца 'field2':")
for _, field2 := range distFields2.Distincts {
	fmt.Println(field2[0])
}
SELECT DISTINCT('field1'), DISTINCT('field2') FROM test_table
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
  "namespace": "test_table",
  "type": "select",
  "aggregations": [
    {
      "fields": [
        "field1"
      ],
      "type": "DISTINCT"
    },
    {
      "fields": [
        "field2"
      ],
      "type": "DISTINCT"
    }
  ]
}'

Получение уникальных значений в столбце с указанием их количества

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

В Go для поддержки фасетов агрегации реализован метод AggregateFacet, возвращающий AggregationFacetRequest, который имеет методы Sort, Limit и Offset.

query := db.Query("test_table")
query.AggregateFacet("name")

it := query.Exec()
if it.Error() != nil {
	panic(it.Error())
}
defer it.Close()

aggFacetRes := it.AggResults()[0]

fmt.Printf("'Значение name' -> Кол. уникальных\n")
for _, facet := range aggFacetRes.Facets {
	fmt.Printf("'%v' -> %v\n", facet.Values[0], facet.Count)
}

AggregateFacet может использоваться для нескольких столбцов одновременно для подсчёта количества уникальных пар:

query := db.Query("test_table")
query.AggregateFacet("name", "country")

it := query.Exec()
if it.Error() != nil {
	panic(it.Error())
}
defer it.Close()

aggFacetRes := it.AggResults()[0]


fmt.Printf("'Значение name' / 'значение country' -> Кол. уникальных пар\n")
for _, facet := range aggFacetRes.Facets {
	fmt.Printf("'%v' / '%v' -> %v\n", facet.Values[0], facet.Values[1], facet.Count)
}
SELECT facet('name') FROM test_table

facet может использоваться для нескольких столбцов одновременно:

SELECT facet('name', 'country') FROM test_table
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
  "namespace": "test_table",
  "type": "select",
  "aggregations": [
    {
      "fields": [
        "name"
      ],
      "type": "FACET"
    }
  ]
}'

FACET может использоваться для нескольких столбцов одновременно:

curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
  "namespace": "test_table",
  "type": "select",
  "aggregations": [
    {
      "fields": [
        "name",
        "country"
      ],
      "type": "FACET"
    }
  ]
}'

Также Reindexer поддерживает сортировку по агрегированным полям facet. Примеры:

query := db.Query("test_table")
query.AggregateFacet("name", "age").Sort("name", false).Sort("age", true)

it := query.Exec()
if it.Error() != nil {
	panic(it.Error())
}
defer it.Close()

aggFacetRes := it.AggResults()[0]
fmt.Printf("'Значение name' / 'Значение age' -> Кол. уникальных пар\n")
for _, facet := range aggFacetRes.Facets {
        fmt.Printf("'%v' / '%v' -> %v\n", facet.Values[0], facet.Values[1], facet.Count)
}
SELECT FACET ('name', 'age' ORDER BY 'name' ASC, 'age' DESC) FROM test_table 
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
  "namespace": "test_table",
  "type": "select",
  "aggregations": [
    {
      "fields": [
        "name",
        "age"
      ],
      "type": "FACET"
    }
  ],
  "sort": [
    {
      "field": "name",
      "desc": false
    },
    {
      "field": "age",
      "desc": true
    }
  ]
}'

Сортировать можно и по значениям count, которые формирует facet. Примеры:

query := db.Query("test_table")
query.AggregateFacet("name", "age").Sort("count", false)

it := query.Exec()
if it.Error() != nil {
	panic(it.Error())
}
defer it.Close()

aggFacetRes := it.AggResults()[0]
fmt.Printf("'Значение name' / 'Значение age' -> Кол. уникальных пар\n")
for _, facet := range aggFacetRes.Facets {
        fmt.Printf("'%v' / '%v' -> %v\n", facet.Values[0], facet.Values[1], facet.Count)
}
SELECT FACET ('name', 'age' ORDER BY 'count' ASC) FROM test_table 
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
  "namespace": "test_table",
  "type": "select",
  "aggregations": [
    {
      "fields": [
        "name",
        "age"
      ],
      "type": "FACET",
      "sort": [
        {
          "field": "count",
          "desc": false
        }
      ]
    }
  ]
}'

Совместно с сортировкой по агрегируемым facet полям можно использовать параметры LIMIT и OFFSET для ограничения выдачи. Примеры:

query := db.Query("test_table")
query.AggregateFacet("name", "age").Sort("count", false).Offset(10).Limit(100)

it := query.Exec()
if it.Error() != nil {
	panic(it.Error())
}
defer it.Close()

aggFacetRes := it.AggResults()[0]
fmt.Printf("'Значение name' / 'Значение age' -> Кол. уникальных пар\n")
for _, facet := range aggFacetRes.Facets {
        fmt.Printf("'%v' / '%v' -> %v\n", facet.Values[0], facet.Values[1], facet.Count)
}
SELECT FACET ('name', 'age' ORDER BY 'count' ASC OFFSET 10 LIMIT 100) FROM test_table 
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
  "namespace": "test_table",
  "type": "select",
  "aggregations": [
    {
      "fields": [
        "name",
        "age"
      ],
      "type": "FACET",
      "sort": [
        {
          "field": "count",
          "desc": false
        }
      ],
      "limit": 100,
      "offset": 10
    }
  ]
}'

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

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

query := db.Query("test_table")
query.AggregateMax("price")
query.AggregateFacet("name").Limit(100)

it := query.Exec()
if it.Error() != nil {
	panic(it.Error())
}
defer it.Close()

aggMaxRes := it.AggResults()[0]
if aggMaxRes.Value != nil {
	fmt.Printf("max price = %v\n", *aggMaxRes.Value)
} else {
	fmt.Println("Нет данных для агрегации")
}

aggFacetRes := it.AggResults()[1]

fmt.Printf("'name' -> count\n")
for _, facet := range aggFacetRes.Facets {
	fmt.Printf("'%v' -> %d\n", facet.Values[0], facet.Count)
}

Агрегатные функции можно указывать через запятую с разными аргументами.

SELECT MAX('price'), facet('name') FROM test_table LIMIT 100
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
  "namespace": "test_table",
  "type": "select",
  "aggregations": [
    {
      "fields": [
        "price"
      ],
      "type": "MAX"
    },
    {
      "fields": [
        "name"
      ],
      "type": "FACET"
    }
  ],
  "limit": 100
}'