mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-11-27 18:12:02 +00:00
1311 lines
96 KiB
Markdown
1311 lines
96 KiB
Markdown
# Синтаксис запросов SELECT
|
||
|
||
`SELECT` осуществляет выборку данных.
|
||
|
||
```sql
|
||
[WITH expr_list|(subquery)]
|
||
SELECT [DISTINCT] expr_list
|
||
[FROM [db.]table | (subquery) | table_function] [FINAL]
|
||
[SAMPLE sample_coeff]
|
||
[ARRAY JOIN ...]
|
||
[GLOBAL] [ANY|ALL] [INNER|LEFT|RIGHT|FULL|CROSS] [OUTER] JOIN (subquery)|table USING columns_list
|
||
[PREWHERE expr]
|
||
[WHERE expr]
|
||
[GROUP BY expr_list] [WITH TOTALS]
|
||
[HAVING expr]
|
||
[ORDER BY expr_list]
|
||
[LIMIT [n, ]m]
|
||
[UNION ALL ...]
|
||
[INTO OUTFILE filename]
|
||
[FORMAT format]
|
||
[LIMIT [offset_value, ]n BY columns]
|
||
```
|
||
|
||
Все секции, кроме списка выражений сразу после SELECT, являются необязательными.
|
||
Ниже секции будут описаны в порядке, почти соответствующем конвейеру выполнения запроса.
|
||
|
||
Если в запросе отсутствуют секции `DISTINCT`, `GROUP BY`, `ORDER BY`, подзапросы в `IN` и `JOIN`, то запрос будет обработан полностью потоково, с использованием O(1) количества оперативки.
|
||
Иначе запрос может съесть много оперативки, если не указаны подходящие ограничения `max_memory_usage`, `max_rows_to_group_by`, `max_rows_to_sort`, `max_rows_in_distinct`, `max_bytes_in_distinct`, `max_rows_in_set`, `max_bytes_in_set`, `max_rows_in_join`, `max_bytes_in_join`, `max_bytes_before_external_sort`, `max_bytes_before_external_group_by`. Подробнее смотрите в разделе "Настройки". Присутствует возможность использовать внешнюю сортировку (с сохранением временных данных на диск) и внешнюю агрегацию. `Merge join` в системе нет.
|
||
|
||
### Секция WITH
|
||
Данная секция представляет собой [CTE](https://ru.wikipedia.org/wiki/Иерархические_и_рекурсивные_запросы_в_SQL), с рядом ограничений:
|
||
1. Рекурсивные запросы не поддерживаются
|
||
2. Если в качестве выражения используется подзапрос, то результат должен содержать ровно одну строку
|
||
3. Результаты выражений нельзя переиспользовать во вложенных запросах
|
||
В дальнейшем, результаты выражений можно использовать в секции SELECT.
|
||
|
||
Пример 1: Использование константного выражения как "переменной"
|
||
```
|
||
WITH '2019-08-01 15:23:00' as ts_upper_bound
|
||
SELECT *
|
||
FROM hits
|
||
WHERE
|
||
EventDate = toDate(ts_upper_bound) AND
|
||
EventTime <= ts_upper_bound
|
||
```
|
||
|
||
Пример 2: Выкидывание выражения sum(bytes) из списка колонок в SELECT
|
||
```
|
||
WITH sum(bytes) as s
|
||
SELECT
|
||
formatReadableSize(s),
|
||
table
|
||
FROM system.parts
|
||
GROUP BY table
|
||
ORDER BY s
|
||
```
|
||
|
||
Пример 3: Использование результатов скалярного подзапроса
|
||
```
|
||
/* запрос покажет TOP 10 самых больших таблиц */
|
||
WITH
|
||
(
|
||
SELECT sum(bytes)
|
||
FROM system.parts
|
||
WHERE active
|
||
) AS total_disk_usage
|
||
SELECT
|
||
(sum(bytes) / total_disk_usage) * 100 AS table_disk_usage,
|
||
table
|
||
FROM system.parts
|
||
GROUP BY table
|
||
ORDER BY table_disk_usage DESC
|
||
LIMIT 10
|
||
```
|
||
|
||
Пример 4: Переиспользование выражения
|
||
В настоящий момент, переиспользование выражения из секции WITH внутри подзапроса возможно только через дублирование.
|
||
```
|
||
WITH ['hello'] AS hello
|
||
SELECT
|
||
hello,
|
||
*
|
||
FROM
|
||
(
|
||
WITH ['hello'] AS hello
|
||
SELECT hello
|
||
)
|
||
|
||
┌─hello─────┬─hello─────┐
|
||
│ ['hello'] │ ['hello'] │
|
||
└───────────┴───────────┘
|
||
```
|
||
|
||
### Секция FROM
|
||
|
||
Если секция FROM отсутствует, то данные будут читаться из таблицы `system.one`.
|
||
Таблица system.one содержит ровно одну строку (то есть, эта таблица выполняет такую же роль, как таблица DUAL, которую можно найти в других СУБД).
|
||
|
||
В секции FROM указывается таблица, из которой будут читаться данные, либо подзапрос, либо табличная функция; дополнительно могут присутствовать ARRAY JOIN и обычный JOIN (смотрите ниже).
|
||
|
||
Вместо таблицы, может быть указан подзапрос SELECT в скобках.
|
||
В этом случае, конвейер обработки подзапроса будет встроен в конвейер обработки внешнего запроса.
|
||
В отличие от стандартного SQL, после подзапроса не нужно указывать его синоним. Для совместимости, присутствует возможность написать AS name после подзапроса, но указанное имя нигде не используется.
|
||
|
||
Вместо таблицы, может быть указана табличная функция. Подробнее смотрите раздел "Табличные функции".
|
||
|
||
Для выполнения запроса, из соответствующей таблицы, вынимаются все столбцы, перечисленные в запросе. Из подзапросов выкидываются столбцы, не нужные для внешнего запроса.
|
||
Если в запросе не перечислено ни одного столбца (например, SELECT count() FROM t), то из таблицы всё равно вынимается один какой-нибудь столбец (предпочитается самый маленький), для того, чтобы можно было хотя бы посчитать количество строк.
|
||
|
||
Модификатор FINAL может быть использован только при SELECT-е из таблицы типа CollapsingMergeTree. При указании FINAL, данные будут выбираться полностью "сколлапсированными". Стоит учитывать, что использование FINAL приводит к выбору кроме указанных в SELECT-е столбцов также столбцов, относящихся к первичному ключу. Также, запрос будет выполняться в один поток, и при выполнении запроса будет выполняться слияние данных. Это приводит к тому, что при использовании FINAL, запрос выполняется медленнее. В большинстве случаев, следует избегать использования FINAL. Подробнее смотрите раздел "Движок CollapsingMergeTree".
|
||
|
||
### Секция SAMPLE {#select-sample-clause}
|
||
|
||
Секция `SAMPLE` позволяет выполнять запросы приближённо. Например, чтобы посчитать статистику по всем визитам, можно обработать 1/10 всех визитов и результат домножить на 10.
|
||
|
||
Сэмплирование имеет смысл, когда:
|
||
|
||
1. Точность результата не важна, например, для оценочных расчетов.
|
||
2. Возможности аппаратной части не позволяют соответствовать строгим критериям. Например, время ответа должно быть <100 мс. При этом точность расчета имеет более низкий приоритет.
|
||
3. Точность результата участвует в бизнес-модели сервиса. Например, пользователи с бесплатной подпиской на сервис могут получать отчеты с меньшей точностью, чем пользователи с премиум подпиской.
|
||
|
||
!!! note "Внимание"
|
||
Не стоит использовать сэмплирование в тех задачах, где важна точность расчетов. Например, при работе с финансовыми отчетами.
|
||
|
||
Свойства сэмплирования:
|
||
|
||
- Сэмплирование работает детерминированно. При многократном выполнении одного и того же запроса `SELECT .. SAMPLE`, результат всегда будет одинаковым.
|
||
- Сэмплирование поддерживает консистентность для разных таблиц. Имеется в виду, что для таблиц с одним и тем же ключом сэмплирования, подмножество данных в выборках будет одинаковым (выборки при этом должны быть сформированы для одинаковой доли данных). Например, выборка по идентификаторам посетителей выберет из разных таблиц строки с одинаковым подмножеством всех возможных идентификаторов. Это свойство позволяет использовать выборки в подзапросах в секции [IN](#select-in-operators), а также объединять выборки с помощью [JOIN](#select-join).
|
||
- Сэмплирование позволяет читать меньше данных с диска. Обратите внимание, для этого необходимо корректно указать ключ сэмплирования. Подробнее см. в разделе [Создание таблицы MergeTree](../operations/table_engines/mergetree.md#table_engine-mergetree-creating-a-table).
|
||
|
||
Сэмплирование поддерживается только таблицами семейства [MergeTree](../operations/table_engines/mergetree.md) и только в том случае, если для таблиц был указан ключ сэмплирования (выражение, на основе которого должна производиться выборка). Подробнее см. в разделе [Создание таблиц MergeTree](../operations/table_engines/mergetree.md#table_engine-mergetree-creating-a-table).
|
||
|
||
Выражение `SAMPLE` в запросе можно задать следующими способами:
|
||
|
||
| Способ задания SAMPLE| Описание |
|
||
| ---------------- | --------- |
|
||
| `SAMPLE k` | Здесь `k` – это дробное число в интервале от 0 до 1.<br/> Запрос будет выполнен по `k` доле данных. Например, если указано `SAMPLE 1/10`, то запрос будет выполнен для выборки из 1/10 данных. [Подробнее](#select-sample-k)|
|
||
| `SAMPLE n` | Здесь `n` – это достаточно большое целое число.</br> Запрос будет выполнен для выборки, состоящей из не менее чем `n` строк. Например, если указано `SAMPLE 10000000`, то запрос будет выполнен для не менее чем 10,000,000 строк. [Подробнее](#select-sample-n) |
|
||
| `SAMPLE k OFFSET m` | Здесь `k` и `m` – числа от 0 до 1.</br> Запрос будет выполнен по `k` доле данных. При этом выборка будет сформирована со смещением на `m` долю. [Подробнее](#select-sample-offset) |
|
||
|
||
#### SAMPLE k {#select-sample-k}
|
||
|
||
Здесь `k` – число в интервале от 0 до 1. Поддерживается как дробная, так и десятичная форма записи. Например, `SAMPLE 1/2` или `SAMPLE 0.5`.
|
||
|
||
Если задано выражение `SAMPLE k`, запрос будет выполнен для `k` доли данных. Рассмотрим пример:
|
||
|
||
```sql
|
||
SELECT
|
||
Title,
|
||
count() * 10 AS PageViews
|
||
FROM hits_distributed
|
||
SAMPLE 0.1
|
||
WHERE
|
||
CounterID = 34
|
||
GROUP BY Title
|
||
ORDER BY PageViews DESC LIMIT 1000
|
||
```
|
||
|
||
В этом примере запрос выполняется по выборке из 0.1 (10%) данных. Значения агрегатных функций не корректируются автоматически, поэтому чтобы получить приближённый результат, значение `count()` нужно вручную умножить на 10.
|
||
|
||
Выборка с указанием относительного коэффициента является "согласованной": для таблиц с одним и тем же ключом сэмплирования, выборка с одинаковой относительной долей всегда будет составлять одно и то же подмножество данных. То есть выборка из разных таблиц, на разных серверах, в разное время, формируется одинаковым образом.
|
||
|
||
#### SAMPLE n {#select-sample-n}
|
||
|
||
Здесь `n` – это достаточно большое целое число. Например, `SAMPLE 10000000`.
|
||
|
||
Если задано выражение `SAMPLE n`, запрос будет выполнен для выборки из не менее `n` строк (но не значительно больше этого значения). Например, если задать `SAMPLE 10000000`, в выборку попадут не менее 10,000,000 строк.
|
||
|
||
!!! note "Примечание"
|
||
Следует иметь в виду, что `n` должно быть достаточно большим числом. Так как минимальной единицей данных для чтения является одна гранула (её размер задаётся настройкой `index_granularity` для таблицы), имеет смысл создавать выборки, размер которых существенно превосходит размер гранулы.
|
||
|
||
При выполнении `SAMPLE n` коэффициент сэмплирования заранее неизвестен (то есть нет информации о том, относительно какого количества данных будет сформирована выборка). Чтобы узнать коэффициент сэмплирования, используйте столбец `_sample_factor`.
|
||
|
||
Виртуальный столбец `_sample_factor` автоматически создается в тех таблицах, для которых задано выражение `SAMPLE BY` (подробнее см. в разделе [Создание таблицы MergeTree](../operations/table_engines/mergetree.md#table_engine-mergetree-creating-a-table)). В столбце содержится коэффициент сэмплирования для таблицы – он рассчитывается динамически по мере добавления данных в таблицу. Ниже приведены примеры использования столбца `_sample_factor`.
|
||
|
||
Предположим, у нас есть таблица, в которой ведется статистика посещений сайта. Пример ниже показывает, как рассчитать суммарное число просмотров:
|
||
|
||
```sql
|
||
SELECT sum(PageViews * _sample_factor)
|
||
FROM visits
|
||
SAMPLE 10000000
|
||
```
|
||
|
||
Следующий пример показывает, как посчитать общее число визитов:
|
||
|
||
```sql
|
||
SELECT sum(_sample_factor)
|
||
FROM visits
|
||
SAMPLE 10000000
|
||
```
|
||
|
||
В примере ниже рассчитывается среднее время на сайте. Обратите внимание, при расчете средних значений, умножать результат на коэффициент сэмплирования не нужно.
|
||
|
||
```sql
|
||
SELECT avg(Duration)
|
||
FROM visits
|
||
SAMPLE 10000000
|
||
```
|
||
|
||
#### SAMPLE k OFFSET m {#select-sample-offset}
|
||
|
||
Здесь `k` и `m` – числа в интервале от 0 до 1. Например, `SAMPLE 0.1 OFFSET 0.5`. Поддерживается как дробная, так и десятичная форма записи.
|
||
|
||
При задании `SAMPLE k OFFSET m`, выборка будет сформирована из `k` доли данных со смещением на долю `m`. Примеры приведены ниже.
|
||
|
||
**Пример 1**
|
||
|
||
```sql
|
||
SAMPLE 1/10
|
||
```
|
||
|
||
В этом примере выборка будет сформирована по 1/10 доле всех данных:
|
||
|
||
`[++------------------]`
|
||
|
||
**Пример 2**
|
||
|
||
```sql
|
||
SAMPLE 1/10 OFFSET 1/2
|
||
```
|
||
|
||
Здесь выборка, которая состоит из 1/10 доли данных, взята из второй половины данных.
|
||
|
||
`[----------++--------]`
|
||
|
||
### Секция ARRAY JOIN {#select-array-join-clause}
|
||
|
||
Позволяет выполнить `JOIN` с массивом или вложенной структурой данных. Смысл похож на функцию [arrayJoin](functions/array_join.md#functions_arrayjoin), но функциональность более широкая.
|
||
|
||
``` sql
|
||
SELECT <expr_list>
|
||
FROM <left_subquery>
|
||
[LEFT] ARRAY JOIN <array>
|
||
[WHERE|PREWHERE <expr>]
|
||
...
|
||
```
|
||
|
||
В запросе может быть указано не более одной секции `ARRAY JOIN`.
|
||
|
||
При использовании `ARRAY JOIN`, порядок выполнения запроса оптимизируется. Несмотря на то что секция `ARRAY JOIN` всегда указывается перед выражением `WHERE / PREWHERE`, преобразование `JOIN` может быть выполнено как до выполнения выражения `WHERE / PREWHERE` (если результат необходим в этом выражении), так и после (чтобы уменьшить объем расчетов). Порядок обработки контролируется оптимизатором запросов.
|
||
|
||
Секция `ARRAY JOIN` поддерживает следующие формы записи:
|
||
|
||
- `ARRAY JOIN` — в этом случае результат `JOIN` не будет содержать пустые массивы;
|
||
- `LEFT ARRAY JOIN` — пустые массивы попадут в результат выполнения `JOIN`. В качестве значения для пустых массивов устанавливается значение по умолчанию. Обычно это 0, пустая строка или NULL, в зависимости от типа элементов массива.
|
||
|
||
Рассмотрим примеры использования `ARRAY JOIN` и `LEFT ARRAY JOIN`. Для начала создадим таблицу, содержащую столбец с типом [Array](../data_types/array.md), и добавим в него значение:
|
||
|
||
``` sql
|
||
CREATE TABLE arrays_test
|
||
(
|
||
s String,
|
||
arr Array(UInt8)
|
||
) ENGINE = Memory;
|
||
|
||
INSERT INTO arrays_test
|
||
VALUES ('Hello', [1,2]), ('World', [3,4,5]), ('Goodbye', []);
|
||
```
|
||
```
|
||
┌─s───────────┬─arr─────┐
|
||
│ Hello │ [1,2] │
|
||
│ World │ [3,4,5] │
|
||
│ Goodbye │ [] │
|
||
└─────────────┴─────────┘
|
||
```
|
||
|
||
В примере ниже используется `ARRAY JOIN`:
|
||
|
||
```sql
|
||
SELECT s, arr
|
||
FROM arrays_test
|
||
ARRAY JOIN arr;
|
||
```
|
||
```
|
||
┌─s─────┬─arr─┐
|
||
│ Hello │ 1 │
|
||
│ Hello │ 2 │
|
||
│ World │ 3 │
|
||
│ World │ 4 │
|
||
│ World │ 5 │
|
||
└───────┴─────┘
|
||
```
|
||
|
||
Следующий пример использует `LEFT ARRAY JOIN`:
|
||
|
||
```sql
|
||
SELECT s, arr
|
||
FROM arrays_test
|
||
LEFT ARRAY JOIN arr;
|
||
```
|
||
```
|
||
┌─s───────────┬─arr─┐
|
||
│ Hello │ 1 │
|
||
│ Hello │ 2 │
|
||
│ World │ 3 │
|
||
│ World │ 4 │
|
||
│ World │ 5 │
|
||
│ Goodbye │ 0 │
|
||
└─────────────┴─────┘
|
||
```
|
||
|
||
#### Использование алиасов
|
||
|
||
Для массива в секции `ARRAY JOIN` может быть указан алиас. В этом случае, элемент массива будет доступен под этим алиасом, а сам массив — под исходным именем. Пример:
|
||
|
||
``` sql
|
||
SELECT s, arr, a
|
||
FROM arrays_test
|
||
ARRAY JOIN arr AS a;
|
||
```
|
||
|
||
```
|
||
┌─s─────┬─arr─────┬─a─┐
|
||
│ Hello │ [1,2] │ 1 │
|
||
│ Hello │ [1,2] │ 2 │
|
||
│ World │ [3,4,5] │ 3 │
|
||
│ World │ [3,4,5] │ 4 │
|
||
│ World │ [3,4,5] │ 5 │
|
||
└───────┴─────────┴───┘
|
||
```
|
||
|
||
Используя алиасы, можно выполнять `JOIN` с внешними массивами:
|
||
|
||
``` sql
|
||
SELECT s, arr_external
|
||
FROM arrays_test
|
||
ARRAY JOIN [1, 2, 3] AS arr_external;
|
||
```
|
||
|
||
```
|
||
┌─s───────────┬─arr_external─┐
|
||
│ Hello │ 1 │
|
||
│ Hello │ 2 │
|
||
│ Hello │ 3 │
|
||
│ World │ 1 │
|
||
│ World │ 2 │
|
||
│ World │ 3 │
|
||
│ Goodbye │ 1 │
|
||
│ Goodbye │ 2 │
|
||
│ Goodbye │ 3 │
|
||
└─────────────┴──────────────┘
|
||
```
|
||
|
||
В секции `ARRAY JOIN` можно указать через запятую сразу несколько массивов. В этом случае, `JOIN` делается с ними одновременно (прямая сумма, а не прямое произведение). Обратите внимание, массивы должны быть одинаковых размеров. Примеры:
|
||
|
||
``` sql
|
||
SELECT s, arr, a, num, mapped
|
||
FROM arrays_test
|
||
ARRAY JOIN arr AS a, arrayEnumerate(arr) AS num, arrayMap(x -> x + 1, arr) AS mapped;
|
||
```
|
||
|
||
```
|
||
┌─s─────┬─arr─────┬─a─┬─num─┬─mapped─┐
|
||
│ Hello │ [1,2] │ 1 │ 1 │ 2 │
|
||
│ Hello │ [1,2] │ 2 │ 2 │ 3 │
|
||
│ World │ [3,4,5] │ 3 │ 1 │ 4 │
|
||
│ World │ [3,4,5] │ 4 │ 2 │ 5 │
|
||
│ World │ [3,4,5] │ 5 │ 3 │ 6 │
|
||
└───────┴─────────┴───┴─────┴────────┘
|
||
```
|
||
|
||
В примере ниже используется функция [arrayEnumerate](functions/array_functions.md#array_functions-arrayenumerate):
|
||
|
||
``` sql
|
||
SELECT s, arr, a, num, arrayEnumerate(arr)
|
||
FROM arrays_test
|
||
ARRAY JOIN arr AS a, arrayEnumerate(arr) AS num;
|
||
```
|
||
|
||
```
|
||
┌─s─────┬─arr─────┬─a─┬─num─┬─arrayEnumerate(arr)─┐
|
||
│ Hello │ [1,2] │ 1 │ 1 │ [1,2] │
|
||
│ Hello │ [1,2] │ 2 │ 2 │ [1,2] │
|
||
│ World │ [3,4,5] │ 3 │ 1 │ [1,2,3] │
|
||
│ World │ [3,4,5] │ 4 │ 2 │ [1,2,3] │
|
||
│ World │ [3,4,5] │ 5 │ 3 │ [1,2,3] │
|
||
└───────┴─────────┴───┴─────┴─────────────────────┘
|
||
```
|
||
|
||
#### ARRAY JOIN с вложенными структурами данных
|
||
|
||
`ARRAY JOIN` также работает с [вложенными структурами данных](../data_types/nested_data_structures/nested.md). Пример:
|
||
|
||
``` sql
|
||
CREATE TABLE nested_test
|
||
(
|
||
s String,
|
||
nest Nested(
|
||
x UInt8,
|
||
y UInt32)
|
||
) ENGINE = Memory;
|
||
|
||
INSERT INTO nested_test
|
||
VALUES ('Hello', [1,2], [10,20]), ('World', [3,4,5], [30,40,50]), ('Goodbye', [], []);
|
||
```
|
||
|
||
```
|
||
┌─s───────┬─nest.x──┬─nest.y─────┐
|
||
│ Hello │ [1,2] │ [10,20] │
|
||
│ World │ [3,4,5] │ [30,40,50] │
|
||
│ Goodbye │ [] │ [] │
|
||
└─────────┴─────────┴────────────┘
|
||
```
|
||
|
||
``` sql
|
||
SELECT s, `nest.x`, `nest.y`
|
||
FROM nested_test
|
||
ARRAY JOIN nest;
|
||
```
|
||
|
||
```
|
||
┌─s─────┬─nest.x─┬─nest.y─┐
|
||
│ Hello │ 1 │ 10 │
|
||
│ Hello │ 2 │ 20 │
|
||
│ World │ 3 │ 30 │
|
||
│ World │ 4 │ 40 │
|
||
│ World │ 5 │ 50 │
|
||
└───────┴────────┴────────┘
|
||
```
|
||
|
||
При указании имени вложенной структуры данных в `ARRAY JOIN`, смысл такой же, как `ARRAY JOIN` со всеми элементами-массивами, из которых она состоит. Пример:
|
||
|
||
``` sql
|
||
SELECT s, `nest.x`, `nest.y`
|
||
FROM nested_test
|
||
ARRAY JOIN `nest.x`, `nest.y`;
|
||
```
|
||
|
||
```
|
||
┌─s─────┬─nest.x─┬─nest.y─┐
|
||
│ Hello │ 1 │ 10 │
|
||
│ Hello │ 2 │ 20 │
|
||
│ World │ 3 │ 30 │
|
||
│ World │ 4 │ 40 │
|
||
│ World │ 5 │ 50 │
|
||
└───────┴────────┴────────┘
|
||
```
|
||
|
||
Такой вариант тоже имеет смысл:
|
||
|
||
``` sql
|
||
SELECT s, `nest.x`, `nest.y`
|
||
FROM nested_test
|
||
ARRAY JOIN `nest.x`;
|
||
```
|
||
|
||
```
|
||
┌─s─────┬─nest.x─┬─nest.y─────┐
|
||
│ Hello │ 1 │ [10,20] │
|
||
│ Hello │ 2 │ [10,20] │
|
||
│ World │ 3 │ [30,40,50] │
|
||
│ World │ 4 │ [30,40,50] │
|
||
│ World │ 5 │ [30,40,50] │
|
||
└───────┴────────┴────────────┘
|
||
```
|
||
|
||
Алиас для вложенной структуры данных можно использовать, чтобы выбрать как результат `JOIN`-а, так и исходный массив. Пример:
|
||
|
||
``` sql
|
||
SELECT s, `n.x`, `n.y`, `nest.x`, `nest.y`
|
||
FROM nested_test
|
||
ARRAY JOIN nest AS n;
|
||
```
|
||
|
||
```
|
||
┌─s─────┬─n.x─┬─n.y─┬─nest.x──┬─nest.y─────┐
|
||
│ Hello │ 1 │ 10 │ [1,2] │ [10,20] │
|
||
│ Hello │ 2 │ 20 │ [1,2] │ [10,20] │
|
||
│ World │ 3 │ 30 │ [3,4,5] │ [30,40,50] │
|
||
│ World │ 4 │ 40 │ [3,4,5] │ [30,40,50] │
|
||
│ World │ 5 │ 50 │ [3,4,5] │ [30,40,50] │
|
||
└───────┴─────┴─────┴─────────┴────────────┘
|
||
```
|
||
|
||
Пример использования функции [arrayEnumerate](functions/array_functions.md#array_functions-arrayenumerate):
|
||
|
||
``` sql
|
||
SELECT s, `n.x`, `n.y`, `nest.x`, `nest.y`, num
|
||
FROM nested_test
|
||
ARRAY JOIN nest AS n, arrayEnumerate(`nest.x`) AS num;
|
||
```
|
||
|
||
```
|
||
┌─s─────┬─n.x─┬─n.y─┬─nest.x──┬─nest.y─────┬─num─┐
|
||
│ Hello │ 1 │ 10 │ [1,2] │ [10,20] │ 1 │
|
||
│ Hello │ 2 │ 20 │ [1,2] │ [10,20] │ 2 │
|
||
│ World │ 3 │ 30 │ [3,4,5] │ [30,40,50] │ 1 │
|
||
│ World │ 4 │ 40 │ [3,4,5] │ [30,40,50] │ 2 │
|
||
│ World │ 5 │ 50 │ [3,4,5] │ [30,40,50] │ 3 │
|
||
└───────┴─────┴─────┴─────────┴────────────┴─────┘
|
||
```
|
||
|
||
### Секция JOIN {#select-join}
|
||
|
||
Соединяет данные в привычном для [SQL JOIN](https://en.wikipedia.org/wiki/Join_(SQL)) смысле.
|
||
|
||
!!! info "Примечание"
|
||
Не связана с функциональностью [ARRAY JOIN](#select-array-join-clause).
|
||
|
||
```sql
|
||
SELECT <expr_list>
|
||
FROM <left_subquery>
|
||
[GLOBAL] [ANY|ALL] [INNER|LEFT|RIGHT|FULL|CROSS] [OUTER] JOIN <right_subquery>
|
||
(ON <expr_list>)|(USING <column_list>) ...
|
||
```
|
||
|
||
Вместо `<left_subquery>` и `<right_subquery>` можно указать имена таблиц. Это эквивалентно подзапросу `SELECT * FROM table`, за исключением особого случая таблицы с движком [Join](../operations/table_engines/join.md) – массива, подготовленного для присоединения.
|
||
|
||
#### Поддерживаемые типы `JOIN` {#select-join-types}
|
||
|
||
- `INNER JOIN` (or `JOIN`)
|
||
- `LEFT JOIN` (or `LEFT OUTER JOIN`)
|
||
- `RIGHT JOIN` (or `RIGHT OUTER JOIN`)
|
||
- `FULL JOIN` (or `FULL OUTER JOIN`)
|
||
- `CROSS JOIN` (or `,` )
|
||
|
||
Смотрите описание стандартного [SQL JOIN](https://en.wikipedia.org/wiki/Join_(SQL)).
|
||
|
||
#### Множественный JOIN
|
||
|
||
При выполнении запросов, ClickHouse перезаписывает множественный `JOIN` как комбинацию двух-табличных объединений и обрабатывает их последовательно. Например, если необходимо объединить четыре таблицы, ClickHouse объединяет первую и вторую таблицы, затем соединяет результат с третьей, а затем с четвертой.
|
||
|
||
Если запрос содержит секцию `WHERE`, ClickHouse пытается пробросить фильтры из этой секции в промежуточный `JOIN`. Если он не может пробросить фильтр в каждый промежуточный `JOIN`, ClickHouse применяет фильтры после того, как все `JOIN` будут выполнены.
|
||
|
||
Для создания запросов мы рекомендуем использоват синтаксис `JOIN ON` или `JOIN USING`. Например:
|
||
|
||
```
|
||
SELECT * FROM t1 JOIN t2 ON t1.a = t2.a JOIN t3 ON t1.a = t3.a
|
||
```
|
||
|
||
В секции `FROM` вы можете использовать разделенные запятыми списки таблиц для объединения. Этот синтаксис работает только при включённой настройке [allow_experimental_cross_to_join_conversion = 1](../operations/settings/settings.md#settings-allow_experimental_cross_to_join_conversion). Например:
|
||
|
||
```
|
||
SELECT * FROM t1, t2, t3 WHERE t1.a = t2.a AND t1.a = t3.a
|
||
```
|
||
|
||
Не смешивайте синтаксисы.
|
||
|
||
ClickHouse не поддерживает синтаксис с запятыми напрямую и мы не рекомендуем его использовать. Алгоритм пытается переписать запрос с помощью секций `CROSS JOIN` и `INNER JOIN` и затем продолжает его выполнение. При переписывании запроса, ClickHouse пытается оптимизировать производительность и потребление памяти. По умолчанию, ClickHouse трактует запятые как `INNER JOIN` и конвертирует их в `CROSS JOIN` когда не может гарантировать, что `INNER JOIN` возвращает запрошенные данные.
|
||
|
||
#### Строгость {#select-join-strictness}
|
||
|
||
- `ALL` — если правая таблица содержит несколько подходящих строк, то ClickHouse выполняет их [декартово произведение](https://ru.wikipedia.org/wiki/Прямое_произведение). Это стандартное поведение `JOIN` в SQL.
|
||
- `ANY` — если в правой таблице несколько соответствующих строк, то присоединяется только первая найденная. Если в правой таблице есть только одна подходящая строка, то результаты `ANY` и `ALL` совпадают.
|
||
- `ASOF` — для объединения последовательностей с нечётким совпадением. `ASOF JOIN` описан ниже по тексту.
|
||
|
||
**Использование ASOF JOIN**
|
||
|
||
`ASOF JOIN` применим в том случае, когда необходимо объединять записи, которые не имеют точного совпадения.
|
||
|
||
Таблицы для `ASOF JOIN` должны иметь столбец с отсортированной последовательностью. Этот столбец не может быть единственным в таблице и должен быть одного из типов: `UInt32`, `UInt64`, `Float32`, `Float64`, `Date` и `DateTime`.
|
||
|
||
Синтаксис `ASOF JOIN`:
|
||
|
||
```
|
||
SELECT expression_list FROM table_1 ASOF JOIN table_2 USING(equi_column1, ... equi_columnN, asof_column)
|
||
```
|
||
|
||
`ASOF JOIN` использует `equi_columnX` для объединения по равенству и `asof_column` для объединения по ближайшему совпадению.
|
||
|
||
Например, рассмотрим следующие таблицы:
|
||
|
||
```
|
||
table_1 table_2
|
||
event | ev_time | user_id event | ev_time | user_id
|
||
----------|---------|---------- ----------|---------|----------
|
||
... ...
|
||
event_1_1 | 12:00 | 42 event_2_1 | 11:59 | 42
|
||
... event_2_2 | 12:30 | 42
|
||
event_1_2 | 13:00 | 42 event_2_3 | 13:00 | 42
|
||
... ...
|
||
```
|
||
|
||
`ASOF JOIN` принимает метку времени пользовательского события из `table_1` и находит такое событие в `table_2` метка времени которого наиболее близка (равна или меньше) к метке времени события из `table_1`. При этом столбец `user_id` используется для объединения по равенству, а столбец `ev_time` для объединения по ближайшему совпадению. В нашем примере `event_1_1` может быть объединено с `event_2_1`, `event_1_2` может быть объединено с `event_2_3`, а `event_2_2` не объединяется.
|
||
|
||
Детали реализации:
|
||
|
||
- `asof_column` должен быть последним в секции `USING`.
|
||
- `ASOF JOIN` не поддержан для движка таблиц [Join](../operations/table_engines/join.md).
|
||
|
||
Чтобы задать значение строгости по умолчанию, используйте сессионный параметр [join_default_strictness](../operations/settings/settings.md#settings-join_default_strictness).
|
||
|
||
#### GLOBAL JOIN
|
||
|
||
При использовании обычного `JOIN` , запрос отправляется на удалённые серверы. На каждом из них выполняются подзапросы для формирования "правой" таблицы, и с этой таблицей выполняется соединение. То есть, "правая" таблица формируется на каждом сервере отдельно.
|
||
|
||
При использовании `GLOBAL ... JOIN`, сначала сервер-инициатор запроса запускает подзапрос для вычисления правой таблицы. Эта временная таблица передаётся на каждый удалённый сервер, и на них выполняются запросы с использованием переданных временных данных.
|
||
|
||
Будьте аккуратны при использовании `GLOBAL` . За дополнительной информацией обращайтесь в раздел [Распределенные подзапросы](#select-distributed-subqueries).
|
||
|
||
**Советы по использованию**
|
||
|
||
Из подзапроса удаляются все столбцы, ненужные для `JOIN`.
|
||
|
||
При запуске `JOIN`, отсутствует оптимизация порядка выполнения по отношению к другим стадиям запроса. Соединение (поиск в "правой" таблице) выполняется до фильтрации в `WHERE` и до агрегации. Чтобы явно задать порядок вычислений, рекомендуется выполнять `JOIN` подзапроса с подзапросом.
|
||
|
||
Пример:
|
||
|
||
```sql
|
||
SELECT
|
||
CounterID,
|
||
hits,
|
||
visits
|
||
FROM
|
||
(
|
||
SELECT
|
||
CounterID,
|
||
count() AS hits
|
||
FROM test.hits
|
||
GROUP BY CounterID
|
||
) ANY LEFT JOIN
|
||
(
|
||
SELECT
|
||
CounterID,
|
||
sum(Sign) AS visits
|
||
FROM test.visits
|
||
GROUP BY CounterID
|
||
) USING CounterID
|
||
ORDER BY hits DESC
|
||
LIMIT 10
|
||
```
|
||
|
||
```
|
||
┌─CounterID─┬───hits─┬─visits─┐
|
||
│ 1143050 │ 523264 │ 13665 │
|
||
│ 731962 │ 475698 │ 102716 │
|
||
│ 722545 │ 337212 │ 108187 │
|
||
│ 722889 │ 252197 │ 10547 │
|
||
│ 2237260 │ 196036 │ 9522 │
|
||
│ 23057320 │ 147211 │ 7689 │
|
||
│ 722818 │ 90109 │ 17847 │
|
||
│ 48221 │ 85379 │ 4652 │
|
||
│ 19762435 │ 77807 │ 7026 │
|
||
│ 722884 │ 77492 │ 11056 │
|
||
└───────────┴────────┴────────┘
|
||
```
|
||
|
||
У подзапросов нет возможности задать имена и нет возможности их использовать для того, чтобы сослаться на столбец из конкретного подзапроса.
|
||
Требуется, чтобы столбцы, указанные в `USING`, назывались одинаково в обоих подзапросах, а остальные столбцы - по-разному. Изменить имена столбцов в подзапросах можно с помощью синонимов (в примере используются синонимы hits и visits).
|
||
|
||
В секции `USING` указывается один или несколько столбцов для соединения, что обозначает условие на равенство этих столбцов. Список столбцов задаётся без скобок. Более сложные условия соединения не поддерживаются.
|
||
|
||
"Правая" таблица (результат подзапроса) располагается в оперативной памяти. Если её не хватает, вы не сможете выполнить `JOIN`.
|
||
|
||
Каждый раз для выполнения запроса с одинаковым `JOIN`, подзапрос выполняется заново — результат не кэшируется. Это можно избежать, используя специальный движок таблиц [Join](../operations/table_engines/join.md), представляющий собой подготовленное множество для соединения, которое всегда находится в оперативке.
|
||
|
||
В некоторых случаях более эффективно использовать `IN` вместо `JOIN`.
|
||
Среди разных типов `JOIN`, наиболее эффективен `ANY LEFT JOIN`, следующий по эффективности `ANY INNER JOIN`. Наименее эффективны `ALL LEFT JOIN` и `ALL INNER JOIN`.
|
||
|
||
Если `JOIN` необходим для соединения с таблицами измерений (dimension tables - сравнительно небольшие таблицы, которые содержат свойства измерений - например, имена для рекламных кампаний), то использование `JOIN` может быть не очень удобным из-за громоздкости синтаксиса, а также из-за того, что правая таблица читается заново при каждом запросе. Специально для таких случаев существует функциональность "Внешние словари", которую следует использовать вместо `JOIN`. Дополнительные сведения смотрите в разделе [Внешние словари](dicts/external_dicts.md).
|
||
|
||
**Ограничения по памяти**
|
||
|
||
ClickHouse использует алгоритм [hash join](https://en.wikipedia.org/wiki/Hash_join). ClickHouse принимает `<right_subquery>` и создает для него хэш-таблицу в RAM. Чтобы ограничить потребление памяти операцией `JOIN`, используйте следующие параметры:
|
||
|
||
- [max_rows_in_join](../operations/settings/query_complexity.md#settings-max_rows_in_join) — ограничивает количество строк в хэш-таблице.
|
||
- [max_bytes_in_join](../operations/settings/query_complexity.md#settings-max_bytes_in_join) — ограничивает размер хэш-таблицы.
|
||
|
||
По достижении любого из этих ограничений, ClickHouse действует в соответствии с настройкой [join_overflow_mode](../operations/settings/query_complexity.md#settings-join_overflow_mode).
|
||
|
||
#### Обработка пустых ячеек и NULL
|
||
|
||
При слиянии таблиц могут появляться пустые ячейки. То, каким образом ClickHouse заполняет эти ячейки, определяется настройкой [join_use_nulls](../operations/settings/settings.md#settings-join_use_nulls).
|
||
|
||
Если ключами `JOIN` выступают поля типа [Nullable](../data_types/nullable.md), то строки, где хотя бы один из ключей имеет значение [NULL](syntax.md#null-literal), не соединяются.
|
||
|
||
#### Ограничения синтаксиса
|
||
|
||
Для множественных секций `JOIN` в одном запросе `SELECT`:
|
||
|
||
- Получение всех столбцов через `*` возможно только при объединении таблиц, но не подзапросов.
|
||
- Секция `PREWHERE` недоступна.
|
||
|
||
Для секций `ON`, `WHERE` и `GROUP BY`:
|
||
|
||
- Нельзя использовать произвольные выражения в секциях `ON`, `WHERE`, и `GROUP BY`, однако можно определить выражение в секции `SELECT` и затем использовать его через алиас в других секциях.
|
||
|
||
### Секция WHERE {#select-where}
|
||
|
||
Позволяет задать выражение, которое ClickHouse использует для фильтрации данных перед всеми другими действиями в запросе кроме выражений, содержащихся в секции [PREWHERE](#select-prewhere). Обычно, это выражение с логическими операторами.
|
||
|
||
Результат выражения должен иметь тип `UInt8`.
|
||
|
||
ClickHouse использует в выражении индексы, если это позволяет [движок таблицы](../operations/table_engines/index.md).
|
||
|
||
Если в секции необходимо проверить [NULL](syntax.md#null-literal), то используйте операторы [IS NULL](operators.md#operator-is-null) и [IS NOT NULL](operators.md#is-not-null), а также соответствующие функции `isNull` и `isNotNull`. В противном случае выражение будет считаться всегда не выполненным.
|
||
|
||
Пример проверки на `NULL`:
|
||
|
||
```bash
|
||
:) SELECT * FROM t_null WHERE y IS NULL
|
||
|
||
SELECT *
|
||
FROM t_null
|
||
WHERE isNull(y)
|
||
|
||
┌─x─┬────y─┐
|
||
│ 1 │ ᴺᵁᴸᴸ │
|
||
└───┴──────┘
|
||
|
||
1 rows in set. Elapsed: 0.002 sec.
|
||
```
|
||
|
||
|
||
### Секция PREWHERE {#select-prewhere}
|
||
|
||
Имеет такой же смысл, как и секция [WHERE](#select-where). Отличие состоит в том, какие данные читаются из таблицы.
|
||
При использовании `PREWHERE`, из таблицы сначала читаются только столбцы, необходимые для выполнения `PREWHERE`. Затем читаются остальные столбцы, нужные для выполнения запроса, но из них только те блоки, в которых выражение в `PREWHERE` истинное.
|
||
|
||
`PREWHERE` имеет смысл использовать, если есть условия фильтрации, которые использует меньшинство столбцов из тех, что есть в запросе, но достаточно сильно фильтрует данные. Таким образом, сокращается количество читаемых данных.
|
||
|
||
Например, полезно писать `PREWHERE` для запросов, которые вынимают много столбцов, но в которых фильтрация производится лишь по нескольким столбцам.
|
||
|
||
`PREWHERE` поддерживается только таблицами семейства `*MergeTree`.
|
||
|
||
В запросе могут быть одновременно указаны секции `PREWHERE` и `WHERE`. В этом случае, `PREWHERE` идёт перед `WHERE`.
|
||
|
||
Если настройка `optimize_move_to_prewhere` выставлена в `1`, то при отсутствии `PREWHERE`, система будет автоматически переносить части выражений из `WHERE` в `PREWHERE` согласно некоторой эвристике.
|
||
|
||
### Секция GROUP BY {#select-group-by-clause}
|
||
|
||
Это одна из наиболее важных частей СУБД.
|
||
|
||
Секция GROUP BY, если есть, должна содержать список выражений. Каждое выражение далее будем называть "ключом".
|
||
При этом, все выражения в секциях SELECT, HAVING, ORDER BY, должны вычисляться из ключей или из агрегатных функций. То есть, каждый выбираемый из таблицы столбец, должен использоваться либо в ключах, либо внутри агрегатных функций.
|
||
|
||
Если запрос содержит столбцы таблицы только внутри агрегатных функций, то секция GROUP BY может не указываться, и подразумевается агрегация по пустому набору ключей.
|
||
|
||
Пример:
|
||
|
||
``` sql
|
||
SELECT
|
||
count(),
|
||
median(FetchTiming > 60 ? 60 : FetchTiming),
|
||
count() - sum(Refresh)
|
||
FROM hits
|
||
```
|
||
|
||
Но, в отличие от стандартного SQL, если в таблице нет строк (вообще нет или после фильтрации с помощью WHERE), в качестве результата возвращается пустой результат, а не результат из одной строки, содержащий "начальные" значения агрегатных функций.
|
||
|
||
В отличие от MySQL (и в соответствии со стандартом SQL), вы не можете получить какое-нибудь значение некоторого столбца, не входящего в ключ или агрегатную функцию (за исключением константных выражений). Для обхода этого вы можете воспользоваться агрегатной функцией any (получить первое попавшееся значение) или min/max.
|
||
|
||
Пример:
|
||
|
||
``` sql
|
||
SELECT
|
||
domainWithoutWWW(URL) AS domain,
|
||
count(),
|
||
any(Title) AS title -- getting the first occurred page header for each domain.
|
||
FROM hits
|
||
GROUP BY domain
|
||
```
|
||
|
||
GROUP BY вычисляет для каждого встретившегося различного значения ключей, набор значений агрегатных функций.
|
||
|
||
Не поддерживается GROUP BY по столбцам-массивам.
|
||
|
||
Не поддерживается указание констант в качестве аргументов агрегатных функций. Пример: sum(1). Вместо этого, вы можете избавиться от констант. Пример: `count()`.
|
||
|
||
#### Обработка NULL
|
||
|
||
При группировке, ClickHouse рассматривает [NULL](syntax.md) как значение, причём `NULL=NULL`.
|
||
|
||
Рассмотрим, что это значит на примере.
|
||
|
||
Пусть есть таблица:
|
||
|
||
```
|
||
┌─x─┬────y─┐
|
||
│ 1 │ 2 │
|
||
│ 2 │ ᴺᵁᴸᴸ │
|
||
│ 3 │ 2 │
|
||
│ 3 │ 3 │
|
||
│ 3 │ ᴺᵁᴸᴸ │
|
||
└───┴──────┘
|
||
```
|
||
|
||
В результате запроса `SELECT sum(x), y FROM t_null_big GROUP BY y` мы получим:
|
||
|
||
```
|
||
┌─sum(x)─┬────y─┐
|
||
│ 4 │ 2 │
|
||
│ 3 │ 3 │
|
||
│ 5 │ ᴺᵁᴸᴸ │
|
||
└────────┴──────┘
|
||
```
|
||
|
||
Видно, что `GROUP BY` для `У = NULL` просуммировал `x`, как будто `NULL` — это значение.
|
||
|
||
Если в `GROUP BY` передать несколько ключей, то в результате мы получим все комбинации выборки, как если бы `NULL` был конкретным значением.
|
||
|
||
#### Модификатор WITH TOTALS
|
||
|
||
Если указан модификатор WITH TOTALS, то будет посчитана ещё одна строчка, в которой в столбцах-ключах будут содержаться значения по умолчанию (нули, пустые строки), а в столбцах агрегатных функций - значения, посчитанные по всем строкам ("тотальные" значения).
|
||
|
||
Эта дополнительная строчка выводится в форматах JSON\*, TabSeparated\*, Pretty\* отдельно от остальных строчек. В остальных форматах эта строчка не выводится.
|
||
|
||
В форматах JSON\* строчка выводится отдельным полем totals. В форматах TabSeparated\* строчка выводится после основного результата, и перед ней (после остальных данных) вставляется пустая строка. В форматах Pretty\* строчка выводится отдельной табличкой после основного результата.
|
||
|
||
`WITH TOTALS` может выполняться по-разному при наличии HAVING. Поведение зависит от настройки totals_mode.
|
||
По умолчанию `totals_mode = 'before_having'`. В этом случае totals считается по всем строчкам, включая непрошедших через HAVING и max_rows_to_group_by.
|
||
|
||
Остальные варианты учитывают в totals только строчки, прошедшие через HAVING, и имеют разное поведение при наличии настройки `max_rows_to_group_by` и `group_by_overflow_mode = 'any'`.
|
||
|
||
`after_having_exclusive` - не учитывать строчки, не прошедшие `max_rows_to_group_by`. То есть в totals попадёт меньше или столько же строчек, чем если бы `max_rows_to_group_by` не было.
|
||
|
||
`after_having_inclusive` - учитывать в totals все строчки, не прошедшие max_rows_to_group_by. То есть в totals попадёт больше или столько же строчек, чем если бы `max_rows_to_group_by` не было.
|
||
|
||
`after_having_auto` - считать долю строчек, прошедших через HAVING. Если она больше некоторого значения (по умолчанию - 50%), то включить все строчки, не прошедшие max_rows_to_group_by в totals, иначе - не включить.
|
||
|
||
`totals_auto_threshold` - по умолчанию 0.5. Коэффициент для работы `after_having_auto`.
|
||
|
||
Если `max_rows_to_group_by` и `group_by_overflow_mode = 'any'` не используются, то все варианты вида `after_having` не отличаются, и вы можете использовать любой из них, например, `after_having_auto`.
|
||
|
||
Вы можете использовать WITH TOTALS в подзапросах, включая подзапросы в секции JOIN (в этом случае соответствующие тотальные значения будут соединены).
|
||
|
||
#### GROUP BY во внешней памяти {#select-group-by-in-external-memory}
|
||
|
||
Можно включить сброс временных данных на диск, чтобы ограничить потребление оперативной памяти при выполнении `GROUP BY`.
|
||
Настройка [max_bytes_before_external_group_by](../operations/settings/settings.md#settings-max_bytes_before_external_group_by) определяет пороговое значение потребления RAM, по достижении которого временные данные `GROUP BY` сбрасываются в файловую систему. Если равно 0 (по умолчанию) - значит выключено.
|
||
|
||
При использовании `max_bytes_before_external_group_by`, рекомендуем выставить `max_memory_usage` приблизительно в два раза больше. Это следует сделать, потому что агрегация выполняется в две стадии: чтение и формирование промежуточных данных (1) и слияние промежуточных данных (2). Сброс данных на файловую систему может производиться только на стадии 1. Если сброса временных данных не было, то на стадии 2 может потребляться до такого же объёма памяти, как на стадии 1.
|
||
|
||
Например, если [max_memory_usage](../operations/settings/settings.md#settings_max_memory_usage) было выставлено в 10000000000, и вы хотите использовать внешнюю агрегацию, то имеет смысл выставить `max_bytes_before_external_group_by` в 10000000000, а max_memory_usage в 20000000000. При срабатывании внешней агрегации (если был хотя бы один сброс временных данных в файловую систему) максимальное потребление оперативки будет лишь чуть-чуть больше `max_bytes_before_external_group_by`.
|
||
|
||
При распределённой обработке запроса внешняя агрегация производится на удалённых серверах. Для того чтобы на сервере-инициаторе запроса использовалось немного оперативки, нужно выставить настройку `distributed_aggregation_memory_efficient` в 1.
|
||
|
||
При слиянии данных, сброшенных на диск, а также при слиянии результатов с удалённых серверов, при включенной настройке `distributed_aggregation_memory_efficient`, потребляется до `1/256 * the_number_of_threads` количество потоков от общего объёма оперативки.
|
||
|
||
При включенной внешней агрегации, если данных было меньше `max_bytes_before_external_group_by` (то есть сброса данных не было), то запрос работает так же быстро, как без внешней агрегации. Если же какие-то временные данные были сброшены, то время выполнения будет в несколько раз больше (примерно в три раза).
|
||
|
||
Если есть `ORDER BY` с `LIMIT` после `GROUP BY`, то объём потребляемой RAM будет зависеть от объёма данных в `LIMIT`, а не во всей таблице. Однако, если `ORDER BY` используется без `LIMIT`, не забудьте включить внешнюю сортировку (`max_bytes_before_external_sort`).
|
||
|
||
### Секция LIMIT BY
|
||
|
||
Запрос с секцией `LIMIT n BY expressions` выбирает первые `n` строк для каждого отличного значения `expressions`. Ключ `LIMIT BY` может содержать любое количество [выражений](syntax.md#syntax-expressions).
|
||
|
||
ClickHouse поддерживает следующий синтаксис:
|
||
|
||
- `LIMIT [offset_value, ]n BY expressions`
|
||
- `LIMIT n OFFSET offset_value BY expressions`
|
||
|
||
Во время обработки запроса, ClickHouse выбирает данные, упорядоченные по ключу сортировки. Ключ сортировки задаётся явно в секции [ORDER BY](#select-order-by) или неявно в свойствах движка таблицы. Затем ClickHouse применяет `LIMIT n BY expressions` и возвращает первые `n` для каждой отличной комбинации `expressions`. Если указан `OFFSET`, то для каждого блока данных, который принадлежит отдельной комбинации `expressions`, ClickHouse отступает `offset_value` строк от начала блока и возвращает не более `n`. Если `offset_value` больше, чем количество строк в блоке данных, ClickHouse не возвращает ни одной строки.
|
||
|
||
`LIMIT BY` не связана с секцией `LIMIT`. Их можно использовать в одном запросе.
|
||
|
||
**Примеры**
|
||
|
||
Образец таблицы:
|
||
|
||
```sql
|
||
CREATE TABLE limit_by(id Int, val Int) ENGINE = Memory;
|
||
INSERT INTO limit_by values(1, 10), (1, 11), (1, 12), (2, 20), (2, 21);
|
||
```
|
||
|
||
Запросы:
|
||
|
||
```sql
|
||
SELECT * FROM limit_by ORDER BY id, val LIMIT 2 BY id
|
||
```
|
||
|
||
```text
|
||
┌─id─┬─val─┐
|
||
│ 1 │ 10 │
|
||
│ 1 │ 11 │
|
||
│ 2 │ 20 │
|
||
│ 2 │ 21 │
|
||
└────┴─────┘
|
||
```
|
||
|
||
```sql
|
||
SELECT * FROM limit_by ORDER BY id, val LIMIT 1, 2 BY id
|
||
```
|
||
|
||
```text
|
||
┌─id─┬─val─┐
|
||
│ 1 │ 11 │
|
||
│ 1 │ 12 │
|
||
│ 2 │ 21 │
|
||
└────┴─────┘
|
||
```
|
||
|
||
Запрос `SELECT * FROM limit_by ORDER BY id, val LIMIT 2 OFFSET 1 BY id` возвращает такой же результат.
|
||
|
||
Следующий запрос выбирает топ 5 рефереров для каждой пары `domain, device_type`, но не более 100 строк (`LIMIT n BY + LIMIT`).
|
||
|
||
```sql
|
||
SELECT
|
||
domainWithoutWWW(URL) AS domain,
|
||
domainWithoutWWW(REFERRER_URL) AS referrer,
|
||
device_type,
|
||
count() cnt
|
||
FROM hits
|
||
GROUP BY domain, referrer, device_type
|
||
ORDER BY cnt DESC
|
||
LIMIT 5 BY domain, device_type
|
||
LIMIT 100
|
||
```
|
||
|
||
Запрос выберет топ 5 рефереров для каждой пары `domain, device_type`, но не более 100 строк (`LIMIT n BY + LIMIT`).
|
||
|
||
`LIMIT n BY` работает с [NULL](syntax.md) как если бы это было конкретное значение. Т.е. в результате запроса пользователь получит все комбинации полей, указанных в `BY`.
|
||
|
||
### Секция HAVING
|
||
|
||
Позволяет отфильтровать результат, полученный после GROUP BY, аналогично секции WHERE.
|
||
WHERE и HAVING отличаются тем, что WHERE выполняется до агрегации (GROUP BY), а HAVING - после.
|
||
Если агрегации не производится, то HAVING использовать нельзя.
|
||
|
||
|
||
### Секция ORDER BY {#select-order-by}
|
||
|
||
Секция ORDER BY содержит список выражений, к каждому из которых также может быть приписано DESC или ASC (направление сортировки). Если ничего не приписано - это аналогично приписыванию ASC. ASC - сортировка по возрастанию, DESC - сортировка по убыванию. Обозначение направления сортировки действует на одно выражение, а не на весь список. Пример: `ORDER BY Visits DESC, SearchPhrase`
|
||
|
||
Для сортировки по значениям типа String есть возможность указать collation (сравнение). Пример: `ORDER BY SearchPhrase COLLATE 'tr'` - для сортировки по поисковой фразе, по возрастанию, с учётом турецкого алфавита, регистронезависимо, при допущении, что строки в кодировке UTF-8. COLLATE может быть указан или не указан для каждого выражения в ORDER BY независимо. Если есть ASC или DESC, то COLLATE указывается после них. При использовании COLLATE сортировка всегда регистронезависима.
|
||
|
||
Рекомендуется использовать COLLATE только для окончательной сортировки небольшого количества строк, так как производительность сортировки с указанием COLLATE меньше, чем обычной сортировки по байтам.
|
||
|
||
Строки, для которых список выражений, по которым производится сортировка, принимает одинаковые значения, выводятся в произвольном порядке, который может быть также недетерминированным (каждый раз разным).
|
||
Если секция ORDER BY отсутствует, то, аналогично, порядок, в котором идут строки, не определён, и может быть недетерминированным.
|
||
|
||
Порядок сортировки `NaN` и `NULL`:
|
||
|
||
- С модификатором `NULLS FIRST` — Сначала `NULL`, затем `NaN`, затем остальные значения.
|
||
- С модификатором `NULLS LAST` — Сначала значения, затем `NaN`, затем `NULL`.
|
||
- По умолчанию — Как с модификатором `NULLS LAST`.
|
||
|
||
Пример:
|
||
|
||
Для таблицы
|
||
|
||
```
|
||
┌─x─┬────y─┐
|
||
│ 1 │ ᴺᵁᴸᴸ │
|
||
│ 2 │ 2 │
|
||
│ 1 │ nan │
|
||
│ 2 │ 2 │
|
||
│ 3 │ 4 │
|
||
│ 5 │ 6 │
|
||
│ 6 │ nan │
|
||
│ 7 │ ᴺᵁᴸᴸ │
|
||
│ 6 │ 7 │
|
||
│ 8 │ 9 │
|
||
└───┴──────┘
|
||
```
|
||
|
||
Выполним запрос `SELECT * FROM t_null_nan ORDER BY y NULLS FIRST`, получим:
|
||
|
||
```
|
||
┌─x─┬────y─┐
|
||
│ 1 │ ᴺᵁᴸᴸ │
|
||
│ 7 │ ᴺᵁᴸᴸ │
|
||
│ 1 │ nan │
|
||
│ 6 │ nan │
|
||
│ 2 │ 2 │
|
||
│ 2 │ 2 │
|
||
│ 3 │ 4 │
|
||
│ 5 │ 6 │
|
||
│ 6 │ 7 │
|
||
│ 8 │ 9 │
|
||
└───┴──────┘
|
||
```
|
||
|
||
Если кроме ORDER BY указан также не слишком большой LIMIT, то расходуется меньше оперативки. Иначе расходуется количество памяти, пропорциональное количеству данных для сортировки. При распределённой обработке запроса, если отсутствует GROUP BY, сортировка частично делается на удалённых серверах, а на сервере-инициаторе запроса производится слияние результатов. Таким образом, при распределённой сортировке, может сортироваться объём данных, превышающий размер памяти на одном сервере.
|
||
|
||
Существует возможность выполнять сортировку во внешней памяти (с созданием временных файлов на диске), если оперативной памяти не хватает. Для этого предназначена настройка `max_bytes_before_external_sort`. Если она выставлена в 0 (по умолчанию), то внешняя сортировка выключена. Если она включена, то при достижении объёмом данных для сортировки указанного количества байт, накопленные данные будут отсортированы и сброшены во временный файл. После того, как все данные будут прочитаны, будет произведено слияние всех сортированных файлов и выдача результата. Файлы записываются в директорию /var/lib/clickhouse/tmp/ (по умолчанию, может быть изменено с помощью параметра tmp_path) в конфиге.
|
||
|
||
На выполнение запроса может расходоваться больше памяти, чем max_bytes_before_external_sort. Поэтому, значение этой настройки должно быть существенно меньше, чем max_memory_usage. Для примера, если на вашем сервере 128 GB оперативки, и вам нужно выполнить один запрос, то выставите max_memory_usage в 100 GB, а max_bytes_before_external_sort в 80 GB.
|
||
|
||
Внешняя сортировка работает существенно менее эффективно, чем сортировка в оперативке.
|
||
|
||
### Секция SELECT
|
||
|
||
После вычислений, соответствующих всем перечисленным выше секциям, производится вычисление выражений, указанных в секции SELECT.
|
||
Вернее, вычисляются выражения, стоящие над агрегатными функциями, если есть агрегатные функции.
|
||
Сами агрегатные функции и то, что под ними, вычисляются при агрегации (GROUP BY).
|
||
Эти выражения работают так, как будто применяются к отдельным строкам результата.
|
||
|
||
### Секция DISTINCT {#select-distinct}
|
||
|
||
Если указано `DISTINCT`, то из всех множеств полностью совпадающих строк результата, будет оставляться только одна строка.
|
||
Результат выполнения будет таким же, как если указано `GROUP BY` по всем указанным полям в `SELECT` и не указаны агрегатные функции. Но имеется несколько отличий от `GROUP BY`:
|
||
|
||
- `DISTINCT` может применяться совместно с `GROUP BY`;
|
||
- при отсутствии `ORDER BY` и наличии `LIMIT`, запрос прекратит выполнение сразу после того, как будет прочитано необходимое количество различных строк - в этом случае использование DISTINCT существенно более оптимально;
|
||
- блоки данных будут выдаваться по мере их обработки, не дожидаясь выполнения всего запроса.
|
||
|
||
`DISTINCT` не поддерживается, если в `SELECT` присутствует хотя бы один столбец типа массив.
|
||
|
||
`DISTINCT` работает с [NULL](syntax.md) как если бы `NULL` был конкретным значением, причём `NULL=NULL`. Т.е. в результате `DISTINCT` разные комбинации с `NULL` встретятся только по одному разу.
|
||
|
||
ClickHouse поддерживает использование в одном запросе секций `DISTINCT` и `ORDER BY` для разных столбцов. Секция `DISTINCT` исполняется перед секцией `ORDER BY`.
|
||
|
||
Таблица для примера:
|
||
|
||
```text
|
||
┌─a─┬─b─┐
|
||
│ 2 │ 1 │
|
||
│ 1 │ 2 │
|
||
│ 3 │ 3 │
|
||
│ 2 │ 4 │
|
||
└───┴───┘
|
||
```
|
||
|
||
При выборке данных запросом `SELECT DISTINCT a FROM t1 ORDER BY b ASC`, мы получаем следующий результат:
|
||
|
||
```text
|
||
┌─a─┐
|
||
│ 2 │
|
||
│ 1 │
|
||
│ 3 │
|
||
└───┘
|
||
```
|
||
|
||
Если изменить направление сортировки `SELECT DISTINCT a FROM t1 ORDER BY b DESC`, то результат получается следующий:
|
||
|
||
```text
|
||
┌─a─┐
|
||
│ 3 │
|
||
│ 1 │
|
||
│ 2 │
|
||
└───┘
|
||
```
|
||
|
||
Строка `2, 4` была удалена перед сортировкой.
|
||
|
||
Учитывайте эту особенность реализации при программировании запросов.
|
||
|
||
### Секция LIMIT
|
||
|
||
`LIMIT m` позволяет выбрать из результата первые `m` строк.
|
||
|
||
`LIMIT n, m` позволяет выбрать из результата первые `m` строк после пропуска первых `n` строк. Синтаксис `LIMIT m OFFSET n` также поддерживается.
|
||
|
||
`n` и `m` должны быть неотрицательными целыми числами.
|
||
|
||
При отсутствии секции `ORDER BY`, однозначно сортирующей результат, результат может быть произвольным и может являться недетерминированным.
|
||
|
||
### Секция UNION ALL
|
||
|
||
Произвольное количество запросов может быть объединено с помощью `UNION ALL`. Пример:
|
||
|
||
``` sql
|
||
SELECT CounterID, 1 AS table, toInt64(count()) AS c
|
||
FROM test.hits
|
||
GROUP BY CounterID
|
||
|
||
UNION ALL
|
||
|
||
SELECT CounterID, 2 AS table, sum(Sign) AS c
|
||
FROM test.visits
|
||
GROUP BY CounterID
|
||
HAVING c > 0
|
||
```
|
||
|
||
Поддерживается только `UNION ALL`. Обычный `UNION` (`UNION DISTINCT`) не поддерживается. Если вам нужен `UNION DISTINCT`, то вы можете написать `SELECT DISTINCT` из подзапроса, содержащего `UNION ALL`.
|
||
|
||
Запросы - части `UNION ALL` могут выполняться параллельно, и их результаты могут возвращаться вперемешку.
|
||
|
||
Структура результатов (количество и типы столбцов) у запросов должна совпадать. Но имена столбцов могут отличаться. В этом случае, имена столбцов для общего результата будут взяты из первого запроса. При объединении выполняется приведение типов. Например, если в двух объединяемых запросах одно и тоже поле имеет типы не-`Nullable` и `Nullable` от совместимого типа, то в результате `UNION ALL` получим поле типа `Nullable`.
|
||
|
||
Запросы - части `UNION ALL` нельзя заключить в скобки. `ORDER BY` и `LIMIT` применяются к отдельным запросам, а не к общему результату. Если вам нужно применить какое-либо преобразование к общему результату, то вы можете разместить все запросы с `UNION ALL` в подзапросе в секции `FROM`.
|
||
|
||
### Секция INTO OUTFILE
|
||
|
||
При указании `INTO OUTFILE filename` (где filename - строковый литерал), результат запроса будет сохранён в файл filename.
|
||
В отличие от MySQL, файл создаётся на стороне клиента. Если файл с таким именем уже существует, это приведёт к ошибке.
|
||
Функциональность доступна в клиенте командной строки и clickhouse-local (попытка выполнить запрос с INTO OUTFILE через HTTP интерфейс приведёт к ошибке).
|
||
|
||
Формат вывода по умолчанию - TabSeparated, как и в неинтерактивном режиме клиента командной строки.
|
||
|
||
### Секция FORMAT
|
||
|
||
При указании FORMAT format вы можете получить данные в любом указанном формате.
|
||
Это может использоваться для удобства или для создания дампов.
|
||
Подробнее смотрите раздел "Форматы".
|
||
Если секция FORMAT отсутствует, то используется формат по умолчанию, который зависит от используемого интерфейса для доступа к БД и от настроек. Для HTTP интерфейса, а также для клиента командной строки, используемого в batch-режиме, по умолчанию используется формат TabSeparated. Для клиента командной строки, используемого в интерактивном режиме, по умолчанию используется формат PrettyCompact (прикольные таблички, компактные).
|
||
|
||
При использовании клиента командной строки данные на клиент передаются во внутреннем эффективном формате. При этом клиент самостоятельно интерпретирует секцию FORMAT запроса и форматирует данные на своей стороне (снимая нагрузку на сеть и сервер).
|
||
|
||
|
||
### Операторы IN {#select-in-operators}
|
||
|
||
Операторы `IN`, `NOT IN`, `GLOBAL IN`, `GLOBAL NOT IN` рассматриваются отдельно, так как их функциональность достаточно богатая.
|
||
|
||
В качестве левой части оператора, может присутствовать как один столбец, так и кортеж.
|
||
|
||
Примеры:
|
||
|
||
``` sql
|
||
SELECT UserID IN (123, 456) FROM ...
|
||
SELECT (CounterID, UserID) IN ((34, 123), (101500, 456)) FROM ...
|
||
```
|
||
|
||
Если слева стоит один столбец, входящий в индекс, а справа - множество констант, то при выполнении запроса, система воспользуется индексом.
|
||
|
||
Не перечисляйте слишком большое количество значений (миллионы) явно. Если множество большое - лучше загрузить его во временную таблицу (например, смотрите раздел "Внешние данные для обработки запроса"), и затем воспользоваться подзапросом.
|
||
|
||
В качестве правой части оператора может быть множество константных выражений, множество кортежей с константными выражениями (показано в примерах выше), а также имя таблицы или подзапрос SELECT в скобках.
|
||
|
||
|
||
Если в качестве правой части оператора указано имя таблицы (например, `UserID IN users`), то это эквивалентно подзапросу `UserID IN (SELECT * FROM users)`. Это используется при работе с внешними данными, отправляемым вместе с запросом. Например, вместе с запросом может быть отправлено множество идентификаторов посетителей, загруженное во временную таблицу users, по которому следует выполнить фильтрацию.
|
||
|
||
Если в качестве правой части оператора, указано имя таблицы, имеющий движок Set (подготовленное множество, постоянно находящееся в оперативке), то множество не будет создаваться заново при каждом запросе.
|
||
|
||
В подзапросе может быть указано более одного столбца для фильтрации кортежей.
|
||
Пример:
|
||
|
||
``` sql
|
||
SELECT (CounterID, UserID) IN (SELECT CounterID, UserID FROM ...) FROM ...
|
||
```
|
||
|
||
Типы столбцов слева и справа оператора IN, должны совпадать.
|
||
|
||
Оператор IN и подзапрос могут встречаться в любой части запроса, в том числе в агрегатных и лямбда функциях.
|
||
Пример:
|
||
|
||
``` sql
|
||
SELECT
|
||
EventDate,
|
||
avg(UserID IN
|
||
(
|
||
SELECT UserID
|
||
FROM test.hits
|
||
WHERE EventDate = toDate('2014-03-17')
|
||
)) AS ratio
|
||
FROM test.hits
|
||
GROUP BY EventDate
|
||
ORDER BY EventDate ASC
|
||
```
|
||
|
||
```
|
||
┌──EventDate─┬────ratio─┐
|
||
│ 2014-03-17 │ 1 │
|
||
│ 2014-03-18 │ 0.807696 │
|
||
│ 2014-03-19 │ 0.755406 │
|
||
│ 2014-03-20 │ 0.723218 │
|
||
│ 2014-03-21 │ 0.697021 │
|
||
│ 2014-03-22 │ 0.647851 │
|
||
│ 2014-03-23 │ 0.648416 │
|
||
└────────────┴──────────┘
|
||
```
|
||
|
||
за каждый день после 17 марта считаем долю хитов, сделанных посетителями, которые заходили на сайт 17 марта.
|
||
Подзапрос в секции IN на одном сервере всегда выполняется только один раз. Зависимых подзапросов не существует.
|
||
|
||
|
||
#### Обработка NULL
|
||
|
||
При обработке запроса оператор IN будет считать, что результат операции с [NULL](syntax.md) всегда равен `0`, независимо от того, находится `NULL` в правой или левой части оператора. Значения `NULL` не входят ни в какое множество, не соответствуют друг другу и не могут сравниваться.
|
||
|
||
Рассмотрим для примера таблицу `t_null`:
|
||
|
||
```
|
||
┌─x─┬────y─┐
|
||
│ 1 │ ᴺᵁᴸᴸ │
|
||
│ 2 │ 3 │
|
||
└───┴──────┘
|
||
```
|
||
|
||
При выполнении запроса `SELECT x FROM t_null WHERE y IN (NULL,3)` получим следующий результат:
|
||
|
||
```
|
||
┌─x─┐
|
||
│ 2 │
|
||
└───┘
|
||
```
|
||
|
||
Видно, что строка, в которой `y = NULL`, выброшена из результатов запроса. Это произошло потому, что ClickHouse не может решить входит ли `NULL` в множество `(NULL,3)`, возвращает результат операции `0`, а `SELECT` выбрасывает эту строку из финальной выдачи.
|
||
|
||
```
|
||
SELECT y IN (NULL, 3)
|
||
FROM t_null
|
||
|
||
┌─in(y, tuple(NULL, 3))─┐
|
||
│ 0 │
|
||
│ 1 │
|
||
└───────────────────────┘
|
||
```
|
||
|
||
|
||
#### Распределённые подзапросы {#select-distributed-subqueries}
|
||
|
||
Существует два варианта IN-ов с подзапросами (аналогично для JOIN-ов): обычный `IN` / `JOIN` и `GLOBAL IN` / `GLOBAL JOIN`. Они отличаются способом выполнения при распределённой обработке запроса.
|
||
|
||
!!! attention
|
||
Помните, что алгоритмы, описанные ниже, могут работать иначе в зависимости от [настройки](../operations/settings/settings.md) `distributed_product_mode`.
|
||
|
||
При использовании обычного IN-а, запрос отправляется на удалённые серверы, и на каждом из них выполняются подзапросы в секциях `IN` / `JOIN`.
|
||
|
||
При использовании `GLOBAL IN` / `GLOBAL JOIN-а`, сначала выполняются все подзапросы для `GLOBAL IN` / `GLOBAL JOIN-ов`, и результаты складываются во временные таблицы. Затем эти временные таблицы передаются на каждый удалённый сервер, и на них выполняются запросы, с использованием этих переданных временных данных.
|
||
|
||
Если запрос не распределённый, используйте обычный `IN` / `JOIN`.
|
||
|
||
Следует быть внимательным при использовании подзапросов в секции `IN` / `JOIN` в случае распределённой обработки запроса.
|
||
|
||
Рассмотрим это на примерах. Пусть на каждом сервере кластера есть обычная таблица **local_table**. Пусть также есть таблица **distributed_table** типа **Distributed**, которая смотрит на все серверы кластера.
|
||
|
||
При запросе к распределённой таблице **distributed_table**, запрос будет отправлен на все удалённые серверы, и на них будет выполнен с использованием таблицы **local_table**.
|
||
|
||
Например, запрос
|
||
|
||
``` sql
|
||
SELECT uniq(UserID) FROM distributed_table
|
||
```
|
||
|
||
будет отправлен на все удалённые серверы в виде
|
||
|
||
``` sql
|
||
SELECT uniq(UserID) FROM local_table
|
||
```
|
||
|
||
, выполнен параллельно на каждом из них до стадии, позволяющей объединить промежуточные результаты; затем промежуточные результаты вернутся на сервер-инициатор запроса, будут на нём объединены, и финальный результат будет отправлен клиенту.
|
||
|
||
Теперь рассмотрим запрос с IN-ом:
|
||
|
||
``` sql
|
||
SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM local_table WHERE CounterID = 34)
|
||
```
|
||
|
||
- расчёт пересечения аудиторий двух сайтов.
|
||
|
||
Этот запрос будет отправлен на все удалённые серверы в виде
|
||
|
||
``` sql
|
||
SELECT uniq(UserID) FROM local_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM local_table WHERE CounterID = 34)
|
||
```
|
||
|
||
То есть, множество в секции IN будет собрано на каждом сервере независимо, только по тем данным, которые есть локально на каждом из серверов.
|
||
|
||
Это будет работать правильно и оптимально, если вы предусмотрели такой случай, и раскладываете данные по серверам кластера таким образом, чтобы данные одного UserID-а лежали только на одном сервере. В таком случае все необходимые данные будут присутствовать на каждом сервере локально. В противном случае результат будет посчитан неточно. Назовём этот вариант запроса "локальный IN".
|
||
|
||
Чтобы исправить работу запроса, когда данные размазаны по серверам кластера произвольным образом, можно было бы указать **distributed_table** внутри подзапроса. Запрос будет выглядеть так:
|
||
|
||
``` sql
|
||
SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM distributed_table WHERE CounterID = 34)
|
||
```
|
||
|
||
Этот запрос будет отправлен на все удалённые серверы в виде
|
||
|
||
``` sql
|
||
SELECT uniq(UserID) FROM local_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM distributed_table WHERE CounterID = 34)
|
||
```
|
||
|
||
На каждом удалённом сервере начнёт выполняться подзапрос. Так как в подзапросе используется распределённая таблица, то подзапрос будет, на каждом удалённом сервере, снова отправлен на каждый удалённый сервер, в виде
|
||
|
||
``` sql
|
||
SELECT UserID FROM local_table WHERE CounterID = 34
|
||
```
|
||
|
||
Например, если у вас кластер из 100 серверов, то выполнение всего запроса потребует 10 000 элементарных запросов, что, как правило, является неприемлемым.
|
||
|
||
В таких случаях всегда следует использовать GLOBAL IN вместо IN. Рассмотрим его работу для запроса
|
||
|
||
``` sql
|
||
SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID GLOBAL IN (SELECT UserID FROM distributed_table WHERE CounterID = 34)
|
||
```
|
||
|
||
На сервере-инициаторе запроса будет выполнен подзапрос
|
||
|
||
``` sql
|
||
SELECT UserID FROM distributed_table WHERE CounterID = 34
|
||
```
|
||
|
||
, и результат будет сложен во временную таблицу в оперативке. Затем запрос будет отправлен на каждый удалённый сервер в виде
|
||
|
||
``` sql
|
||
SELECT uniq(UserID) FROM local_table WHERE CounterID = 101500 AND UserID GLOBAL IN _data1
|
||
```
|
||
|
||
, и вместе с запросом, на каждый удалённый сервер будет отправлена временная таблица `_data1` (имя временной таблицы - implementation defined).
|
||
|
||
Это гораздо более оптимально, чем при использовании обычного IN. Но при этом, следует помнить о нескольких вещах:
|
||
|
||
1. При создании временной таблицы данные не уникализируются. Чтобы уменьшить объём передаваемых по сети данных, укажите в подзапросе DISTINCT (для обычного IN-а этого делать не нужно).
|
||
2. Временная таблица будет передана на все удалённые серверы. Передача не учитывает топологию сети. Например, если 10 удалённых серверов расположены в удалённом относительно сервера-инициатора запроса датацентре, то по каналу в удалённый датацентр данные будет переданы 10 раз. Старайтесь не использовать большие множества при использовании GLOBAL IN.
|
||
3. При передаче данных на удалённые серверы не настраивается ограничение использования сетевой полосы. Вы можете перегрузить сеть.
|
||
4. Старайтесь распределять данные по серверам так, чтобы в GLOBAL IN-ах не было частой необходимости.
|
||
5. Если в GLOBAL IN есть частая необходимость, то спланируйте размещение кластера ClickHouse таким образом, чтобы в каждом датацентре была хотя бы одна реплика каждого шарда, и среди них была быстрая сеть - чтобы запрос целиком можно было бы выполнить, передавая данные в пределах одного датацентра.
|
||
|
||
В секции `GLOBAL IN` также имеет смысл указывать локальную таблицу - в случае, если эта локальная таблица есть только на сервере-инициаторе запроса, и вы хотите воспользоваться данными из неё на удалённых серверах.
|
||
|
||
### Экстремальные значения
|
||
|
||
Вы можете получить в дополнение к результату также минимальные и максимальные значения по столбцам результата. Для этого выставите настройку **extremes** в 1. Минимумы и максимумы считаются для числовых типов, дат, дат-с-временем. Для остальных столбцов будут выведены значения по умолчанию.
|
||
|
||
Вычисляются дополнительные две строчки - минимумы и максимумы, соответственно. Эти две дополнительные строки выводятся в [форматах](../interfaces/formats.md) `JSON*`, `TabSeparated*`, и `Pretty*` отдельно от остальных строчек. В остальных форматах они не выводится.
|
||
|
||
Во форматах `JSON*`, экстремальные значения выводятся отдельным полем 'extremes'. В форматах `TabSeparated*`, строка выводится после основного результата и после 'totals' если есть. Перед ней (после остальных данных) вставляется пустая строка. В форматах `Pretty*`, строка выводится отдельной таблицей после основного результата и после `totals` если есть.
|
||
|
||
Экстремальные значения вычисляются для строк перед `LIMIT`, но после `LIMIT BY`. Однако при использовании `LIMIT offset, size`, строки перед `offset` включаются в `extremes`. В потоковых запросах, в результате может учитываться также небольшое количество строчек, прошедших `LIMIT`.
|
||
|
||
### Замечания
|
||
|
||
В секциях `GROUP BY`, `ORDER BY`, в отличие от диалекта MySQL, и в соответствии со стандартным SQL, не поддерживаются позиционные аргументы.
|
||
Например, если вы напишите `GROUP BY 1, 2` - то это будет воспринято, как группировка по константам (то есть, агрегация всех строк в одну).
|
||
|
||
Вы можете использовать синонимы (алиасы `AS`) в любом месте запроса.
|
||
|
||
В любом месте запроса, вместо выражения, может стоять звёздочка. При анализе запроса звёздочка раскрывается в список всех столбцов таблицы (за исключением `MATERIALIZED` и `ALIAS` столбцов). Есть лишь немного случаев, когда оправдано использовать звёздочку:
|
||
|
||
- при создании дампа таблицы;
|
||
- для таблиц, содержащих всего несколько столбцов - например, системных таблиц;
|
||
- для получения информации о том, какие столбцы есть в таблице; в этом случае, укажите `LIMIT 1`. Но лучше используйте запрос `DESC TABLE`;
|
||
- при наличии сильной фильтрации по небольшому количеству столбцов с помощью `PREWHERE`;
|
||
- в подзапросах (так как из подзапросов выкидываются столбцы, не нужные для внешнего запроса).
|
||
|
||
В других случаях использование звёздочки является издевательством над системой, так как вместо преимуществ столбцовой СУБД вы получаете недостатки. То есть использовать звёздочку не рекомендуется.
|
||
|
||
[Оригинальная статья](https://clickhouse.yandex/docs/ru/query_language/select/) <!--hide-->
|