mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-11-14 19:45:11 +00:00
1488 lines
128 KiB
ReStructuredText
1488 lines
128 KiB
ReStructuredText
Запросы
|
||
-------
|
||
|
||
CREATE DATABASE
|
||
~~~~~~~~~~~~~~~
|
||
Создание базы данных db_name
|
||
|
||
.. code-block:: sql
|
||
|
||
CREATE DATABASE [IF NOT EXISTS] db_name
|
||
|
||
``База данных`` - это просто директория для таблиц.
|
||
Если написано ``IF NOT EXISTS``, то запрос не будет возвращать ошибку, если база данных уже существует.
|
||
|
||
CREATE TABLE
|
||
~~~~~~~~~~~~
|
||
Запрос ``CREATE TABLE`` может иметь несколько форм.
|
||
|
||
.. code-block:: sql
|
||
|
||
CREATE [TEMPORARY] TABLE [IF NOT EXISTS] [db.]name
|
||
(
|
||
name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
|
||
name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
|
||
...
|
||
) ENGINE = engine
|
||
|
||
Создаёт таблицу с именем name в БД db или текущей БД, если db не указана, со структурой, указанной в скобках, и движком engine.
|
||
Структура таблицы представляет список описаний столбцов. Индексы, если поддерживаются движком, указываются в качестве параметров для движка таблицы.
|
||
|
||
Описание столбца, это ``name type``, в простейшем случае. Пример: ``RegionID UInt32``.
|
||
Также могут быть указаны выражения для значений по умолчанию - смотрите ниже.
|
||
|
||
.. code-block:: sql
|
||
|
||
CREATE [TEMPORARY] TABLE [IF NOT EXISTS] [db.]name AS [db2.]name2 [ENGINE = engine]
|
||
|
||
Создаёт таблицу с такой же структурой, как другая таблица. Можно указать другой движок для таблицы. Если движок не указан, то будет выбран такой же движок, как у таблицы ``db2.name2``.
|
||
|
||
.. code-block:: sql
|
||
|
||
CREATE [TEMPORARY] TABLE [IF NOT EXISTS] [db.]name ENGINE = engine AS SELECT ...
|
||
|
||
Создаёт таблицу со структурой, как результат запроса ``SELECT``, с движком engine, и заполняет её данными из SELECT-а.
|
||
|
||
Во всех случаях, если указано ``IF NOT EXISTS``, то запрос не будет возвращать ошибку, если таблица уже существует. В этом случае, запрос будет ничего не делать.
|
||
|
||
Значения по умолчанию
|
||
"""""""""""""""""""""
|
||
В описании столбца, может быть указано выражение для значения по умолчанию, одного из следующих видов:
|
||
``DEFAULT expr``, ``MATERIALIZED expr``, ``ALIAS expr``.
|
||
Пример: ``URLDomain String DEFAULT domain(URL)``.
|
||
|
||
Если выражение для значения по умолчанию не указано, то в качестве значений по умолчанию будут использоваться нули для чисел, пустые строки для строк, пустые массивы для массивов, а также ``0000-00-00`` для дат и ``0000-00-00 00:00:00`` для дат с временем. NULL-ы не поддерживаются.
|
||
|
||
В случае, если указано выражение по умолчанию, то указание типа столбца не обязательно. При отсутствии явно указанного типа, будет использован тип выражения по умолчанию. Пример: ``EventDate DEFAULT toDate(EventTime)`` - для столбца EventDate будет использован тип Date.
|
||
|
||
При наличии явно указанного типа данных и выражения по умолчанию, это выражение будет приводиться к указанному типу с использованием функций приведения типа. Пример: ``Hits UInt32 DEFAULT 0`` - имеет такой же смысл, как ``Hits UInt32 DEFAULT toUInt32(0)``.
|
||
|
||
В качестве выражения для умолчания, может быть указано произвольное выражение от констант и столбцов таблицы. При создании и изменении структуры таблицы, проверяется, что выражения не содержат циклов. При INSERT-е проверяется разрешимость выражений - что все столбцы, из которых их можно вычислить, переданы.
|
||
|
||
``DEFAULT expr``
|
||
|
||
Обычное значение по умолчанию. Если в запросе INSERT не указан соответствующий столбец, то он будет заполнен путём вычисления соответствующего выражения.
|
||
|
||
``MATERIALIZED expr``
|
||
|
||
Материализованное выражение. Такой столбец не может быть указан при INSERT, то есть, он всегда вычисляется.
|
||
При INSERT без указания списка столбцов, такие столбцы не рассматриваются.
|
||
Также этот столбец не подставляется при использовании звёздочки в запросе SELECT - чтобы сохранить инвариант, что дамп, полученный путём ``SELECT *``, можно вставить обратно в таблицу INSERT-ом без указания списка столбцов.
|
||
|
||
``ALIAS expr``
|
||
|
||
Синоним. Такой столбец вообще не хранится в таблице.
|
||
Его значения не могут быть вставлены в таблицу, он не подставляется при использовании звёздочки в запросе SELECT.
|
||
Он может быть использован в SELECT-ах - в таком случае, во время разбора запроса, алиас раскрывается.
|
||
|
||
При добавлении новых столбцов с помощью запроса ALTER, старые данные для этих столбцов не записываются. Вместо этого, при чтении старых данных, для которых отсутствуют значения новых столбцов, выполняется вычисление выражений по умолчанию налету. При этом, если выполнение выражения требует использования других столбцов, не указанных в запросе, то эти столбцы будут дополнительно прочитаны, но только для тех блоков данных, для которых это необходимо.
|
||
|
||
Если добавить в таблицу новый столбец, а через некоторое время изменить его выражение по умолчанию, то используемые значения для старых данных (для данных, где значения не хранились на диске) поменяются. Также заметим, что при выполнении фоновых слияний, данные для столбцов, отсутствующих в одном из сливаемых кусков, записываются в объединённый кусок.
|
||
|
||
Отсутствует возможность задать значения по умолчанию для элементов вложенных структур данных.
|
||
|
||
Временные таблицы
|
||
"""""""""""""""""
|
||
Во всех случаях, если указано ``TEMPORARY``, то будет создана временная таблица. Временные таблицы обладают следующими особенностями:
|
||
- временные таблицы исчезают после завершения сессии; в том числе, при обрыве соединения;
|
||
- временная таблица создаётся с движком Memory; все остальные движки таблиц не поддерживаются;
|
||
- для временной таблицы нет возможности указать БД: она создаётся вне баз данных;
|
||
- если временная таблица имеет то же имя, что и некоторая другая, то, при упоминании в запросе без указания БД, будет использована временная таблица;
|
||
- при распределённой обработке запроса, используемые в запросе временные таблицы, передаются на удалённые серверы.
|
||
|
||
В большинстве случаев, временные таблицы создаются не вручную, а при использовании внешних данных для запроса, или при распределённом ``(GLOBAL) IN``. Подробнее см. соответствующие разделы
|
||
|
||
CREATE VIEW
|
||
~~~~~~~~~~~~
|
||
|
||
.. code-block:: sql
|
||
|
||
CREATE [MATERIALIZED] VIEW [IF NOT EXISTS] [db.]name [ENGINE = engine] [POPULATE] AS SELECT ...
|
||
|
||
Создаёт представление. Представления бывают двух видов - обычные и материализованные (MATERIALIZED).
|
||
|
||
Обычные представления не хранят никаких данных, а всего лишь производят чтение из другой таблицы. То есть, обычное представление - не более чем сохранённый запрос. При чтении из представления, этот сохранённый запрос, используется в качестве подзапроса в секции FROM.
|
||
|
||
Для примера, пусть вы создали представление:
|
||
|
||
.. code-block:: sql
|
||
|
||
CREATE VIEW view AS SELECT ...
|
||
|
||
и написали запрос:
|
||
|
||
.. code-block:: sql
|
||
|
||
SELECT a, b, c FROM view
|
||
|
||
Этот запрос полностью эквивалентен использованию подзапроса:
|
||
|
||
.. code-block:: sql
|
||
|
||
SELECT a, b, c FROM (SELECT ...)
|
||
|
||
Материализованные (MATERIALIZED) представления хранят данные, преобразованные соответствующим запросом SELECT.
|
||
|
||
При создании материализованного представления, можно указать ENGINE - движок таблицы для хранения данных. По умолчанию, будет использован тот же движок, что и у таблицы, из которой делается запрос SELECT.
|
||
|
||
Материализованное представление устроено следующим образом: при вставке данных в таблицу, указанную в SELECT-е, кусок вставляемых данных преобразуется этим запросом SELECT, и полученный результат вставляется в представление.
|
||
|
||
Если указано POPULATE, то при создании представления, в него будут вставлены имеющиеся данные таблицы, как если бы был сделан запрос ``CREATE TABLE ... AS SELECT ...`` . Иначе, представление будет содержать только данные, вставляемые в таблицу после создания представления. Не рекомендуется использовать POPULATE, так как вставляемые в таблицу данные во время создания представления, не попадут в него.
|
||
|
||
Запрос ``SELECT`` может содержать ``DISTINCT``, ``GROUP BY``, ``ORDER BY``, ``LIMIT``... Следует иметь ввиду, что соответствующие преобразования будут выполняться независимо, на каждый блок вставляемых данных. Например, при наличии ``GROUP BY``, данные будут агрегироваться при вставке, но только в рамках одной пачки вставляемых данных. Далее, данные не будут доагрегированы. Исключение - использование ENGINE, производящего агрегацию данных самостоятельно, например, ``SummingMergeTree``.
|
||
|
||
Недоработано выполнение запросов ``ALTER`` над материализованными представлениями, поэтому они могут быть неудобными для использования.
|
||
|
||
Представления выглядят так же, как обычные таблицы. Например, они перечисляются в результате запроса ``SHOW TABLES``.
|
||
|
||
Отсутствует отдельный запрос для удаления представлений. Чтобы удалить представление, следует использовать ``DROP TABLE``.
|
||
|
||
ATTACH
|
||
~~~~~~
|
||
Запрос полностью аналогичен запросу ``CREATE``, но
|
||
- вместо слова ``CREATE`` используется слово ``ATTACH``;
|
||
- запрос не создаёт данные на диске, а предполагает, что данные уже лежат в соответствующих местах, и всего лишь добавляет информацию о таблице в сервер.
|
||
После выполнения запроса ATTACH, сервер будет знать о существовании таблицы.
|
||
|
||
Этот запрос используется при старте сервера. Сервер хранит метаданные таблиц в виде файлов с запросами ``ATTACH``, которые он просто исполняет при запуске (за исключением системных таблиц, создание которых явно вписано в сервер).
|
||
|
||
DROP
|
||
~~~~
|
||
Запрос имеет два вида: ``DROP DATABASE`` и ``DROP TABLE``.
|
||
|
||
.. code-block:: sql
|
||
|
||
DROP DATABASE [IF EXISTS] db
|
||
|
||
Удаляет все таблицы внутри базы данных db, а затем саму базу данных db.
|
||
Если указано ``IF EXISTS`` - не выдавать ошибку, если база данных не существует.
|
||
|
||
.. code-block:: sql
|
||
|
||
DROP TABLE [IF EXISTS] [db.]name
|
||
|
||
Удаляет таблицу.
|
||
Если указано ``IF EXISTS`` - не выдавать ошибку, если таблица не существует или база данных не существует.
|
||
|
||
DETACH
|
||
~~~~~~
|
||
Удаляет из сервера информацию о таблице `name`. Сервер перестаёт знать о существовании таблицы.
|
||
|
||
.. code-block:: sql
|
||
|
||
DETACH TABLE [IF EXISTS] [db.]name
|
||
|
||
Но ни данные, ни метаданные таблицы не удаляются. При следующем запуске сервера, сервер прочитает метаданные и снова узнает о таблице.
|
||
Также, "отцепленную" таблицу можно прицепить заново запросом ``ATTACH`` (за исключением системных таблиц, для которых метаданные не хранятся).
|
||
|
||
Запроса ``DETACH DATABASE`` нет.
|
||
|
||
RENAME
|
||
~~~~~~
|
||
Переименовывает одну или несколько таблиц.
|
||
|
||
.. code-block:: sql
|
||
|
||
RENAME TABLE [db11.]name11 TO [db12.]name12, [db21.]name21 TO [db22.]name22, ...
|
||
|
||
Все таблицы переименовываются под глобальной блокировкой. Переименовывание таблицы является лёгкой операцией. Если вы указали после TO другую базу данных, то таблица будет перенесена в эту базу данных. При этом, директории с базами данных должны быть расположены в одной файловой системе (иначе возвращается ошибка).
|
||
|
||
ALTER
|
||
~~~~~
|
||
Запрос ``ALTER`` поддерживается только для таблиц типа ``*MergeTree``, а также ``Merge`` и ``Distributed``. Запрос имеет несколько вариантов.
|
||
|
||
Манипуляции со столбцами
|
||
""""""""""""""""""""""""
|
||
Изменение структуры таблицы.
|
||
|
||
.. code-block:: sql
|
||
|
||
ALTER TABLE [db].name ADD|DROP|MODIFY COLUMN ...
|
||
|
||
В запросе указывается список из одного или более действий через запятую.
|
||
Каждое действие - операция над столбцом.
|
||
|
||
Существуют следующие действия:
|
||
|
||
.. code-block:: sql
|
||
|
||
ADD COLUMN name [type] [default_expr] [AFTER name_after]
|
||
|
||
Добавляет в таблицу новый столбец с именем name, типом type и выражением для умолчания ``default_expr`` (смотрите раздел "Значения по умолчанию"). Если указано ``AFTER name_after`` (имя другого столбца), то столбец добавляется (в список столбцов таблицы) после указанного. Иначе, столбец добавляется в конец таблицы. Внимательный читатель может заметить, что отсутствует возможность добавить столбец в начало таблицы. Для цепочки действий, name_after может быть именем столбца, который добавляется в одном из предыдущих действий.
|
||
|
||
Добавление столбца всего лишь меняет структуру таблицы, и не производит никаких действий с данными - соответствующие данные не появляются на диске после ALTER-а. При чтении из таблицы, если для какого-либо столбца отсутствуют данные, то он заполняется значениями по умолчанию (выполняя выражение по умолчанию, если такое есть, или нулями, пустыми строками). Также, столбец появляется на диске при слиянии кусков данных (см. MergeTree).
|
||
|
||
Такая схема позволяет добиться мгновенной работы запроса ALTER и отсутствия необходимости увеличивать объём старых данных.
|
||
|
||
.. code-block:: sql
|
||
|
||
DROP COLUMN name
|
||
|
||
Удаляет столбец с именем name.
|
||
Удаляет данные из файловой системы. Так как это представляет собой удаление целых файлов, запрос выполняется почти мгновенно.
|
||
|
||
.. code-block:: sql
|
||
|
||
MODIFY COLUMN name [type] [default_expr]
|
||
|
||
Изменяет тип столбца name на type и/или выражение для умолчания на default_expr. При изменении типа, значения преобразуются так, как если бы к ним была применена функция toType.
|
||
|
||
Если изменяется только выражение для умолчания, то запрос не делает никакой сложной работы и выполняется мгновенно.
|
||
|
||
Изменение типа столбца - это единственное действие, которое выполняет сложную работу - меняет содержимое файлов с данными. Для больших таблиц, выполнение может занять длительное время.
|
||
|
||
Выполнение производится в несколько стадий:
|
||
- подготовка временных (новых) файлов с изменёнными данными;
|
||
- переименование старых файлов;
|
||
- переименование временных (новых) файлов в старые;
|
||
- удаление старых файлов.
|
||
|
||
Из них, длительной является только первая стадия. Если на этой стадии возникнет сбой, то данные не поменяются.
|
||
Если на одной из следующих стадий возникнет сбой, то данные будет можно восстановить вручную. За исключением случаев, когда старые файлы удалены из файловой системы, а данные для новых файлов не доехали на диск и потеряны.
|
||
|
||
Не поддерживается изменение типа столбца у массивов и вложенных структур данных.
|
||
|
||
Запрос ``ALTER`` позволяет создавать и удалять отдельные элементы (столбцы) вложенных структур данных, но не вложенные структуры данных целиком. Для добавления вложенной структуры данных, вы можете добавить столбцы с именем вида ``name.nested_name`` и типом ``Array(T)`` - вложенная структура данных полностью эквивалентна нескольким столбцам-массивам с именем, имеющим одинаковый префикс до точки.
|
||
|
||
Отсутствует возможность удалять столбцы, входящие в первичный ключ или ключ для сэмплирования (в общем, входящие в выражение ``ENGINE``). Изменение типа у столбцов, входящих в первичный ключ возможно только в том случае, если это изменение не приводит к изменению данных (например, разрешено добавление значения в Enum или изменение типа с ``DateTime`` на ``UInt32``).
|
||
|
||
Если возможностей запроса ``ALTER`` не хватает для нужного изменения таблицы, вы можете создать новую таблицу, скопировать туда данные с помощью запроса ``INSERT SELECT``, затем поменять таблицы местами с помощью запроса ``RENAME``, и удалить старую таблицу.
|
||
|
||
Запрос ``ALTER`` блокирует все чтения и записи для таблицы. То есть, если на момент запроса ``ALTER``, выполнялся долгий ``SELECT``, то запрос ``ALTER`` сначала дождётся его выполнения. И в это время, все новые запросы к той же таблице, будут ждать, пока завершится этот ``ALTER``.
|
||
|
||
Для таблиц, которые не хранят данные самостоятельно (типа ``Merge`` и ``Distributed``), ``ALTER`` всего лишь меняет структуру таблицы, но не меняет структуру подчинённых таблиц. Для примера, при ALTER-е таблицы типа ``Distributed``, вам также потребуется выполнить запрос ``ALTER`` для таблиц на всех удалённых серверах.
|
||
|
||
Запрос ``ALTER`` на изменение столбцов реплицируется. Соответствующие инструкции сохраняются в ZooKeeper, и затем каждая реплика их применяет. Все запросы ``ALTER`` выполняются в одном и том же порядке. Запрос ждёт выполнения соответствующих действий на всех репликах. Но при этом, запрос на изменение столбцов в реплицируемой таблице можно прервать, и все действия будут осуществлены асинхронно.
|
||
|
||
Манипуляции с партициями и кусками
|
||
""""""""""""""""""""""""""""""""""
|
||
Работает только для таблиц семейства ``MergeTree``. Существуют следующие виды операций:
|
||
|
||
* ``DETACH PARTITION`` - перенести партицию в директорию detached и забыть про неё.
|
||
* ``DROP PARTITION`` - удалить партицию.
|
||
* ``ATTACH PART|PARTITION`` - добавить в таблицу новый кусок или партицию из директории ``detached``.
|
||
* ``FREEZE PARTITION`` - создать бэкап партиции.
|
||
* ``FETCH PARTITION`` - скачать партицию с другого сервера.
|
||
|
||
Ниже будет рассмотрен каждый вид запроса по-отдельности.
|
||
|
||
Партицией (partition) в таблице называются данные за один календарный месяц. Это определяется значениями ключа-даты, указанной в параметрах движка таблицы. Данные за каждый месяц хранятся отдельно, чтобы упростить всевозможные манипуляции с этими данными.
|
||
|
||
Куском (part) в таблице называется часть данных одной партиции, отсортированная по первичному ключу.
|
||
|
||
Чтобы посмотреть набор кусков и партиций таблицы, можно воспользоваться системной таблицей ``system.parts``:
|
||
|
||
.. code-block:: sql
|
||
|
||
SELECT * FROM system.parts WHERE active
|
||
|
||
``active`` - учитывать только активные куски. Неактивными являются, например, исходные куски оставшиеся после слияния в более крупный кусок - такие куски удаляются приблизительно через 10 минут после слияния.
|
||
|
||
Другой способ посмотреть набор кусков и партиций - зайти в директорию с данными таблицы.
|
||
Директория с данными - ``/var/lib/clickhouse/data/database/table/``,
|
||
где ``/var/lib/clickhouse/`` - путь к данным ClickHouse, database - имя базы данных, table - имя таблицы. Пример:
|
||
|
||
.. code-block:: bash
|
||
|
||
$ ls -l /var/lib/clickhouse/data/test/visits/
|
||
total 48
|
||
drwxrwxrwx 2 clickhouse clickhouse 20480 мая 13 02:58 20140317_20140323_2_2_0
|
||
drwxrwxrwx 2 clickhouse clickhouse 20480 мая 13 02:58 20140317_20140323_4_4_0
|
||
drwxrwxrwx 2 clickhouse clickhouse 4096 мая 13 02:55 detached
|
||
-rw-rw-rw- 1 clickhouse clickhouse 2 мая 13 02:58 increment.txt
|
||
|
||
Здесь ``20140317_20140323_2_2_0``, ``20140317_20140323_4_4_0`` - директории кусков.
|
||
|
||
Рассмотрим по порядку имя первого куска: ``20140317_20140323_2_2_0``.
|
||
* ``20140317`` - минимальная дата данных куска
|
||
* ``20140323`` - максимальная дата данных куска
|
||
* ``2`` - минимальный номер блока данных
|
||
* ``2`` - максимальный номер блока данных
|
||
* ``0`` - уровень куска - глубина дерева слияний, которыми он образован
|
||
|
||
Каждый кусок относится к одной партиции и содержит данные только за один месяц.
|
||
``201403`` - имя партиции. Партиция представляет собой набор кусков за один месяц.
|
||
|
||
При работающем сервере, нельзя вручную изменять набор кусков или их данные на файловой системе, так как сервер не будет об этом знать.
|
||
Для нереплицируемых таблиц, вы можете это делать при остановленном сервере, хотя это не рекомендуется.
|
||
Для реплицируемых таблиц, набор кусков нельзя менять в любом случае.
|
||
|
||
Директория ``detached`` содержит куски, не используемые сервером - отцепленные от таблицы с помощью запроса ``ALTER ... DETACH``. Также в эту директорию переносятся куски, признанные повреждёнными, вместо их удаления. Вы можете в любое время добавлять, удалять, модифицировать данные в директории detached - сервер не будет об этом знать, пока вы не сделаете запрос ``ALTER TABLE ... ATTACH``.
|
||
|
||
.. code-block:: sql
|
||
|
||
ALTER TABLE [db.]table DETACH PARTITION 'name'
|
||
|
||
Перенести все данные для партиции с именем name в директорию detached и забыть про них.
|
||
Имя партиции указывается в формате YYYYMM. Оно может быть указано в одинарных кавычках или без них.
|
||
|
||
После того, как запрос будет выполнен, вы можете самостоятельно сделать что угодно с данными в директории detached, например, удалить их из файловой системы, или ничего не делать.
|
||
|
||
Запрос реплицируется - данные будут перенесены в директорию detached и забыты на всех репликах. Запрос может быть отправлен только на реплику-лидер. Вы можете узнать, является ли реплика лидером, сделав SELECT в системную таблицу system.replicas. Или, проще, вы можете выполнить запрос на всех репликах, и на всех кроме одной, он кинет исключение.
|
||
|
||
.. code-block:: sql
|
||
|
||
ALTER TABLE [db.]table DROP PARTITION 'name'
|
||
|
||
Аналогично операции ``DETACH``. Удалить данные из таблицы. Куски с данными будут помечены как неактивные и будут полностью удалены примерно через 10 минут. Запрос реплицируется - данные будут удалены на всех репликах.
|
||
|
||
.. code-block:: sql
|
||
|
||
ALTER TABLE [db.]table ATTACH PARTITION|PART 'name'
|
||
|
||
Добавить данные в таблицу из директории detached.
|
||
|
||
Существует возможность добавить данные для целой партиции (PARTITION) или отдельный кусок (PART). В случае PART, укажите полное имя куска в одинарных кавычках.
|
||
|
||
Запрос реплицируется. Каждая реплика проверяет, если ли данные в директории detached. Если данные есть - проверяет их целостность, проверяет их соответствие данным на сервере-инициаторе запроса, и если всё хорошо, то добавляет их. Если нет, то скачивает данные с реплики-инициатора запроса, или с другой реплики, на которой уже добавлены эти данные.
|
||
|
||
То есть, вы можете разместить данные в директории detached на одной реплике и, с помощью запроса ALTER ... ATTACH добавить их в таблицу на всех репликах.
|
||
|
||
.. code-block:: sql
|
||
|
||
ALTER TABLE [db.]table FREEZE PARTITION 'name'
|
||
|
||
Создаёт локальный бэкап одной или нескольких партиций. В качестве имени может быть указано полное имя партиции (например, 201403) или его префикс (например, 2014) - тогда бэкап будет создан для всех соответствующих партиций.
|
||
|
||
Запрос делает следующее: для снэпшота данных на момент его выполнения, создаёт hardlink-и на данные таблиц в директории ``/var/lib/clickhouse/shadow/N/...``
|
||
|
||
``/var/lib/clickhouse/`` - рабочая директория ClickHouse из конфига.
|
||
``N`` - инкрементальный номер бэкапа.
|
||
|
||
Структура директорий внутри бэкапа создаётся такой же, как внутри ``/var/lib/clickhouse/``.
|
||
Также делает chmod всех файлов, запрещая запись в них.
|
||
|
||
Создание бэкапа происходит почти мгновенно (но сначала дожидается окончания выполняющихся в данный момент запросов к соответствующей таблице). Бэкап изначально не занимает места на диске. При дальнейшей работе системы, бэкап может отнимать место на диске, по мере модификации данных. Если бэкап делается для достаточно старых данных, то он не будет отнимать место на диске.
|
||
|
||
После создания бэкапа, данные из ``/var/lib/clickhouse/shadow/`` можно скопировать на удалённый сервер и затем удалить на локальном сервере.
|
||
Весь процесс бэкапа не требует остановки сервера.
|
||
|
||
Запрос ``ALTER ... FREEZE PARTITION`` не реплицируется. То есть, локальный бэкап создаётся только на локальном сервере.
|
||
|
||
В качестве альтернативного варианта, вы можете скопировать данные из директории ``/var/lib/clickhouse/data/database/table`` вручную.
|
||
Но если это делать при запущенном сервере, то возможны race conditions при копировании директории с добавляющимися/изменяющимися файлами, и бэкап может быть неконсистентным. Этот вариант может использоваться, если сервер не запущен - тогда полученные данные будут такими же, как после запроса ``ALTER TABLE t FREEZE PARTITION``.
|
||
|
||
``ALTER TABLE ... FREEZE PARTITION`` копирует только данные, но не метаданные таблицы. Чтобы сделать бэкап метаданных таблицы, скопируйте файл ``/var/lib/clickhouse/metadata/database/table.sql``
|
||
|
||
Для восстановления из бэкапа:
|
||
* создайте таблицу, если её нет, с помощью запроса CREATE. Запрос можно взять из .sql файла (замените в нём ``ATTACH`` на ``CREATE``);
|
||
* скопируйте данные из директории data/database/table/ внутри бэкапа в директорию ``/var/lib/clickhouse/data/database/table/detached/``
|
||
* выполните запросы ``ALTER TABLE ... ATTACH PARTITION YYYYMM``, где ``YYYYMM`` - месяц, для каждого месяца.
|
||
|
||
Таким образом, данные из бэкапа будут добавлены в таблицу.
|
||
Восстановление из бэкапа, так же, не требует остановки сервера.
|
||
|
||
Бэкапы и репликация
|
||
"""""""""""""""""""
|
||
Репликация защищает от аппаратных сбоев. В случае, если на одной из реплик у вас исчезли все данные, то восстановление делается по инструкции в разделе "Восстановление после сбоя".
|
||
|
||
Для защиты от аппаратных сбоев, обязательно используйте репликацию. Подробнее про репликацию написано в разделе "Репликация данных".
|
||
|
||
Бэкапы защищают от человеческих ошибок (случайно удалили данные, удалили не те данные или не на том кластере, испортили данные).
|
||
Для баз данных большого объёма, бывает затруднительно копировать бэкапы на удалённые серверы. В этих случаях, для защиты от человеческой ошибки, можно держать бэкап на том же сервере (он будет лежать в ``/var/lib/clickhouse/shadow/``).
|
||
|
||
.. code-block:: sql
|
||
|
||
ALTER TABLE [db.]table FETCH PARTITION 'name' FROM 'path-in-zookeeper'
|
||
|
||
Запрос работает только для реплицируемых таблиц.
|
||
|
||
Скачивает указанную партицию с шарда, путь в ``ZooKeeper`` к которому указан в секции ``FROM`` и помещает в директорию ``detached`` указанной таблицы.
|
||
|
||
Не смотря на то, что запрос называется ``ALTER TABLE``, он не изменяет структуру таблицы, и не изменяет сразу доступные данные в таблице.
|
||
|
||
Данные помещаются в директорию ``detached``, и их можно прикрепить с помощью запроса ``ALTER TABLE ... ATTACH``.
|
||
|
||
В секции ``FROM`` указывается путь в ``ZooKeeper``. Например, ``/clickhouse/tables/01-01/visits``.
|
||
Перед скачиванием проверяется существование партиции и совпадение структуры таблицы. Автоматически выбирается наиболее актуальная реплика среди живых реплик.
|
||
|
||
Запрос ``ALTER ... FETCH PARTITION`` не реплицируется. То есть, партиция будет скачана в директорию detached только на локальном сервере. Заметим, что если вы после этого добавите данные в таблицу с помощью запроса ``ALTER TABLE ... ATTACH``, то данные будут добавлены на всех репликах (на одной из реплик будут добавлены из директории detached, а на других - загружены с соседних реплик).
|
||
|
||
Синхронность запросов ALTER
|
||
"""""""""""""""""""""""""""
|
||
|
||
Для нереплицируемых таблиц, все запросы ``ALTER`` выполняются синхронно. Для реплицируемых таблиц, запрос всего лишь добавляет инструкцию по соответствующим действиям в ``ZooKeeper``, а сами действия осуществляются при первой возможности. Но при этом, запрос может ждать завершения выполнения этих действий на всех репликах.
|
||
|
||
Для запросов ``ALTER ... ATTACH|DETACH|DROP`` можно настроить ожидание, с помощью настройки ``replication_alter_partitions_sync``.
|
||
Возможные значения: ``0`` - не ждать, ``1`` - ждать выполнения только у себя (по умолчанию), ``2`` - ждать всех.
|
||
|
||
SHOW DATABASES
|
||
~~~~~~~~~~~~~~
|
||
|
||
.. code-block:: sql
|
||
|
||
SHOW DATABASES [INTO OUTFILE filename] [FORMAT format]
|
||
|
||
Выводит список всех баз данных.
|
||
Запрос полностью аналогичен запросу ``SELECT name FROM system.databases [INTO OUTFILE filename] [FORMAT format]``.
|
||
|
||
Смотрите также раздел "Форматы".
|
||
|
||
SHOW TABLES
|
||
~~~~~~~~~~~
|
||
|
||
.. code-block:: sql
|
||
|
||
SHOW TABLES [FROM db] [LIKE 'pattern'] [INTO OUTFILE filename] [FORMAT format]
|
||
|
||
Выводит список таблиц
|
||
* из текущей БД или из БД db, если указано FROM db;
|
||
* всех, или имя которых соответствует шаблону pattern, если указано LIKE 'pattern';
|
||
|
||
Запрос полностью аналогичен запросу: ``SELECT name FROM system.tables WHERE database = 'db' [AND name LIKE 'pattern'] [INTO OUTFILE filename] [FORMAT format]``
|
||
Смотрите также раздел "Оператор LIKE".
|
||
|
||
SHOW PROCESSLIST
|
||
~~~~~~~~~~~~~~~~
|
||
|
||
.. code-block:: sql
|
||
|
||
SHOW PROCESSLIST [INTO OUTFILE filename] [FORMAT format]
|
||
|
||
Выводит список запросов, выполняющихся в данный момент времени, кроме запросов ``SHOW PROCESSLIST``.
|
||
|
||
Выдаёт таблицу, содержащую столбцы:
|
||
|
||
**user** - пользователь, под которым был задан запрос. Следует иметь ввиду, что при распределённой обработке запроса на удалённые серверы запросы отправляются под пользователем default. И SHOW PROCESSLIST показывает имя пользователя для конкретного запроса, а не для запроса, который данный запрос инициировал.
|
||
|
||
**address** - имя хоста, с которого был отправлен запрос. При распределённой обработке запроса на удалённых серверах — это имя хоста-инициатора запроса. Чтобы проследить, откуда был задан распределённый запрос изначально, следует смотреть SHOW PROCESSLIST на сервере-инициаторе запроса.
|
||
|
||
**elapsed** - время выполнения запроса, в секундах. Запросы выводятся упорядоченными по убыванию времени выполнения.
|
||
|
||
**rows_read**, **bytes_read** - сколько было прочитано строк, байт несжатых данных при обработке запроса. При распределённой обработке запроса суммируются данные со всех удалённых серверов. Именно эти данные используются для ограничений и квот.
|
||
|
||
**memory_usage** - текущее потребление оперативки в байтах. Смотрите настройку max_memory_usage.
|
||
|
||
**query** - сам запрос. В запросах INSERT данные для вставки не выводятся.
|
||
|
||
**query_id** - идентификатор запроса. Непустой, только если был явно задан пользователем. При распределённой обработке запроса идентификатор запроса не передаётся на удалённые серверы.
|
||
|
||
Запрос полностью аналогичен запросу: ``SELECT * FROM system.processes [INTO OUTFILE filename] [FORMAT format]``.
|
||
|
||
Полезный совет (выполните в консоли):
|
||
|
||
.. code-block:: bash
|
||
|
||
watch -n1 "clickhouse-client --query='SHOW PROCESSLIST'"
|
||
|
||
SHOW CREATE TABLE
|
||
~~~~~~~~~~~~~~~~~
|
||
|
||
.. code-block:: sql
|
||
|
||
SHOW CREATE TABLE [db.]table [INTO OUTFILE filename] [FORMAT format]
|
||
|
||
Возвращает один столбец statement типа ``String``, содержащий одно значение - запрос ``CREATE``, с помощью которого создана указанная таблица.
|
||
|
||
DESCRIBE TABLE
|
||
~~~~~~~~~~~~~~
|
||
|
||
.. code-block:: sql
|
||
|
||
DESC|DESCRIBE TABLE [db.]table [INTO OUTFILE filename] [FORMAT format]
|
||
|
||
Возвращает два столбца: ``name``, ``type`` типа ``String``, в которых описаны имена и типы столбцов указанной таблицы.
|
||
|
||
Вложенные структуры данных выводятся в "развёрнутом" виде. То есть, каждый столбец - по отдельности, с именем через точку.
|
||
|
||
EXISTS
|
||
~~~~~~
|
||
|
||
.. code-block:: sql
|
||
|
||
EXISTS TABLE [db.]name [INTO OUTFILE filename] [FORMAT format]
|
||
|
||
Возвращает один столбец типа ``UInt8``, содержащий одно значение - ``0``, если таблицы или БД не существует и ``1``, если таблица в указанной БД существует.
|
||
|
||
USE
|
||
~~~
|
||
|
||
.. code-block:: sql
|
||
|
||
USE db
|
||
|
||
Позволяет установить текущую базу данных для сессии.
|
||
Текущая база данных используется для поиска таблиц, если база данных не указана в запросе явно через точку перед именем таблицы.
|
||
При использовании HTTP протокола, запрос не может быть выполнен, так как понятия сессии не существует.
|
||
|
||
SET
|
||
~~~
|
||
|
||
.. code-block:: sql
|
||
|
||
SET [GLOBAL] param = value
|
||
|
||
Позволяет установить настройку ``param`` в значение ``value``. Также можно одним запросом установить все настройки из заданного профиля настроек - для этого, укажите в качестве имени настройки profile. Подробнее смотри раздел "Настройки".
|
||
Настройка устанавливается на сессию, или на сервер (глобально), если указано ``GLOBAL``.
|
||
При установке глобальной настройки, настройка на все уже запущенные сессии, включая текущую сессию, не устанавливается, а будет использована только для новых сессий.
|
||
|
||
Настройки, заданные с помощью ``SET GLOBAL`` имеют меньший приоритет по сравнению с настройками, указанными в профиле пользователя, в конфигурационном файле. То есть, переопределить такие настройки с помощью ``SET GLOBAL`` невозможно.
|
||
|
||
При перезапуске сервера, теряются глобальные настройки, установленные с помощью ``SET GLOBAL``.
|
||
Установить настройки, которые переживут перезапуск сервера, можно только с помощью конфигурационного файла сервера. (Это не может быть сделано с помощью запроса ``SET``.)
|
||
|
||
OPTIMIZE
|
||
~~~~~~~~
|
||
|
||
.. code-block:: sql
|
||
|
||
OPTIMIZE TABLE [db.]name [PARTITION partition] [FINAL]
|
||
|
||
Просит движок таблицы сделать что-нибудь, что может привести к более оптимальной работе.
|
||
Поддерживается только движками ``*MergeTree``, в котором выполнение этого запроса инициирует внеочередное слияние кусков данных.
|
||
Если указан ``PARTITION``, то оптимизация будет производиться только для указаной партиции.
|
||
Если указан ``FINAL``, то оптимизация будет производиться даже когда все данные уже лежат в одном куске.
|
||
|
||
INSERT
|
||
~~~~~~
|
||
Запрос имеет несколько вариантов.
|
||
|
||
.. code-block:: sql
|
||
|
||
INSERT INTO [db.]table [(c1, c2, c3)] VALUES (v11, v12, v13), (v21, v22, v23), ...
|
||
|
||
Вставляет в таблицу table строчки с перечисленными значениями.
|
||
Запрос полностью аналогичен запросу вида:
|
||
|
||
.. code-block:: sql
|
||
|
||
INSERT INTO [db.]table [(c1, c2, c3)] FORMAT Values (v11, v12, v13), (v21, v22, v23), ...
|
||
|
||
.. code-block:: sql
|
||
|
||
INSERT INTO [db.]table [(c1, c2, c3)] FORMAT format ...
|
||
|
||
Вставка данных в произвольном указанном формате.
|
||
Сами данные идут после format, после всех пробельных символов до первого перевода строки, если он есть, включая его, или после всех пробельных символов, если переводов строки нет. Рекомендуется писать данные начиная со следующей строки (это важно, если данные начинаются с пробельных символов).
|
||
|
||
Пример:
|
||
|
||
.. code-block:: sql
|
||
|
||
INSERT INTO t FORMAT TabSeparated
|
||
11 Hello, world!
|
||
22 Qwerty
|
||
|
||
Подробнее про форматы данных смотрите в разделе "Форматы".
|
||
В разделе "Интерфейсы" описано, как можно вставлять данные отдельно от запроса, при использовании клиента командной строки или HTTP интерфейса.
|
||
|
||
В запросе может быть опционально указан список столбцов для вставки. В этом случае, в остальные столбцы записываются значения по умолчанию.
|
||
Значения по умолчанию вычисляются из DEFAULT выражений, указанных в определении таблицы, или, если ``DEFAULT`` не прописан явно - используются нули, пустые строки. Если настройка ``strict_insert_defaults`` выставлена в 1, то все столбцы, для которых нет явных DEFAULT-ов, должны быть указаны в запросе.
|
||
|
||
.. code-block:: sql
|
||
|
||
INSERT INTO [db.]table [(c1, c2, c3)] SELECT ...
|
||
|
||
Вставка в таблицу результата запроса ``SELECT``.
|
||
Имена и типы данных результата выполнения SELECT-а должны точно совпадать со структурой таблицы, в которую вставляются данные, или с указанным списком столбцов.
|
||
Для изменения имён столбцов следует использовать синонимы (AS) в запросе ``SELECT``.
|
||
Для изменения типов данных следует использовать функции преобразования типов (смотрите раздел "Функции").
|
||
|
||
Ни один из форматов данных не позволяет использовать в качестве значений выражения.
|
||
То есть, вы не можете написать ``INSERT INTO t VALUES (now(), 1 + 1, DEFAULT)``.
|
||
|
||
Не поддерживаются другие запросы на модификацию части данных: ``UPDATE``, ``DELETE``, ``REPLACE``, ``MERGE``, ``UPSERT``, ``INSERT UPDATE``.
|
||
Впрочем, вы можете удалять старые данные с помощью запроса ``ALTER TABLE ... DROP PARTITION``.
|
||
|
||
SELECT
|
||
~~~~~~
|
||
|
||
Его величество, запрос SELECT.
|
||
|
||
.. code-block:: sql
|
||
|
||
SELECT [DISTINCT] expr_list
|
||
[FROM [db.]table | (subquery) | table_function] [FINAL]
|
||
[SAMPLE sample_coeff]
|
||
[ARRAY JOIN ...]
|
||
[GLOBAL] ANY|ALL INNER|LEFT 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]
|
||
|
||
Все секции, кроме списка выражений сразу после 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`` в системе нет.
|
||
|
||
Секция 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
|
||
"""""""""""""
|
||
|
||
Секция SAMPLE позволяет выполнить запрос приближённо. Приближённое выполнение запроса поддерживается только таблицами типа MergeTree* и только если при создании таблицы было указано выражение, по которому производится выборка (смотрите раздел "Движок MergeTree").
|
||
|
||
``SAMPLE`` имеет вид ``SAMPLE k``, где ``k`` - дробное число в интервале от 0 до 1, или ``SAMPLE n``, где n - достаточно большое целое число.
|
||
|
||
В первом случае, запрос будет выполнен по k-доле данных. Например, если указано ``SAMPLE 0.1``, то запрос будет выполнен по 10% данных.
|
||
Во втором случае, запрос будет выполнен по выборке из не более n строк. Например, если указано ``SAMPLE 10000000``, то запрос будет выполнен по не более чем 10 000 000 строкам.
|
||
|
||
Пример:
|
||
|
||
.. code-block:: sql
|
||
|
||
SELECT
|
||
Title,
|
||
count() * 10 AS PageViews
|
||
FROM hits_distributed
|
||
SAMPLE 0.1
|
||
WHERE
|
||
CounterID = 34
|
||
AND toDate(EventDate) >= toDate('2013-01-29')
|
||
AND toDate(EventDate) <= toDate('2013-02-04')
|
||
AND NOT DontCountHits
|
||
AND NOT Refresh
|
||
AND Title != ''
|
||
GROUP BY Title
|
||
ORDER BY PageViews DESC LIMIT 1000
|
||
|
||
В этом примере, запрос выполняется по выборке из 0.1 (10%) данных. Значения агрегатных функций не корректируются автоматически, поэтому для получения приближённого результата, значение count() вручную домножается на 10.
|
||
|
||
При использовании варианта вида ``SAMPLE 10000000``, нет информации, какая относительная доля данных была обработана, и на что следует домножить агрегатные функции, поэтому такой способ записи подходит не для всех случаев.
|
||
|
||
Выборка с указанием относительного коэффициента является "согласованной": если рассмотреть все возможные данные, которые могли бы быть в таблице, то выборка (при использовании одного выражения сэмплирования, указанного при создании таблицы), с одинаковым коэффициентом, выбирает всегда одно и то же подмножество этих всевозможных данных. То есть, выборка из разных таблиц, на разных серверах, в разное время, делается одинаковым образом.
|
||
|
||
Например, выборка по идентификаторам посетителей, выберет из разных таблиц строки с одинаковым подмножеством всех возможных идентификаторов посетителей. Это позволяет использовать выборку в подзапросах в секции IN, а также при ручном сопоставлении результатов разных запросов с выборками.
|
||
|
||
Секция ARRAY JOIN
|
||
"""""""""""""""""
|
||
|
||
Позволяет выполнить JOIN с массивом или вложенной структурой данных. Смысл похож на функцию arrayJoin, но функциональность более широкая.
|
||
|
||
``ARRAY JOIN`` - это, по сути, ``INNER JOIN`` с массивом. Пример:
|
||
|
||
::
|
||
|
||
:) CREATE TABLE arrays_test (s String, arr Array(UInt8)) ENGINE = Memory
|
||
|
||
CREATE TABLE arrays_test
|
||
(
|
||
s String,
|
||
arr Array(UInt8)
|
||
) ENGINE = Memory
|
||
|
||
Ok.
|
||
|
||
0 rows in set. Elapsed: 0.001 sec.
|
||
|
||
:) INSERT INTO arrays_test VALUES ('Hello', [1,2]), ('World', [3,4,5]), ('Goodbye', [])
|
||
|
||
INSERT INTO arrays_test VALUES
|
||
|
||
Ok.
|
||
|
||
3 rows in set. Elapsed: 0.001 sec.
|
||
|
||
:) SELECT * FROM arrays_test
|
||
|
||
SELECT *
|
||
FROM arrays_test
|
||
|
||
┌─s───────┬─arr─────┐
|
||
│ Hello │ [1,2] │
|
||
│ World │ [3,4,5] │
|
||
│ Goodbye │ [] │
|
||
└─────────┴─────────┘
|
||
|
||
3 rows in set. Elapsed: 0.001 sec.
|
||
|
||
:) SELECT s, arr FROM arrays_test ARRAY JOIN arr
|
||
|
||
SELECT s, arr
|
||
FROM arrays_test
|
||
ARRAY JOIN arr
|
||
|
||
┌─s─────┬─arr─┐
|
||
│ Hello │ 1 │
|
||
│ Hello │ 2 │
|
||
│ World │ 3 │
|
||
│ World │ 4 │
|
||
│ World │ 5 │
|
||
└───────┴─────┘
|
||
|
||
5 rows in set. Elapsed: 0.001 sec.
|
||
|
||
Для массива в секции ARRAY JOIN может быть указан алиас. В этом случае, элемент массива будет доступен под этим алиасом, а сам массив - под исходным именем. Пример:
|
||
|
||
::
|
||
|
||
:) SELECT s, arr, a FROM arrays_test ARRAY JOIN arr AS a
|
||
|
||
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 │
|
||
└───────┴─────────┴───┘
|
||
|
||
5 rows in set. Elapsed: 0.001 sec.
|
||
|
||
В секции ARRAY JOIN может быть указано несколько массивов одинаковых размеров через запятую. В этом случае, JOIN делается с ними одновременно (прямая сумма, а не прямое произведение). Пример:
|
||
|
||
::
|
||
|
||
:) 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
|
||
|
||
SELECT s, arr, a, num, mapped
|
||
FROM arrays_test
|
||
ARRAY JOIN arr AS a, arrayEnumerate(arr) AS num, arrayMap(lambda(tuple(x), plus(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 │
|
||
└───────┴─────────┴───┴─────┴────────┘
|
||
|
||
5 rows in set. Elapsed: 0.002 sec.
|
||
|
||
:) SELECT s, arr, a, num, arrayEnumerate(arr) FROM arrays_test ARRAY JOIN arr AS a, arrayEnumerate(arr) AS num
|
||
|
||
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] │
|
||
└───────┴─────────┴───┴─────┴─────────────────────┘
|
||
|
||
5 rows in set. Elapsed: 0.002 sec.
|
||
|
||
ARRAY JOIN также работает с вложенными структурами данных. Пример:
|
||
|
||
::
|
||
|
||
:) CREATE TABLE nested_test (s String, nest Nested(x UInt8, y UInt32)) ENGINE = Memory
|
||
|
||
CREATE TABLE nested_test
|
||
(
|
||
s String,
|
||
nest Nested(
|
||
x UInt8,
|
||
y UInt32)
|
||
) ENGINE = Memory
|
||
|
||
Ok.
|
||
|
||
0 rows in set. Elapsed: 0.006 sec.
|
||
|
||
:) INSERT INTO nested_test VALUES ('Hello', [1,2], [10,20]), ('World', [3,4,5], [30,40,50]), ('Goodbye', [], [])
|
||
|
||
INSERT INTO nested_test VALUES
|
||
|
||
Ok.
|
||
|
||
3 rows in set. Elapsed: 0.001 sec.
|
||
|
||
:) SELECT * FROM nested_test
|
||
|
||
SELECT *
|
||
FROM nested_test
|
||
|
||
┌─s───────┬─nest.x──┬─nest.y─────┐
|
||
│ Hello │ [1,2] │ [10,20] │
|
||
│ World │ [3,4,5] │ [30,40,50] │
|
||
│ Goodbye │ [] │ [] │
|
||
└─────────┴─────────┴────────────┘
|
||
|
||
3 rows in set. Elapsed: 0.001 sec.
|
||
|
||
:) SELECT s, nest.x, nest.y FROM nested_test ARRAY JOIN nest
|
||
|
||
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 │
|
||
└───────┴────────┴────────┘
|
||
|
||
5 rows in set. Elapsed: 0.001 sec.
|
||
|
||
При указании имени вложенной структуры данных в ARRAY JOIN, смысл такой же, как ARRAY JOIN со всеми элементами-массивами, из которых она состоит. Пример:
|
||
|
||
::
|
||
|
||
:) SELECT s, nest.x, nest.y FROM nested_test ARRAY JOIN nest.x, nest.y
|
||
|
||
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 │
|
||
└───────┴────────┴────────┘
|
||
|
||
5 rows in set. Elapsed: 0.001 sec.
|
||
|
||
Такой вариант тоже имеет смысл:
|
||
|
||
::
|
||
|
||
:) SELECT s, nest.x, nest.y FROM nested_test ARRAY JOIN nest.x
|
||
|
||
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] │
|
||
└───────┴────────┴────────────┘
|
||
|
||
5 rows in set. Elapsed: 0.001 sec.
|
||
|
||
Алиас для вложенной структуры данных можно использовать, чтобы выбрать как результат JOIN-а, так и исходный массив. Пример:
|
||
|
||
::
|
||
|
||
:) SELECT s, n.x, n.y, nest.x, nest.y FROM nested_test ARRAY JOIN nest AS n
|
||
|
||
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] │
|
||
└───────┴─────┴─────┴─────────┴────────────┘
|
||
|
||
5 rows in set. Elapsed: 0.001 sec.
|
||
|
||
Пример использования функции arrayEnumerate:
|
||
|
||
::
|
||
|
||
:) SELECT s, n.x, n.y, nest.x, nest.y, num FROM nested_test ARRAY JOIN nest AS n, arrayEnumerate(nest.x) AS num
|
||
|
||
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 │
|
||
└───────┴─────┴─────┴─────────┴────────────┴─────┘
|
||
|
||
5 rows in set. Elapsed: 0.002 sec.
|
||
|
||
В запросе может быть указано не более одной секции ARRAY JOIN.
|
||
|
||
Соответствующее преобразование может выполняться как до секции WHERE/PREWHERE (если его результат нужен в этой секции), так и после выполнения WHERE/PREWHERE (чтобы уменьшить объём вычислений).
|
||
|
||
Секция JOIN
|
||
"""""""""""
|
||
Обычный JOIN, не имеет отношения к ARRAY JOIN, который описан выше.
|
||
|
||
.. code-block:: sql
|
||
|
||
[GLOBAL] ANY|ALL INNER|LEFT [OUTER] JOIN (subquery)|table USING columns_list
|
||
|
||
Выполняет соединение с данными из подзапроса. В начале выполнения запроса, выполняется подзапрос, указанный после JOIN, и его результат сохраняется в память. Затем производится чтение из "левой" таблицы, указанной в секции FROM, и во время этого чтения, для каждой прочитанной строчки из "левой" таблицы, из таблицы-результата подзапроса ("правой" таблицы) выбираются строчки, соответствующие условию на совпадение значений столбцов, указанных в USING.
|
||
|
||
Вместо подзапроса может быть указано имя таблицы. Это эквивалентно подзапросу ``SELECT * FROM table``, кроме особого случая, когда таблица имеет движок Join - подготовленное множество для соединения.
|
||
|
||
Из подзапроса удаляются все ненужные для JOIN-а столбцы.
|
||
|
||
JOIN-ы бывают нескольких видов:
|
||
|
||
``INNER`` или ``LEFT`` - тип:
|
||
Если указано INNER, то в результат попадают только строки, для которых найдена соответствующая строка в "правой" таблице.
|
||
Если указано LEFT, то для строчек "левой" таблицы, для которых нет соответствующих в "правой" таблице, будут присоединены значения "по умолчанию" - нули, пустые строки. Вместо LEFT может быть написано LEFT OUTER - слово OUTER ни на что не влияет.
|
||
|
||
``ANY`` или ``ALL`` - строгость:
|
||
Если указано ``ANY``, то при наличии в "правой" таблице нескольких соответствующих строк, будет присоединена только первая попавшаяся.
|
||
Если указано ``ALL``, то при наличии в "правой" таблице нескольких соответствующих строк, данные будут размножены по количеству этих строк.
|
||
|
||
Использование ALL соответствует обычной семантике JOIN-а из стандартного SQL.
|
||
Использование ANY является более оптимальным. Если известно, что в "правой" таблице есть не более одной подходящей строки, то результаты ANY и ALL совпадают. Обязательно необходимо указать ANY или ALL (ни один из этих вариантов не выбран по умолчанию).
|
||
|
||
``GLOBAL`` - распределённость:
|
||
|
||
При использовании обычного JOIN-а, запрос отправляется на удалённые серверы, и на каждом из них выполняются подзапросы для формирования "правой" таблицы, и с этой таблицей выполняется соединение. То есть, "правая" таблица формируется на каждом сервере отдельно.
|
||
|
||
При использовании ``GLOBAL ... JOIN-а``, сначала, на сервере-инициаторе запроса, выполняется подзапрос для вычисления "правой" таблицы, и затем эта временная таблица передаётся на каждый удалённый сервер, и на них выполняются запросы, с использованием этих переданных временных данных.
|
||
|
||
Следует быть аккуратным при использовании GLOBAL JOIN-ов. Подробнее читайте в разделе "Распределённые подзапросы" ниже.
|
||
|
||
Возможны все комбинации JOIN-ов. Например, ``GLOBAL ANY LEFT OUTER JOIN``.
|
||
|
||
При выполнении JOIN-а отсутствует оптимизация порядка выполнения по отношению к другим стадиям запроса: соединение (поиск в "правой" таблице) выполняется до фильтрации в WHERE, до агрегации. Поэтому, чтобы явно задать порядок вычислений, рекомендуется выполнять JOIN подзапроса с подзапросом.
|
||
|
||
Пример:
|
||
|
||
::
|
||
|
||
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-ов, вы можете разместить их в подзапросах.
|
||
|
||
Каждый раз для выполнения запроса с одинаковым JOIN-ом, подзапрос выполняется заново - результат не кэшируется. Это можно избежать, используя специальный движок таблиц Join, представляющий собой подготовленное множество для соединения, которое всегда находится в оперативке. Подробнее смотрите в разделе "Движки таблиц, Join".
|
||
|
||
В некоторых случаях, вместо использования JOIN достаточно использовать IN - это более эффективно.
|
||
Среди разных типов JOIN-ов, наиболее эффективен ANY LEFT JOIN, затем ANY INNER JOIN; наименее эффективны ALL LEFT JOIN и ALL INNER JOIN.
|
||
|
||
Если JOIN необходим для соединения с таблицами измерений (dimension tables - сравнительно небольшие таблицы, которые содержат свойства измерений - например, имена для рекламных кампаний), то использование JOIN может быть не очень удобным из-за громоздкости синтаксиса, а также из-за того, что правая таблица читается заново при каждом запросе. Специально для таких случаев существует функциональность "Внешние словари", которую следует использовать вместо JOIN. Подробнее смотрите раздел "Внешние словари".
|
||
|
||
Секция WHERE
|
||
""""""""""""
|
||
|
||
Секция WHERE, если есть, должна содержать выражение, имеющее тип UInt8. Обычно это какое-либо выражение с операторами сравнения и логическими операторами.
|
||
Это выражение будет использовано для фильтрации данных перед всеми остальными преобразованиями.
|
||
|
||
Выражение анализируется на возможность использования индексов, если индексы поддерживаются движком таблицы.
|
||
|
||
Секция PREWHERE
|
||
"""""""""""""""
|
||
|
||
Имеет такой же смысл, как и секция WHERE. Отличие состоит в том, какие данные читаются из таблицы.
|
||
При использовании PREWHERE, из таблицы сначала читаются только столбцы, необходимые для выполнения PREWHERE. Затем читаются остальные столбцы, нужные для выполнения запроса, но из них только те блоки, в которых выражение в PREWHERE истинное.
|
||
|
||
PREWHERE имеет смысл использовать, если есть условия фильтрации, не подходящие под индексы, которые использует меньшинство столбцов из тех, что есть в запросе, но достаточно сильно фильтрует данные. Таким образом, сокращается количество читаемых данных.
|
||
|
||
Например, полезно писать PREWHERE для запросов, которые вынимают много столбцов, но в которых фильтрация производится лишь по нескольким столбцам.
|
||
|
||
PREWHERE поддерживается только таблицами семейства ``*MergeTree``.
|
||
|
||
В запросе могут быть одновременно указаны секции PREWHERE и WHERE. В этом случае, PREWHERE идёт перед WHERE.
|
||
|
||
Следует иметь ввиду, что указывать в PREWHERE только столбцы, по которым существует индекс, имеет мало смысла, так как при использовании индекса и так читаются лишь блоки данных, соответствующие индексу.
|
||
|
||
Если настройка optimize_move_to_prewhere выставлена в 1, то при отсутствии PREWHERE, система будет автоматически переносить части выражений из WHERE в PREWHERE согласно некоторой эвристике.
|
||
|
||
Секция GROUP BY
|
||
"""""""""""""""
|
||
|
||
Это одна из наиболее важных частей СУБД.
|
||
|
||
Секция GROUP BY, если есть, должна содержать список выражений. Каждое выражение далее будем называть "ключом".
|
||
При этом, все выражения в секциях SELECT, HAVING, ORDER BY, должны вычисляться из ключей или из агрегатных функций. То есть, каждый выбираемый из таблицы столбец, должен использоваться либо в ключах, либо внутри агрегатных функций.
|
||
|
||
Если запрос содержит столбцы таблицы только внутри агрегатных функций, то секция GROUP BY может не указываться, и подразумевается агрегация по пустому набору ключей.
|
||
|
||
Пример:
|
||
|
||
.. code-block:: sql
|
||
|
||
SELECT
|
||
count(),
|
||
median(FetchTiming > 60 ? 60 : FetchTiming),
|
||
count() - sum(Refresh)
|
||
FROM hits
|
||
|
||
Но, в отличие от стандартного SQL, если в таблице нет строк (вообще нет или после фильтрации с помощью WHERE), в качестве результата возвращается пустой результат, а не результат из одной строки, содержащий "начальные" значения агрегатных функций.
|
||
|
||
В отличие от MySQL (и в соответствии со стандартом SQL), вы не можете получить какое-нибудь значение некоторого столбца, не входящего в ключ или агрегатную функцию (за исключением константных выражений). Для обхода этого вы можете воспользоваться агрегатной функцией any (получить первое попавшееся значение) или min/max.
|
||
|
||
Пример:
|
||
|
||
.. code-block:: sql
|
||
|
||
SELECT
|
||
domainWithoutWWW(URL) AS domain,
|
||
count(),
|
||
any(Title) AS title -- для каждого домена достаём первый попавшийся заголовок страницы
|
||
FROM hits
|
||
GROUP BY domain
|
||
|
||
GROUP BY вычисляет для каждого встретившегося различного значения ключей, набор значений агрегатных функций.
|
||
|
||
Не поддерживается GROUP BY по столбцам-массивам.
|
||
|
||
Не поддерживается указание констант в качестве аргументов агрегатных функций. Пример: sum(1). Вместо этого, вы можете избавиться от констант. Пример: ``count()``.
|
||
|
||
Модификатор 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 во внешней памяти
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
||
Существует возможность включить сброс временных данных на диск для ограничения потребления оперативной памяти при GROUP BY.
|
||
Настройка ``max_bytes_before_external_group_by`` - потребление оперативки, при котором временные данные GROUP BY сбрасываются в файловую систему. Если равно 0 (по умолчанию) - значит выключено.
|
||
|
||
При использовании ``max_bytes_before_external_group_by`` рекомендуется выставить max_memory_usage примерно в два раза больше. Это следует сделать, потому что агрегация выполняется в две стадии: чтение и формирование промежуточных данных (1) и слияние промежуточных данных (2). Сброс данных на файловую систему может производиться только на стадии 1. Если сброса временных данных не было, то на стадии 2 может потребляться до такого же объёма памяти, как на стадии 1.
|
||
|
||
Например, если у вас ``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 * количество потоков от общего объёма оперативки.
|
||
|
||
При включенной внешней агрегации, если данных было меньше ``max_bytes_before_external_group_by`` (то есть сброса данных не было), то запрос работает так же быстро, как без внешней агрегации. Если же какие-то временные данные были сброшены, то время выполнения будет в несколько раз больше (примерно в три раза).
|
||
|
||
Если после GROUP BY у вас есть ORDER BY с небольшим LIMIT, то на ORDER BY не будет тратиться существенного количества оперативки.
|
||
Но если есть ORDER BY без LIMIT, то не забудьте включить внешнюю сортировку (``max_bytes_before_external_sort``).
|
||
|
||
Модификатор LIMIT N BY
|
||
^^^^^^^^^^^^^^^^^^^^^^
|
||
|
||
LIMIT N BY COLUMNS позволяет выбрать топ N строк для каждой группы COLUMNS. LIMIT N BY не связан с LIMIT и они могут использоваться в одном запросе. Ключ для LIMIT N BY может содержать произвольное число колонок или выражений.
|
||
|
||
Пример:
|
||
|
||
.. code-block:: 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.
|
||
|
||
Секция HAVING
|
||
"""""""""""""
|
||
|
||
Позволяет отфильтровать результат, полученный после GROUP BY, аналогично секции WHERE.
|
||
WHERE и HAVING отличаются тем, что WHERE выполняется до агрегации (GROUP BY), а HAVING - после.
|
||
Если агрегации не производится, то HAVING использовать нельзя.
|
||
|
||
Секция 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-ы идут отдельно от остальных значений. Вне зависимости от порядка сортировки, NaN-ы помещаются в конец. То есть, при сортировке по возрастанию, они как будто больше всех чисел, а при сортировке по убыванию - как будто меньше всех.
|
||
|
||
Если кроме 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
|
||
"""""""""""""""
|
||
|
||
Если указано DISTINCT, то из всех множеств полностью совпадающих строк результата, будет оставляться только одна строка.
|
||
Результат выполнения будет таким же, как если указано GROUP BY по всем указанным полям в SELECT-е и не указаны агрегатные функции. Но имеется несколько отличий от GROUP BY:
|
||
|
||
- DISTINCT может применяться совместно с GROUP BY;
|
||
- при отсутствии ORDER BY и наличии LIMIT, запрос прекратит выполнение сразу после того, как будет прочитано необходимое количество различных строк - в этом случае использование DISTINCT существенно более оптимально;
|
||
- блоки данных будут выдаваться по мере их обработки, не дожидаясь выполнения всего запроса.
|
||
|
||
DISTINCT не поддерживается, если в SELECT-е присутствует хотя бы один столбец типа массив.
|
||
|
||
Секция LIMIT
|
||
""""""""""""
|
||
|
||
LIMIT m позволяет выбрать из результата первые m строк.
|
||
LIMIT n, m позволяет выбрать из результата первые m строк после пропуска первых n строк.
|
||
|
||
n и m должны быть неотрицательными целыми числами.
|
||
|
||
При отсутствии секции ORDER BY, однозначно сортирующей результат, результат может быть произвольным и может являться недетерминированным.
|
||
|
||
Секция UNION ALL
|
||
""""""""""""""""
|
||
|
||
Произвольное количество запросов может быть объединено с помощью UNION ALL. Пример:
|
||
|
||
.. code-block:: 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 могут выполняться параллельно, и их результаты могут возвращаться вперемешку.
|
||
|
||
Структура результатов (количество и типы столбцов) у запросов должна совпадать. Но имена столбцов могут отличаться. В этом случае, имена столбцов для общего результата будут взяты из первого запроса.
|
||
|
||
Запросы - части 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
|
||
""""""""""""
|
||
|
||
Операторы ``IN``, ``NOT IN``, ``GLOBAL IN``, ``GLOBAL NOT IN`` рассматриваются отдельно, так как их функциональность достаточно богатая.
|
||
|
||
В качестве левой части оператора, может присутствовать как один столбец, так и кортеж.
|
||
|
||
Примеры:
|
||
|
||
.. code-block:: 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 (подготовленное множество, постоянно находящееся в оперативке), то множество не будет создаваться заново при каждом запросе.
|
||
|
||
В подзапросе может быть указано более одного столбца для фильтрации кортежей.
|
||
Пример:
|
||
|
||
.. code-block:: sql
|
||
|
||
SELECT (CounterID, UserID) IN (SELECT CounterID, UserID FROM ...) FROM ...
|
||
|
||
Типы столбцов слева и справа оператора IN, должны совпадать.
|
||
|
||
Оператор IN и подзапрос могут встречаться в любой части запроса, в том числе в агрегатных и лямбда функциях.
|
||
Пример:
|
||
|
||
::
|
||
|
||
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 на одном сервере всегда выполняется только один раз. Зависимых подзапросов не существует.
|
||
|
||
|
||
.. _queries-distributed-subrequests:
|
||
|
||
Распределённые подзапросы
|
||
"""""""""""""""""""""""""
|
||
|
||
Существует два варианта IN-ов с подзапросами (аналогично для JOIN-ов): обычный ``IN`` / ``JOIN`` и ``GLOBAL IN`` / ``GLOBAL JOIN``. Они отличаются способом выполнения при распределённой обработке запроса.
|
||
|
||
.. attention::
|
||
Помните, что алгоритмы, описанные ниже, могут работать иначе в зависимости от :ref:`настройки <settings-distributed_product_mode>` ``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**.
|
||
|
||
Например, запрос
|
||
|
||
.. code-block:: sql
|
||
|
||
SELECT uniq(UserID) FROM distributed_table
|
||
|
||
будет отправлен на все удалённые серверы в виде
|
||
|
||
.. code-block:: sql
|
||
|
||
SELECT uniq(UserID) FROM local_table
|
||
|
||
|
||
, выполнен параллельно на каждом из них до стадии, позволяющей объединить промежуточные результаты; затем промежуточные результаты вернутся на сервер-инициатор запроса, будут на нём объединены, и финальный результат будет отправлен клиенту.
|
||
|
||
Теперь рассмотрим запрос с IN-ом:
|
||
|
||
.. code-block:: sql
|
||
|
||
SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM local_table WHERE CounterID = 34)
|
||
|
||
- расчёт пересечения аудиторий двух сайтов.
|
||
|
||
Этот запрос будет отправлен на все удалённые серверы в виде
|
||
|
||
.. code-block:: 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** внутри подзапроса. Запрос будет выглядеть так:
|
||
|
||
.. code-block:: sql
|
||
|
||
SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM distributed_table WHERE CounterID = 34)
|
||
|
||
Этот запрос будет отправлен на все удалённые серверы в виде
|
||
|
||
.. code-block:: sql
|
||
|
||
SELECT uniq(UserID) FROM local_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM distributed_table WHERE CounterID = 34)
|
||
|
||
На каждом удалённом сервере начнёт выполняться подзапрос. Так как в подзапросе используется распределённая таблица, то подзапрос будет, на каждом удалённом сервере, снова отправлен на каждый удалённый сервер, в виде
|
||
|
||
.. code-block:: sql
|
||
|
||
SELECT UserID FROM local_table WHERE CounterID = 34
|
||
|
||
Например, если у вас кластер из 100 серверов, то выполнение всего запроса потребует 10 000 элементарных запросов, что, как правило, является неприемлемым.
|
||
|
||
В таких случаях всегда следует использовать GLOBAL IN вместо IN. Рассмотрим его работу для запроса
|
||
|
||
.. code-block:: sql
|
||
|
||
SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID GLOBAL IN (SELECT UserID FROM distributed_table WHERE CounterID = 34)
|
||
|
||
На сервере-инициаторе запроса будет выполнен подзапрос
|
||
|
||
.. code-block:: sql
|
||
|
||
SELECT UserID FROM distributed_table WHERE CounterID = 34
|
||
|
||
, и результат будет сложен во временную таблицу в оперативке. Затем запрос будет отправлен на каждый удалённый сервер в виде
|
||
|
||
.. code-block:: sql
|
||
|
||
SELECT uniq(UserID) FROM local_table WHERE CounterID = 101500 AND UserID GLOBAL IN _data1
|
||
|
||
, и вместе с запросом, на каждый удалённый сервер будет отправлена временная таблица _data1 (имя временной таблицы - implementation defined).
|
||
|
||
Это гораздо более оптимально, чем при использовании обычного IN. Но при этом, следует помнить о нескольких вещах:
|
||
|
||
#. При создании временной таблицы данные не уникализируются. Чтобы уменьшить объём передаваемых по сети данных, укажите в подзапросе DISTINCT (для обычного IN-а этого делать не нужно).
|
||
#. Временная таблица будет передана на все удалённые серверы. Передача не учитывает топологию сети. Например, если 10 удалённых серверов расположены в удалённом относительно сервера-инициатора запроса датацентре, то по каналу в удалённый датацентр данные будет переданы 10 раз. Старайтесь не использовать большие множества при использовании GLOBAL IN.
|
||
#. При передаче данных на удалённые серверы не настраивается ограничение использования сетевой полосы. Вы можете перегрузить сеть.
|
||
#. Старайтесь распределять данные по серверам так, чтобы в GLOBAL IN-ах не было частой необходимости.
|
||
#. Если в GLOBAL IN есть частая необходимость, то спланируйте размещение кластера ClickHouse таким образом, чтобы в каждом датацентре была хотя бы одна реплика каждого шарда, и среди них была быстрая сеть - чтобы запрос целиком можно было бы выполнить, передавая данные в пределах одного датацентра.
|
||
|
||
В секции ``GLOBAL IN`` также имеет смысл указывать локальную таблицу - в случае, если эта локальная таблица есть только на сервере-инициаторе запроса, и вы хотите воспользоваться данными из неё на удалённых серверах.
|
||
|
||
Экстремальные значения
|
||
""""""""""""""""""""""
|
||
|
||
Вы можете получить в дополнение к результату также минимальные и максимальные значения по столбцам результата. Для этого выставите настройку **extremes** в 1. Минимумы и максимумы считаются для числовых типов, дат, дат-с-временем. Для остальных столбцов будут выведены значения по умолчанию.
|
||
|
||
Вычисляются дополнительные две строчки - минимумы и максимумы, соответственно. Эти дополнительные две строчки выводятся в форматах JSON*, TabSeparated*, Pretty* отдельно от остальных строчек. В остальных форматах они не выводится.
|
||
|
||
В форматах JSON* экстремальные значения выводятся отдельным полем extremes. В форматах TabSeparated* строчка выводится после основного результата и после totals, если есть. Перед ней (после остальных данных) вставляется пустая строка. В форматах Pretty* строчка выводится отдельной табличкой после основного результата и после totals, если есть.
|
||
|
||
Экстремальные значения считаются по строчкам, прошедшим через LIMIT. Но при этом, при использовании LIMIT offset, size, строчки до offset учитываются в extremes. В потоковых запросах, в результате может учитываться также небольшое количество строчек, прошедших LIMIT.
|
||
|
||
Замечания
|
||
"""""""""
|
||
|
||
В секциях ``GROUP BY``, ``ORDER BY``, в отличие от диалекта MySQL, и в соответствии со стандартным SQL, не поддерживаются позиционные аргументы.
|
||
Например, если вы напишите ``GROUP BY 1, 2`` - то это будет воспринято, как группировка по константам (то есть, агрегация всех строк в одну).
|
||
|
||
Вы можете использовать синонимы (алиасы ``AS``) в любом месте запроса.
|
||
|
||
В любом месте запроса, вместо выражения, может стоять звёздочка. При анализе запроса звёздочка раскрывается в список всех столбцов таблицы (за исключением ``MATERIALIZED`` и ``ALIAS`` столбцов). Есть лишь немного случаев, когда оправдано использовать звёздочку:
|
||
|
||
* при создании дампа таблицы;
|
||
* для таблиц, содержащих всего несколько столбцов - например, системных таблиц;
|
||
* для получения информации о том, какие столбцы есть в таблице; в этом случае, укажите ``LIMIT 1``. Но лучше используйте запрос ``DESC TABLE``;
|
||
* при наличии сильной фильтрации по небольшому количеству столбцов с помощью ``PREWHERE``;
|
||
* в подзапросах (так как из подзапросов выкидываются столбцы, не нужные для внешнего запроса).
|
||
|
||
В других случаях использование звёздочки является издевательством над системой, так как вместо преимуществ столбцовой СУБД вы получаете недостатки. То есть использовать звёздочку не рекомендуется.
|
||
|
||
KILL QUERY
|
||
~~~~~~~~~~
|
||
|
||
.. code-block:: sql
|
||
|
||
KILL QUERY WHERE <where expression to SELECT FROM system.processes query> [SYNC|ASYNC|TEST] [FORMAT format]
|
||
|
||
Пытается завершить исполняющиеся в данный момент запросы.
|
||
Запросы для завершения выбираются из таблицы system.processes для которых выражение expression_for_system.processes истинно.
|
||
|
||
Примеры:
|
||
|
||
.. code-block:: sql
|
||
|
||
KILL QUERY WHERE query_id='2-857d-4a57-9ee0-327da5d60a90'
|
||
|
||
Завершает все запросы с указанным query_id.
|
||
|
||
.. code-block:: sql
|
||
|
||
KILL QUERY WHERE user='username' SYNC
|
||
|
||
Синхронно завершает все запросы пользователя ``username``.
|
||
|
||
Readonly-пользователи могут совершать только свои запросы.
|
||
По-умолчанию используется асинхронный вариант запроса (``ASYNC``), который завершается не ожидая завершения запросов.
|
||
Синхронный вариант (``SYNC``) ожидает завершения всех запросов и построчно выводит информацию о процессах по ходу их завершения.
|
||
Ответ содержит колонку ``kill_status``, которая может принимать следующие значения:
|
||
|
||
#. 'finished' - запрос успешно завершился;
|
||
#. 'waiting' - запросу отправлен сигнал завершения, ожидается его завершение;
|
||
#. остальные значения описывают причину невозможности завершения запроса.
|
||
|
||
Тестовый вариант запроса (``TEST``) только проверяет права пользователя и выводит список запросов для завершения.
|