Manticore разработан для эффективного масштабирования за счёт возможностей распределённого поиска. Распределённый поиск полезен для улучшения задержки выполнения запросов (т.е. времени поиска) и пропускной способности (т.е. максимального количества запросов в секунду) в многосерверных, многоядерных или многоядерных средах. Это критично для приложений, которым нужно искать в огромных объёмах данных (т.е. миллиарды записей и терабайты текста).
Основная идея заключается в горизонтальном разделении данных для поиска между узлами поиска и их параллельной обработке.
Разбиение выполняется вручную. Для настройки необходимо:
- Развернуть несколько экземпляров Manticore на разных серверах
- Распределить разные части вашего набора данных по разным экземплярам
- Настроить специальную распределённую таблицу на некоторых из
searchdэкземпляров - Направить ваши запросы к распределённой таблице
Этот тип таблицы содержит только ссылки на другие локальные и удалённые таблицы — поэтому её нельзя индексировать напрямую. Вместо этого необходимо переиндексировать таблицы, на которые она ссылается.
Когда Manticore получает запрос к распределённой таблице, он выполняет следующие шаги:
- Подключается к настроенным удалённым агентам
- Отправляет им запрос
- Одновременно выполняет поиск по настроенным локальным таблицам (в то время как удалённые агенты ищут)
- Получает результаты поиска от удалённых агентов
- Объединяет все результаты, удаляя дубликаты
- Отправляет объединённые результаты клиенту
С точки зрения приложения, нет различий между поиском в обычной таблице или в распределённой таблице. Другими словами, распределённые таблицы полностью прозрачны для приложения, и невозможно определить, была ли запрошенная таблица распределённой или локальной.
Узнайте больше о удалённых узлах.
Мультизапросы, или пакетные запросы, позволяют отправлять несколько поисковых запросов в Manticore в рамках одного сетевого запроса.
👍 Зачем использовать мультизапросы?
Основная причина — производительность. Отправляя запросы в Manticore пакетом, а не по одному, вы экономите время за счет сокращения сетевых обходов. Кроме того, отправка запросов пакетом позволяет Manticore выполнять определенные внутренние оптимизации. Если оптимизации пакета неприменимы, запросы будут обрабатываться по отдельности.
⛔ Когда не следует использовать мультизапросы?
Мультизапросы требуют, чтобы все поисковые запросы в пакете были независимыми, что не всегда так. Иногда запрос B зависит от результатов запроса A, то есть запрос B можно настроить только после выполнения запроса A. Например, вы можете захотеть отобразить результаты из вторичного индекса только в том случае, если в основной таблице не найдено результатов, или вы можете указать смещение во втором наборе результатов на основе количества совпадений в первом наборе результатов. В этих случаях вам потребуется использовать отдельные запросы (или отдельные пакеты).
При использовании библиотек-коннекторов, таких как mysqli в PHP, вы можете добавить несколько запросов, а затем выполнить их все как единый пакет. Это будет работать как один пакет мультизапросов.
Примечание: Если вы используете консольный клиент MySQL, по умолчанию он интерпретирует точку с запятой (;) как разделитель и отправляет каждый запрос на сервер по отдельности; это не пакет мультизапросов. Чтобы переопределить это поведение, переопределите разделитель на стороне клиента на другой символ с помощью внутренней команды delimiter. После внесения этого изменения клиент будет отправлять всю строку с точками с запятой без изменений, что позволит работать "магии мультизапросов".
Такое побочное поведение консольного клиента часто может сбивать с толку, потому что вы можете заметить, что одна и та же последовательность команд ведет себя по-разному в консоли клиента MySQL по сравнению с другим протоколом, таким как SQL-over-HTTP. Это именно потому, что сам консольный клиент MySQL разделяет запросы с помощью точек с запятой, но другие протоколы могут отправлять всю последовательность как единый пакет.
Вы можете выполнить несколько поисковых запросов с помощью SQL, разделив их точкой с запятой. Когда Manticore получает запрос в таком формате от клиента, будут применены все оптимизации между операторами.
Мультизапросы не поддерживают запросы с FACET. Количество мультизапросов в одном пакете не должно превышать max_batch_queries.
- SQL
SELECT id, price FROM products WHERE MATCH('remove hair') ORDER BY price DESC; SELECT id, price FROM products WHERE MATCH('remove hair') ORDER BY price ASCИз консольного клиента MySQL/MariaDB:
DELIMITER _
SELECT id, price FROM products WHERE MATCH('remove hair') ORDER BY price DESC; SELECT id, price FROM products WHERE MATCH('remove hair') ORDER BY price ASC_
Существует две основные оптимизации, о которых следует знать: оптимизация общих запросов и оптимизация общих поддеревьев.
Оптимизация общих запросов означает, что searchd определит все те запросы в пакете, где различаются только настройки сортировки и группировки, и выполнит поиск только один раз. Например, если пакет состоит из 3 запросов, все они для "ipod nano", но 1-й запрос запрашивает топ-10 результатов, отсортированных по цене, 2-й запрос группирует по ID поставщика и запрашивает топ-5 поставщиков, отсортированных по рейтингу, а 3-й запрос запрашивает максимальную цену, полнотекстовый поиск по "ipod nano" будет выполнен только один раз, и его результаты будут повторно использованы для построения 3 различных наборов результатов.
Фасетный поиск — это особенно важный случай, который выигрывает от этой оптимизации. Действительно, фасетный поиск может быть реализован путем выполнения нескольких запросов: один для получения самих результатов поиска, и несколько других с тем же полнотекстовым запросом, но с разными настройками группировки для получения всех требуемых групп результатов (топ-3 автора, топ-5 поставщиков и т.д.). Пока полнотекстовый запрос и настройки фильтрации остаются неизменными, сработает оптимизация общих запросов и значительно повысит производительность.
Оптимизация общих поддеревьев еще интереснее. Она позволяет searchd использовать сходства между пакетными полнотекстовыми запросами. Она определяет общие части полнотекстовых запросов (поддеревья) во всех запросах и кэширует их между запросами. Например, рассмотрим следующий пакет запросов:
donald trump president
donald trump barack obama john mccain
donald trump speech
Есть общая часть из двух слов donald trump, которую можно вычислить только один раз, затем закэшировать и использовать во всех запросах. И оптимизация общих поддеревьев делает именно это. Размер кэша на запрос строго контролируется директивами subtree_docs_cache и subtree_hits_cache (чтобы кэширование всех шестнадцати газиллионов документов, соответствующих "i am", не исчерпало оперативную память и мгновенно не убило ваш сервер).
Как можно определить, были ли запросы в пакете фактически оптимизированы? Если да, то соответствующий журнал запросов будет содержать поле "multiplier", указывающее, сколько запросов было обработано вместе:
Обратите внимание на поле "x3". Оно означает, что этот запрос был оптимизирован и обработан в подпакете из 3 запросов.
- log
[Sun Jul 12 15:18:17.000 2009] 0.040 sec x3 [ext/0/rel 747541 (0,20)] [lj] the
[Sun Jul 12 15:18:17.000 2009] 0.040 sec x3 [ext/0/ext 747541 (0,20)] [lj] the
[Sun Jul 12 15:18:17.000 2009] 0.040 sec x3 [ext/0/ext 747541 (0,20)] [lj] theДля справки, вот как выглядел бы обычный журнал, если бы запросы не были пакетными:
- log
[Sun Jul 12 15:18:17.062 2009] 0.059 sec [ext/0/rel 747541 (0,20)] [lj] the
[Sun Jul 12 15:18:17.156 2009] 0.091 sec [ext/0/ext 747541 (0,20)] [lj] the
[Sun Jul 12 15:18:17.250 2009] 0.092 sec [ext/0/ext 747541 (0,20)] [lj] theОбратите внимание, как время на запрос в случае мультизапроса улучшилось в 1,5–2,3 раза в зависимости от конкретного режима сортировки.
Мультизапросы в основном поддерживаются для пакетной обработки запросов и получения метаинформации после таких пакетов. Из-за этого ограничения в пакетах разрешено только небольшое подмножество операторов. В одном пакете можно комбинировать только операторы SELECT, SHOW и SET.
Вы можете использовать SELECT как обычно; однако обратите внимание, что все запросы будут выполняться вместе за один проход. Если запросы не связаны, нет пользы от мультизапросов. Демон обнаружит это и запустит запросы по одному.
Вы можете использовать SHOW для обработки предупреждений, статуса, статуса агента, метаданных, профиля и плана. Все остальные операторы SHOW в пакетах будут тихо игнорироваться без вывода. Например, вы не можете выполнить SHOW TABLES, SHOW THREADS или SHOW VARIABLES, или любой другой оператор, не упомянутый выше, при пакетной обработке.
Вы можете использовать SET только для SET PROFILING. Все остальные команды SET ... будут тихо игнорироваться.
Порядок выполнения также отличается. Демон обрабатывает пакеты в два прохода.
Сначала он собирает все операторы SELECT и выполняет все встреченные операторы SET PROFILING одновременно. Как побочный эффект, только последний оператор SET PROFILING является эффективным. Если вы выполните мультизапрос, например: SET PROFILING=1; SELECT...; SHOW META; SHOW PROFILE; SET PROFILING=0, вы не увидите никакого профиля, потому что на первом проходе демон выполняет SET PROFILING=1, а затем сразу SET PROFILING=0.
Во-вторых, демон пытается выполнить один пакетный запрос со всеми собранными операторами SELECT. Если операторы не связаны, он выполнит их один за другим.
Наконец, он проходит по исходной последовательности пакета и возвращает данные подрезультата и метаданные из набора результатов для каждого SELECT и SHOW. Поскольку все операторы SET PROFILING были выполнены на первом проходе, они пропускаются на этом втором проходе.
each SELECT and SHOW. Since all SET PROFILING statements were executed in the first pass, they are skipped on this second pass.
Manticore поддерживает подзапросы SELECT в SQL следующем формате:
SELECT * FROM (SELECT ... ORDER BY cond1 LIMIT X) ORDER BY cond2 LIMIT Y
Внешний SELECT допускает только конструкции ORDER BY и LIMIT. В настоящее время подзапросы используются в двух случаях:
-
Когда у вас есть запрос с двумя функциями ранжирования UDF, одна очень быстрая, другая медленная, и выполняется полнотекстовый поиск с большим набором результатов совпадений. Без подвыборки запрос выглядел бы так:
SELECT id,slow_rank() as slow,fast_rank() as fast FROM index WHERE MATCH(‘some common query terms’) ORDER BY fast DESC, slow DESC LIMIT 20 OPTION max_matches=1000;С подвыборками запрос можно переписать следующим образом:
SELECT * FROM (SELECT id,slow_rank() as slow,fast_rank() as fast FROM index WHERE MATCH(‘some common query terms’) ORDER BY fast DESC LIMIT 100 OPTION max_matches=1000) ORDER BY slow DESC LIMIT 20;В исходном запросе UDF
slow_rank()вычисляется для всего набора результатов совпадений. С помощью подзапросов SELECT для всего набора совпадений вычисляется толькоfast_rank(), аslow_rank()вычисляется для ограниченного набора. -
Второй случай полезен для больших наборов результатов, поступающих из распределённой таблицы.
Для этого запроса:
SELECT * FROM my_dist_index WHERE some_conditions LIMIT 50000;Если у вас 20 узлов, каждый узел может отправить мастеру максимум 50K записей, что в сумме дает 20 x 50K = 1M записей. Однако, поскольку мастер возвращает только 50K (из 1M), может быть достаточно, чтобы узлы отправляли только топ 10K записей. С подзапросом вы можете переписать запрос так:
SELECT * FROM (SELECT * FROM my_dist_index WHERE some_conditions LIMIT 10000) ORDER by some_attr LIMIT 50000;В этом случае узлы получают только внутренний запрос и выполняют его. Это означает, что мастер получит только 20x10K=200K записей. Мастер возьмет все полученные записи, переупорядочит их по ВНЕШНЕМУ условию и вернет лучшие 50K записей. Подвыборка помогает сократить трафик между мастером и узлами, а также уменьшить время обработки мастера (так как он обрабатывает только 200K вместо 1M записей).
Группировка результатов поиска часто полезна для получения количества совпадений по группам или других агрегаций. Например, она полезна для создания графика, иллюстрирующего количество соответствующих постов в блоге за месяц, или для группировки результатов веб-поиска по сайту или постов на форуме по автору и т.д.
Manticore поддерживает группировку результатов поиска по одному или нескольким столбцам и вычисляемым выражениям. Результаты могут:
- Сортироваться внутри группы
- Возвращать более одной строки на группу
- Иметь фильтрацию групп
- Иметь сортировку групп
- Агрегироваться с использованием агрегационных функций
- SQL
- JSON
Общий синтаксис
SELECT {* | SELECT_expr [, SELECT_expr ...]}
...
GROUP BY {field_name | alias } [, ...]
[HAVING where_condition]
[WITHIN GROUP ORDER BY field_name {ASC | DESC} [, ...]]
...
SELECT_expr: { field_name | function_name(...) }
where_condition: {aggregation expression alias | COUNT(*)}Формат JSON-запроса в настоящее время поддерживает базовую группировку, которая может извлекать агрегированные значения и их count(*).
{
"table": "<table_name>",
"limit": 0,
"aggs": {
"<aggr_name>": {
"terms": {
"field": "<attribute>",
"size": <int value>
}
}
}
}Стандартный вывод запроса возвращает набор результатов без группировки, который можно скрыть с помощью limit (или size).
Для агрегации требуется установить size для размера результирующего набора группы.
Группировка довольно проста - просто добавьте "GROUP BY что-то" в конец вашего SELECT запроса. Этим "что-то" может быть:
- Любое не полнотекстовое поле из таблицы: целочисленное, с плавающей точкой, строковое, MVA (многозначный атрибут)
- Или, если вы использовали псевдоним в списке
SELECT, вы также можете группировать по нему
Вы можете опустить любые агрегационные функции в списке SELECT, и это все равно будет работать:
- SQL
SELECT release_year FROM films GROUP BY release_year LIMIT 5;+--------------+
| release_year |
+--------------+
| 2004 |
| 2002 |
| 2001 |
| 2005 |
| 2000 |
+--------------+Однако в большинстве случаев вы захотите получить некоторые агрегированные данные для каждой группы, такие как:
COUNT(*), чтобы просто получить количество элементов в каждой группе- или
AVG(field), чтобы вычислить среднее значение поля в группе
Для HTTP JSON-запросов использование одного блока aggs с limit=0 на основном уровне запроса работает аналогично SQL-запросу с GROUP BY и COUNT(*), обеспечивая эквивалентное поведение и производительность.
- SQL1
- SQL2
- JSON
- PHP
- Python
- Python-asyncio
- Javascript
- Java
- C#
- Rust
- TypeScript
- Go
SELECT release_year, count(*) FROM films GROUP BY release_year LIMIT 5;SELECT release_year, AVG(rental_rate) FROM films GROUP BY release_year LIMIT 5;POST /search -d '
{
"table" : "films",
"limit": 0,
"aggs" :
{
"release_year" :
{
"terms" :
{
"field":"release_year",
"size":100
}
}
}
}
'$index->setName('films');
$search = $index->search('');
$search->limit(0);
$search->facet('release_year','release_year',100);
$results = $search->get();
print_r($results->getFacets());res =searchApi.search({"table":"films","limit":0,"aggs":{"release_year":{"terms":{"field":"release_year","size":100}}}})res = await searchApi.search({"table":"films","limit":0,"aggs":{"release_year":{"terms":{"field":"release_year","size":100}}}})res = await searchApi.search({"table":"films","limit":0,"aggs":{"release_year":{"terms":{"field":"release_year","size":100}}}});HashMap<String,Object> aggs = new HashMap<String,Object>(){{
put("release_year", new HashMap<String,Object>(){{
put("terms", new HashMap<String,Object>(){{
put("field","release_year");
put("size",100);
}});
}});
}};
searchRequest = new SearchRequest();
searchRequest.setIndex("films");
searchRequest.setLimit(0);
query = new HashMap<String,Object>();
query.put("match_all",null);
searchRequest.setQuery(query);
searchRequest.setAggs(aggs);
searchResponse = searchApi.search(searchRequest);var agg = new Aggregation("release_year", "release_year");
agg.Size = 100;
object query = new { match_all=null };
var searchRequest = new SearchRequest("films", query);
searchRequest.Aggs = new List<Aggregation> {agg};
var searchResponse = searchApi.Search(searchRequest);let query = SearchQuery::new();
let aggTerms1 = AggTerms::new {
fields: "release_year".to_string(),
size: Some(100),
};
let agg1 = Aggregation {
terms: Some(Box::new(aggTerms1)),
..Default::default(),
};
let mut aggs = HashMap::new();
aggs.insert("release_year".to_string(), agg1);
let search_req = SearchRequest {
table: "films".to_string(),
query: Some(Box::new(query)),
aggs: serde_json::json!(aggs),
..Default::default(),
};
let search_res = search_api.search(search_req).await;res = await searchApi.search({
index: 'test',
limit: 0,
aggs: {
cat_id: {
terms: { field: "cat", size: 1 }
}
}
});query := map[string]interface{} {};
searchRequest.SetQuery(query);
aggTerms := manticoreclient.NewAggregationTerms()
aggTerms.SetField("cat")
aggTerms.SetSize(1)
aggregation := manticoreclient.NewAggregation()
aggregation.setTerms(aggTerms)
searchRequest.SetAggregation(aggregation)
res, _, _ := apiClient.SearchAPI.Search(context.Background()).SearchRequest(*searchRequest).Execute()+--------------+----------+
| release_year | count(*) |
+--------------+----------+
| 2004 | 108 |
| 2002 | 108 |
| 2001 | 91 |
| 2005 | 93 |
| 2000 | 97 |
+--------------+----------++--------------+------------------+
| release_year | avg(rental_rate) |
+--------------+------------------+
| 2004 | 2.78629661 |
| 2002 | 3.08259249 |
| 2001 | 3.09989142 |
| 2005 | 2.90397978 |
| 2000 | 3.17556739 |
+--------------+------------------+{
"took": 2,
"timed_out": false,
"hits": {
"total": 10000,
"hits": [
]
},
"release_year": {
"group_brand_id": {
"buckets": [
{
"key": 2004,
"doc_count": 108
},
{
"key": 2002,
"doc_count": 108
},
{
"key": 2000,
"doc_count": 97
},
{
"key": 2005,
"doc_count": 93
},
{
"key": 2001,
"doc_count": 91
}
]
}
}
}Array
(
[release_year] => Array
(
[buckets] => Array
(
[0] => Array
(
[key] => 2009
[doc_count] => 99
)
[1] => Array
(
[key] => 2008
[doc_count] => 102
)
[2] => Array
(
[key] => 2007
[doc_count] => 93
)
[3] => Array
(
[key] => 2006
[doc_count] => 103
)
[4] => Array
(
[key] => 2005
[doc_count] => 93
)
[5] => Array
(
[key] => 2004
[doc_count] => 108
)
[6] => Array
(
[key] => 2003
[doc_count] => 106
)
[7] => Array
(
[key] => 2002
[doc_count] => 108
)
[8] => Array
(
[key] => 2001
[doc_count] => 91
)
[9] => Array
(
[key] => 2000
[doc_count] => 97
)
)
)
){'aggregations': {u'release_year': {u'buckets': [{u'doc_count': 99,
u'key': 2009},
{u'doc_count': 102,
u'key': 2008},
{u'doc_count': 93,
u'key': 2007},
{u'doc_count': 103,
u'key': 2006},
{u'doc_count': 93,
u'key': 2005},
{u'doc_count': 108,
u'key': 2004},
{u'doc_count': 106,
u'key': 2003},
{u'doc_count': 108,
u'key': 2002},
{u'doc_count': 91,
u'key': 2001},
{u'doc_count': 97,
u'key': 2000}]}},
'hits': {'hits': [], 'max_score': None, 'total': 1000},
'profile': None,
'timed_out': False,
'took': 0}{'aggregations': {u'release_year': {u'buckets': [{u'doc_count': 99,
u'key': 2009},
{u'doc_count': 102,
u'key': 2008},
{u'doc_count': 93,
u'key': 2007},
{u'doc_count': 103,
u'key': 2006},
{u'doc_count': 93,
u'key': 2005},
{u'doc_count': 108,
u'key': 2004},
{u'doc_count': 106,
u'key': 2003},
{u'doc_count': 108,
u'key': 2002},
{u'doc_count': 91,
u'key': 2001},
{u'doc_count': 97,
u'key': 2000}]}},
'hits': {'hits': [], 'max_score': None, 'total': 1000},
'profile': None,
'timed_out': False,
'took': 0}{"took":0,"timed_out":false,"aggregations":{"release_year":{"buckets":[{"key":2009,"doc_count":99},{"key":2008,"doc_count":102},{"key":2007,"doc_count":93},{"key":2006,"doc_count":103},{"key":2005,"doc_count":93},{"key":2004,"doc_count":108},{"key":2003,"doc_count":106},{"key":2002,"doc_count":108},{"key":2001,"doc_count":91},{"key":2000,"doc_count":97}]}},"hits":{"total":1000,"hits":[]}}class SearchResponse {
took: 0
timedOut: false
aggregations: {release_year={buckets=[{key=2009, doc_count=99}, {key=2008, doc_count=102}, {key=2007, doc_count=93}, {key=2006, doc_count=103}, {key=2005, doc_count=93}, {key=2004, doc_count=108}, {key=2003, doc_count=106}, {key=2002, doc_count=108}, {key=2001, doc_count=91}, {key=2000, doc_count=97}]}}
hits: class SearchResponseHits {
maxScore: null
total: 1000
hits: []
}
profile: null
}class SearchResponse {
took: 0
timedOut: false
aggregations: {release_year={buckets=[{key=2009, doc_count=99}, {key=2008, doc_count=102}, {key=2007, doc_count=93}, {key=2006, doc_count=103}, {key=2005, doc_count=93}, {key=2004, doc_count=108}, {key=2003, doc_count=106}, {key=2002, doc_count=108}, {key=2001, doc_count=91}, {key=2000, doc_count=97}]}}
hits: class SearchResponseHits {
maxScore: null
total: 1000
hits: []
}
profile: null
}class SearchResponse {
took: 0
timedOut: false
aggregations: {release_year={buckets=[{key=2009, doc_count=99}, {key=2008, doc_count=102}, {key=2007, doc_count=93}, {key=2006, doc_count=103}, {key=2005, doc_count=93}, {key=2004, doc_count=108}, {key=2003, doc_count=106}, {key=2002, doc_count=108}, {key=2001, doc_count=91}, {key=2000, doc_count=97}]}}
hits: class SearchResponseHits {
maxScore: null
total: 1000
hits: []
}
profile: null
}{
"took":0,
"timed_out":false,
"aggregations":
{
"cat_id":
{
"buckets":
[{
"key":1,
"doc_count":1
}]
}
},
"hits":
{
"total":5,
"hits":[]
}
}{
"took":0,
"timed_out":false,
"aggregations":
{
"cat_id":
{
"buckets":
[{
"key":1,
"doc_count":1
}]
}
},
"hits":
{
"total":5,
"hits":[]
}
}По умолчанию группы не сортируются, и следующее, что вы обычно хотите сделать, - это упорядочить их по какому-либо признаку, например, по полю, по которому вы группируете:
- SQL
SELECT release_year, count(*) from films GROUP BY release_year ORDER BY release_year asc limit 5;+--------------+----------+
| release_year | count(*) |
+--------------+----------+
| 2000 | 97 |
| 2001 | 91 |
| 2002 | 108 |
| 2003 | 106 |
| 2004 | 108 |
+--------------+----------+В качестве альтернативы, вы можете сортировать по агрегации:
- по
count(*), чтобы отображать группы с наибольшим количеством элементов первыми - по
avg(rental_rate), чтобы показать фильмы с наивысшим рейтингом первыми. Обратите внимание, что в примере это делается через псевдоним:avg(rental_rate)сначала отображается наavgв спискеSELECT, а затем мы просто делаемORDER BY avg
- SQL1
- SQL2
SELECT release_year, count(*) FROM films GROUP BY release_year ORDER BY count(*) desc LIMIT 5;SELECT release_year, AVG(rental_rate) avg FROM films GROUP BY release_year ORDER BY avg desc LIMIT 5;+--------------+----------+
| release_year | count(*) |
+--------------+----------+
| 2004 | 108 |
| 2002 | 108 |
| 2003 | 106 |
| 2006 | 103 |
| 2008 | 102 |
+--------------+----------++--------------+------------+
| release_year | avg |
+--------------+------------+
| 2006 | 3.26184368 |
| 2000 | 3.17556739 |
| 2001 | 3.09989142 |
| 2002 | 3.08259249 |
| 2008 | 2.99000049 |
+--------------+------------+В некоторых случаях вы можете захотеть группировать не по одному полю, а по нескольким полям одновременно, например, по категории фильма и году:
- SQL
- JSON
SELECT category_id, release_year, count(*) FROM films GROUP BY category_id, release_year ORDER BY category_id ASC, release_year ASC;POST /search -d '
{
"size": 0,
"table": "films",
"aggs": {
"cat_release": {
"composite": {
"size":5,
"sources": [
{ "category": { "terms": { "field": "category_id" } } },
{ "release year": { "terms": { "field": "release_year" } } }
]
}
}
}
}
'+-------------+--------------+----------+
| category_id | release_year | count(*) |
+-------------+--------------+----------+
| 1 | 2000 | 5 |
| 1 | 2001 | 2 |
| 1 | 2002 | 6 |
| 1 | 2003 | 6 |
| 1 | 2004 | 5 |
| 1 | 2005 | 10 |
| 1 | 2006 | 4 |
| 1 | 2007 | 5 |
| 1 | 2008 | 7 |
| 1 | 2009 | 14 |
| 2 | 2000 | 10 |
| 2 | 2001 | 5 |
| 2 | 2002 | 6 |
| 2 | 2003 | 6 |
| 2 | 2004 | 10 |
| 2 | 2005 | 4 |
| 2 | 2006 | 5 |
| 2 | 2007 | 8 |
| 2 | 2008 | 8 |
| 2 | 2009 | 4 |
+-------------+--------------+----------+{
"took": 0,
"timed_out": false,
"hits": {
"total": 1000,
"total_relation": "eq",
"hits": []
},
"aggregations": {
"cat_release": {
"after_key": {
"category": 1,
"release year": 2007
},
"buckets": [
{
"key": {
"category": 1,
"release year": 2008
},
"doc_count": 7
},
{
"key": {
"category": 1,
"release year": 2009
},
"doc_count": 14
},
{
"key": {
"category": 1,
"release year": 2005
},
"doc_count": 10
},
{
"key": {
"category": 1,
"release year": 2004
},
"doc_count": 5
},
{
"key": {
"category": 1,
"release year": 2007
},
"doc_count": 5
}
]
}
}
}Иногда полезно видеть не один элемент на группу, а несколько. Это легко достигается с помощью GROUP N BY. Например, в следующем случае мы получаем два фильма для каждого года, а не один, который вернул бы простой GROUP BY release_year.
- SQL
SELECT release_year, title FROM films GROUP 2 BY release_year ORDER BY release_year DESC LIMIT 6;+--------------+-----------------------------+
| release_year | title |
+--------------+-----------------------------+
| 2009 | ALICE FANTASIA |
| 2009 | ALIEN CENTER |
| 2008 | AMADEUS HOLY |
| 2008 | ANACONDA CONFESSIONS |
| 2007 | ANGELS LIFE |
| 2007 | ARACHNOPHOBIA ROLLERCOASTER |
+--------------+-----------------------------+Еще одно важное требование аналитики - сортировка элементов внутри группы. Для этого используйте предложение WITHIN GROUP ORDER BY ... {ASC|DESC}. Например, давайте получим фильм с наивысшим рейтингом для каждого года. Обратите внимание, что это работает параллельно с простым ORDER BY:
WITHIN GROUP ORDER BYсортирует результаты внутри группы- в то время как просто
GROUP BYсортирует сами группы
Эти два работают полностью независимо.
- SQL
SELECT release_year, title, rental_rate FROM films GROUP BY release_year WITHIN GROUP ORDER BY rental_rate DESC ORDER BY release_year DESC LIMIT 5;+--------------+------------------+-------------+
| release_year | title | rental_rate |
+--------------+------------------+-------------+
| 2009 | AMERICAN CIRCUS | 4.990000 |
| 2008 | ANTHEM LUKE | 4.990000 |
| 2007 | ATTACKS HATE | 4.990000 |
| 2006 | ALADDIN CALENDAR | 4.990000 |
| 2005 | AIRPLANE SIERRA | 4.990000 |
+--------------+------------------+-------------+HAVING expression - это полезное предложение для фильтрации групп. В то время как WHERE применяется до группировки, HAVING работает с группами. Например, давайте оставим только те годы, когда средняя арендная ставка фильмов за этот год была выше 3. Мы получаем только четыре года:
- SQL
SELECT release_year, avg(rental_rate) avg FROM films GROUP BY release_year HAVING avg > 3;+--------------+------------+
| release_year | avg |
+--------------+------------+
| 2002 | 3.08259249 |
| 2001 | 3.09989142 |
| 2000 | 3.17556739 |
| 2006 | 3.26184368 |
+--------------+------------+Примечание: Значение total_found в метаинформации поискового запроса отражает количество групп, соответствующих условию HAVING. Это обеспечивает правильную пагинацию при использовании предложений HAVING с GROUP BY.
Есть функция GROUPBY(), которая возвращает ключ текущей группы. Это полезно во многих случаях, особенно когда вы GROUP BY по MVA или по значению JSON.
Её также можно использовать в HAVING, например, чтобы оставить только года 2000 и 2002.
Обратите внимание, что GROUPBY() не рекомендуется использовать при группировке по нескольким полям одновременно. Она всё равно будет работать, но так как ключ группы в этом случае является составным из значений полей, он может отображаться не так, как вы ожидаете.
- SQL
SELECT release_year, count(*) FROM films GROUP BY release_year HAVING GROUPBY() IN (2000, 2002);+--------------+----------+
| release_year | count(*) |
+--------------+----------+
| 2002 | 108 |
| 2000 | 97 |
+--------------+----------+Manticore поддерживает группировку по MVA. Чтобы показать, как это работает, создадим таблицу "shoes" с MVA "sizes" и вставим туда несколько документов:
create table shoes(title text, sizes multi);
insert into shoes values(0,'nike',(40,41,42)),(0,'adidas',(41,43)),(0,'reebook',(42,43));
так что у нас есть:
SELECT * FROM shoes;
+---------------------+----------+---------+
| id | sizes | title |
+---------------------+----------+---------+
| 1657851069130080265 | 40,41,42 | nike |
| 1657851069130080266 | 41,43 | adidas |
| 1657851069130080267 | 42,43 | reebook |
+---------------------+----------+---------+
Если теперь сделать GROUP BY "sizes", то будет обработан весь наш многозначный атрибут и возвращена агрегация по каждому значению, в данном случае просто количество:
- SQL
- JSON
- PHP
- Python
- Javascript
- Python-asyncio
- Java
- C#
- Rust
- Go
SELECT groupby() gb, count(*) FROM shoes GROUP BY sizes ORDER BY gb asc;POST /search -d '
{
"table" : "shoes",
"limit": 0,
"aggs" :
{
"sizes" :
{
"terms" :
{
"field":"sizes",
"size":100
}
}
}
}
'$index->setName('shoes');
$search = $index->search('');
$search->limit(0);
$search->facet('sizes','sizes',100);
$results = $search->get();
print_r($results->getFacets());res =searchApi.search({"table":"shoes","limit":0,"aggs":{"sizes":{"terms":{"field":"sizes","size":100}}}})res = await searchApi.search({"table":"shoes","limit":0,"aggs":{"sizes":{"terms":{"field":"sizes","size":100}}}});res = await searchApi.search({"table":"shoes","limit":0,"aggs":{"sizes":{"terms":{"field":"sizes","size":100}}}})HashMap<String,Object> aggs = new HashMap<String,Object>(){{
put("release_year", new HashMap<String,Object>(){{
put("terms", new HashMap<String,Object>(){{
put("field","release_year");
put("size",100);
}});
}});
}};
searchRequest = new SearchRequest();
searchRequest.setIndex("films");
searchRequest.setLimit(0);
query = new HashMap<String,Object>();
query.put("match_all",null);
searchRequest.setQuery(query);
searchRequest.setAggs(aggs);
searchResponse = searchApi.search(searchRequest);var agg = new Aggregation("release_year", "release_year");
agg.Size = 100;
object query = new { match_all=null };
var searchRequest = new SearchRequest("films", query);
searchRequest.Limit = 0;
searchRequest.Aggs = new List<Aggregation> {agg};
var searchResponse = searchApi.Search(searchRequest);let query = SearchQuery::new();
let aggTerms1 = AggTerms::new {
fields: "release_year".to_string(),
size: Some(100),
};
let agg1 = Aggregation {
terms: Some(Box::new(aggTerms1)),
..Default::default(),
};
let mut aggs = HashMap::new();
aggs.insert("release_year".to_string(), agg1);
let search_req = SearchRequest {
table: "films".to_string(),
query: Some(Box::new(query)),
aggs: serde_json::json!(aggs),
limit: serde_json::json!(0),
..Default::default(),
};
let search_res = search_api.search(search_req).await;query := map[string]interface{} {};
searchRequest.SetQuery(query);
aggTerms := manticoreclient.NewAggregationTerms()
aggTerms.SetField("mva_field")
aggTerms.SetSize(2)
aggregation := manticoreclient.NewAggregation()
aggregation.setTerms(aggTerms)
searchRequest.SetAggregation(aggregation)
res, _, _ := apiClient.SearchAPI.Search(context.Background()).SearchRequest(*searchRequest).Execute()+------+----------+
| gb | count(*) |
+------+----------+
| 40 | 1 |
| 41 | 2 |
| 42 | 2 |
| 43 | 2 |
+------+----------+{
"took": 0,
"timed_out": false,
"hits": {
"total": 3,
"hits": [
]
},
"aggregations": {
"sizes": {
"buckets": [
{
"key": 43,
"doc_count": 2
},
{
"key": 42,
"doc_count": 2
},
{
"key": 41,
"doc_count": 2
},
{
"key": 40,
"doc_count": 1
}
]
}
}
}Array
(
[sizes] => Array
(
[buckets] => Array
(
[0] => Array
(
[key] => 43
[doc_count] => 2
)
[1] => Array
(
[key] => 42
[doc_count] => 2
)
[2] => Array
(
[key] => 41
[doc_count] => 2
)
[3] => Array
(
[key] => 40
[doc_count] => 1
)
)
)
){'aggregations': {u'sizes': {u'buckets': [{u'doc_count': 2, u'key': 43},
{u'doc_count': 2, u'key': 42},
{u'doc_count': 2, u'key': 41},
{u'doc_count': 1, u'key': 40}]}},
'hits': {'hits': [], 'max_score': None, 'total': 3},
'profile': None,
'timed_out': False,
'took': 0}{'aggregations': {u'sizes': {u'buckets': [{u'doc_count': 2, u'key': 43},
{u'doc_count': 2, u'key': 42},
{u'doc_count': 2, u'key': 41},
{u'doc_count': 1, u'key': 40}]}},
'hits': {'hits': [], 'max_score': None, 'total': 3},
'profile': None,
'timed_out': False,
'took': 0}{"took":0,"timed_out":false,"aggregations":{"sizes":{"buckets":[{"key":43,"doc_count":2},{"key":42,"doc_count":2},{"key":41,"doc_count":2},{"key":40,"doc_count":1}]}},"hits":{"total":3,"hits":[]}}class SearchResponse {
took: 0
timedOut: false
aggregations: {release_year={buckets=[{key=43, doc_count=2}, {key=42, doc_count=2}, {key=41, doc_count=2}, {key=40, doc_count=1}]}}
hits: class SearchResponseHits {
maxScore: null
total: 3
hits: []
}
profile: null
}class SearchResponse {
took: 0
timedOut: false
aggregations: {release_year={buckets=[{key=43, doc_count=2}, {key=42, doc_count=2}, {key=41, doc_count=2}, {key=40, doc_count=1}]}}
hits: class SearchResponseHits {
maxScore: null
total: 3
hits: []
}
profile: null
}class SearchResponse {
took: 0
timedOut: false
aggregations: {release_year={buckets=[{key=43, doc_count=2}, {key=42, doc_count=2}, {key=41, doc_count=2}, {key=40, doc_count=1}]}}
hits: class SearchResponseHits {
maxScore: null
total: 3
hits: []
}
profile: null
}
<!-- request TypeScript -->
``` typescript
res = await searchApi.search({
index: 'test',
aggs: {
mva_agg: {
terms: { field: "mva_field", size: 2 }
}
}
});{
"took":0,
"timed_out":false,
"aggregations":
{
"mva_agg":
{
"buckets":
[{
"key":1,
"doc_count":4
},
{
"key":2,
"doc_count":2
}]
}
},
"hits":
{
"total":4,
"hits":[]
}
}{
"took":0,
"timed_out":false,
"aggregations":
{
"mva_agg":
{
"buckets":
[{
"key":1,
"doc_count":4
},
{
"key":2,
"doc_count":2
}]
}
},
"hits":
{
"total":5,
"hits":[]
}
}If you have a field of type JSON, you can GROUP BY any node from it. To demonstrate this, let's create a table "products" with a few documents, each having a color in the "meta" JSON field:
create table products(title text, meta json);
insert into products values(0,'nike','{"color":"red"}'),(0,'adidas','{"color":"red"}'),(0,'puma','{"color":"green"}');
This gives us:
SELECT * FROM products;
+---------------------+-------------------+--------+
| id | meta | title |
+---------------------+-------------------+--------+
| 1657851069130080268 | {"color":"red"} | nike |
| 1657851069130080269 | {"color":"red"} | adidas |
| 1657851069130080270 | {"color":"green"} | puma |
+---------------------+-------------------+--------+
To group the products by color, we can simply use GROUP BY meta.color, and to display the corresponding group key in the SELECT list, we can use GROUPBY():
- SQL
- JSON
- PHP
- Python
- Javascript
- Java
- C#
- Rust
- TypeScript
- Go
SELECT groupby() color, count(*) from products GROUP BY meta.color;POST /search -d '
{
"table" : "products",
"limit": 0,
"aggs" :
{
"color" :
{
"terms" :
{
"field":"meta.color",
"size":100
}
}
}
}
'$index->setName('products');
$search = $index->search('');
$search->limit(0);
$search->facet('meta.color','color',100);
$results = $search->get();
print_r($results->getFacets());res =searchApi.search({"table":"products","limit":0,"aggs":{"color":{"terms":{"field":"meta.color","size":100}}}})res = await searchApi.search({"table":"products","limit":0,"aggs":{"color":{"terms":{"field":"meta.color","size":100}}}});HashMap<String,Object> aggs = new HashMap<String,Object>(){{
put("color", new HashMap<String,Object>(){{
put("terms", new HashMap<String,Object>(){{
put("field","meta.color");
put("size",100);
}});
}});
}};
searchRequest = new SearchRequest();
searchRequest.setIndex("products");
searchRequest.setLimit(0);
query = new HashMap<String,Object>();
query.put("match_all",null);
searchRequest.setQuery(query);
searchRequest.setAggs(aggs);
searchResponse = searchApi.search(searchRequest);var agg = new Aggregation("color", "meta.color");
agg.Size = 100;
object query = new { match_all=null };
var searchRequest = new SearchRequest("products", query);
searchRequest.Limit = 0;
searchRequest.Aggs = new List<Aggregation> {agg};
var searchResponse = searchApi.Search(searchRequest);let query = SearchQuery::new();
let aggTerms1 = AggTerms::new {
fields: "meta.color".to_string(),
size: Some(100),
};
let agg1 = Aggregation {
terms: Some(Box::new(aggTerms1)),
..Default::default(),
};
let mut aggs = HashMap::new();
aggs.insert("color".to_string(), agg1);
let search_req = SearchRequest {
table: "products".to_string(),
query: Some(Box::new(query)),
aggs: serde_json::json!(aggs),
limit: serde_json::json!(0),
..Default::default(),
};
let search_res = search_api.search(search_req).await;res = await searchApi.search({
index: 'test',
aggs: {
json_agg: {
terms: { field: "json_field.year", size: 1 }
}
}
});query := map[string]interface{} {};
searchRequest.SetQuery(query);
aggTerms := manticoreclient.NewAggregationTerms()
aggTerms.SetField("json_field.year")
aggTerms.SetSize(2)
aggregation := manticoreclient.NewAggregation()
aggregation.setTerms(aggTerms)
searchRequest.SetAggregation(aggregation)
res, _, _ := apiClient.SearchAPI.Search(context.Background()).SearchRequest(*searchRequest).Execute()+-------+----------+
| color | count(*) |
+-------+----------+
| red | 2 |
| green | 1 |
+-------+----------+{
"took": 0,
"timed_out": false,
"hits": {
"total": 3,
"hits": [
]
},
"aggregations": {
"color": {
"buckets": [
{
"key": "green",
"doc_count": 1
},
{
"key": "red",
"doc_count": 2
}
]
}
}
}Array
(
[color] => Array
(
[buckets] => Array
(
[0] => Array
(
[key] => green
[doc_count] => 1
)
[1] => Array
(
[key] => red
[doc_count] => 2
)
)
)
){'aggregations': {u'color': {u'buckets': [{u'doc_count': 1,
u'key': u'green'},
{u'doc_count': 2, u'key': u'red'}]}},
'hits': {'hits': [], 'max_score': None, 'total': 3},
'profile': None,
'timed_out': False,
'took': 0}<!-- request Python-asyncio -->
``` python
res = await searchApi.search({"table":"products","limit":0,"aggs":{"color":{"terms":{"field":"meta.color","size":100}}}}){'aggregations': {u'color': {u'buckets': [{u'doc_count': 1,
u'key': u'green'},
{u'doc_count': 2, u'key': u'red'}]}},
'hits': {'hits': [], 'max_score': None, 'total': 3},
'profile': None,
'timed_out': False,
'took': 0}{"took":0,"timed_out":false,"aggregations":{"color":{"buckets":[{"key":"green","doc_count":1},{"key":"red","doc_count":2}]}},"hits":{"total":3,"hits":[]}}class SearchResponse {
took: 0
timedOut: false
aggregations: {color={buckets=[{key=green, doc_count=1}, {key=red, doc_count=2}]}}
hits: class SearchResponseHits {
maxScore: null
total: 3
hits: []
}
profile: null
}
class SearchResponse {
took: 0
timedOut: false
aggregations: {color={buckets=[{key=green, doc_count=1}, {key=red, doc_count=2}]}}
hits: class SearchResponseHits {
maxScore: null
total: 3
hits: []
}
profile: null
}
class SearchResponse {
took: 0
timedOut: false
aggregations: {color={buckets=[{key=green, doc_count=1}, {key=red, doc_count=2}]}}
hits: class SearchResponseHits {
maxScore: null
total: 3
hits: []
}
profile: null
}
{
"took":0,
"timed_out":false,
"aggregations":
{
"json_agg":
{
"buckets":
[{
"key":2000,
"doc_count":2
},
{
"key":2001,
"doc_count":2
}]
}
},
"hits":
{
"total":4,
"hits":[]
}
}{
"took":0,
"timed_out":false,
"aggregations":
{
"json_agg":
{
"buckets":
[{
"key":2000,
"doc_count":2
},
{
"key":2001,
"doc_count":2
}]
}
},
"hits":
{
"total":4,
"hits":[]
}
}Помимо COUNT(*), которая возвращает количество элементов в каждой группе, вы можете использовать различные другие агрегационные функции:
В то время как COUNT(*) возвращает количество всех элементов в группе, COUNT(DISTINCT field) возвращает количество уникальных значений поля в группе, что может существенно отличаться от общего количества. Например, в группе может быть 100 элементов, но у всех одинаковое значение для определённого поля. COUNT(DISTINCT field) помогает это определить. Чтобы продемонстрировать это, создадим таблицу "students" с именем студента, возрастом и специальностью:
CREATE TABLE students(name text, age int, major string);
INSERT INTO students values(0,'John',21,'arts'),(0,'William',22,'business'),(0,'Richard',21,'cs'),(0,'Rebecca',22,'cs'),(0,'Monica',21,'arts');
так что у нас есть:
MySQL [(none)]> SELECT * from students;
+---------------------+------+----------+---------+
| id | age | major | name |
+---------------------+------+----------+---------+
| 1657851069130080271 | 21 | arts | John |
| 1657851069130080272 | 22 | business | William |
| 1657851069130080273 | 21 | cs | Richard |
| 1657851069130080274 | 22 | cs | Rebecca |
| 1657851069130080275 | 21 | arts | Monica |
+---------------------+------+----------+---------+
В примере видно, что если мы сгруппируем по major и отобразим как COUNT(*), так и COUNT(DISTINCT age), становится ясно, что есть два студента, выбравшие специальность "cs" с двумя уникальными возрастами, а для специальности "arts" также два студента, но всего один уникальный возраст.
В запросе может быть не более одной COUNT(DISTINCT).
По умолчанию подсчёты являются приближенными
На самом деле, некоторые из них точные, а некоторые приближённые. Подробнее об этом ниже.
Manticore поддерживает два алгоритма для вычисления количества уникальных значений. Один — устаревший алгоритм, который использует много памяти и обычно медленнее. Он собирает пары {group; value}, сортирует их и периодически удаляет дубликаты. Преимуществом этого подхода является гарантированная точность подсчётов в простой таблице. Его можно включить, установив опцию distinct_precision_threshold в 0.
Другой алгоритм (включён по умолчанию) загружает подсчёты в хеш-таблицу и возвращает её размер. Если хеш-таблица становится слишком большой, содержимое переносится в HyperLogLog. Здесь подсчёты становятся приближенными, так как HyperLogLog — вероятностный алгоритм. Преимущества заключаются в том, что максимальное использование памяти на группу фиксировано и зависит от точности HyperLogLog. Общее использование памяти также зависит от настройки max_matches, отражающей количество групп.
Опция distinct_precision_threshold задаёт порог, ниже которого подсчёты гарантированно точные. Настройка точности HyperLogLog и порог для конверсии из хеш-таблицы в HyperLogLog выводятся из этой настройки. Важно использовать эту опцию с осторожностью, так как её удвоение удваивает максимальное количество памяти, необходимой для вычисления подсчётов. Максимальное использование памяти можно примерно оценить по формуле: 64 * max_matches * distinct_precision_threshold. Обратите внимание, что это худший сценарий, и в большинстве случаев вычисления используют значительно меньше оперативной памяти.
COUNT(DISTINCT) для распределённой таблицы или таблицы реального времени, состоящей из нескольких дисковых чанков, может возвращать неточные результаты, но результат должен быть точным для распределённой таблицы, состоящей из локальных обычных или RT таблиц с одинаковой схемой (идентичный набор/порядок полей, но с возможными разными настройками токенизации).
- SQL
SELECT major, count(*), count(distinct age) FROM students GROUP BY major;+----------+----------+---------------------+
| major | count(*) | count(distinct age) |
+----------+----------+---------------------+
| arts | 2 | 1 |
| business | 1 | 1 |
| cs | 2 | 2 |
+----------+----------+---------------------+Часто вы хотите лучше понять содержимое каждой группы. Для этого можно использовать GROUP N BY, но он вернёт дополнительные строки, которые могут быть нежелательными в выводе. GROUP_CONCAT() расширяет вашу группировку, объединяя значения определённого поля в группе. Возьмём предыдущий пример и улучшим его, показывая все возраста в каждой группе.
GROUP_CONCAT(field) возвращает список значений, разделённых запятыми.
- SQL
SELECT major, count(*), count(distinct age), group_concat(age) FROM students GROUP BY major+----------+----------+---------------------+-------------------+
| major | count(*) | count(distinct age) | group_concat(age) |
+----------+----------+---------------------+-------------------+
| arts | 2 | 1 | 21,21 |
| business | 1 | 1 | 22 |
| cs | 2 | 2 | 21,22 |
+----------+----------+---------------------+-------------------+- SQL
SELECT release_year year, sum(rental_rate) sum, min(rental_rate) min, max(rental_rate) max, avg(rental_rate) avg FROM films GROUP BY release_year ORDER BY year asc LIMIT 5;+------+------------+----------+----------+------------+
| year | sum | min | max | avg |
+------+------------+----------+----------+------------+
| 2000 | 308.030029 | 0.990000 | 4.990000 | 3.17556739 |
| 2001 | 282.090118 | 0.990000 | 4.990000 | 3.09989142 |
| 2002 | 332.919983 | 0.990000 | 4.990000 | 3.08259249 |
| 2003 | 310.940063 | 0.990000 | 4.990000 | 2.93339682 |
| 2004 | 300.920044 | 0.990000 | 4.990000 | 2.78629661 |
+------+------------+----------+----------+------------+Группировка выполняется в фиксированной памяти, которая зависит от настройки max_matches. Если max_matches позволяет хранить все найденные группы, результаты будут на 100% точными. Если же значение max_matches меньше, точность результатов ухудшается.
При параллельной обработке всё становится сложнее. Если включён pseudo_sharding и/или используется RT таблица с несколькими дисковыми чанками, каждый чанк или псевдо-шард получает набор результатов размером не более max_matches. Это может привести к неточностям в агрегатах и подсчётах групп при объединении результатов из разных потоков. Чтобы исправить это, можно либо увеличить значение max_matches, либо отключить параллельную обработку.
Manticore попытается увеличить max_matches до значения max_matches_increase_threshold, если обнаружит, что группировка может вернуть неточные результаты. Определение основывается на количестве уникальных значений группирующего атрибута, получаемом из вторичных индексов (если они есть).
Для обеспечения точных агрегатов и/или подсчётов групп при использовании RT таблиц или pseudo_sharding можно включить accurate_aggregation. Это попытается увеличить max_matches до порога, а если порог недостаточно высок, Manticore отключит параллельную обработку для запроса.
- SQL
MySQL [(none)]> SELECT release_year year, count(*) FROM films GROUP BY year limit 5;
+------+----------+
| year | count(*) |
+------+----------+
| 2004 | 108 |
| 2002 | 108 |
| 2001 | 91 |
| 2005 | 93 |
| 2000 | 97 |
+------+----------+
MySQL [(none)]> SELECT release_year year, count(*) FROM films GROUP BY year limit 5 option max_matches=1;
+------+----------+
| year | count(*) |
+------+----------+
| 2004 | 76 |
+------+----------+
MySQL [(none)]> SELECT release_year year, count(*) FROM films GROUP BY year limit 5 option max_matches=2;
+------+----------+
| year | count(*) |
+------+----------+
| 2004 | 76 |
| 2002 | 74 |
+------+----------+
MySQL [(none)]> SELECT release_year year, count(*) FROM films GROUP BY year limit 5 option max_matches=3;
+------+----------+
| year | count(*) |
+------+----------+
| 2004 | 108 |
| 2002 | 108 |
| 2001 | 91 |
+------+----------+