Russian
English

ClickHouse

Руководство

— Алексей Миловидов

Содержание


Введение

Что такое ClickHouse

ClickHouse - столбцовая СУБД для OLAP (Columnar DBMS).

В обычной, "строковой" СУБД, данные хранятся в таком порядке:

5123456789123456789     1       Евробаскет - Греция - Босния и Герцеговина - example.com      1       2011-09-01 01:03:02     6274717   1294101174      11409   612345678912345678      0       33      6       http://www.example.com/basketball/team/123/match/456789.html http://www.example.com/basketball/team/123/match/987654.html       0       1366    768     32      10      3183      0       0       13      0\0     1       1       0       0                       2011142 -1      0               0       01321     613     660     2011-09-01 08:01:17     0       0       0       0       utf-8   1466    0       0       0       5678901234567890123               277789954       0       0       0       0       0
5234985259563631958     0       Консалтинг, налогообложение, бухгалтерский учет, право       1       2011-09-01 01:03:02     6320881   2111222333      213     6458937489576391093     0       3       2       http://www.example.ru/         0       800     600       16      10      2       153.1   0       0       10      63      1       1       0       0                       2111678 000       0       588     368     240     2011-09-01 01:03:17     4       0       60310   0       windows-1251    1466    0       000               778899001       0       0       0       0       0
...

То есть, значения, относящиеся к одной строке, хранятся рядом. Примеры строковых СУБД: MySQL, Postgres, MS SQL Server и т. п.

В столбцовых СУБД, данные хранятся в таком порядке:

WatchID:    5385521489354350662     5385521490329509958     5385521489953706054     5385521490476781638     5385521490583269446     5385521490218868806     5385521491437850694   5385521491090174022      5385521490792669254     5385521490420695110     5385521491532181574     5385521491559694406     5385521491459625030     5385521492275175494   5385521492781318214      5385521492710027334     5385521492955615302     5385521493708759110     5385521494506434630     5385521493104611398
JavaEnable: 1       0       1       0       0       0       1       0       1       1       1       1       1       1       0       1       0       0       1       1
Title:      Yandex  Announcements - Investor Relations - Yandex     Yandex — Contact us — Moscow    Yandex — Mission        Ru      Yandex — History — History of Yandex    Yandex Financial Releases - Investor Relations - Yandex Yandex — Locations      Yandex Board of Directors - Corporate Governance - Yandex       Yandex — Technologies
GoodEvent:  1       1       1       1       1       1       1       1       1       1       1       1       1       1       1       1       1       1       1       1
EventTime:  2016-05-18 05:19:20     2016-05-18 08:10:20     2016-05-18 07:38:00     2016-05-18 01:13:08     2016-05-18 00:04:06     2016-05-18 04:21:30     2016-05-18 00:34:16     2016-05-18 07:35:49     2016-05-18 11:41:59     2016-05-18 01:13:32
...

В примерах изображён только порядок расположения данных. То есть, значения из разных столбцов хранятся отдельно, а данные одного столбца - вместе. Примеры столбцовых СУБД: Vertica, Paraccel (Actian Matrix) (Amazon Redshift), Sybase IQ, Exasol, Infobright, InfiniDB, MonetDB (VectorWise) (Actian Vector), LucidDB, SAP HANA, Google Dremel, Google PowerDrill, Metamarkets Druid, kdb+ и т. п.

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

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

Будем говорить, что OLAP (онлайн обработка аналитических запросов) сценарий работы - это: - подавляющее большинство запросов - на чтение; - данные обновляются достаточно большими пачками (> 1000 строк), а не по одной строке, или не обновляются вообще; - данные добавляются в БД, но не изменяются; - при чтении, вынимается достаточно большое количество строк из БД, но только небольшое подмножество столбцов; - таблицы являются "широкими", то есть, содержат большое количество столбцов; - запросы идут сравнительно редко (обычно не более сотни в секунду на сервер); - при выполнении простых запросов, допустимы задержки в районе 50мс; - значения в столбцах достаточно мелкие - числа и небольшие строки (пример - 60 байт на URL); - требуется высокая пропускная способность при обработке одного запроса (до миллиардов строк в секунду на один сервер); - транзакции отсутствуют; - низкие требования к консистентности данных; - в запросе одна большая таблица, все таблицы кроме одной маленькие; - результат выполнения запроса существенно меньше исходных данных - то есть, данные фильтруются или агрегируются; результат выполнения помещается в оперативку на одном сервере;

Легко видеть, что OLAP сценарий работы существенно отличается от других распространённых сценариев работы (например, OLTP или Key-Value сценариев работы). Таким образом, не имеет никакого смысла пытаться использовать OLTP или Key-Value БД для обработки аналитических запросов, если вы хотите получить приличную производительность ("выше плинтуса"). Например, если вы попытаетесь использовать для аналитики MongoDB или Elliptics - вы получите анекдотически низкую производительность по сравнению с OLAP-СУБД.

Столбцовые СУБД лучше (от 100 раз по скорости обработки большинства запросов) подходят для OLAP сценария работы по следующим причинам:

1. По I/O. 1.1. Для выполнения аналитического запроса, требуется прочитать небольшое количество столбцов таблицы. В столбцовой БД для этого можно читать только нужные данные. Например, если вам требуется только 5 столбцов из 100, то следует рассчитывать на 20-кратное уменьшение ввода-вывода. 1.2. Так как данные читаются пачками, то их проще сжимать. Данные, лежащие по столбцам также лучше сжимаются. За счёт этого, дополнительно уменьшается объём ввода-вывода. 1.3. За счёт уменьшения ввода-вывода, больше данных влезает в системный кэш.

Для примера, для запроса "посчитать количество записей для каждой рекламной системы", требуется прочитать один столбец "идентификатор рекламной системы", который занимает 1 байт в несжатом виде. Если большинство переходов было не с рекламных систем, то можно рассчитывать хотя бы на десятикратное сжатие этого столбца. При использовании быстрого алгоритма сжатия, возможно разжатие данных со скоростью более нескольких гигабайт несжатых данных в секунду. То есть, такой запрос может выполняться со скоростью около нескольких миллиардов строк в секунду на одном сервере. На практике, такая скорость действительно достигается.

milovidov@████████.yandex.ru:~$ clickhouse-client
ClickHouse client version 0.0.52053.
Connecting to localhost:9000.
Connected to ClickHouse server version 0.0.52053.

:) SELECT CounterID, count() FROM hits GROUP BY CounterID ORDER BY count() DESC LIMIT 20

SELECT
    CounterID,
    count()
FROM hits
GROUP BY CounterID
ORDER BY count() DESC
LIMIT 20

┌─CounterID─┬──count()─┐
│    114208 │ 56057344 │
│    115080 │ 51619590 │
│      3228 │ 44658301 │
│     38230 │ 42045932 │
│    145263 │ 42042158 │
│     91244 │ 38297270 │
│    154139 │ 26647572 │
│    150748 │ 24112755 │
│    242232 │ 21302571 │
│    338158 │ 13507087 │
│     62180 │ 12229491 │
│     82264 │ 12187441 │
│    232261 │ 12148031 │
│    146272 │ 11438516 │
│    168777 │ 11403636 │
│   4120072 │ 11227824 │
│  10938808 │ 10519739 │
│     74088 │  9047015 │
│    115079 │  8837972 │
│    337234 │  8205961 │
└───────────┴──────────┘

20 rows in set. Elapsed: 0.153 sec. Processed 1.00 billion rows, 4.00 GB (6.53 billion rows/s., 26.10 GB/s.)

:)

2. По CPU. Так как для выполнения запроса надо обработать достаточно большое количество строк, становится актуальным диспетчеризовывать все операции не для отдельных строк, а для целых векторов, или реализовать движок выполнения запроса так, чтобы издержки на диспетчеризацию были примерно нулевыми. Если этого не делать, то при любой не слишком плохой дисковой подсистеме, интерпретатор запроса неизбежно упрётся в CPU. Имеет смысл не только хранить данные по столбцам, но и обрабатывать их, по возможности, тоже по столбцам.

Есть два способа это сделать: 1. Векторный движок. Все операции пишутся не для отдельных значений, а для векторов. То есть, вызывать операции надо достаточно редко, и издержки на диспетчеризацию становятся пренебрежимо маленькими. Код операции содержит в себе хорошо оптимизированный внутренний цикл. 2. Кодогенерация. Для запроса генерируется код, в котором подставлены все косвенные вызовы.

В "обычных" БД этого не делается, так как не имеет смысла при выполнении простых запросов. Хотя есть исключения. Например, в MemSQL кодогенерация используется для уменьшения latency при выполнении SQL запросов. (Для сравнения - в аналитических СУБД, требуется оптимизация throughput, а не latency).

Стоит заметить, что для эффективности по CPU требуется, чтобы язык запросов был декларативным (SQL, MDX) или хотя бы векторным (J, K). То есть, чтобы запрос содержал циклы только в неявном виде, открывая возможности для оптимизации.

Отличительные возможности ClickHouse

1. По-настоящему столбцовая СУБД. 2. Сжатие данных. 3. Хранение данных на диске. 4. Параллельная обработка запроса на многих процессорных ядрах. 5. Распределённая обработка запроса на многих серверах. 6. Поддержка SQL. 7. Векторный движок. 8. Обновление данных в реальном времени. 9. Наличие индексов. 10. Подходит для онлайн запросов. 11. Поддержка приближённых вычислений. 12. Поддержка вложенных структур данных. Поддержка массивов в качестве типов данных. 13. Поддержка ограничений на сложность запросов, а также квот. 14. Репликация данных, поддержка целостности данных на репликах.

Рассмотрим некоторые возможности подробнее.

1. По-настоящему столбцовая СУБД.

В по-настоящему столбцовой СУБД рядом со значениями не хранится никакого "мусора". Например, должны поддерживаться значения постоянной длины, чтобы не хранить рядом со значениями типа "число" их длины. Для примера, миллиард значений типа UInt8 должен действительно занимать в несжатом виде около 1GB, иначе это сильно ударит по эффективности использования CPU. Очень важно хранить данные компактно (без "мусора") в том числе в несжатом виде, так как скорость разжатия (использование CPU) зависит, в основном, от объёма несжатых данных.

Этот пункт пришлось выделить, так как существуют системы, которые могут хранить значания отдельных столбцов по отдельности, но не могут эффективно выполять аналитические запросы в силу оптимизации под другой сценарий работы. Примеры: HBase, BigTable, Cassandra, HyperTable. В этих системах вы получите throughput в районе сотен тысяч строк в секунду, но не сотен миллионов строк в секунду.

Также стоит заметить, что ClickHouse является СУБД, а не одной базой данных. То есть, ClickHouse позволяет создавать таблицы и базы данных в runtime, загружать данные и выполнять запросы без переконфигурирования и перезапуска сервера.

2. Сжатие данных.

Некоторые столбцовые СУБД (InfiniDB CE, MonetDB) не используют сжатие данных. Но сжатие данных действительно серьёзно увеличивает производительность.

3. Хранение данных на диске.

Многие столбцовые СУБД (SAP HANA, Metamarkets Druid, Google PowerDrill) могут работать только в оперативке. Но оперативки (даже на тысячах серверах) слишком мало для хранения всех хитов и визитов в Яндекс.Метрике.

4. Параллельная обработка запроса на многих процессорных ядрах.

Большие запросы естественным образом распараллеливаются.

5. Распределённая обработка запроса на многих серверах.

Почти все перечисленные ранее столбцовые СУБД не поддерживают распределённую обработку запроса. В ClickHouse данные могут быть расположены на разных шардах. Каждый шард может представлять собой группу реплик, которые используются для отказоустойчивости. Запрос будет выполнен на всех шардах параллельно. Это делается прозрачно для пользователя.

6. Поддержка SQL.

Если вы знаете, что такое стандартный SQL, то говорить о поддержке SQL всё-таки нельзя. Не поддерживаются NULL-ы. Все функции названы по-другому. Тем не менее, это - декларативный язык запросов на основе SQL и во многих случаях не отличимый от SQL. Поддерживаются JOIN-ы. Поддерживаются подзапросы в секциях FROM, IN, JOIN.

7. Векторный движок.

Данные не только хранятся по столбцам, но и обрабатываются по векторам - кусочкам столбцов. За счёт этого достигается высокая эффективность по CPU.

8. Обновление данных в реальном времени.

ClickHouse поддерживает таблицы с первичным ключом. Для того, чтобы можно было быстро выполнять запросы по диапазону первичного ключа, данные инкрементально сортируются с помощью merge дерева. За счёт этого, поддерживается постоянное добавление данных в таблицу. Блокировки при добавлении данных отсутствуют.

9. Наличие индексов.

Наличие первичного ключа позволяет, например, вынимать данные для конкретных клиентов (счётчиков Метрики), для заданного диапазона времени, с низкими задержками - менее десятков миллисекунд.

10. Подходит для онлайн запросов.

Это позволяет использовать систему в качестве бэкенда для веб интерфейса. Низкие задержки позволяют не откладывать выполнение запроса, а выполнять его в момент загрузки страницы интерфейса Яндекс.Метрики. То есть, в режиме онлайн.

11. Поддержка приближённых вычислений.

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

2. Поддерживается возможность выполнить запрос на основе части (выборки) данных и получить приближённый результат. При этом, с диска будет считано пропорционально меньше данных.

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

14. Репликация данных, поддержка целостности данных на репликах.

Используется асинхронная multimaster репликация. После записи на любую доступную реплику, данные распространяются на все остальные реплики. Система поддерживает полную идентичность данных на разных репликах. Восстановление после сбоя осуществляется автоматически, а в сложных случаях - "по кнопке".

Подробнее смотрите раздел "Репликация данных".

Особенности ClickHouse, которые могут считаться недостатками

1. Отсутствие транзакций.

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

3. Отсутствие полноценной реализации UPDATE/DELETE.

Постановка задачи в Яндекс.Метрике

Нужно получать произвольные отчёты на основе хитов и визитов, с произвольными сегментами, задаваемыми пользователем. Данные для отчётов обновляются в реальном времени. Запросы должны выполняться сразу (в режиме онлайн). Отчёты должно быть возможно строить за произвольный период. Требуется вычислять сложные агрегаты типа количества уникальных посетителей. На данный момент (апрель 2014), каждый день в Яндекс.Метрику поступает около 12 миллиардов событий (хитов и кликов мыши). Все эти события должны быть сохранены для возможности строить произвольные отчёты. Один запрос может потребовать просканировать сотни миллионов строк за время не более нескольких секунд, или миллионы строк за время не более нескольких сотен миллисекунд.

Агрегированные и неагрегированные данные

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

Но агрегированные данные являются очень ограниченным решением, по следующим причинам: - вы должны заранее знать перечень отчётов, необходимых пользователю; - то есть, пользователь не может построить произвольный отчёт; - при агрегации по большому количествую ключей, объём данных не уменьшается и агрегация бесполезна; - при большом количестве отчётов, получается слишком много вариантов агрегации (комбинаторный взрыв); - при агрегации по ключам высокой кардинальности (например, URL) объём данных уменьшается не сильно (менее чем в 2 раза); - из-за этого, объём данных при агрегации может не уменьшиться, а вырасти; - пользователи будут смотреть не все отчёты, которые мы для них посчитаем - то есть, большая часть вычислений бесполезна; - возможно нарушение логической целостности данных для разных агрегаций;

Как видно, если ничего не агрегировать, и работать с неагрегированными данными, то это даже может уменьшить объём вычислений.

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

В Яндекс.Метрике есть специализированная система для агрегированных данных - Metrage, на основе которой работает большинство отчётов. Также в Яндекс.Метрике с 2009 года использовалась специализированная OLAP БД для неагрегированных данных - OLAPServer, на основе которой раньше работал конструктор отчётов. OLAPServer хорошо подходил для неагрегированных данных, но содержал много ограничений, не позволяющих использовать его для всех отчётах так, как хочется: отсутствие поддержки типов данных (только числа), невозможность инкрементального обновления данных в реальном времени (только перезаписью данных за сутки). OLAPServer не является СУБД, а является специализированной БД.

Чтобы снять ограничения OLAPServer-а и решить задачу работы с неагрегированными данными для всех отчётов, разработана СУБД ClickHouse.

Использование в Яндекс.Метрике и других отделах Яндекса

В Яндекс.Метрике ClickHouse используется для нескольких задач. Основная задача - построение отчётов в режиме онлайн по неагрегированным данным. Для решения этой задачи используется кластер из 374 серверов, хранящий более 8 триллионов строк (более 1 квадриллиона значений) в базе данных. Объём сжатых данных, без учёта дублирования и репликации, составляет около 800 ТБ. Объём несжатых данных (в формате tsv) составил бы, приблизительно, 7 ПБ.

Также ClickHouse используется: - для хранения данных Вебвизора; - для обработки промежуточных данных; - для построения глобальных отчётов Аналитиками; - для выполнения запросов в целях отладки движка Метрики; - для анализа логов работы API и пользовательского интерфейса.

ClickHouse имеет более десятка инсталляций в других отделах Яндекса: в Вертикальных сервисах, Маркете, Директе, БК, Бизнес аналитике, Мобильной разработке, AdFox, Персональных сервисах и т п.

Возможные аналоги

Доступных аналогов нет. На данный момент (май 2016) не существует доступных (open-source, бесплатных) систем, обладающих всеми перечисленными выше возможностями. Но эти возможности являются абсолютно строго необходимыми для Яндекс.Метрики.

Возможные глупые вопросы

1. Почему бы не использовать системы типа map-reduce?

Системами типа map-reduce будем называть системы распределённых вычислений, в которых операция reduce сделана на основе распределённой сортировки. Таким образом, к ним относятся YAMR, Hadoop, YT.

Такие системы не подходят для онлайн запросов в силу слишком большой latency. То есть, не могут быть использованы в качестве бэкенда для веб интерфейса. Такие системы не подходят для обновления данных в реальном времени. Распределённая сортировка не является оптимальным способом выполнения операции reduce, если результат выполнения операции и все промежуточные результаты, при их наличии, помещаются в оперативку на одном сервере, как обычно бывает в запросах, выполняющихся в режиме онлайн. В таком случае, оптимальным способом выполнения операции reduce является хэш-таблица. Частым способом оптимизации map-reduce задач является предагрегация (частичный reduce) с использованием хэш-таблицы в оперативке. Эта оптимизация делается пользователем в ручном режиме. Распределённая сортировка является основной причиной тормозов при выполнении несложных map-reduce задач.

Системы типа map-reduce позволяют выполнять произвольный код на кластере. Но для OLAP задач лучше подходит декларативный язык запросов, который позволяет быстро проводить исследования. Для примера, для Hadoop существует Hive и Pig. Также смотрите Cloudera Impala, Shark для Spark а также Spark SQL, Presto, Apache Drill. Впрочем, производительность при выполнении таких задач является сильно неоптимальной по сравнению со специализированными системами, а сравнительно высокая latency не позволяет использовать эти системы в качестве бэкенда для веб-интерфейса.

YT позволяет хранить группы столбцов по отдельности. Но YT нельзя назвать по-настоящему столбцовой системой, так как в системе отсутствуют типы данных постоянной длины (чтобы можно было эффективно хранить числа без "мусора"), а также за счёт отсутствия векторного движка. Задачи в YT выполняются с помощью произвольного кода в режиме streaming, то есть, не могут быть достаточно оптимизированы (до сотен миллионов строк в секунду на один сервер). В YT в 2014-2016 годах находится в разработке функциональность "динамических сортированных таблиц" с использованием Merge Tree, строгой типизацией значений и языком запросов типа SQL. Динамические сортированные таблицы не подходят для OLAP задач, так как данные в них хранятся по строкам. Разработка языка запросов в YT всё ещё находится в зачаточной стадии, что не позволяет ориентироваться на эту функциональность. Разработчики YT рассматривают динамические сортированные таблицы для применения в OLTP и Key-Value сценариях работы.

Производительность

По результатам внутреннего тестирования, ClickHouse обладает наиболее высокой производительностью (как наиболее высоким throughput на длинных запросах, так и наиболее низкой latency на коротких запросах), при соответствующем сценарии работы, среди доступных для тестирования систем подобного класса. Результаты тестирования можно посмотреть на отдельной странице.

Пропускная способность при обработке одного большого запроса

Пропускную способность можно измерять в строчках в секунду и в мегабайтах в секунду. При условии, что данные помещаются в page cache, не слишком сложный запрос обрабатывается на современном железе со скоростью около 2-10 GB/sec. несжатых данных на одном сервере (в простейшем случае скорость может достигать 30 GB/sec). Если данные не помещаются в page cache, то скорость работы зависит от скорости дисковой подсистемы и коэффициента сжатия данных. Например, если дисковая подсистема позволяет читать данные со скоростью 400 MB/sec., а коэффициент сжатия данных составляет 3, то скорость будет около 1.2GB/sec. Для получения скорости в строчках в секунду, следует поделить скорость в байтах в секунду на суммарный размер используемых в запросе столбцов. Например, если вынимаются столбцы на 10 байт, то скорость будет в районе 100-200 млн. строчек в секунду.

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

Задержки при обработке коротких запросов.

Если запрос использует первичный ключ, и выбирает для обработки не слишком большое количество строчек (сотни тысяч), и использует не слишком большое количество столбцов, то вы можете рассчитывать на latency менее 50 миллисекунд (от единиц миллисекунд в лучшем случае), при условии, что данные помещаются в page cache. Иначе latency вычисляется из количества seek-ов. Если вы используйте вращающиеся диски, то на не слишком сильно нагруженной системе, latency вычисляется по формуле: seek time (10 мс.) * количество столбцов в запросе * количество кусков с данными.

Пропускная способность при обработке большого количества коротких запросов.

При тех же условиях, ClickHouse может обработать несколько сотен (до нескольких тысяч в лучшем случае) запросов в секунду на одном сервере. Так как такой сценарий работы не является типичным для аналитических СУБД, рекомендуется рассчитывать не более чем на 100 запросов в секунду.

Производительность при вставке данных.

Данные рекомендуется вставлять пачками не менее 1000 строк или не более одного запроса в секунду. При вставке в таблицу типа MergeTree из tab-separated дампа, скорость вставки будет в районе 50-200 МБ/сек. Если вставляются строчки размером около 1 КБ, то скорость будет в районе 50 000 - 200 000 строчек в секунду. Если строчки маленькие - производительность в строчках в секунду будет выше (на данных БК - > 500 000 строк в секунду, на данных Graphite - > 1 000 000 строк в секунду). Для увеличения производительности, можно производить несколько запросов INSERT параллельно - при этом производительность растёт линейно.

Начало работы

Системные требования

Система некроссплатформенная. Требуется ОС Linux Ubuntu не более старая, чем Precise (12.04); архитектура x86_64. Рекомендуется использовать Ubuntu Trusty или Ubuntu Precise. Терминал должен работать в кодировке UTF-8 (как по умолчанию в Ubuntu).

Установка

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

Установка из пакетов

Пропишите в /etc/apt/sources.list (или в отдельный файл /etc/apt/sources.list.d/metrika.list) репозитории:

Для использования из внутренней сети Яндекса, на Ubuntu Trusty (14.04):

deb http://dist.yandex.ru/metrika-trusty/ stable/amd64/

Для использования из внутренней сети Яндекса, на Ubuntu Precise (12.04):

deb http://dist.yandex.ru/metrika/ stable/amd64/

Репозитории для использования вне Яндекса будут добавлены позже.

Затем выполните:

sudo apt-get update
sudo apt-get install clickhouse-client clickhouse-server-common

Также можно скачать и установить пакеты вручную, отсюда: https://dist.yandex.ru/metrika/stable/amd64/.

ClickHouse содержит настройки ограничения доступа. Они расположены в файле users.xml (рядом с config.xml). По умолчанию, разрешён доступ отовсюду для пользователя default без пароля. См. секцию users/default/networks. Подробнее смотрите в разделе "конфигурационные файлы".

Установка из исходников

Для сборки воспользуйтесь инструкцией: build.md

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

Клиент: dbms/src/Client/ Сервер: dbms/src/Server/

Для сервера создаёте директории с данными, например:

/opt/clickhouse/data/default/
/opt/clickhouse/metadata/default/

(Настраивается в конфиге сервера.) Сделайте chown под нужного пользователя.

Обратите внимание на путь к логам в конфиге сервера (src/dbms/src/Server/config.xml).

Запуск

Для запуска сервера (в качестве демона), выполните:

sudo service clickhouse-server start

Смотрите логи в директории

/var/log/clickhouse-server/

Если сервер не стартует - проверьте правильность конфигурации в файле

/etc/clickhouse-server/config.xml

Также можно запустить сервер из консоли:

clickhouse-server --config-file=/etc/clickhouse-server/config.xml

При этом, лог будет выводиться в консоль - удобно для разработки. Если конфигурационный файл лежит в текущей директории, то указывать параметр --config-file не требуется - по умолчанию будет использован файл ./config.xml

Соединиться с сервером можно с помощью клиента командной строки:

clickhouse-client

Параметры по умолчанию обозначают - соединяться с localhost:9000, от имени пользователя default без пароля. Клиент может быть использован для соединения с удалённым сервером. Пример:

clickhouse-client --host=example.com

Подробнее смотри раздел "Клиент командной строки".

Проверим работоспособность системы:

milovidov@milovidov-Latitude-E6320:~/work/metrica/src/dbms/src/Client$ ./clickhouse-client
ClickHouse client version 0.0.18749.
Connecting to localhost:9000.
Connected to ClickHouse server version 0.0.18749.

:) SELECT 1

SELECT 1

┌─1─┐
│ 1 │
└───┘

1 rows in set. Elapsed: 0.003 sec.

:)

Поздравляю, система работает!

Тестовые данные

Если вы сотрудник Яндекса, вы можете воспользоваться тестовыми данными Яндекс.Метрики для изучения возможностей системы. Как загрузить тестовые данные, написано здесь. Если вы внешний пользователь системы, вы можете воспользоваться использовать общедоступные данные, способы загрузки которых указаны здесь.

Если возникли вопросы

Если вы являетесь сотрудником Яндекса, обращайтесь на внутреннюю рассылку по ClickHouse. Вы можете подписаться на эту рассылку, чтобы получать анонсы, быть в курсе нововведений, а также видеть вопросы, которые возникают у других пользователей. Иначе вы можете задавать вопросы на Stackoverflow с тегом 'clickhouse' или участвовать в обсуждениях на Google Groups. Также вы можете отправить приватное сообщение для разрабочиков по адресу clickhouse-feedback@yandex-team.com.

Интерфейсы

Для изучения возможностей системы, загрузки данных в таблицы, ручных запросов, используйте программу clickhouse-client.

HTTP интерфейс

HTTP интерфейс позволяет использовать ClickHouse на любой платформе, из любого языка программирования. У нас он используется для работы из Java и Perl, а также из shell-скриптов. В других отделах, HTTP интерфейс используется из Perl, Python и Go. HTTP интерфейс более ограничен по сравнению с родным интерфейсом, но является более совместимым.

По умолчанию, clickhouse-server слушает HTTP на порту 8123 (это можно изменить в конфиге). Если запросить GET / без параметров, то вернётся строка "Ok." (с переводом строки на конце). Это может быть использовано в скриптах проверки живости.

$ curl 'http://localhost:8123/'
Ok.

Запрос отправляется в виде параметра URL query. Или POST-ом. Или начало запроса в параметре query, а продолжение POST-ом (зачем это нужно, будет объяснено ниже). В случае успеха, вам вернётся код ответа 200 и результат обработки запроса в теле ответа. В случае ошибки, вам вернётся код ответа 500 и текст с описанием ошибки в теле ответа.

При использовании метода GET, выставляется настройка readonly. То есть, для запросов, модифицирующие данные, можно использовать только метод POST. Сам запрос при этом можно отправлять как в теле POST-а, так и в параметре URL.

Примеры:

$ curl 'http://localhost:8123/?query=SELECT%201'
1

$ wget -O- -q 'http://localhost:8123/?query=SELECT 1'
1

$ GET 'http://localhost:8123/?query=SELECT 1'
1

$ echo -ne 'GET /?query=SELECT%201 HTTP/1.0\r\n\r\n' | nc localhost 8123
HTTP/1.0 200 OK
Connection: Close
Date: Fri, 16 Nov 2012 19:21:50 GMT

1

Как видно, curl немного неудобен тем, что надо URL-эскейпить пробелы. wget сам всё эскейпит, но его не рекомендуется использовать, так как он плохо работает по HTTP 1.1 при использовании keep-alive и Transfer-Encoding: chunked.

$ echo 'SELECT 1' | curl 'http://localhost:8123/' --data-binary @-
1

$ echo 'SELECT 1' | curl 'http://localhost:8123/?query=' --data-binary @-
1

$ echo '1' | curl 'http://localhost:8123/?query=SELECT' --data-binary @-
1

Если часть запроса отправляется в параметре, а часть POST-ом, то между этими двумя кусками данных ставится перевод строки. Пример (так работать не будет):

$ echo 'ECT 1' | curl 'http://localhost:8123/?query=SEL' --data-binary @-
Code: 59, e.displayText() = DB::Exception: Syntax error: failed at position 0: SEL
ECT 1
, expected One of: SHOW TABLES, SHOW DATABASES, SELECT, INSERT, CREATE, ATTACH, RENAME, DROP, DETACH, USE, SET, OPTIMIZE., e.what() = DB::Exception

По умолчанию, данные возвращаются в формате TabSeparated (подробнее смотри раздел "Форматы"). Можно попросить любой другой формат - с помощью секции FORMAT запроса.

$ echo 'SELECT 1 FORMAT Pretty' | curl 'http://localhost:8123/?' --data-binary @-
┏━━━┓
┃ 1 ┃
┡━━━┩
│ 1 │
└───┘

Возможность передавать данные POST-ом нужна для INSERT-запросов. В этом случае вы можете написать начало запроса в параметре URL, а вставляемые данные передать POST-ом. Вставляемыми данными может быть, например, tab-separated дамп, полученный из MySQL. Таким образом, запрос INSERT заменяет LOAD DATA LOCAL INFILE из MySQL.

Примеры:

Создаём таблицу:

echo 'CREATE TABLE t (a UInt8) ENGINE = Memory' | POST 'http://localhost:8123/'

Используем привычный запрос INSERT для вставки данных:

echo 'INSERT INTO t VALUES (1),(2),(3)' | POST 'http://localhost:8123/'

Данные можно отправить отдельно от запроса:

echo '(4),(5),(6)' | POST 'http://localhost:8123/?query=INSERT INTO t VALUES'

Можно указать любой формат для данных. Формат Values - то же, что используется при записи INSERT INTO t VALUES:

echo '(7),(8),(9)' | POST 'http://localhost:8123/?query=INSERT INTO t FORMAT Values'

Можно вставить данные из tab-separated дампа, указав соответствующий формат:

echo -ne '10\n11\n12\n' | POST 'http://localhost:8123/?query=INSERT INTO t FORMAT TabSeparated'

Прочитаем содержимое таблицы. Данные выводятся в произвольном порядке из-за параллельной обработки запроса:

$ GET 'http://localhost:8123/?query=SELECT a FROM t'
7
8
9
10
11
12
1
2
3
4
5
6

Удаляем таблицу.

POST 'http://localhost:8123/?query=DROP TABLE t'

Для запросов, которые не возвращают таблицу с данными, в случае успеха, выдаётся пустое тело ответа.

Вы можете использовать сжатие при передаче данных. Формат сжатых данных нестандартный, и вам придётся использовать для работы с ним специальную программу compressor (sudo apt-get install compressor-metrika-yandex).

Если вы указали в URL compress=1, то сервер будет сжимать отправляемые вам данные. Если вы указали в URL decompress=1, то сервер будет разжимать те данные, которые вы передаёте ему POST-ом.

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

В параметре URL database может быть указана БД по умолчанию.

$ echo 'SELECT number FROM numbers LIMIT 10' | curl 'http://localhost:8123/?database=system' --data-binary @-
0
1
2
3
4
5
6
7
8
9

По умолчанию используется БД, которая прописана в настройках сервера, как БД по умолчанию. По умолчанию, это - БД default. Также вы всегда можете указать БД через точку перед именем таблицы.

Имя пользователя и пароль могут быть указаны в одном из двух вариантов: 1. С использованием HTTP Basic Authentification. Пример:

echo 'SELECT 1' | curl 'http://user:password@localhost:8123/' -d @-
2. В параметрах URL user и password. Пример:
echo 'SELECT 1' | curl 'http://localhost:8123/?user=user&password=password' -d @-
Если имя пользователя не указано, то используется имя пользователя default. Если пароль не указан, то используется пустой пароль.

Также в параметрах URL вы можете указать любые настроки, которые будут использованы для обработки одного запроса, или целые профили настроек. Пример:

http://localhost:8123/?profile=web&max_rows_to_read=1000000000&query=SELECT+1

Подробнее см. раздел "Настройки".

$ echo 'SELECT number FROM system.numbers LIMIT 10' | curl 'http://localhost:8123/?' --data-binary @-
0
1
2
3
4
5
6
7
8
9

Об остальных параметрах смотри раздел "SET".

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

Может быть передан необязательный параметр query_id - идентификатор запроса, произвольная строка. Подробнее смотрите раздел "Настройки, replace_running_query".

Может быть передан необязательный параметр quota_key - ключ квоты, произвольная строка. Подробнее смотрите раздел "Квоты".

HTTP интерфейс позволяет передать внешние данные (внешние временные таблицы) для использования запроса. Подробнее смотрите раздел "Внешние данные для обработки запроса"

Родной интерфейс (TCP)

Родной интерфейс используется в клиенте командной строки clickhouse-client, при межсерверном взаимодействии для распределённой обработки запроса, а также в программах на C++. Будет рассмотрен только клиент командной строки.

Клиент командной строки

$ clickhouse-client
ClickHouse client version 0.0.26176.
Connecting to localhost:9000.
Connected to ClickHouse server version 0.0.26176.

:) SELECT 1

Программа clickhouse-client принимает следующие параметры, все из которых являются необязательными:

--host, -h - имя сервера, по умолчанию - localhost. Вы можете использовать как имя, так и IPv4 или IPv6 адрес.

--port - порт, к которому соединяться, по умолчанию - 9000. Замечу, что для HTTP и родного интерфейса используются разные порты.

--user, -u - имя пользователя, по умолчанию - default.

--password - пароль, по умолчанию - пустая строка.

--query, -q - запрос для выполнения, при использовании в неинтерактивном режиме.

--database, -d - выбрать текущую БД, по умолчанию - текущая БД из настроек сервера (по умолчанию - БД default).

--multiline, -m - если указано - разрешить многострочные запросы, не отправлять запрос по нажатию Enter.

--multiquery, -n - если указано - разрешить выполнять несколько запросов, разделённых точкой с запятой. Работает только в неинтерактивном режиме.

--format, -f - использовать указанный формат по умолчанию для вывода результата.

--vertical, -E - если указано, использовать формат Vertical по умолчанию для вывода результата. То же самое, что --format=Vertical. В этом формате каждое значение выводится на отдельной строке, что удобно для отображения широких таблиц.

--time, -t - если указано, в неинтерактивном режиме вывести время выполнения запроса в stderr.

--stacktrace - если указано, в случае исключения, выводить также его стек трейс.

--config-file - имя конфигурационного файла, в котором есть дополнительные настройки или изменены умолчания для настроек, указанных выше. По умолчанию, ищутся файлы в следующем порядке: ./clickhouse-client.xml ~/./clickhouse-client/config.xml /etc/clickhouse-client/config.xml Настройки берутся только из первого найденного файла.

Также вы можете указать любые настроки, которые будут использованы для обработки запросов. Например, clickhouse-client --max_threads=1. Подробнее см. раздел "Настройки".

Клиент может быть использован в интерактивном и неинтерактивном (batch) режиме. Чтобы использовать batch режим, укажите параметр query, или отправьте данные в stdin (проверяется, что stdin - не терминал), или и то, и другое. Аналогично HTTP интерфейсу, при использовании одновременно параметра query и отправке данных в stdin, запрос составляется из конкатенации параметра query, перевода строки, и данных в stdin. Это удобно для больших INSERT запросов. В batch режиме в качестве формата данных по умолчанию используется формат TabSeparated. Формат может быть указан в секции FORMAT запроса.

По умолчанию, в batch режиме вы можете выполнить только один запрос. Чтобы выполнить несколько запросов из "скрипта", используйте параметр --multiquery. Это работает для всех запросов кроме INSERT. Результаты запросов выводятся подряд без дополнительных разделителей. Также, при необходимости выполнить много запросов, вы можете запускать clickhouse-client на каждый запрос. Заметим, что запуск программы clickhouse-client может занимать десятки миллисекунд.

В интерактивном режиме, вы получите командную строку, в которую можно вводить запросы. Если не указано multiline (по умолчанию): Чтобы выполнить запрос, нажмите Enter. Точка с запятой на конце запроса не обязательна. Чтобы ввести запрос, состоящий из нескольких строк, перед переводом строки, введите символ обратного слеша: \ - тогда после нажатия Enter, вам предложат ввести следующую строку запроса. Если указано multiline (многострочный режим): Чтобы выполнить запрос, завершите его точкой с запятой и нажмите Enter. Если в конце введённой строки не было точки с запятой, то вам предложат ввести следующую строчку запроса. Исполняется только один запрос, поэтому всё, что введено после точки с запятой, игнорируется.

Вместо или после точки с запятой может быть указано \G. Это обозначает использование формата Vertical. В этом формате каждое значение выводится на отдельной строке, что удобно для широких таблиц. Столь необычная функциональность добавлена для совместимости с MySQL CLI.

Командная строка сделана на основе readline (и history) (или libedit, или без какой-либо библиотеки, в зависимости от сборки) - то есть, в ней работают привычные сочетания клавиш, а также присутствует история. История пишется в ~/.clickhouse-client-history.

По умолчанию, в качестве формата, используется формат PrettyCompact (красивые таблички). Вы можете изменить формат с помощью секции FORMAT запроса, или с помощью указания \G на конце запроса, с помощью аргумента командной строки --format или --vertical, или с помощью конфигурационного файла клиента.

Чтобы выйти из клиента, нажмите Ctrl+D (или Ctrl+C), или наберите вместо запроса одно из: "exit", "quit", "logout", "учше", "йгше", "дщпщге", "exit;", "quit;", "logout;", "учшеж", "йгшеж", "дщпщгеж", "q", "й", "\q", "\Q", ":q", "\й", "\Й", "Жй"

При выполнении запроса, клиент показывает: 1. Прогресс выполнение запроса, который обновляется не чаще, чем 10 раз в секунду (по умолчанию). При быстрых запросах, прогресс может не успеть отобразиться. 2. Отформатированный запрос после его парсинга - для отладки. 3. Результат в заданном формате. 4. Количество строк результата, прошедшее время, а также среднюю скорость выполнения запроса.

Вы можете прервать длинный запрос, нажав Ctrl+C. При этом вам всё-равно придётся чуть-чуть подождать, пока сервер остановит запрос. На некоторых стадиях выполнения, запрос невозможно прервать. Если вы не дождётесь и нажмёте Ctrl+C второй раз, то клиент будет завершён.

Клиент командной строки позволяет передать внешние данные (внешние временные таблицы) для использования запроса. Подробнее смотрите раздел "Внешние данные для обработки запроса"

Язык запросов

Синтаксис

В системе есть два вида парсеров: полноценный парсер SQL (recursive descend parser) и парсер форматов данных (быстрый потоковый парсер). Во всех случаях кроме запроса INSERT, используется только полноценный парсер SQL. В запросе INSERT используется оба парсера:

INSERT INTO t VALUES (1, 'Hello, world'), (2, 'abc'), (3, 'def')

Фрагмент INSERT INTO t VALUES парсится полноценным парсером, а данные (1, 'Hello, world'), (2, 'abc'), (3, 'def') - быстрым потоковым парсером. Данные могут иметь любой формат. При получении запроса, сервер заранее считывает в оперативку не более max_query_size байт запроса (по умолчанию, 1МБ), а всё остальное обрабатывается потоково. Таким образом, в системе нет проблем с большими INSERT запросами, как в MySQL.

При использовании формата Values в INSERT запросе может сложиться иллюзия, что данные парсятся также, как выражения в запросе SELECT, но это не так - формат Values гораздо более ограничен.

Далее пойдёт речь о полноценном парсере. О парсерах форматов, смотри раздел "Форматы".

Пробелы

Между синтаксическими конструкциями (в том числе, в начале и конце запроса) может быть расположено произвольное количество пробельных символов. К пробельным символам относятся пробел, таб, перевод строки, CR, form feed.

Комментарии

Поддерживаются комментарии в SQL-стиле и C-стиле. Комментарии в SQL-стиле: от -- до конца строки. Пробел после -- может не ставиться. Комментарии в C-стиле: от /* до */. Такие комментарии могут быть многострочными. Пробелы тоже не обязательны.

Ключевые слова

Ключевые слова (например, SELECT) регистронезависимы. Всё остальное (имена столбцов, функций и т. п.), в отличие от стандарта SQL, регистрозависимо. Ключевые слова не зарезервированы (а всего лишь парсятся как ключевые слова в соответствующем контексте).

Идентификаторы

Идентификаторы (имена столбцов, функций, типов данных) могут быть квотированными или не квотированными. Не квотированные идентификаторы начинаются на букву латинского алфавита или подчёркивание; продолжаются на букву латинского алфавита или подчёркивание или цифру. Короче говоря, должны соответствовать регекспу ^[a-zA-Z_][0-9a-zA-Z_]*$. Примеры: x, _1, X_y__Z123_. Квотированные идентификаторы расположены в обратных кавычках `id` (также, как в MySQL), и могут обозначать произвольный (непустой) набор байт. При этом, внутри записи такого идентификатора, символы (например, символ обратной кавычки) могут экранироваться с помощью обратного слеша. Правила экранирования такие же, как в строковых литералах (см. ниже). Рекомендуется использовать идентификаторы, которые не нужно квотировать.

Литералы

Бывают числовые, строковые и составные литералы.

Числовые литералы

Числовой литерал пытается распарситься: - сначала как 64-битное число без знака, с помощью функци strtoull; - если не получилось - то как 64-битное число со знаком, с помощью функци strtoll; - если не получилось - то как число с плавающей запятой, с помощью функци strtod; - иначе - ошибка.

Соответствующее значение будет иметь тип минимального размера, который вмещает значение. Например, 1 парсится как UInt8, а 256 - как UInt16. Подробнее смотрите "Типы данных".

Примеры: 1, 18446744073709551615, 0xDEADBEEF, 01, 0.1, 1e100, -1e-100, inf, nan.

Строковые литералы

Поддерживаются только строковые литералы в одинарных кавычках. Символы внутри могут быть экранированы с помощью обратного слеша. Следующие escape-последовательности имеют соответствующее специальное значение: \b, \f, \r, \n, \t, \0. Во всех остальных случаях, последовательности вида \x, где x - любой символ, преобразуется в x. Таким образом, могут быть использованы последовательности \' и \\. Значение будет иметь тип String.

Составные литералы

Поддерживаются конструкции для массивов: [1, 2, 3] и кортежей: (1, 'Hello, world!', 2). На самом деле, это вовсе не литералы, а выражение с оператором создания массива и оператором создания кортежа, соответственно. Подробнее смотри в разделе "Операторы2". Массив должен состоять хотя бы из одного элемента, а кортеж - хотя бы из двух. Кортежи носят служебное значение для использования в секции IN запроса SELECT. Кортежи могут быть получены в качестве результата запроса, но не могут быть сохранены в базу (за исключением таблиц типа Memory).

Функции

Функции записываются как идентификатор со списком аргументов (возможно, пустым) в скобках. В отличие от стандартного SQL, даже в случае пустого списка аргументов, скобки обязательны. Пример: now(). Бывают обычные и агрегатные функции (смотрите раздел "Агрегатные функции"). Некоторые агрегатные функции могут содержать два списка аргументов в круглых скобках. Пример: quantile(0.9)(x). Такие агрегатные функции называются "параметрическими", а первый список аргументов называется "параметрами". Синтаксис агрегатных функций без параметров ничем не отличается от обычных функций.

Операторы

Операторы преобразуются в соответствующие им функции во время парсинга запроса, с учётом их приоритета и ассоциативности. Например, выражение 1 + 2 * 3 + 4 преобразуется в plus(plus(1, multiply(2, 3)), 4). Подробнее смотрите раздел "Операторы2" ниже.

Типы данных и движки таблиц

Типы данных и движки таблиц в запросе CREATE записываются также, как идентификаторы или также как функции. То есть, могут содержать или не содержать список аргументов в круглых скобках. Подробнее смотрите разделы "Типы данных", "Движки таблиц", "CREATE".

Синонимы

В запросе SELECT, в выражениях могут быть указаны синонимы с помощью ключевого слова AS. Слева от AS стоит любое выражение. Справа от AS стоит идентификатор - имя для синонима. В отличие от стандартного SQL, синонимы могут объявляться не только на верхнем уровне выражений:

SELECT (1 AS n) + 2, n

В отличие от стандартного SQL, синонимы могут использоваться во всех секциях запроса, а не только SELECT.

Звёздочка

В запросе SELECT, вместо выражения может стоять звёздочка. Подробнее смотрите раздел "SELECT".

Выражения

Выражение представляет собой функцию, идентификатор, литерал, применение оператора, выражение в скобках, подзапрос, звёздочку; и может содержать синоним. Список выражений - одно выражение или несколько выражений через запятую. Функции и операторы, в свою очередь, в качестве аргументов, могут иметь произвольные выражения.

Запросы

CREATE DATABASE

CREATE DATABASE [IF NOT EXISTS] db_name

- создаёт базу данных db_name. База данных - это просто директория для таблиц. Если написано IF NOT EXISTS, то запрос не будет возвращать ошибку, если база данных уже существует.

CREATE TABLE

Запрос CREATE TABLE может иметь несколько форм.

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. Также могут быть указаны выражения для значений по умолчанию - смотрите ниже.

CREATE [TEMPORARY] TABLE [IF NOT EXISTS] [db.]name AS [db2.]name2 [ENGINE = engine]

Создаёт таблицу с такой же структурой, как другая таблица. Можно указать другой движок для таблицы. Если движок не указан, то будет выбран такой же движок, как у таблицы db2.name2.

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

CREATE [MATERIALIZED] VIEW [IF NOT EXISTS] [db.]name [ENGINE = engine] [POPULATE] AS SELECT ...

Создаёт представление. Представления бывают двух видов - обычные и материализованные (MATERIALIZED).

Обычные представления не хранят никаких данных, а всего лишь производят чтение из другой таблицы. То есть, обычное представление - не более чем сохранённый запрос. При чтении из представления, этот сохранённый запрос, используется в качестве подзапроса в секции FROM. Для примера, пусть вы создали представление:

CREATE VIEW view AS SELECT ...
и написали запрос:
SELECT a, b, c FROM view
Этот запрос полностью эквивалентен использованию подзапроса:
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.

DROP DATABASE [IF EXISTS] db

Удаляет все таблицы внутри базы данных db, а затем саму базу данных db. Если указано IF EXISTS - не выдавать ошибку, если база данных не существует.

DROP TABLE [IF EXISTS] [db.]name

Удаляет таблицу. Если указано IF EXISTS - не выдавать ошибку, если таблица не существует или база данных не существует.

DETACH

DETACH TABLE [IF EXISTS] [db.]name

Удаляет из сервера информацию о таблице. Сервер перестаёт знать о существовании таблицы. Но ни данные, ни метаданные таблицы не удаляются. При следующем запуске сервера, сервер прочитает метаданные и снова узнает о таблице. Также, "отцепленную" таблицу можно прицепить заново запросом ATTACH (за исключением системных таблиц, для которых метаданные не хранятся).

Запроса DETACH DATABASE нет.

RENAME

RENAME TABLE [db11.]name11 TO [db12.]name12, [db21.]name21 TO [db22.]name22, ...

Переименовывает одну или несколько таблиц. Все таблицы переименовываются под глобальной блокировкой. Переименовывание таблицы является лёгкой операцией. Если вы указали после TO другую базу данных, то таблица будет перенесена в эту базу данных. При этом, директории с базами данных должны быть расположены в одной файловой системе (иначе возвращается ошибка).

ALTER

Запрос ALTER поддерживается только для таблиц типа *MergeTree, а также Merge и Distributed. Запрос имеет несколько вариантов.

Манипуляции со столбцами

ALTER TABLE [db].name ADD|DROP|MODIFY COLUMN ...

Позволяет изменить структуру таблицы. В запросе указывается список из одного или более действий через запятую. Каждое действие - операция над столбцом.

Существуют следующие действия:

ADD COLUMN name [type] [default_expr] [AFTER name_after]

Добавляет в таблицу новый столбец с именем name, типом type и выражением для умолчания default_expr (смотрите раздел "Значения по умолчанию"). Если указано AFTER name_after (имя другого столбца), то столбец добавляется (в список столбцов таблицы) после указанного. Иначе, столбец добавляется в конец таблицы. Внимательный читатель может заметить, что отсутствует возможность добавить столбец в начало таблицы. Для цепочки действий, name_after может быть именем столбца, который добавляется в одном из предыдущих действий.

Добавление столбца всего лишь меняет структуру таблицы, и не производит никаких действий с данными - соответствующие данные не появляются на диске после ALTER-а. При чтении из таблицы, если для какого-либо столбца отсутствуют данные, то он заполняется значениями по умолчанию (выполняя выражение по умолчанию, если такое есть, или нулями, пустыми строками). Также, столбец появляется на диске при слиянии кусков данных (см. MergeTree).

Такая схема позволяет добиться мгновенной работы запроса ALTER и отсутствия необходимости увеличивать объём старых данных.

DROP COLUMN name

Удаляет столбец с именем name.

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

MODIFY COLUMN name [type] [default_expr]

Изменяет тип столбца name на type и/или выражение для умолчания на default_expr. При изменении типа, значения преобразуются так, как если бы к ним была применена функция toType.

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

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

Выполнение производится в несколько стадий: - подготовка временных (новых) файлов с изменёнными данными; - переименование старых файлов; - переименование временных (новых) файлов в старые; - удаление старых файлов.

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

Не поддерживается изменение типа столбца у массивов и вложенных структур данных.

Запрос ALTER позволяет создавать и удалять отдельные элементы (столбцы) вложенных структур данных, но не вложенные структуры данных целиком. Для добавления вложенной структуры данных, вы можете добавить столбцы с именем вида name.nested_name и типом Array(T) - вложенная структура данных полностью эквивалентна нескольким столбцам-массивам с именем, имеющим одинаковый префикс до точки.

Отсутствует возможность удалять или изменять тип у столбцов, входящих в первичный ключ, или ключ для сэмплирования (в общем, входящих в выражение ENGINE).

Если возможностей запроса 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:

SELECT * FROM system.parts WHERE active

active - учитывать только активные куски. Неактивными являются, например, исходные куски оставшиеся после слияния в более крупный кусок - такие куски удаляются приблизительно через 10 минут после слияния.

Другой способ посмотреть набор кусков и партиций - зайти в директорию с данными таблицы. Директория с данными - /opt/clickhouse/data/database/table/, где /opt/clickhouse/ - путь к данным ClickHouse, database - имя базы данных, table - имя таблицы. Пример:

$ ls -l /opt/clickhouse/data/test/visits/
total 48
drwxrwxrwx 2 metrika metrika 20480 мая   13 02:58 20140317_20140323_2_2_0
drwxrwxrwx 2 metrika metrika 20480 мая   13 02:58 20140317_20140323_4_4_0
drwxrwxrwx 2 metrika metrika  4096 мая   13 02:55 detached
-rw-rw-rw- 1 metrika metrika     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.

Для реплицируемых таблиц также присутствует директория unreplicated. В этой директории может располагаться часть данных таблицы, не участвующая в репликации. Такие данные присутствуют только если таблица была преобразована из нереплицируемой (смотрите раздел "Преобразование из MergeTree в ReplicatedMergeTree").

ALTER TABLE [db.]table DETACH [UNREPLICATED] PARTITION 'name'

Перенести все данные для партиции с именем name в директорию detached и забыть про них. Имя партиции указывается в формате YYYYMM. Оно может быть указано в одинарных кавычках или без них.

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

Если указано UNREPLICATED (работает только для реплицируемых таблиц), то запрос будет выполнен для unreplicated части реплицируемой таблицы.

Запрос реплицируется - данные будут перенесены в директорию detached и забыты на всех репликах. Запрос может быть отправлен только на реплику-лидер. Вы можете узнать, является ли реплика лидером, сделав SELECT в системную таблицу system.replicas. Или, проще, вы можете выполнить запрос на всех репликах, и на всех кроме одной, он кинет исключение.

ALTER TABLE [db.]table DROP [UNREPLICATED] PARTITION 'name'

Аналогично операции DETACH. Удалить данные из таблицы. Куски с данными будут помечены как неактивные и будут полностью удалены примерно через 10 минут. Запрос реплицируется - данные будут удалены на всех репликах.

ALTER TABLE [db.]table ATTACH [UNREPLICATED] PARTITION|PART 'name'

Добавить данные в таблицу из директории detached. Или, если указано UNREPLICATED, то переносит данные из unreplicated части в реплицируемые данные.

Существует возможность добавить данные для целой партиции (PARTITION) или отдельный кусок (PART). В случае PART, укажите полное имя куска в одинарных кавычках.

Запрос реплицируется. Каждая реплика проверяет, если ли данные в директории detached. Если данные есть - проверяет их целостность, проверяет их соответствие данным на сервере-инициаторе запроса, и если всё хорошо, то добавляет их. Если нет, то скачивает данные с реплики-инициатора запроса, или с другой реплики, на которой уже добавлены эти данные.

То есть, вы можете разместить данные в директории detached на одной реплике и, с помощью запроса ALTER ... ATTACH добавить их в таблицу на всех репликах.

ALTER TABLE [db.]table FREEZE PARTITION 'name'

Создаёт локальный бэкап одной или нескольких партиций. В качестве имени может быть указано полное имя партиции (например, 201403) или его префикс (например, 2014) - тогда бэкап будет создан для всех соответствующих партиций.

Запрос делает следующее: для снэпшота данных на момент его выполнения, создаёт hardlink-и на данные таблиц в директории /opt/clickhouse/shadow/N/... /opt/clickhouse/ - рабочая директория ClickHouse из конфига. N - инкрементальный номер бэкапа. Структура директорий внутри бэкапа создаётся такой же, как внутри /opt/clickhouse/. Также делает chmod всех файлов, запрещая запись в них.

Создание бэкапа происходит почти мгновенно (но сначала дожидается окончания выполняющихся в данный момент запросов к соответствующей таблице). Бэкап изначально не занимает места на диске. При дальнейшей работе системы, бэкап может отнимать место на диске, по мере модификации данных. Если бэкап делается для достаточно старых данных, то он не будет отнимать место на диске.

После создания бэкапа, данные из /opt/clickhouse/shadow/ можно скопировать на удалённый сервер и затем удалить на локальном сервере. Весь процесс бэкапа не требует остановки сервера.

Запрос ALTER ... FREEZE PARTITION не реплицируется. То есть, локальный бэкап создаётся только на локальном сервере.

В качестве альтернативного варианта, вы можете скопировать данные из директории /opt/clickhouse/data/database/table вручную. Но если это делать при запущенном сервере, то возможны race conditions при копировании директории с добавляющимися/изменяющимися файлами, и бэкап может быть неконсистентным. Этот вариант может использоваться, если сервер не запущен - тогда полученные данные будут такими же, как после запроса ALTER TABLE t FREEZE PARTITION.

ALTER TABLE ... FREEZE PARTITION копирует только данные, но не метаданные таблицы. Чтобы сделать бэкап метаданных таблицы, скопируйте файл /opt/clickhouse/metadata/database/table.sql

Для восстановления из бэкапа: - создайте таблицу, если её нет, с помощью запроса CREATE. Запрос можно взять из .sql файла (замените в нём ATTACH на CREATE); - скопируйте данные из директории data/database/table/ внутри бэкапа в директорию /opt/clickhouse/data/database/table/detached/ - выполните запросы ALTER TABLE ... ATTACH PARTITION YYYYMM, где YYYYMM - месяц, для каждого месяца.

Таким образом, данные из бэкапа будут добавлены в таблицу. Восстановление из бэкапа, так же, не требует остановки сервера.

Бэкапы и репликация

Репликация защищает от аппаратных сбоев. В случае, если на одной из реплик у вас исчезли все данные, то восстановление делается по инструкции в разделе "Восстановление после сбоя".

Для защиты от аппаратных сбоев, обязательно используйте репликацию. Подробнее про репликацию написано в разделе "Репликация данных".

Бэкапы защищают от человеческих ошибок (случайно удалили данные, удалили не те данные или не на том кластере, испортили данные). Для баз данных большого объёма, бывает затруднительно копировать бэкапы на удалённые серверы. В этих случаях, для защиты от человеческой ошибки, можно держать бэкап на том же сервере (он будет лежать в /opt/clickhouse/shadow/).

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

SHOW DATABASES [FORMAT format]

Выводит список всех баз данных. Запрос полностью аналогичен запросу SELECT name FROM system.databases [FORMAT format] Смотрите также раздел "Форматы".

SHOW TABLES

SHOW TABLES [FROM db] [LIKE 'pattern'] [FORMAT format]

Вывозит список таблиц - из текущей БД или из БД db, если указано FROM db; - всех, или имя которых соответствует шаблону pattern, если указано LIKE 'pattern';

Запрос полностью аналогичен запросу: SELECT name FROM system.tables WHERE database = 'db' [AND name LIKE 'pattern'] [FORMAT format] Смотрите также раздел "Оператор LIKE".

SHOW PROCESSLIST

SHOW PROCESSLIST [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 [FORMAT format].

Полезный совет (выполните в консоли):

watch -n1 "clickhouse-client --query='SHOW PROCESSLIST'"

SHOW CREATE TABLE

SHOW CREATE TABLE [db.]table [FORMAT format]

Возвращает один столбец statement типа String, содержащий одно значение - запрос CREATE, с помощью которого создана указанная таблица.

DESCRIBE TABLE

DESC|DESCRIBE TABLE [db.]table [FORMAT format]

Возвращает два столбца: name, type типа String, в которых описаны имена и типы столбцов указанной таблицы.

Вложенные структуры данных выводятся в "развёрнутом" виде. То есть, каждый столбец - по отдельности, с именем через точку.

EXISTS

EXISTS TABLE [db.]name [FORMAT format]

Возвращает один столбец типа UInt8, содержащий одно значение - 0, если таблицы или БД не существует и 1, если таблица в указанной БД существует.

USE

USE db

Позволяет установить текущую базу данных для сессии. Текущая база данных используется для поиска таблиц, если база данных не указана в запросе явно через точку перед именем таблицы. При использовании HTTP протокола, запрос не может быть выполнен, так как понятия сессии не существует.

SET

SET [GLOBAL] param = value

Позволяет установить настройку param в значение value. Также можно одним запросом установить все настройки из заданного профиля настроек - для этого, укажите в качестве имени настройки profile. Подробнее смотри раздел "Настройки". Настройка устанавливается на сессию, или на сервер (глобально), если указано GLOBAL. При установке глобальной настроки, настройка на все уже запущенные сессии, включая текущую сессию, не устанавливается, а будет использована только для новых сессий.

Настройки, заданные с помощью SET GLOBAL имеют меньший приоритет по сравнению с настройками, указанными в профиле пользователя, в конфигурационном файле. То есть, переопределить такие настройки с помощью SET GLOBAL невозможно.

При перезапуске сервера, теряются глобальные настройки, установленные с помощью SET GLOBAL. Установить настройки, которые переживут перезапуск сервера, можно только с помощью конфигурационного файла сервера. (Это не может быть сделано с помощью запроса SET.)

OPTIMIZE

OPTIMIZE TABLE [db.]name

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

Для реплицируемых таблиц, запрос OPTIMIZE применяется только к нереплицируемым данным (эти данные присутствуют только после преобразования нереплицируемой таблицы в реплицируемую). Если такие отсутствуют, то запрос ничего не делает.

INSERT

Запрос имеет несколько вариантов.

INSERT INTO [db.]table [(c1, c2, c3)] VALUES (v11, v12, v13), (v21, v22, v23), ...

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

INSERT INTO [db.]table [(c1, c2, c3)] FORMAT Values (v11, v12, v13), (v21, v22, v23), ...
INSERT INTO [db.]table [(c1, c2, c3)] FORMAT format ...

Вставка данных в произвольном указанном формате. Сами данные идут после format, после всех пробельных символов до первого перевода строки, если он есть, включая его, или после всех пробельных символов, если переводов строки нет. Рекомендуется писать данные начиная со следующей строки (это важно, если данные начинаются с пробельных символов).

Пример:

INSERT INTO t FORMAT TabSeparated
11  Hello, world!
22  Qwerty

Подробнее про форматы данных смотрите в разделе "Форматы". В разделе "Интерфейсы" описано, как можно вставлять данные отдельно от запроса, при использовании клиента командной строки или HTTP интерфейса.

В запросе может быть опционально указан список столбцов для вставки. В этом случае, в остальные столбцы записываются значения по умолчанию. Значения по умолчанию вычисляются из DEFAULT выражений, указанных в определении таблицы, или, если DEFAULT не прописан явно - используются нули, пустые строки. Если настройка strict_insert_defaults выставлена в 1, то все столбцы, для которых нет явных DEFAULT-ов, должны быть указаны в запросе.

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.

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 ...]
    [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. Подробнее смотрите в разделе "Настройки". Присутствует возможность использовать внешнюю сортировку (с сохранением временных данных на диск). Внешней агрегации, а также 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 строкам. Пример:

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, который описан выше.

[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 может не указываться, и подразумевается агрегация по пустому набору ключей.

Пример:

SELECT
    count(),
    median(FetchTiming > 60 ? 60 : FetchTiming),
    count() - sum(Refresh)
FROM hits

Но, в отличие от стандартного SQL, если в таблице нет строк (вообще нет, или после фильтрации с помощью WHERE), в качестве результата возвращается пустой результат, а не результат из одной строки, содержащий "начальные" значения агрегатных функций.

В отличие от MySQL (и в соответствии со стандартом SQL), вы не можете получить какое-нибудь значение некоторого столбца, не входящего в ключ или агрегатную функцию (за исключением константных выражений). Для обхода этого, вы можете воспользоваться агрегатной функцией any (получить первое попавшееся значение) или min/max.

Пример:

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 - в этом случае, соответствующие тотальные значения будут соединены.

Секция 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 (по умолчанию), то внешняя сортировка выключена. Если она включена, то при достижении объёмом данных для сортировки указанного количества байт, накопленные данные будут отсортированы и сброшены во временный файл. После того, как все данные будут прочитаны, будет произведено слияние всех сортированных файлов и выдача результата. Файлы записываются в директорию /opt/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. Пример:

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.

Секция FORMAT

При указании FORMAT format, вы можете получить данные в любом указанном формате. Это может использоваться для удобства или для создания дампов. Подробнее смотрите раздел "Форматы". Если секция FORMAT отсутствует, то используется формат по умолчанию, который зависит от используемого интерфейся для доступа к БД и от настроек. Для HTTP интерфейса, а также для клиента командной строки, используемого в batch режиме, по умолчанию используется формат TabSeparated. Для клиента командной строки, используемого в интерактивном режиме, по умолчанию используется формат PrettyCompact (прикольные таблички, компактные).

При использовании клиента командной строки, данные на клиент передаются во внутреннем эффективном формате. При этом, клиент самостоятельно интерпретирует секцию FORMAT запроса и форматирует данные на своей стороне (снимая нагрузку на сеть и сервер).

Операторы IN

Операторы IN, NOT IN, GLOBAL IN, GLOBAL NOT IN рассматриваются отдельно, так как их функциональность достаточно богатая.

В качестве левой части оператора, может присутствовать как один столбец, так и кортеж.

Примеры:

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 (подготовленное множество, постоянно находящееся в оперативке), то множество не будет создаваться заново при каждом запросе.

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

SELECT (CounterID, UserID) IN (SELECT CounterID, UserID FROM ...) FROM ...

Типы столбцов слева и справа оператора IN, должны совпадать.

Исключение: если слева от IN стоит массив, то проверяется принадлежность хотя бы одного элемента массива множеству. Например, [1, 2, 3] IN (3, 4, 5) вернёт 1. Это немного нелогично, но удобно для реализации некоторой функциональности в Яндекс.Метрике.

Если слева от NOT IN стоит массив, то проверяется, что хотя бы один элемент массива не принадлежит множеству. Например, [1, 2, 3] NOT IN (3, 4, 5) вернёт 1. Это совсем нелогично, и лучше не полагаться на такое поведение. Лучше воспользуйтесь функциями высшего порядка. Пример: arrayAll(x -> x IN (3, 4, 5), [1, 2, 3]) вернёт 0 - проверяет принадлежность всех элементов массива множеству.

Оператор 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, на одном сервере, всегда выполняется только один раз. Зависимых подзапросов не существует.

Распределённые подзапросы

Существует два варианта IN-ов с подзапросами (аналогично, для JOIN-ов): обычный IN / JOIN и GLOBAL IN / GLOBAL JOIN. Они отличаются способом выполнения при распределённой обработке запроса.

При использовании обычного IN-а, запрос отправляется на удалённые серверы, и на каждом из них выполняются подзапросы в секциях IN / JOIN.

При использовании GLOBAL IN / GLOBAL JOIN-а, сначала выполняются все подзапросы для GLOBAL IN / GLOBAL JOIN-ов, и результаты складываются во временные таблицы. Затем эти временные таблицы передаются на каждый удалённый сервер, и на них выполняются запросы, с использованием этих переданных временных данных.

Если запрос не распределённый, используйте обычный IN / JOIN.

Следует быть внимательным при использовании подзапросов в секции IN / JOIN в случае распределённой обработки запроса.

Рассмотрим это на примерах. Пусть на каждом сервере кластера есть обычная таблица local_table. Пусть также есть таблица distributed_table типа Distributed, которая смотрит на все серверы кластера.

При запросе к распределённой таблице distributed_table, запрос будет отправлен на все удалённые серверы, и на них будет выполнен с использованием таблицы local_table.

Например, запрос SELECT uniq(UserID) FROM distributed_table будет отправлен на все удалённые серверы в виде SELECT uniq(UserID) FROM local_table , выполнен параллельно на каждом из них до стадии, позволяющей объединить промежуточные результаты; затем промежуточные результаты вернутся на сервер-инициатор запроса, будут на нём объединены, и финальный результат будет отправлен клиенту.

Теперь рассмотрим запрос с IN-ом: SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM local_table WHERE CounterID = 34) - расчёт пересечения аудиторий двух сайтов.

Этот запрос будет отправлен на все удалённые серверы в виде 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 внутри подзапроса. Запрос будет выглядить так: SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM distributed_table WHERE CounterID = 34)

Этот запрос будет отправлен на все удалённые серверы в виде SELECT uniq(UserID) FROM local_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM distributed_table WHERE CounterID = 34) На каждом удалённом сервере начнёт выполняться подзапрос. Так как в подзапросе используется распределённая таблица, то подзапрос будет, на каждом удалённом сервере, снова отправлен на каждый удалённый сервер, в виде SELECT UserID FROM local_table WHERE CounterID = 34 Например, если у вас кластер из 100 серверов, то выполнение всего запроса потребует 10 000 элементарных запросов, что, как правило, является неприемлимым.

В таких случаях, всегда следует использовать GLOBAL IN вместо IN. Рассмотрим его работу для запроса SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID GLOBAL IN (SELECT UserID FROM distributed_table WHERE CounterID = 34)

На сервере-инициаторе запроса, будет выполнен подзапрос SELECT UserID FROM distributed_table WHERE CounterID = 34 , и результат будет сложен во временную таблицу в оперативке. Затем запрос будет отправлен на каждый удалённый сервер в виде 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. Минимумы и максимумы считаются для числовых типов, дат, дат-с-временем. Для остальных столбцов, будут выведены значения по умолчанию.

Вычисляются дополнительные две строчки - минимумы и максимумы, соответственно. Эти дополнительные две строчки выводятся в форматах 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; - в подзапросах (так как из подзапросов выкидываются столбцы, не нужные для внешнего запроса). В других случаях, использование звёздочки является издевательством над системой, так как вместо преимуществ столбцовой СУБД вы получаете недостатки. То есть, использовать звёздочку не рекомендуется.

Внешние данные для обработки запроса

ClickHouse позволяет отправить на сервер данные, необходимые для обработки одного запроса, вместе с запросом SELECT. Такие данные будут положены во временную таблицу (см. раздел "Временные таблицы") и смогут использоваться в запросе (например, в операторах IN).

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

Если вам нужно будет выполнить более одного запроса с достаточно большими внешними данными - лучше не использовать эту функциональность, а загрузить данные в БД заранее.

Внешние данные могут быть загружены как с помощью клиента командной строки (в неинтерактивном режиме), так и через HTTP-интерфейс.

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

--external --file=... [--name=...] [--format=...] [--types=...|--structure=...]

Таких секций может быть несколько - по числу передаваемых таблиц. --external - маркер начала секции. --file - путь к файлу с дампом таблицы, или -, что обозначает stdin. Из stdin может быть считана только одна таблица.

Следующие параметры не обязательные: --name - имя таблицы. Если не указано - используется _data. --format - формат данных в файле. Если не указано - используется TabSeparated.

Должен быть указан один из следующих параметров: --types - список типов столбцов через запятую. Например, UInt64,String. Столбцы будут названы _1, _2, ... --structure - структура таблицы, в форме UserID UInt64, URL String. Определяет имена и типы столбцов.

Файлы, указанные в file, будут разобраны форматом, указанным в format, с использованием типов данных, указанных в types или structure. Таблица будет загружена на сервер, и доступна там в качестве временной таблицы с именем name.

Примеры:

echo -ne "1\n2\n3\n" | clickhouse-client --query="SELECT count() FROM test.visits WHERE TraficSourceID IN _data" --external --file=- --types=Int8
849897
cat /etc/passwd | sed 's/:/\t/g' | clickhouse-client --query="SELECT shell, count() AS c FROM passwd GROUP BY shell ORDER BY c DESC" --external --file=- --name=passwd --structure='login String, unused String, uid UInt16, gid UInt16, comment String, home String, shell String'
/bin/sh 20
/bin/false      5
/bin/bash       4
/usr/sbin/nologin       1
/bin/sync       1

При использовании HTTP интерфейса, внешние данные передаются в формате multipart/form-data. Каждая таблица передаётся отдельным файлом. Имя таблицы берётся из имени файла. В query_string передаются параметры name_format, name_types, name_structure, где name - имя таблицы, которой соответствуют эти параметры. Смысл параметров такой же, как при использовании клиента командной строки.

Пример:

cat /etc/passwd | sed 's/:/\t/g' > passwd.tsv

curl -F 'passwd=@passwd.tsv;' 'http://localhost:8123/?query=SELECT+shell,+count()+AS+c+FROM+passwd+GROUP+BY+shell+ORDER+BY+c+DESC&passwd_structure=login+String,+unused+String,+uid+UInt16,+gid+UInt16,+comment+String,+home+String,+shell+String'
/bin/sh 20
/bin/false      5
/bin/bash       4
/usr/sbin/nologin       1
/bin/sync       1

При распределённой обработке запроса, временные таблицы передаются на все удалённые серверы.

Движки таблиц

Движок таблицы (тип таблицы) определяет: - как и где хранятся данные - куда их писать и откуда читать; - какие запросы поддерживаются, и каким образом; - конкуррентный доступ к данным; - использование индексов, если есть; - возможно ли многопоточное выполнение запроса; - репликацию данных; - при чтении, движок обязан лишь достать нужный набор столбцов; но в некоторых случаях, запрос может быть частично обработан в рамках движка таблицы.

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

TinyLog

Самый простой движок таблиц, который хранит данные на диске. Каждый столбец хранится в отдельном сжатом файле. При записи, данные дописываются в конец файлов. Конкуррентный доступ к данным никак не ограничивается: - если вы одновременно читаете из таблицы и в другом запросе пишете в неё, то чтение будет завершено с ошибкой; - если вы одновременно пишите в таблицу в нескольких запросах, то данные будут битыми. Типичный способ использования этой таблицы - это write-once: сначала один раз только пишем данные, а потом сколько угодно читаем. Запросы выполняются в один поток. То есть, этот движок предназначен для сравнительно маленьких таблиц (рекомендуется до 1 000 000 строк). Этот движок таблиц имеет смысл использовать лишь в случае, если у вас есть много маленьких таблиц, так как он проще, чем движок Log (требуется открывать меньше файлов). Случай, когда у вас много маленьких таблиц, является гарантированно плохим по производительности, но может уже использоваться при работе с другой СУБД, и вам может оказаться удобнее перейти на использование таблиц типа TinyLog. Индексы не поддерживаются.

В Яндекс.Метрике таблицы типа TinyLog используются для промежуточных данных, обрабатываемых маленькими пачками.

Log

Отличается от TinyLog тем, что вместе с файлами столбцов лежит небольшой файл "засечек". Засечки пишутся на каждый блок данных и содержат смещение - с какого места нужно читать файл, чтобы пропустить заданное количество строк. Это позволяет читать данные из таблицы в несколько потоков. При конкуррентном доступе к данным, чтения могут выполняться одновременно, а записи блокируют чтения и друг друга. Движок Log не поддерживает индексы. Также, если при записи в таблицу произошёл сбой, то таблица станет битой, и чтения из неё будут возвращать ошибку. Движок Log подходит для временных данных, write-once таблиц, а также для тестовых и демонстрационных целей.

Memory

Хранит данные в оперативке, в несжатом виде. Данные хранятся именно в таком виде, в каком они получаются при чтении. То есть, само чтение из этой таблицы полностью бесплатно. Конкуррентный доступ к данным синхронизируется. Блокировки короткие: чтения и записи не блокируют друг друга. Индексы не поддерживаются. Чтение распараллеливается. За счёт отсутствия чтения с диска, разжатия и десериализации данных, удаётся достичь максимальной производительности (выше 10 ГБ/сек.) на простых запросах. (Стоит заметить, что во многих случаях, производительность движка MergeTree, почти такая же высокая.) При перезапуске сервера, данные из таблицы исчезают и таблица становится пустой. Обычно, использование этого движка таблиц является неоправданным. Тем не менее, он может использоваться для тестов, а также в задачах, где важно достичь максимальной скорости на не очень большом количестве строк (примерно до 100 000 000).

Движок Memory используется системой для временных таблиц - внешних данных запроса (смотрите раздел "Внешние данные для обработки запроса"), для реализации GLOBAL IN (смотрите раздел "Операторы IN").

Merge

Движок Merge (не путайте с движком MergeTree) не хранит данные самостоятельно, а позволяет читать одновременно из произвольного количества других таблиц. Чтение автоматически распараллеливается. Запись в таблицу не поддерживается. При чтении будут использованы индексы тех таблиц, из которых реально идёт чтение, если они существуют. Движок Merge принимает параметры: имя базы данных и регулярное выражение для таблиц. Пример:

Merge(hits, '^WatchLog')

- данные будут читаться из таблиц в базе hits, имена которых соответствуют регекспу '^WatchLog'.

Вместо имени базы данных может использоваться константное выражение, возвращающее строку. Например, currentDatabase().

Регулярные выражения - re2 (как PCRE, но без особых извратов), регистрозависимые. Смотрите замечание об экранировании в регулярных выражениях в разделе "match".

При выборе таблиц для чтения, сама Merge-таблица не будет выбрана, даже если попадает под регексп - чтобы не возникло циклов. Впрочем, вы можете создать две Merge-таблицы, которые будут пытаться бесконечно читать данные друг-друга. Этого делать не нужно.

Типичный способ использования движка Merge - возможность работы с большим количеством таблиц типа TinyLog, как с одной.

Виртуальные столбцы

Виртуальные столбцы - столбцы, предоставляемые движком таблиц, независимо от определения таблицы. То есть, такие столбцы не указываются в CREATE TABLE, но доступны для SELECT-а.

Виртуальные столбцы отличаются от обычных следующими особенностями: - они не указываются в определении таблицы; - в них нельзя вставить данные при INSERT-е; - при INSERT-е без указания списка столбцов, виртуальные столбцы не учитываются; - они не выбираются при использовании звёздочки (SELECT *); - виртуальные столбцы не показываются в запросах SHOW CREATE TABLE и DESC TABLE;

Таблица типа Merge содержит виртуальный столбец _table типа String. (Если в таблице уже есть столбец _table, то виртуальный столбец называется _table1; если уже есть _table1, то _table2 и т. п.) Он содержит имя таблицы, из которой были прочитаны данные.

Если секция WHERE/PREWHERE содержит (в качестве одного из элементов конъюнкции или в качестве всего выражения) условия на столбец _table, не зависящие от других столбцов таблицы, то эти условия используются как индекс: условия выполняются над множеством имён таблиц, из которых нужно читать данные, и чтение будет производиться только из тех таблиц, для которых условия сработали.

Distributed

Движок Distributed не хранит данные самостоятельно, а позволяет обрабатывать запросы распределённо, на нескольких серверах. Чтение автоматически распараллеливается. При чтении будут использованы индексы таблиц на удалённых серверах, если есть. Движок Distributed принимает параметры: имя кластера в конфигурационном файле сервера, имя удалённой базы данных, имя удалённой таблицы, а также (не обязательно) ключ шардирования. Пример:

Distributed(calcs, default, hits[, sharding_key])

- данные будут читаться со всех серверов кластера calcs, из таблицы default.hits, расположенной на каждом сервере кластера. Данные не только читаются, но и частично (настолько, насколько это возможно) обрабатываются на удалённых серверах. Например, при запросе с GROUP BY, данные будут агрегированы на удалённых серверах, промежуточные состояния агрегатных функций будут отправлены на запросивший сервер; затем данные будут доагрегированы.

Вместо имени базы данных может использоваться константное выражение, возвращающее строку. Например, currentDatabase().

calcs - имя кластера в конфигурационном файле сервера.

Кластеры задаются следующим образом:

<!-- Группы удалённых серверов, которые могут быть подключены в таблицах типа Distributed. -->
<remote_servers>
    <test>
        <node>
            <host>example01-01-1</host>
            <port>9000</port>
            <weight>1</weight>  <!-- Не обязательно. Вес шарда при записи данных. По умолчанию, 1.-->
        </node>
        <node>
            <host>example01-01-2</host>
            <port>9001</port>
        </node>
    </test>
</remote_servers>

Здесь задан кластер с именем test, состоящий из двух серверов (шардов).

Также возможно задать кластер, состоящий из шардов и реплик:

<remote_servers>
    <logs>
        <shard>
            <!-- Не обязательно. Вес шарда при записи данных. По умолчанию, 1. -->
            <weight>1</weight>
            <!-- Не обязательно. Записывать ли данные только на одну, любую из реплик. По умолчанию, false - записывать данные на все реплики. -->
            <internal_replication>false</internal_replication>
            <replica>
                <host>example01-01-1</host>
                <port>9000</port>
            </replica>
            <replica>
                <host>example01-01-2</host>
                <port>9000</port>
            </replica>
        </shard>
        <shard>
            <weight>2</weight>
            <internal_replication>false</internal_replication>
            <replica>
                <host>example01-02-1</host>
                <port>9000</port>
            </replica>
            <replica>
                <host>example01-02-2</host>
                <port>9000</port>
            </replica>
        </shard>
    </logs>
</remote_servers>

Здесь задан кластер с именем logs, состоящий из двух шардов, каждый из которых состоит из двух реплик. Шардами называются серверы, содержащие разные части данных (чтобы прочитать все данные, нужно идти на все шарды). Репликами называются дублирующие серверы (чтобы прочитать данные, можно идти за данными на любую из реплик).

В качестве параметров для каждого сервера указываются host, port и, не обязательно, user, password. host - адрес удалённого сервера. Может быть указан домен, или IPv4 или IPv6 адрес. В случае указания домена, при старте сервера делается DNS запрос, и результат запоминается на всё время работы сервера. Если DNS запрос неуспешен, то сервер не запускается. Если вы изменяете DNS-запись, перезапустите сервер. port - TCP-порт для межсерверного взаимодействия (в конфиге - tcp_port, обычно 9000). Не перепутайте с http_port. user - имя пользователя для соединения с удалённым сервером. По-умолчанию - default. Этот пользователь должен иметь доступ для соединения с указанным сервером. Доступы настраиваются в файле users.xml, подробнее смотрите в разделе "Права доступа". password - пароль для соединения с удалённым сервером, в открытом виде. По-умолчанию - пустая строка.

При указании реплик, для каждого из шардов, при чтении, будет вырбана одна из доступных реплик. Можно настроить алгоритм балансировки нагрузки (то есть, предпочтения, на какую из реплик идти) - см. настройку load_balancing. Если соединение с сервером не установлено, то будет произведена попытка соединения с небольшим таймаутом. Если соединиться не удалось, то будет выбрана следующая реплика, и так для всех реплик. Если попытка соединения для всех реплик не удалась, то будут снова произведены попытки соединения по кругу, и так несколько раз. Это работает в пользу отказоустойчивости, хотя и не обеспечивает полную отказоустойчивость: удалённый сервер может принять соединение, но не работать, или плохо работать.

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

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

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

Для просмотра имеющихся кластеров, вы можете использовать системную таблицу system.clusters.

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

Не поддерживаются Distributed таблицы, смотрящие на другие Distributed таблицы (за исключением случаев, когда у Distributed таблицы всего один шард). Вместо этого, сделайте так, чтобы Distributed таблица смотрела на "конечные" таблицы.

Как видно, движок Distributed требует прописывания кластера в конфигурационный файл; добавление кластеров и серверов требует перезапуска. Если вам необходимо каждый раз отправлять запрос на неизвестный набор шардов и реплик, вы можете не создавать Distributed таблицу, а воспользоваться табличной функцией remote. Смотрите раздел "Табличные функции".

Есть два способа записывать данные на кластер:

Во первых, вы можете самостоятельно определять, на какие серверы какие данные записывать, и выполнять запись непосредственно на каждый шард. То есть, делать INSERT в те таблицы, на которые "смотрит" распределённая таблица. Это наиболее гибкое решение - вы можете использовать любую схему шардирования, которая может быть нетривиальной из-за требований предметной области. Также это является наиболее оптимальным решением, так как данные могут записываться на разные шарды полностью независимо.

Во вторых, вы можете делать INSERT в Distributed таблицу. В этом случае, таблица будет сама распределять вставляемые данные по серверам. Для того, чтобы писать в Distributed таблицу, у неё должен быть задан ключ шардирования (последний параметр). Также, если шард всего-лишь один, то запись работает и без указания ключа шардирования (так как в этом случае он не имеет смысла).

У каждого шарда в конфигурационном файле может быть задан "вес" (weight). По умолчанию, вес равен единице. Данные будут распределяться по шардам в количестве, пропорциональном весу шарда. Например, если есть два шарда, и у первого выставлен вес 9, а у второго 10, то на первый будет отправляться 9 / 19 доля строк, а на второй - 10 / 19.

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

Если он выставлен в true, то для записи будет выбираться первая живая реплика и данные будут писаться на неё. Этот вариант следует использовать, если Distributed таблица "смотрит" на реплицируемые таблицы. То есть, если таблица, в которую будут записаны данные, будет сама заниматься их репликацией.

Если он выставлен в false (по умолчанию), то данные будут записываться на все реплики. По сути, это означает, что Distributed таблица занимается репликацией данных самостоятельно. Это хуже, чем использование реплицируемых таблиц, так как не контролируется консистентность реплик, и они со временем будут содержать немного разные данные.

Для выбора шарда, на который отправляется строка данных, вычисляется выражение шардирования, и берётся его остаток от деления на суммарный вес шардов. Строка отправляется на шард, соответствующий полуинтервалу остатков от prev_weights до prev_weights + weight, где prev_weights - сумма весов шардов с меньшим номером, а weight - вес этого шарда. Например, если есть два шарда, и у первого выставлен вес 9, а у второго 10, то строка будет отправляться на первый шард для остатков из диапазона [0, 9), а на второй - для остатков из диапазона [10, 19).

Выражением шардирование может быть произвольное выражение от констант и столбцов таблицы, возвращающее целое число. Например, вы можете использовать выражение rand() для случайного распределения данных, или UserID - для распределения по остатку от деления идентификатора посетителя (тогда данные одного посетителя будут расположены на одном шарде, что упростит выполнение IN и JOIN по посетителям). Если распределение какого-либо столбца недостаточно равномерное, вы можете обернуть его в хэш функцию: intHash64(UserID).

Простой остаток от деления является довольно ограниченным решением для шардирования и подходит не для всех случаев. Он подходит для среднего и большого объёма данных (десятки серверов), но не для очень больших объёмов данных (сотни серверов и больше). В последнем случае, лучше использовать схему шардирования, продиктованную требованиями предметной области, и не использовать возможность записи в Distributed таблицы.

Ещё раз отметим, что Distributed таблица не умеет автоматически перешардировать данные. Во многих случаях можно обойтись без этого. Запросы SELECT отправляются на все шарды, и работают независимо от того, каким образом данные распределены по шардам (они могут быть распределены полностью случайно). При добавлении нового шарда, можно не переносить на него старые данные, а записывать новые данные с большим весом - данные будут распределены слегка неравномерно, но запросы будут работать корректно и достаточно эффективно.

Беспокоиться о схеме шардирования имеет смысл в следующих случаях: - используются запросы, требующие соединение данных (IN, JOIN) по определённому ключу - тогда если данные шардированы по этому ключу, то можно использовать локальные IN, JOIN вместо GLOBAL IN, GLOBAL JOIN, что кардинально более эффективно. - используется большое количество серверов (сотни и больше) и большое количество маленьких запросов (запросы отдельных клиентов - сайтов, рекламодателей, партнёров) - тогда, для того, чтобы маленькие запросы не затрагивали весь кластер, имеет смысл располагать данные одного клиента на одном шарде, или (вариант, который используется в Яндекс.Метрике) сделать двухуровневое шардирование: разбить весь кластер на "слои", где слой может состоять из нескольких шардов; данные для одного клиента располагаются на одном слое, но в один слой можно по мере необходимости добавлять шарды, в рамках которых данные распределены произвольным образом; создаются распределённые таблицы на каждый слой и одна общая распределённая таблица для глобальных запросов.

Запись данных осуществляется полностью асинхронно. При INSERT-е в Distributed таблицу, блок данных всего лишь записывается в локальную файловую систему. Данные отправляются на удалённые серверы в фоне, при первой возможности. Вы должны проверять, успешно ли отправляются данные, проверяя список файлов (данные, ожидающие отправки) в директории таблицы: /opt/clickhouse/data/database/table/.

Если после INSERT-а в Distributed таблицу, сервер перестал существовать или был грубо перезапущен (например, в следствие аппаратного сбоя), то записанные данные могут быть потеряны. Если в директории таблицы обнаружен повреждённый кусок данных, то он переносится в поддиректорию broken и больше не используется.

MergeTree

Движок MergeTree поддерживает индекс по первичному ключу и по дате, и обеспечивает возможность обновления данных в реальном времени. Это наиболее продвинутый движок таблиц в ClickHouse. Не путайте с движком Merge.

Движок принимает параметры: имя столбца типа Date, содержащего дату; выражение для семплирования (не обязательно); кортеж, определяющий первичный ключ таблицы; гранулированность индекса. Пример:

Пример без поддержки сэмплирования:

MergeTree(EventDate, (CounterID, EventDate), 8192)

Пример с поддержкой сэмплирования:

MergeTree(EventDate, intHash32(UserID), (CounterID, EventDate, intHash32(UserID)), 8192)

В таблице типа MergeTree обязательно должен быть отдельный столбец, содержащий дату. В этом примере, это - столбец EventDate. Тип столбца с датой - обязательно Date (а не DateTime).

Первичным ключом может быть кортеж из произвольных выражений (обычно это просто кортеж столбцов) или одно выражение.

Выражение для сэмплирования (использовать не обязательно) - произвольное выражение. Оно должно также присутствовать в первичном ключе. В примере используется хэширование по идентификатору посетителя, чтобы псевдослучайно перемешать данные в таблице для каждого CounterID и EventDate. То есть, при использовании секции SAMPLE в запросе, вы получите равномерно-псевдослучайную выборку данных для подмножества посетителей.

Таблица реализована, как набор кусочков. Каждый кусочек сортирован по первичному ключу. Также, для каждого кусочка прописана минимальная и максимальная дата. При вставке в таблицу, создаётся новый сортированный кусочек. В фоне, периодически инициируется процесс слияния. При слиянии, выбирается несколько кусочков, обычно наименьших, и сливаются в один большой сортированный кусочек.

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

При вставке, данные относящиеся к разным месяцам, разбиваются на разные кусочки. Кусочки, соответствующие разным месяцам, никогда не объединяются. Это сделано, чтобы обеспечить локальность модификаций данных (для упрощения бэкапов).

Кусочки объединяются до некоторого предельного размера - чтобы не было слишком длительных слияний.

Для каждого кусочка также пишется индексный файл. Индексный файл содержит значение первичного ключа для каждой index_granularity строки таблицы. То есть, это - разреженный индекс сортированных данных.

Для столбцов также пишутся "засечки" каждую index_granularity строку, чтобы данные можно было читать в определённом диапазоне.

При чтении из таблицы, запрос SELECT анализируется на предмет того, можно ли использовать индексы. Индекс может использоваться, если в секции WHERE/PREWHERE, в качестве одного из элементов конъюнкции, или целиком, есть выражение, представляющее операции сравнения на равенства, неравенства, а также IN над столбцами, входящими в первичный ключ / дату, а также логические связки над ними.

Таким образом, обеспечивается возможность быстро выполнять запросы по одному или многим диапазонам первичного ключа. Например, в указанном примере, будут быстро работать запросы для конкретного счётчика; для конкретного счётчика и диапазона дат; для конкретного счётчика и даты, для нескольких счётчиков и диапазона дат и т. п.

SELECT count() FROM table WHERE EventDate = toDate(now()) AND CounterID = 34
SELECT count() FROM table WHERE EventDate = toDate(now()) AND (CounterID = 34 OR CounterID = 42)
SELECT count() FROM table WHERE ((EventDate >= toDate('2014-01-01') AND EventDate <= toDate('2014-01-31')) OR EventDate = toDate('2014-05-01')) AND CounterID IN (101500, 731962, 160656) AND (CounterID = 101500 OR EventDate != toDate('2014-05-01'))

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

В этом примере, индекс не может использоваться:

SELECT count() FROM table WHERE CounterID = 34 OR URL LIKE '%upyachka%'

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

Для конкуррентного доступа к таблице, используется мульти-версионность. То есть, при одновременном чтении и обновлении таблицы, данные будут читаться из набора кусочков, актуального на момент запроса. Длинных блокировок нет. Вставки никак не мешают чтениям.

Чтения из таблицы автоматически распараллеливаются.

Поддерживается запрос OPTIMIZE, который вызывает один внеочередной шаг слияния.

Вы можете использовать одну большую таблицу, постоянно добавляя в неё данные небольшими пачками - именно для этого предназначен движок MergeTree.

Для всех типов таблиц семейства MergeTree возможна репликация данных - смотрите раздел "Репликация данных".

CollapsingMergeTree

Движок достаточно специфичен для Яндекс.Метрики. Отличается от MergeTree тем, что позволяет автоматически удалять - "схлопывать" некоторые пары строк при слиянии.

В Яндекс.Метрике есть обычные логи (например, лог хитов) и логи изменений. Логи изменений используются, чтобы инкрементально считать статистику по постоянно меняющимся данным. Например - логи изменений визитов, логи изменений истории посетителей. Визиты в Яндекс.Метрике постоянно меняются - например, увеличивается количество хитов в визите. Изменением какого либо объекта будем называть пару (?старые значения, ?новые значения). Старые значения могут отсутствовать, если объект создался. Новые значения могут отсутствовать, если объект удалился. Если объект изменился, но был раньше и не удалился - присутствует оба значения. В лог изменений, для каждого изменения, пишется от одной до двух записей. Каждая запись содержит все те же атрибуты, что и сам объект, и ещё специальный атрибут, который позволяет отличить старые и новые значения. Видно, что при изменении объектов, в лог изменений лишь дописываются новые записи и не трогаются уже имеющиеся.

Лог изменений позволяет инкрементально считать почти любую статистику. Для этого надо учитывать "новые" строки с положительным знаком, и "старые" строки с отрицательным знаком. То есть, возможно инкрементально считать все статистики, алгебраическая структура которых содержит операцию взятия обратного элемента. Большинство статистик именно такие. Также удаётся посчитать "идемпотентные" статистики, например, количество уникальных посетителей, так как при изменении визитов, уникальные посетители не удаляются.

Это - основная идея, благодаря которой Яндекс.Метрика работает в реальном времени.

CollapsingMergeTree принимает дополнительный параметр - имя столбца типа Int8, содержащего "знак" строки. Пример:

CollapsingMergeTree(EventDate, (CounterID, EventDate, intHash32(UniqID), VisitID), 8192, Sign)

Здесь Sign - столбец, содержащий -1 для "старых" значений и 1 для "новых" значений.

При слиянии, для каждой группы идущих подряд одинаковых значений первичного ключа (столбцов, по которым сортируются данные), остаётся не более одной строки со значением столбца sign_column = -1 ("отрицательной строки") и не более одиной строки со значением столбца sign_column = 1 ("положительной строки"). То есть - производится схлопывание записей из лога изменений.

Если количество положительных и отрицательных строк совпадает - то пишет первую отрицательную и последнюю положительную строку. Если положительных на 1 больше, чем отрицательных - то пишет только последнюю положительную строку. Если отрицательных на 1 больше, чем положительных - то пишет только первую отрицательную строку. Иначе - логическая ошибка, и ни одна из таких строк не пишется. (Логическая ошибка может возникать, если случано один кусок лога был вставлен более одного раза. Поэтому, об ошибке всего лишь пишется в лог сервера, и слияние продолжает работать.)

Как видно, от схлопывания не должны меняться результаты расчётов статистик. Изменения постепенно схлопываются так что в конце-концов, для почти каждого объекта, остаются лишь его последние значения. По сравнению с MergeTree, движок CollapsingMergeTree позволяет в несколько раз уменьшить объём данных.

Существует несколько способов получения полностью "схлопнутых" данных из таблицы типа CollapsingMergeTree: 1. Написать запрос с GROUP BY и агрегатными функциями, учитывающими знак. Например, чтобы посчитать количество, надо вместо count() написать sum(Sign); чтобы посчитать сумму чего-либо, надо вместо sum(x) написать sum(Sign * x) и т. п., а также добавить HAVING sum(Sign) > 0. Не все величины можно посчитать подобным образом. Например, агрегатные функции min, max не могут быть переписаны. 2. Если необходимо вынимать данные без агрегации (например, проверить наличие строк, самые новые значения которых удовлетворяют некоторым условиям), можно использовать модификатор FINAL для секции FROM. Это вариант существенно менее эффективен.

SummingMergeTree

Отличается от MergeTree тем, что суммирует данные при слиянии.

SummingMergeTree(EventDate, (OrderID, EventDate, BannerID, ...), 8192)

Столбцы для суммирования заданы неявно. При слиянии, для всех строчек с одинаковым значением первичного ключа (в примере - OrderID, EventDate, BannerID, ...), производится суммирование значений в числовых столбцах, не входящих в первичный ключ.

SummingMergeTree(EventDate, (OrderID, EventDate, BannerID, ...), 8192, (Shows, Clicks, Cost, ...))

Явно заданные столбцы для суммирования (последний параметр - Shows, Clicks, Cost, ...). При слиянии, для всех строчек с одинаковым значением первичного ключа, производится суммирование значений в указанных столбцах. Указанные столбцы также должны быть числовыми и не входить в первичный ключ.

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

Для остальных столбцов, не входящих в первичный ключ, при слиянии выбирается первое попавшееся значение.

При чтении, суммирование не делается само по себе. Если оно необходимо - напишите соответствующий GROUP BY.

Дополнительно, таблица может иметь вложенные структуры данных, которые обрабатываются особым образом. Если название вложенной таблицы заканчинвается на Map и она содержит не менее двух столбцов, удовлетворяющих следующим критериям: - первый столбец - числовой ((U)IntN, Date, DateTime), назовем его условно key, - остальные столбцы - арифметические ((U)IntN, Float32/64), условно (values...), то такая вложенная таблица воспринимается как отображение key => (values...) и при слиянии ее строк выполняется слияние элементов двух множеств по key со сложением соответствующих (values...). Примеры:

[(1, 100)] + [(2, 150)] -> [(1, 100), (2, 150)]
[(1, 100)] + [(1, 150)] -> [(1, 250)]
[(1, 100)] + [(1, 150), (2, 150)] -> [(1, 250), (2, 150)]
[(1, 100), (2, 150)] + [(1, -100)] -> [(2, 150)]

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

Этот движок таблиц разработан по просьбе БК, и является мало полезным. Помните, что при хранении лишь предагрегированных данных, вы теряете часть преимуществ системы.

AggregatingMergeTree

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

Чтобы это работало, используются: тип данных AggregateFunction, а также модификаторы -State и -Merge для агрегатных функций. Рассмотрим подробнее.

Существует тип данных AggregateFunction. Это параметрический тип данных. В качестве параметров передаются: имя аргетатной функции, затем типы её аргументов. Примеры:

CREATE TABLE t
(
    column1 AggregateFunction(uniq, UInt64),
    column2 AggregateFunction(anyIf, String, UInt8),
    column3 AggregateFunction(quantiles(0.5, 0.9), UInt64)
) ENGINE = ...

Столбец такого типа хранит состояние агрегатной функции.

Чтобы получить значение такого типа, следует использовать агрегатные функции с суффиксом State. Пример: uniqState(UserID), quantilesState(0.5, 0.9)(SendTiming) - в отличие от соответствующих функций uniq, quantiles, такие функции возвращают не готовое значение, а состояние. То есть, значение типа AggregateFunction.

Значение типа AggregateFunction нельзя вывести в Pretty-форматах. В других форматах, значения такого типа выводятся в виде implementation-specific бинарных данных. То есть, значения типа AggregateFunction не предназначены для вывода, сохранения в дамп.

Единственную полезную вещь, которую можно сделать со значениями типа AggregateFunction - это объединить состояния и получить результат, по сути - доагрегировать до конца. Для этого используются агрегатные функции с суффиксом Merge. Пример: uniqMerge(UserIDState), где UserIDState имеет тип AggregateFunction.

То есть, агрегатная функция с суффиксом Merge берёт множество состояний, объединяет их, и возвращает готовый результат. Для примера, эти два запроса возвращают один и тот же результат:

SELECT uniq(UserID) FROM table
SELECT uniqMerge(state) FROM (SELECT uniqState(UserID) AS state FROM table GROUP BY RegionID)

Существует движок AggregatingMergeTree. Он занимается тем, что при слияниях, выполняет объединение состояний агрегатных функций из разных строчек таблицы с одним значением первичного ключа.

В таблицу, содержащую столбцы типа AggregateFunction невозможно вставить строчку обычным запросом INSERT, так как невозможно явно указать значение типа AggregateFunction. Вместо этого, для вставки данных, следует использовать INSERT SELECT с агрегатными функциями -State.

При SELECT-е из таблицы AggregatingMergeTree, используйте GROUP BY и агрегатные функции с модификатором -Merge, чтобы доагрегировать данные.

Таблицы типа AggregatingMergeTree могут использоваться для инкрементальной агрегации данных, в том числе, для агрегирующих материализованных представлений.

Пример: Создаём материализованное представление типа AggregatingMergeTree, следящее за таблицей test.visits:

CREATE MATERIALIZED VIEW test.basic
ENGINE = AggregatingMergeTree(StartDate, (CounterID, StartDate), 8192)
AS SELECT
    CounterID,
    StartDate,
    sumState(Sign)    AS Visits,
    uniqState(UserID) AS Users
FROM test.visits
GROUP BY CounterID, StartDate;

Вставляем данные в таблицу test.visits. Данные будут также вставлены в представление, где они будут агрегированы:

INSERT INTO test.visits ...

Делаем SELECT из представления, используя GROUP BY, чтобы доагрегировать данные:

SELECT
    StartDate,
    sumMerge(Visits) AS Visits,
    uniqMerge(Users) AS Users
FROM test.basic
GROUP BY StartDate
ORDER BY StartDate;

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

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

Null

При записи в таблицу типа Null, данные игнорируются. При чтении из таблицы типа Null, возвращается пустота.

Тем не менее, есть возможность создать материализованное представление над таблицей типа Null. Тогда данные, записываемые в таблицу, будут попадать в представление.

View

Используется для реализации представлений (подробнее см. запрос CREATE VIEW). Не хранит данные, а хранит только указанный запрос SELECT. При чтении из таблицы, выполняет его (с удалением из запроса всех ненужных столбцов).

MaterializedView

Используется для реализации материализованных представлений (подробнее см. запрос CREATE MATERIALIZED VIEW). Для хранения данных, использует другой движок, который был указан при создании представления. При чтении из таблицы, просто использует этот движок.

Set

Представляет собой множество, постоянно находящееся в оперативке. Предназначено для использования в правой части оператора IN (смотрите раздел "Операторы IN").

В таблицу можно вставлять данные INSERT-ом - будут добавлены новые элементы в множество, с игнорированием дубликатов. Но из таблицы нельзя, непосредственно, делать SELECT. Единственная возможность чтения - использование в правой части оператора IN.

Данные постоянно находятся в оперативке. При INSERT-е, в директорию таблицы на диске, также пишутся блоки вставленных данных. При запуске сервера, эти данные считываются в оперативку. То есть, после перезапуска, данные остаются на месте.

При грубом перезапуске сервера, блок данных на диске может быть потерян или повреждён. В последнем случае, может потребоваться вручную удалить файл с повреждёнными данными.

Join

Представляет собой подготовленную структуру данных для JOIN-а, постоянно находящуюся в оперативке.

Join(ANY|ALL, LEFT|INNER, k1[, k2, ...])

Параметры движка: ANY|ALL - строгость, LEFT|INNER - тип. Эти параметры (задаются без кавычек) должны соответствовать тому JOIN-у, для которого будет использоваться таблица. k1, k2, ... - ключевые столбцы из секции USING, по которым будет делаться соединение.

Таблица не может использоваться для GLOBAL JOIN-ов.

В таблицу можно вставлять данные INSERT-ом, аналогично движку Set. В случае ANY, данные для дублирующихся ключей будут проигнорированы; в случае ALL - будут учитываться. Из таблицы нельзя, непосредственно, делать SELECT. Единственная возможность чтения - использование в качестве "правой" таблицы для JOIN.

Хранение данных на диске аналогично движку Set.

Buffer

Буферизует записываемые данные в оперативке, периодически сбрасывая их в другую таблицу. При чтении, производится чтение данных одновременно из буфера и из другой таблицы.

Buffer(database, table, num_layers, min_time, max_time, min_rows, max_rows, min_bytes, max_bytes)

Параметры движка: database, table - таблица, в которую сбрасывать данные. Вместо имени базы данных может использоваться константное выражение, возвращающее строку. num_layers - уровень паралеллизма. Физически таблица будет представлена в виде num_layers независимых буферов. Рекомендуемое значение - 16. min_time, max_time, min_rows, max_rows, min_bytes, max_bytes - условия для сброса данных из буфера.

Данные сбрасываются из буфера и записываются в таблицу назначения, если выполнены все min-условия или хотя бы одно max-условие. min_time, max_time - условие на время в секундах от момента первой записи в буфер; min_rows, max_rows - условие на количество строк в буфере; min_bytes, max_bytes - условие на количество байт в буфере.

При записи, данные вставляются в случайный из num_layers буферов. Или, если размер куска вставляемых данных достаточно большой (больше max_rows или max_bytes), то он записывается в таблицу назначения минуя буфер.

Условия для сброса данных учитываются отдельно для каждого из num_layers буферов. Например, если num_layers = 16 и max_bytes = 100000000, то максимальный расход оперативки будет 1.6 GB.

Пример:

CREATE TABLE merge.hits_buffer AS merge.hits ENGINE = Buffer(merge, hits, 16, 10, 100, 10000, 1000000, 10000000, 100000000)

Создаём таблицу merge.hits_buffer такой же структуры как merge.hits и движком Buffer. При записи в эту таблицу, данные буферизуются в оперативке и, в дальнейшем, записываются в таблицу merge.hits. Создаётся 16 буферов. Данные, имеющиеся в каждом из них будут сбрасываться, если прошло сто секунд, или записан миллион строк, или записано сто мегабайт данных; или если одновременно прошло десять секунд и записано десять тысяч строк и записано десять мегабайт данных. Для примера, если записана всего лишь одна строка, то через сто секунд она будет сброшена в любом случае. А если записано много строк, то они будут сброшены раньше.

При остановке сервера, при DROP TABLE или DETACH TABLE, данные из буфера тоже сбрасываются в таблицу назначения.

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

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

Если множество столбцов таблицы Buffer не совпадает с множеством столбцов подчинённой таблицы, то будут вставлено подмножество столбцов, которое присутствует в обеих таблицах.

Если у одного из столбцов таблицы Buffer и подчинённой таблицы не совпадает тип, то в лог сервера будет записано сообщение об ошибке и буфер будет очищен. То же самое происходит, если подчинённая таблица не существует в момент сброса буфера.

Если есть необходимость выполнить ALTER для подчинённой таблицы и для таблицы Buffer, то рекомендуется удалить таблицу Buffer, затем выполнить ALTER подчинённой таблицы, а затем создать таблицу Buffer заново.

При нештатном перезапуске сервера, данные, находящиеся в буфере, будут потеряны.

Для таблиц типа Buffer неправильно работают PREWHERE, FINAL и SAMPLE. Эти условия пробрасываются в таблицу назначения, но не используются для обработки данных в буфере. В связи с этим, рекомендуется использовать таблицу типа Buffer только для записи, а читать из таблицы назначения.

При добавлении данных в Buffer, один из буферов блокируется. Это приводит к задержкам, если одновременно делается чтение из таблицы.

Данные, вставляемые в таблицу Buffer, попадают в подчинённую таблицу в порядке, возможно отличающимся от порядка вставки, и блоками, возможно отличающимися от вставленных блоков. В связи с этим, трудно корректно использовать таблицу типа Buffer для записи в CollapsingMergeTree. Чтобы избежать проблемы, можно выставить num_layers в 1.

Если таблица назначения является реплицируемой, то при записи в таблицу Buffer будут потеряны некоторые ожидаемые свойства реплицируемых таблиц. Из-за произвольного изменения порядка строк и размеров блоков данных, перестаёт работать дедубликация данных, в результате чего исчезает возможность надёжной exactly once записи в реплицируемые таблицы.

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

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

Заметим, что даже для таблиц типа Buffer не имеет смысла вставлять данные по одной строке, так как таким образом будет достигнута скорость всего лишь в несколько тысяч строк в секунду, тогда как при вставке более крупными блоками, достижимо более миллиона строк в секунду (смотрите раздел "Производительность").

Репликация данных

ReplicatedMergeTree

ReplicatedCollapsingMergeTree

ReplicatedAggregatingMergeTree

ReplicatedSummingMergeTree

Репликация поддерживается только для таблиц семейства MergeTree. Репликация работает на уровне отдельных таблиц, а не всего сервера. То есть, на сервере могут быть расположены одновременно реплицируемые и не реплицируемые таблицы.

Реплицируются INSERT, ALTER (см. подробности в описании запроса ALTER). Реплицируются сжатые данные, а не тексты запросов. Запросы CREATE, DROP, ATTACH, DETACH, RENAME не реплицируются - то есть, относятся к одному серверу. Запрос CREATE TABLE создаёт новую реплицируемую таблицу на том сервере, где выполняется запрос; а если на других серверах такая таблица уже есть - добавляет новую реплику. Запрос DROP TABLE удаляет реплику, расположенную на том сервере, где выполняется запрос. Запрос RENAME переименовывает таблицу на одной из реплик - то есть, реплицируемые таблицы на разных репликах могут называться по разному.

Репликация никак не связана с шардированием. На каждом шарде репликация работает независимо.

Репликация является опциональной возможностью. Для использования репликации, укажите в конфигурационном файле адреса ZooKeeper кластера. Пример:

<zookeeper>
    <node index="1">
        <host>example1</host>
        <port>2181</port>
    </node>
    <node index="2">
        <host>example2</host>
        <port>2181</port>
    </node>
    <node index="3">
        <host>example3</host>
        <port>2181</port>
    </node>
</zookeeper>

Используйте версию ZooKeeper не старее 3.4.5. Для примера, в Ubuntu Precise слишком старая версия в пакете. Достаточно новую версию для Ubuntu Precise можно взять из репозитория https://dist.yandex.net/metrika/stable/amd64/.

Можно указать любой имеющийся у вас ZooKeeper-кластер - система будет использовать в нём одну директорию для своих данных (директория указывается при создании реплицируемой таблицы).

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

При запросах SELECT, ZooKeeper не используется. То есть, репликация никак не влияет на производительность SELECT-ов - запросы работают так же быстро, как и для нереплицируемых таблиц.

При каждом запросе INSERT (точнее, на каждый вставляемый блок данных; запрос INSERT содержит один блок, или по блоку на каждые max_insert_block_size = 1048576 строк), делается около десятка записей в ZooKeeper в рамках нескольких транзакций. Это приводит к некоторому увеличению задержек при INSERT-е, по сравнению с нереплицируемыми таблицами. Но если придерживаться обычных рекомендаций - вставлять данные пачками не более одного INSERT-а в секунду, то это не составляет проблем. На всём кластере ClickHouse, использующим для координации один кластер ZooKeeper, может быть в совокупности несколько сотен INSERT-ов в секунду. Пропускная способность при вставке данных (количество строчек в секунду) такая же высокая, как для нереплицируемых таблиц.

Для очень больших кластеров, можно использовать разные кластеры ZooKeeper для разных шардов. Впрочем, на кластере Яндекс.Метрики (примерно 300 серверов) такой необходимости не возникает.

Репликация асинхронная, мульти-мастер. Запросы INSERT (а также ALTER) можно отправлять на любой доступный сервер. Данные вставятся на этот сервер, а затем приедут на остальные серверы. В связи с асинхронностью, только что вставленные данные, появляются на остальных репликах с небольшой задержкой. Если часть реплик недоступна - данные на них запишутся тогда, когда они станут доступны. Если реплика доступна, то задержка составляет столько времени, сколько требуется для передачи блока сжатых данных по сети.

Кворумная запись отсутствует. То есть, вы не можете записать данные с подтверждением их получения более одной репликой. Если вы записали пачку данных на одну реплику, и данные ещё не успели разъехаться по остальным репликам, после чего сервер с этими данными перестал существовать, то эта пачка данных будет потеряна.

Каждый блок данных записывается атомарно. Запрос INSERT разбивается на блоки данных размером до max_insert_block_size = 1048576 строк. То есть, если в запросе INSERT менее 1048576 строк, то он делается атомарно.

Блоки данных дедублицируются. При многократной записи одного и того же блока данных (блоков данных одинакового размера, содержащих одни и те же строчки в одном и том же порядке), блок будет записан только один раз. Это сделано для того, чтобы в случае сбоя в сети, когда клиентское приложение не может понять, были ли данные записаны в БД, можно было просто повторить запрос INSERT. При этом не имеет значения, на какую реплику будут отправлены INSERT-ы с одинаковыми данными. То есть, обеспечивается идемпотентность INSERT-ов. Это работает только для последних 100 вставленных в таблицу блоков.

При репликации, по сети передаются только исходные вставляемые данные. Дальнейшие преобразования данных (слияния) координируются и делаются на всех репликах одинаковым образом. За счёт этого минимизируется использование сети, и благодаря этому, репликация хорошо работает при расположении реплик в разных датацентрах. (Стоит заметить, что дублирование данных в разных датацентрах, по сути, является основной задачей репликации).

Количество реплик одних и тех же данных может быть произвольным. В Яндекс.Метрике в продакшене используется двухкратная репликация. На каждом сервере используется RAID-5 или RAID-6, в некоторых случаях RAID-10. Это является сравнительно надёжным и удобным для эксплуатации решением.

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

Создание реплицируемых таблиц

В начало имени движка таблицы добавляется Replicated. Например, ReplicatedMergeTree.

Также добавляются два параметра в начало списка параметров - путь к таблице в ZooKeeper, имя реплики в ZooKeeper.

Пример: ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}/hits', '{replica}', EventDate, intHash32(UserID), (CounterID, EventDate, intHash32(UserID), EventTime), 8192)

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

<macros>
    <layer>05</layer>
    <shard>02</shard>
    <replica>example05-02-1.yandex.ru</replica>
</macros>

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

/clickhouse/tables/ - общий префикс. Рекомендуется использовать именно его.

{layer}-{shard} - идентификатор шарда. В данном примере он состоит из двух частей, так как на кластере Яндекс.Метрики используется двухуровневое шардирование. Для большинства задач, оставьте только подстановку {shard}, которая будет раскрываться в идентификатор шарда.

hits - имя узла для таблицы в ZooKeeper. Разумно делать его таким же, как имя таблицы. Оно указывается явно, так как, в отличие от имени таблицы, оно не меняется после запроса RENAME.

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

Можно не использовать подстановки, а прописать всё явно. Это может быть удобным для тестирования и при настройке маленьких кластеров, но менее удобным при работе с большими кластерами.

Выполните запрос CREATE TABLE на каждой реплике. Запрос создаёт новую реплицируемую таблицу, или добавляет новую реплику к имеющимся.

Если вы добавляете новую реплику после того, как таблица на других репликах уже содержит некоторые данные, то после выполнения запроса, данные на новую реплику будут скачаны с других реплик. То есть, новая реплика синхронизирует себя с остальными.

Для удаления реплики, выполните запрос DROP TABLE. При этом, удаляется только одна реплика - расположенная на том сервере, где вы выполняете запрос.

Восстановление после сбоя

Если при старте сервера, недоступен ZooKeeper, реплицируемые таблицы переходят в режим только для чтения. Система будет пытаться периодически установить соединение с ZooKeeper.

Если при INSERT-е недоступен ZooKeeper, или происходит ошибка при взаимодействии с ним, будет выкинуто исключение.

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

Обнаруженные битые куски данных (с файлами несоответствующего размера) или неизвестные куски (куски, записанные в файловую систему, но информация о которых не была записана в ZooKeeper) переносятся в поддиректорию detached (не удаляются). Недостающие куски скачиваются с реплик.

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

При старте сервера (или создании новой сессии с ZooKeeper), проверяется только количество и размеры всех файлов. Если у файлов совпадают размеры, но изменены байты где-то посередине, то это обнаруживается не сразу, а только при попытке их прочитать при каком-либо запросе SELECT - запрос кинет исключение о несоответствующей чексумме или размере сжатого блока. В этом случае, куски данных добавляются в очередь на проверку, и при необходимости, скачиваются с реплик.

Если обнаруживается, что локальный набор данных слишком сильно отличается от ожидаемого, то срабатывает защитный механизм - сервер сообщает об этом в лог и отказывается запускаться. Это сделано, так как такой случай может свидетельствовать об ошибке конфигурации - например, если реплика одного шарда была случайно сконфигурирована, как реплика другого шарда. Тем не менее, пороги защитного механизма поставлены довольно низкими, и такая ситуация может возникнуть и при обычном восстановлении после сбоя. В этом случае, восстановление делается полуавтоматически - "по кнопке".

Для запуска восстановления, создайте в ZooKeeper узел /path_to_table/replica_name/flags/force_restore_data с любым содержимым и запустите сервер. При старте, сервер удалит этот флаг и запустит восстановление.

Восстановление в случае потери всех данных

Если на одном из серверов исчезли все данные и метаданные, восстановление делается следующим образом:

1. Установите на сервер ClickHouse. Корректно пропишите подстановки в конфигурационном файле, отвечающие за идентификатор шарда и реплики, если вы их используете.

2. Если у вас были нереплицируемые таблицы, которые должны быть вручную продублированы на серверах, скопируйте их данные (в директории /opt/clickhouse/data/db_name/table_name/) с реплики. Если в реплицируемых таблицах были unreplicated куски (они есть, только если вы выполняли миграцию с MergeTree на ReplicatedMergeTree - см. "Преобразование из MergeTree в ReplicatedMergeTree"), то синхронизируйте их тоже.

3. Скопируйте с реплики определения таблиц, находящиеся в /opt/clickhouse/metadata/. Если в определениях таблиц, идентификатор шарда или реплики, прописаны в явном виде - исправьте их, чтобы они соответствовали данной реплике. (Альтернативный вариант - запустить сервер и сделать самостоятельно все запросы ATTACH TABLE, которые должны были бы быть в соответствующих .sql файлах в /opt/clickhouse/metadata/.)

4. Создайте в ZooKeeper узел /path_to_table/replica_name/flags/force_restore_data с любым содержимым и запустите сервер (перезапустите, если уже запущен). Данные будут скачаны с реплик.

В качестве альтернативного варианта восстановления, вы можете удалить из ZooKeeper информацию о потерянной реплике - /path_to_table/replica_name, и затем создать реплику заново, как написано в разделе "Создание реплицируемых таблиц".

Отсутствует ограничение на использование сетевой полосы при восстановлении. Имейте это ввиду, если восстанавливаете сразу много реплик.

Преобразование из MergeTree в ReplicatedMergeTree

Здесь и далее, под MergeTree подразумеваются все движки таблиц семейства MergeTree, так же для ReplicatedMergeTree.

Если у вас была таблица типа MergeTree, репликация которой делалась вручную, вы можете преобразовать её в реплицируемую таблицу. Это может понадобиться лишь в случаях, когда вы уже успели накопить большое количество данных в таблице типа MergeTree, а сейчас хотите включить репликацию.

Для этого есть два варианта:

1. Оставить старые данные "как есть", не синхронизируя их.

Для этого, переименуйте имеющуюся MergeTree таблицу, затем создайте со старым именем таблицу типа ReplicatedMergeTree. В директории с данными для новой таблицы (/opt/clickhouse/data/db_name/table_name/), создайте поддиректорию unreplicated, и перенесите туда данные из старой таблицы. Затем перезапустите сервер.

При запросах на чтение, реплицируемая таблица будет также читать из unreplicated набора данных. Целостность этих данных никак не контролируется.

2. Добавить старые данные в набор реплицируемых данных.

Если на разных репликах данные отличаются, то сначала синхронизируйте их, либо удалите эти данные на всех репликах кроме одной.

Переименуйте имеющуюся MergeTree таблицу, затем создайте со старым именем таблицу типа ReplicatedMergeTree. Перенесите данные из старой таблицы в поддиректорию detached в директории с данными новой таблицы (/opt/clickhouse/data/db_name/table_name/). Затем добавьте эти куски данных в рабочий набор с помощью выполнения запросов ALTER TABLE ATTACH PART на одной из реплик.

Если на остальных репликах есть точно такие же куски, они будут добавлены в рабочий набор на них. Если нет - куски будут скачаны с той реплики, где они есть.

Преобразование из ReplicatedMergeTree в MergeTree

Создайте таблицу типа MergeTree с другим именем. Перенесите в её директорию с данными все данные из директории с данными таблицы типа ReplicatedMergeTree. Затем удалите таблицу типа ReplicatedMergeTree и перезапустите сервер.

Если вы хотите избавиться от таблицы ReplicatedMergeTree, не запуская сервер, то - удалите соответствующий файл .sql в директории с метаданными (/opt/clickhouse/metadata/); - удалите соответствующий путь в ZooKeeper (/path_to_table/replica_name); После этого, вы можете запустить сервер, создать таблицу типа MergeTree, перенести данные в её директорию, и перезапустить сервер.

Восстановление в случае потери или повреждения метаданных на ZooKeeper кластере

Если вы продолбали ZooKeeper, то вы можете сохранить данные, переместив их в нереплицируемую таблицу, как описано в пункте выше.

Системные таблицы

Системные таблицы используются для реализации части функциональности системы, а также предоставляют доступ к информации о работе системы. Вы не можете удалить системную таблицу (хотя можете сделать DETACH). Для системных таблиц нет файлов с данными на диске и файлов с метаданными. Сервер создаёт все системные таблицы при старте. В системные таблицы нельзя записывать данные - можно только читать. Системные таблицы расположены в базе данных system.

system.one

Таблица содержит одну строку с одним столбцом dummy типа UInt8, содержащим значени 0. Эта таблица используется, если в SELECT запросе не указана секция FROM. То есть, это - аналог таблицы DUAL, которую можно найти в других СУБД.

system.numbers

Таблица содержит один столбец с именем number типа UInt64, содержащим почти все натуральные числа, начиная с нуля. Эту таблицу можно использовать для тестов, а также если вам нужно сделать перебор. Чтения из этой таблицы не распараллеливаются.

system.numbers_mt

То же самое, что и system.numbers, но чтение распараллеливается. Числа могут возвращаться в произвольном порядке. Используется для тестов.

system.tables

Таблица содержит столбцы database, name, engine типа String. Для каждой таблицы, о которой знает сервер, будет присутствовать соответствующая запись в таблице system.tables. Недоработка: Движки таблиц (engine) указаны без параметров. Эта системная таблица используется для реализации запросов SHOW TABLES.

system.databases

Таблица содержит один столбец name типа String - имя базы данных. Для каждой базы данных, о которой знает сервер, будет присутствовать соответствующая запись в таблице. Эта системная таблица используется для реализации запроса SHOW DATABASES.

system.processes

Эта системная таблица используется для реализации запроса SHOW PROCESSLIST. Столбцы:

user String              - имя пользователя, который задал запрос. При распределённой обработке запроса, относится к пользователю, с помощью которого сервер-инициатор запроса отправил запрос на данный сервер, а не к имени пользователя, который задал распределённый запрос на сервер-инициатор запроса.

address String           - IP-адрес, с которого задан запрос. При распределённой обработке запроса, аналогично.

elapsed Float64          - время в секундах, прошедшее от начала выполнения запроса.

rows_read UInt64         - количество прочитанных из таблиц строк. При распределённой обработке запроса, на сервере-инициаторе запроса, представляет собой сумму по всем удалённым серверам.

bytes_read UInt64        - количество прочитанных из таблиц байт, в несжатом виде. При распределённой обработке запроса, на сервере-инициаторе запроса, представляет собой сумму по всем удалённым серверам.

total_rows_approx UInt64 - приблизительная оценка общего количества строк, которые должны быть прочитаны. При распределённой обработке запроса, на сервере-инициаторе запроса, представляет собой сумму по всем удалённым серверам. Может обновляться в процессе выполнения запроса, когда становятся известны новые источники для обработки.

memory_usage UInt64      - потребление памяти запросом. Может не учитывать некоторые виды выделенной памяти.

query String             - текст запроса. В случае INSERT - без данных для INSERT-а.

query_id String          - идентификатор запроса, если был задан.

system.events

Содержит информацию о количестве произошедших в системе событий, для профилирования и мониторинга. Пример: количество обработанных запросов типа SELECT. Столбцы: event String - имя события, value UInt64 - количество.

system.clusters

Содержит информацию о доступных в конфигурационном файле кластерах и серверах, которые в них входят. Столбцы:

cluster String      - имя кластера
shard_num UInt32    - номер шарда в кластере, начиная с 1
shard_weight UInt32 - относительный вес шарда при записи данных
replica_num UInt32  - номер реплики в шарде, начиная с 1
host_name String    - имя хоста, как прописано в конфиге
host_address String - IP-адрес хоста, полученный из DNS
port UInt16         - порт, на который обращаться для соединения с сервером
user String         - имя пользователя, которого использовать для соединения с сервером

system.columns

Содержит информацию о столбцах всех таблиц. С помощью этой таблицы можно получить информацию аналогично запросу DESCRIBE TABLE, но для многих таблиц сразу.

database String           - имя базы данных, в которой находится таблица
table String              - имя таблицы
name String               - имя столбца
type String               - тип столбца
default_type String       - тип (DEFAULT, MATERIALIZED, ALIAS) выражения для значения по умолчанию, или пустая строка, если оно не описано
default_expression String - выражение для значения по умолчанию, или пустая строка, если оно не описано

system.dictionaries

Содержит информацию о внешних словарях. Столбцы:

name String                   - имя словаря
type String                   - тип словаря: Flat, Hashed, Cache
origin String                 - путь к конфигурационному файлу, в котором описан словарь
attribute.names Array(String) - массив имён атрибутов, предоставляемых словарём
attribute.types Array(String) - соответствующий массив типов атрибутов, предоставляемых словарём
has_hierarchy UInt8           - является ли словарь иерархическим
bytes_allocated UInt64        - количество оперативной памяти, которое использует словарь
hit_rate Float64              - для cache-словарей - доля использований, для которых значение было в кэше
element_count UInt64          - количество хранящихся в словаре элементов
load_factor Float64           - доля заполненности словаря (для hashed словаря - доля заполнения хэш-таблицы)
creation_time DateTime        - время создания или последней успешной перезагрузки словаря
last_exception String         - текст ошибки, возникшей при создании или перезагрузке словаря, если словарь не удалось создать
source String                 - текст, описывающий источник данных для словаря

Заметим, что количество оперативной памяти, которое использует словарь, не является пропорциональным количеству элементов, хранящихся в словаре. Так, для flat и cached словарей, все ячейки памяти выделяются заранее, независимо от реальной заполненности словаря.

system.functions

Содержит информацию об обычных и агрегатных функциях. Столбцы:

name String           - имя функции
is_aggregate UInt8    - является ли функция агрегатной

system.merges

Содержит информацию о производящихся прямо сейчас слияниях для таблиц семейства MergeTree. Столбцы:

database String                    - имя базы данных, в которой находится таблица
table String                       - имя таблицы
elapsed Float64                    - время в секундах, прошедшее от начала выполнения слияния
progress Float64                   - доля выполненной работы от 0 до 1
num_parts UInt64                   - количество сливаемых кусков
result_part_name String            - имя куска, который будет образован в результате слияния
total_size_bytes_compressed UInt64 - суммарный размер сжатых данных сливаемых кусков
total_size_marks UInt64            - суммарное количество засечек в сливаемых кусках
bytes_read_uncompressed UInt64     - количество прочитанных байт, разжатых
rows_read UInt64                   - количество прочитанных строк
bytes_written_uncompressed UInt64  - количество записанных байт, несжатых
rows_written UInt64                - количество записанных строк

system.parts

Содержит информацию о кусках таблиц семейства MergeTree. Столбцы:

database String            - имя базы данных, в которой находится таблица, к которой относится кусок
table String               - имя таблицы, к которой относится кусок
engine String              - имя движка таблицы, без параметров
partition String           - имя партиции - имеет формат YYYYMM
name String                - имя куска
replicated UInt8           - относится ли кусок к реплицируемым данным
active UInt8               - используется ли кусок в таблице, или же он уже не нужен и скоро будет удалён - неактивные куски остаются после слияния
marks UInt64               - количество засечек - умножьте на гранулированность индекса (обычно 8192), чтобы получить примерное количество строк в куске
bytes UInt64               - количество байт в сжатом виде
modification_time DateTime - время модификации директории с куском - обычно соответствует времени создания куска
remove_time DateTime       - только для неактивных кусков - время, когда кусок стал неактивным
refcount UInt32            - количество мест, в котором кусок используется - значение больше 2 говорит о том, что этот кусок участвует в запросах или в слияниях

system.replicas

Содержит информацию и статус для реплицируемых таблиц, расположенных на локальном сервере. Эту таблицу можно использовать для мониторинга. Таблица содержит по строчке для каждой Replicated*-таблицы.

Пример:

SELECT *
FROM system.replicas
WHERE table = 'visits'
FORMAT Vertical

Row 1:
──────
database:           merge
table:              visits
engine:             ReplicatedCollapsingMergeTree
is_leader:          1
is_readonly:        0
is_session_expired: 0
future_parts:       1
parts_to_check:     0
zookeeper_path:     /clickhouse/tables/01-06/visits
replica_name:       example01-06-1.yandex.ru
replica_path:       /clickhouse/tables/01-06/visits/replicas/example01-06-1.yandex.ru
columns_version:    9
queue_size:         1
inserts_in_queue:   0
merges_in_queue:    1
log_max_index:      596273
log_pointer:        596274
total_replicas:     2
active_replicas:    2

Столбцы:

database:           имя БД
table:              имя таблицы
engine:             имя движка таблицы

is_leader:          является ли реплика лидером
В один момент времени, не более одной из реплик является лидером. Лидер отвечает за выбор фоновых слияний, которые следует произвести.
Замечу, что запись можно осуществлять на любую реплику (доступную и имеющую сессию в ZK), независимо от лидерства.

is_readonly:        находится ли реплика в режиме "только для чтения"
Этот режим включается, если в конфиге нет секции с ZK; если при переинициализации сессии в ZK произошла неизвестная ошибка; во время переинициализации сессии с ZK.

is_session_expired: истекла ли сессия с ZK.
В основном, то же самое, что и is_readonly.

future_parts:       количество кусков с данными, которые появятся в результате INSERT-ов или слияний, которых ещё предстоит сделать

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

zookeeper_path:     путь к данным таблицы в ZK
replica_name:       имя реплики в ZK; разные реплики одной таблицы имеют разное имя
replica_path:       путь к данным реплики в ZK. То же самое, что конкатенация zookeeper_path/replicas/replica_path.

columns_version:    номер версии структуры таблицы
Обозначает, сколько раз был сделан ALTER. Если на репликах разные версии, значит некоторые реплики сделали ещё не все ALTER-ы.

queue_size:         размер очереди действий, которых предстоит сделать
К действиям относятся вставки блоков данных, слияния, и некоторые другие действия.
Как правило, совпадает с future_parts.

inserts_in_queue:   количество вставок блоков данных, которых предстоит сделать
Обычно вставки должны быстро реплицироваться. Если величина большая - значит что-то не так.

merges_in_queue:    количество слияний, которых предстоит сделать
Бывают длинные слияния - то есть, это значение может быть больше нуля продолжительное время.

Следующие 4 столбца имеют ненулевое значение только если активна сессия с ZK.

log_max_index:      максимальный номер записи в общем логе действий
log_pointer:        максимальный номер записи из общего лога действий, которую реплика скопировала в свою очередь для выполнения, плюс единица
Если log_pointer сильно меньше log_max_index, значит что-то не так.

total_replicas:     общее число известных реплик этой таблицы
active_replicas:    число реплик этой таблицы, имеющих сессию в ZK; то есть, число работающих реплик

Если запрашивать все столбцы, то таблица может работать слегка медленно, так как на каждую строчку делается несколько чтений из ZK. Если не запрашивать последние 4 столбца (log_max_index, log_pointer, total_replicas, active_replicas), то таблица работает быстро.

Например, так можно проверить, что всё хорошо:

SELECT
    database,
    table,
    is_leader,
    is_readonly,
    is_session_expired,
    future_parts,
    parts_to_check,
    columns_version,
    queue_size,
    inserts_in_queue,
    merges_in_queue,
    log_max_index,
    log_pointer,
    total_replicas,
    active_replicas
FROM system.replicas
WHERE
       is_readonly
    OR is_session_expired
    OR future_parts > 20
    OR parts_to_check > 10
    OR queue_size > 20
    OR inserts_in_queue > 10
    OR log_max_index - log_pointer > 10
    OR total_replicas < 2
    OR active_replicas < total_replicas

Если этот запрос ничего не возвращает - значит всё хорошо.

system.settings

Содержит информацию о настройках, используемых в данный момент. То есть, используемых для выполнения запроса, с помощью которого вы читаете из таблицы system.settings.

Столбцы:

name String   - имя настройки
value String  - значение настройки
changed UInt8 - была ли настройка явно задана в конфиге или изменена явным образом

Пример:

SELECT *
FROM system.settings
WHERE changed

┌─name───────────────────┬─value───────┬─changed─┐
│ max_threads            │ 8           │       1 │
│ use_uncompressed_cache │ 0           │       1 │
│ load_balancing         │ random      │       1 │
│ max_memory_usage       │ 10000000000 │       1 │
└────────────────────────┴─────────────┴─────────┘

system.zookeeper

Позволяет читать данные из ZooKeeper кластера, описанного в конфигурации. В запросе обязательно в секции WHERE должно присутствовать условие на равенство path - путь в ZooKeeper, для детей которого вы хотите получить данные.

Запрос SELECT * FROM system.zookeeper WHERE path = '/clickhouse' выведет данные по всем детям узла /clickhouse. Чтобы вывести данные по всем узлам в корне, напишите path = '/'. Если узла, указанного в path не существует, то будет брошено исключение.

Столбцы:

name String          - имя узла
path String          - путь к узлу
value String         - значение узла
dataLength Int32     - размер значения
numChildren Int32    - количество детей
czxid Int64          - идентификатор транзакции, в которой узел был создан
mzxid Int64          - идентификатор транзакции, в которой узел был последний раз изменён
pzxid Int64          - идентификатор транзакции, последний раз удаливший или добавивший детей
ctime DateTime       - время создания узла
mtime DateTime       - время последней модификации узла
version Int32        - версия узла - количество раз, когда узел был изменён
cversion Int32       - количество добавлений или удалений детей
aversion Int32       - количество изменений ACL
ephemeralOwner Int64 - для эфемерных узлов - идентификатор сессии, которая владеет этим узлом

Пример:

SELECT *
FROM system.zookeeper
WHERE path = '/clickhouse/tables/01-08/visits/replicas'
FORMAT Vertical

Row 1:
──────
name:           example01-08-1.yandex.ru
value:
czxid:          932998691229
mzxid:          932998691229
ctime:          2015-03-27 16:49:51
mtime:          2015-03-27 16:49:51
version:        0
cversion:       47
aversion:       0
ephemeralOwner: 0
dataLength:     0
numChildren:    7
pzxid:          987021031383
path:           /clickhouse/tables/01-08/visits/replicas

Row 2:
──────
name:           example01-08-2.yandex.ru
value:
czxid:          933002738135
mzxid:          933002738135
ctime:          2015-03-27 16:57:01
mtime:          2015-03-27 16:57:01
version:        0
cversion:       37
aversion:       0
ephemeralOwner: 0
dataLength:     0
numChildren:    7
pzxid:          987021252247
path:           /clickhouse/tables/01-08/visits/replicas

Табличные функции

Табличные функции могут указываться в секции FROM вместо имени БД и таблицы. Табличные функции можно использовать только если не выставлена настройка readonly. Табличные функции не имеют отношения к другим функциям.

merge

merge(db_name, 'tables_regexp') - создаёт временную таблицу типа Merge. Подробнее смотрите раздел "Движки таблиц, Merge". Структура таблицы берётся из первой попавшейся таблицы, подходящей под регулярное выражение.

remote

remote('addresses_expr', db, table[, 'user'[, 'password']]) или remote('addresses_expr', db.table[, 'user'[, 'password']]) - позволяет обратиться к удалённым серверам без создания таблицы типа Distributed.

addresses_expr - выражение, генерирующее адреса удалённых серверов.

Это может быть просто один адрес сервера. Адрес сервера - это хост:порт, или только хост. Хост может быть указан в виде имени сервера, или в виде IPv4 или IPv6 адреса. IPv6 адрес указывается в квадратных скобках. Порт - TCP-порт удалённого сервера. Если порт не указан, используется tcp_port из конфигурационного файла сервера (по умолчанию - 9000).

Замечание: в качестве исключения, при указании IPv6-адреса, обязательно также указывать порт.

Примеры:

example01-01-1
example01-01-1:9000
localhost
127.0.0.1
[::]:9000
[2a02:6b8:0:1111::11]:9000

Могут быть указаны адреса через запятую - в этом случае, запрос пойдёт на все указанные адреса (как на шарды с разными данными) и будет обработан распределённо.

Пример:

example01-01-1,example01-02-1

Часть выражения может быть указана в фигурных скобках. Предыдущий пример может быть записан следующим образом:

example01-0{1,2}-1

В фигурных скобках может быть указан диапазон (неотрицательных целых) чисел через две точки. В этом случае, диапазон раскрывается в множество значений, генерирующих адреса шардов. Если запись первого числа начинается с нуля, то значения формируются с таким же выравниванием нулями. Предыдущий пример может быть записан следующим образом:

example01-{01..02}-1

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

Адреса или их фрагменты в фигурных скобках, могут быть указаны через символ |. В этом случае, соответствующие множества адресов понимаются как реплики - запрос будет отправлен на первую живую реплику. При этом, реплики перебираются в порядке, согласно текущей настройке load_balancing. Пример:

example01-{01..02}-{1|2}

В этом примере указано два шарда, в каждом из которых имеется две реплики.

Количество генерируемых адресов ограничено некоторой константой - сейчас это 1000 штук.

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

Табличная функция remote может быть полезна для следующих случаев: - обращение на конкретный сервер в целях сравнения данных, отладки и тестирования; - запросы между разными кластерами ClickHouse в целях исследований; - нечастых распределённых запросов, задаваемых вручную; - распределённых запросов, где набор серверов определяется каждый раз заново.

Имя пользователя может быть не задано - тогда используется имя пользователя 'default'. Пароль может быть не задан - тогда используется пустой пароль.

Форматы

Формат определяет, в каком виде данные отдаются вам при SELECT-е и в каком виде принимаются при INSERT-е.

Native

Самый эффективный формат. Данные пишутся и читаются блоками в бинарном виде. Для каждого блока пишется количество строк, количество столбцов, имена и типы столбцов, а затем кусочки столбцов этого блока, один за другим. То есть, этот формат является "столбцовым" - не преобразует столбцы в строки. Именно этот формат используется в родном интерфейсе - при межсерверном взаимодействии, при использовании клиента командной строки, при работе клиентов, написанных на C++.

Вы можете использовать этот формат для быстрой генерации дампов, которые могут быть прочитаны только СУБД ClickHouse. Вряд ли имеет смысл работать с этим форматом самостоятельно.

TabSeparated

В TabSeparated формате данные пишутся по строкам. Каждая строчка содержит значения, разделённые табами. После каждого значения идёт таб, кроме последнего значения в строке, после которого идёт перевод строки. Везде подразумеваются исключительно unix-переводы строк. Последняя строка также обязана содержать перевод строки на конце. Значения пишутся в текстовом виде, без обрамляющих кавычек, с экранированием служебных символов.

Целые числа пишутся в десятичной форме. Числа могут содержать лишний символ "+" в начале (но при записи он не пишется). Неотрицательные числа не могут содержать знак отрицания. При чтении допустим парсинг пустой строки, как числа ноль, или (для знаковых типов) строки, состоящей из одного минуса, как числа ноль. Числа, не помещающиеся в соответствующий тип данных, могут парсится, как некоторое другое число, без сообщения об ошибке.

Числа с плавающей запятой пишутся в десятичной форме. При этом, десятичный разделитель - точка. Поддерживается экспоненциальная запись, а также inf, +inf, -inf, nan. Запись числа с плавающей запятой может начинаться или заканчиваться на десятичную точку. При записи, возможна потеря точности чисел с плавающей запятой. При чтении, допустимо чтение не обязательно наиболее близкого к десятичной записи машинно-представимого числа.

Даты пишутся в формате YYYY-MM-DD, читаются в том же формате, но с любыми символами в качестве разделителей. Даты-с-временем пишутся в формате YYYY-MM-DD hh:mm:ss, читаются в том же формате, но с любыми символами в качестве разделителей. Всё это происходит в системном часовом поясе на момент старта клиента (если клиент занимается форматированием данных) или сервера. Для дат-с-временем не указывается, действует ли daylight saving time. То есть, если в дампе есть времена во время перевода стрелок назад, то дамп не соответствует данным однозначно, и при чтении будет выбрано какое-либо из двух времён. При чтении, некорректные даты и даты-с-временем могут парситься с естественным переполнением или как нулевые даты/даты-с-временем без сообщения об ошибке.

В качестве исключения, поддерживается также чтение даты-с-временем в формате unix timestamp, если он состоит ровно из 10 десятичных цифр. При этом, результат не зависит от часового пояса. Различение форматов YYYY-MM-DD hh:mm:ss и NNNNNNNNNN делается автоматически.

Строки пишутся с экранированием спец-символов с помощью обратного слеша. Используются следующие escape-последовательности: \b, \f, \r, \n, \t, \0, \', \\. При чтении, также поддерживаются любые последовательности вида \x, где x - любой символ - такие последовательности преобразуется в x. Таким образом, при чтении поддерживаются форматы, где перевод строки может быть записан как \n и как \ и перевод строки. Например, строка Hello world, где между словами вместо пробела стоит перевод строки, может быть считана в любом из следующих вариантов:

Hello\nworld
Hello\
world

Второй вариант поддерживается, так как его использует MySQL при записи tab-separated дампа.

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

Массивы пишутся в виде списка значений через запятую в квадратных скобках. Элементы массива - числа пишутся как обычно, а даты, даты-с-временем и строки - в одинарных кавычках с такими же правилами экранирования, как указано выше.

Формат TabSeparated удобен для обработки данных произвольными программами и скриптами. Он используется по умолчанию в HTTP-интерфейсе, а также в batch-режиме клиента командной строки. Также формат позволяет переносить данные между разными СУБД. Например, вы можете получить дамп из MySQL и загрузить его в ClickHouse, или наоборот.

Формат TabSeparated поддерживает вывод тотальных значений (при использовании WITH TOTALS) и экстремальных значений (при настройке extremes выставленной в 1). В этих случаях, после основных данных выводятся тотальные значения, и экстремальные значения. Основной результат, тотальные значения и экстремальные значения, отделяются друг от друга пустой строкой. Пример:

SELECT EventDate, count() AS c FROM test.hits GROUP BY EventDate WITH TOTALS ORDER BY EventDate FORMAT TabSeparated
2014-03-17      1406958
2014-03-18      1383658
2014-03-19      1405797
2014-03-20      1353623
2014-03-21      1245779
2014-03-22      1031592
2014-03-23      1046491

0000-00-00      8873898

2014-03-17      1031592
2014-03-23      1406958

TabSeparatedWithNames

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

TabSeparatedWithNamesAndTypes

Отличается от формата TabSeparated тем, что в первой строке пишутся имена столбцов, а во второй - типы столбцов. При чтении, первая и вторая строка полностью игнорируется.

TabSeparatedRaw

Отличается от формата TabSeparated тем, что строки пишутся без экранирования. Этот формат подходит только для записи (вывода результата выполнения запроса), но не для чтения (приёма данных для вставки в таблицу).

BlockTabSeparated

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

RowBinary

Пишет данные по строкам, в бинарном виде. Строки и значения уложены подряд, без разделителей. Формат менее эффективен, чем формат Native, так как является строковым.

Pretty

Пишет данные в виде Unicode-art табличек, также используя ANSI-escape последовательности для установки цветов в терминале. Рисуется полная сетка таблицы и, таким образом, каждая строчка занимает две строки в терминале. Каждый блок результата выводится в виде отдельной таблицы. Это нужно, чтобы можно было выводить блоки без буферизации результата (буферизация потребовалась бы, чтобы заранее вычислить видимую ширину всех значений.) Для защиты от вываливания слишком большого количества данных в терминал, выводится только первые 10 000 строк. Если строк больше или равно 10 000, то будет написано "Showed first 10 000." Формат подходит только для записи (вывода результата выполнения запроса), но не для чтения.

Формат Pretty поддерживает вывод тотальных значений (при использовании WITH TOTALS) и экстремальных значений (при настройке extremes выставленной в 1). В этих случаях, после основных данных выводятся тотальные значения, и экстремальные значения, в отдельных табличках. Пример (показан для формата PrettyCompact):

SELECT EventDate, count() AS c FROM test.hits GROUP BY EventDate WITH TOTALS ORDER BY EventDate FORMAT PrettyCompact
┌──EventDate─┬───────c─┐
│ 2014-03-17 │ 1406958 │
│ 2014-03-18 │ 1383658 │
│ 2014-03-19 │ 1405797 │
│ 2014-03-20 │ 1353623 │
│ 2014-03-21 │ 1245779 │
│ 2014-03-22 │ 1031592 │
│ 2014-03-23 │ 1046491 │
└────────────┴─────────┘

Totals:
┌──EventDate─┬───────c─┐
│ 0000-00-00 │ 8873898 │
└────────────┴─────────┘

Extremes:
┌──EventDate─┬───────c─┐
│ 2014-03-17 │ 1031592 │
│ 2014-03-23 │ 1406958 │
└────────────┴─────────┘

PrettyCompact

Отличается от Pretty тем, что не рисуется сетка между строками - результат более компактный. Этот формат используется по умолчанию в клиенте командной строки в интерактивном режиме.

PrettyCompactMonoBlock

Отличается от PrettyCompact тем, что строки (до 10 000 штук) буферизуются и затем выводятся в виде одной таблицы, а не по блокам.

PrettySpace

Отличается от PrettyCompact тем, что вместо сетки используется пустое пространство (пробелы).

PrettyNoEscapes

Отличается от Pretty тем, что не используются ANSI-escape последовательности. Это нужно для отображения этого формата в браузере, а также при использовании утилиты командной строки watch. Пример:

watch -n1 "clickhouse-client --query='SELECT * FROM system.events FORMAT PrettyCompactNoEscapes'"

Для отображения в браузере, вы можете использовать HTTP интерфейс.

PrettyCompactNoEscapes

Аналогично.

PrettySpaceNoEscapes

Аналогично.

Vertical

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

Values

Выводит каждую строку в скобках. Строки разделены запятыми. После последней строки запятой нет. Значения внутри скобок также разделены запятыми. Числа выводятся в десятичном виде без кавычек. Массивы выводятся в квадратных скобках. Строки, даты, даты-с-временем выводятся в кавычках. Правила экранирования и особенности парсинга аналогичны формату TabSeparated. При форматировании, лишние пробелы не ставятся, а при парсинге - допустимы и пропускаются (за исключением пробелов внутри значений типа массив, которые недопустимы).

Именно этот формат используется в запросе INSERT INTO t VALUES ... Но вы также можете использовать его для вывода.

JSON

Выводит данные в формате JSON. Кроме таблицы с данными, также выводятся имена и типы столбцов, и некоторая дополнительная информация - общее количество выведенных строк, а также количество строк, которое могло бы быть выведено, если бы не было LIMIT-а. Пример:

SELECT SearchPhrase, count() AS c FROM test.hits GROUP BY SearchPhrase WITH TOTALS ORDER BY c DESC LIMIT 5 FORMAT JSON
{
        "meta":
        [
                {
                        "name": "SearchPhrase",
                        "type": "String"
                },
                {
                        "name": "c",
                        "type": "UInt64"
                }
        ],

        "data":
        [
                {
                        "SearchPhrase": "",
                        "c": "8267016"
                },
                {
                        "SearchPhrase": "интерьер ванной комнаты",
                        "c": "2166"
                },
                {
                        "SearchPhrase": "яндекс",
                        "c": "1655"
                },
                {
                        "SearchPhrase": "весна 2014 мода",
                        "c": "1549"
                },
                {
                        "SearchPhrase": "фриформ фото",
                        "c": "1480"
                }
        ],

        "totals":
        {
                "SearchPhrase": "",
                "c": "8873898"
        },

        "extremes":
        {
                "min":
                {
                        "SearchPhrase": "",
                        "c": "1480"
                },
                "max":
                {
                        "SearchPhrase": "",
                        "c": "8267016"
                }
        },

        "rows": 5,

        "rows_before_limit_at_least": 141137
}

JSON совместим с JavaScript. Для этого, дополнительно эскейпятся некоторые символы: символ прямого слеша / экранируется в виде \/; альтернативные переводы строк U+2028, U+2029, на которых ломаются некоторые браузеры, экранируются в виде \uXXXX-последовательностей. Эскейпятся ASCII control characters: backspace, form feed, line feed, carriage return, horizontal tab в виде \b, \f, \n, \r, \t соответственно, а также остальные байты из диапазона 00-1F с помощью \uXXXX-последовательностей. Невалидные UTF-8 последовательности заменяются на replacement character и, таким образом, выводимый текст будет состоять из валидных UTF-8 последовательностей. Числа типа UInt64 и Int64, для совместимости с JavaScript, выводятся в двойных кавычках.

rows - общее количество выведенных строчек. rows_before_limit_at_least - не менее скольких строчек получилось бы, если бы не было LIMIT-а. Выводится только если запрос содержит LIMIT. В случае, если запрос содержит GROUP BY, rows_before_limit_at_least - точное число строк, которое получилось бы, если бы не было LIMIT-а.

totals - тотальные значения (при использовании WITH TOTALS). extremes - экстремальные значения (при настройке extremes, выставленной в 1).

Формат подходит только для записи, но не для чтения.

JSONCompact

Отличается от JSON только тем, что строчки данных выводятся в массивах, а не в object-ах. Пример:

{
        "meta":
        [
                {
                        "name": "SearchPhrase",
                        "type": "String"
                },
                {
                        "name": "c",
                        "type": "UInt64"
                }
        ],

        "data":
        [
                ["", "8267016"],
                ["интерьер ванной комнаты", "2166"],
                ["яндекс", "1655"],
                ["весна 2014 мода", "1549"],
                ["фриформ фото", "1480"]
        ],

        "totals": ["","8873898"],

        "extremes":
        {
                "min": ["","1480"],
                "max": ["","8267016"]
        },

        "rows": 5,

        "rows_before_limit_at_least": 141137
}

Null

Ничего не выводит. При этом, запрос обрабатывается, а при использовании клиента командной строки, данные ещё и передаются на клиент. Используется для тестов, в том числе, тестов производительности. Очевидно, формат подходит только для записи, но не для чтения.

Типы данных

UInt8, UInt16, UInt32, UInt64, Int8, Int16, Int32, Int64

Целые числа фиксированной длины, без знака или со знаком.

Float32, Float64

Числа с плавающей запятой, то же, что и float, double в языке C. В отличие от стандартного SQL, числа с плавающей запятой поддерживают inf, -inf, а также nan-ы. Смотрите замечание о сортировке nan-ов в разделе "Секция ORDER BY". Не рекомендуется хранить числа с плавающей запятой в таблицах.

String

Строки произвольной длины. Длина не ограничена. Значение может содержать произвольный набор байт, включая нулевые байты. Таким образом, тип String заменяет типы VARCHAR, BLOB, CLOB и т. п. из других СУБД.

Кодировки

В ClickHouse нет понятия кодировок. Строки могут содержать произвольный набор байт, который хранится и выводится, как есть. Если вам нужно хранить тексты, рекомендуется использовать кодировку UTF-8. По крайней мере, если у вас терминал работает в кодировке UTF-8 (это рекомендуется), вы сможете читать и писать свои значения без каких-либо преобразований. Также, некоторые функции по работе со строками, имеют отдельные варианты, которые работают при допущении, что строка содержит набор байт, представляющий текст в кодировке UTF-8. Например, функция length вычисляет длину строки в байтах, а функция lengthUTF8 - длину строки в кодовых точках Unicode, при допущении, что значение в кодировке UTF-8.

FixedString(N)

Строка фиксированной длины N байт (не символов, не кодовых точек). N должно быть строго положительным натуральным числом. При чтении строки, содержащей меньшее число байт, строка дополняется до N байт дописыванием нулевых байт справа. При чтении строки, содержащей большее число байт, выдаётся сообщение об ошибке. При записи строки, нулевые байты с конца строки не вырезаются, а выводятся. Обратите внимание, как это поведение отличается от поведения MySQL для типа CHAR (строки дополняются пробелами, пробелы перед выводом вырезаются).

С типом FixedString(N) умеет работать меньше функций, чем с типом String - то есть, он менее удобен в использовании.

Date

Дата. Хранится в двух байтах в виде (беззнакового) числа дней, прошедших от 1970-01-01. Позволяет хранить значения от чуть больше, чем начала unix-эпохи до верхнего порога, определяющегося константой на этапе компиляции (сейчас - до 2038 года, но может быть расширено до 2106 года). Минимальное значение выводится как 0000-00-00.

Дата хранится без учёта часового пояса.

DateTime

Дата-с-временем. Хранится в 4 байтах, в виде (беззнакового) unix timestamp. Позволяет хранить значения в том же интервале, что и для типа Date. Минимальное значение выводится как 0000-00-00 00:00:00. Время хранится с точностью до одной секунды (без учёта секунд координации).

Часовые пояса

Дата-с-временем преобразуется из текстового (разбитого на составляющие) в бинарный вид и обратно, с использованием системного часового пояса на момент старта клиента или сервера. В текстовом виде, теряется информация о том, был ли произведён перевод стрелок.

Поддерживаются только часовые пояса, для которых для всего диапазона времён, с которым вы будете работать, не существовало моментов времени, в которые время отличалось от UTC на нецелое число часов (без учёта секунд координации).

То есть, при работе с датой в виде текста (например, при сохранении текстовых дампов), следует иметь ввиду о проблемах с неоднозначностью во время перевода стрелок назад, и о проблемах с соответствием данных, при смене часового пояса.

Array(T)

Массив из элементов типа T. Типом T может быть любой тип, в том числе, массив. Многомерные массивы не рекомендуется использовать, так как их поддержка довольно слабая (например, многомерные массивы нельзя сохранить в какую-либо таблицу кроме таблиц типа Memory).

Tuple(T1, T2, ...)

Кортежи не могут быть записаны в таблицы (кроме таблиц типа Memory). Они используется для временной группировки столбцов. Столбцы могут группироваться при использовании выражения IN в запросе, а также для указания нескольких формальных параметров лямбда-функций. Подробнее смотрите раздел "Операторы IN", "Функции высшего порядка".

Кортежи могут быть выведены в результате выполнения запроса. В этом случае, в текстовых форматах кроме JSON*, значения выводятся в круглых скобках через запятую. В форматах JSON*, кортежи выводятся в виде массивов (в квадратных скобках).

Вложенные структуры данных

Nested(Name1 Type1, Name2 Type2, ...)

Вложенная структура данных - это как будто вложенная таблица. Параметры вложенной структуры данных - имена и типы столбцов, указываются так же, как в запроса CREATE. Каждой строке таблицы может соответствовать произвольное количество строк вложенной структуры данных.

Пример:

CREATE TABLE test.visits
(
    CounterID UInt32,
    StartDate Date,
    Sign Int8,
    IsNew UInt8,
    VisitID UInt64,
    UserID UInt64,
    ...
    Goals Nested
    (
        ID UInt32,
        Serial UInt32,
        EventTime DateTime,
        Price Int64,
        OrderID String,
        CurrencyID UInt32
    ),
    ...
) ENGINE = CollapsingMergeTree(StartDate, intHash32(UserID), (CounterID, StartDate, intHash32(UserID), VisitID), 8192, Sign)

В этом примере объявлена вложенная структура данных Goals, содержащая данные о достижении целей. Каждой строке таблицы visits может соответствовать от нуля до произвольного количества достижений целей.

Поддерживается только один уровень вложенности.

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

Пример:

SELECT
    Goals.ID,
    Goals.EventTime
FROM test.visits
WHERE CounterID = 101500 AND length(Goals.ID) < 5
LIMIT 10

┌─Goals.ID───────────────────────┬─Goals.EventTime───────────────────────────────────────────────────────────────────────────┐
│ [1073752,591325,591325]        │ ['2014-03-17 16:38:10','2014-03-17 16:38:48','2014-03-17 16:42:27']                       │
│ [1073752]                      │ ['2014-03-17 00:28:25']                                                                   │
│ [1073752]                      │ ['2014-03-17 10:46:20']                                                                   │
│ [1073752,591325,591325,591325] │ ['2014-03-17 13:59:20','2014-03-17 22:17:55','2014-03-17 22:18:07','2014-03-17 22:18:51'] │
│ []                             │ []                                                                                        │
│ [1073752,591325,591325]        │ ['2014-03-17 11:37:06','2014-03-17 14:07:47','2014-03-17 14:36:21']                       │
│ []                             │ []                                                                                        │
│ []                             │ []                                                                                        │
│ [591325,1073752]               │ ['2014-03-17 00:46:05','2014-03-17 00:46:05']                                             │
│ [1073752,591325,591325,591325] │ ['2014-03-17 13:28:33','2014-03-17 13:30:26','2014-03-17 18:51:21','2014-03-17 18:51:45'] │
└────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────────────────┘

Проще всего понимать вложенную структуру данных, как набор из нескольких столбцов-массивов одинаковых длин.

Единственное место, где в запросе SELECT можно указать имя целой вложенной структуры данных, а не отдельных столбцов - секция ARRAY JOIN. Подробнее см. раздел "Секция ARRAY JOIN". Пример:

SELECT
    Goal.ID,
    Goal.EventTime
FROM test.visits
ARRAY JOIN Goals AS Goal
WHERE CounterID = 101500 AND length(Goals.ID) < 5
LIMIT 10

┌─Goal.ID─┬──────Goal.EventTime─┐
│ 1073752 │ 2014-03-17 16:38:10 │
│  591325 │ 2014-03-17 16:38:48 │
│  591325 │ 2014-03-17 16:42:27 │
│ 1073752 │ 2014-03-17 00:28:25 │
│ 1073752 │ 2014-03-17 10:46:20 │
│ 1073752 │ 2014-03-17 13:59:20 │
│  591325 │ 2014-03-17 22:17:55 │
│  591325 │ 2014-03-17 22:18:07 │
│  591325 │ 2014-03-17 22:18:51 │
│ 1073752 │ 2014-03-17 11:37:06 │
└─────────┴─────────────────────┘

Вы не можете сделать SELECT целой вложенной структуры данных. Можно лишь явно перечислить отдельные столбцы - её составляющие.

При запросе INSERT, вы должны передать все составляющие столбцы-массивы вложенной структуры данных по-отдельности (как если бы это были отдельные столбцы-массивы). При вставке проверяется, что они имеют одинаковые длины.

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

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

AggregateFunction(name, types_of_arguments...)

Промежуточное состояние агрегатной функции. Чтобы его получить, используются агрегатные функции с суффиксом -State. Подробнее смотрите в разделе "AggregatingMergeTree".

Служебные типы данных

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

Set

Используется для представления правой части выражения IN.

Expression

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

Булевы значения

Отдельного типа для булевых значений нет. Для них используется тип UInt8, в котором используются только значения 0 и 1.

Операторы

Все операторы преобразуются в соответствующие функции на этапе парсинга запроса, с учётом их приоритетов и ассоциативности. Далее будут перечислены группы операторов в порядке их приоритета (чем выше, тем раньше оператор связывается со своими аргументами).

Операторы доступа

a[N] - доступ к элементу массива, функция arrayElement(a, N). a.N - доступ к элементу кортежа, функция tupleElement(a, N).

Оператор числового отрицания

-a - функция negate(a).

Операторы умножения и деления

a * b - функция multiply(a, b) a / b - функция divide(a, b) a % b - функция modulo(a, b)

Операторы сложения и вычитания

a + b - функция plus(a, b) a - b - функция minus(a, b)

Операторы сравнения

a = b - функция equals(a, b) a == b - функция equals(a, b) a != b - функция notEquals(a, b) a <> b - функция notEquals(a, b) a <= b - функция lessOrEquals(a, b) a >= b - функция greaterOrEquals(a, b) a < b - функция less(a, b) a > b - функция greater(a, b) a LIKE s - функция like(a, b) a NOT LIKE s - функция notLike(a, b)

Операторы для работы с множествами

Смотрите раздел "Операторы IN".

a IN ... - функция in(a, b) a NOT IN ... - функция notIn(a, b) a GLOBAL IN ... - функция globalIn(a, b) a GLOBAL NOT IN ... - функция globalNotIn(a, b)

Оператор логического отрицания

NOT a - функция not(a)

Оператор логического "И".

a AND b - функция and(a, b)

Оператор логического "ИЛИ".

a OR b - функция or(a, b)

Условный оператор

a ? b : c - функция if(a, b, c)

Оператор создания лямбда-выражения

x -> expr - функция lambda(x, expr)

Следующие операторы не имеют приоритета, так как представляют собой скобки:

Оператор создания массива

[x1, ...] - функция array(x1, ...)

Оператор создания кортежа

(x1, x2, ...) - функция tuple(x2, x2, ...)

Ассоциативность

Все бинарные операторы имеют левую ассоциативность. Например, 1 + 2 + 3 преобразуется в plus(plus(1, 2), 3). Иногда это работает не так, как ожидается. Например, SELECT 4 > 3 > 2 выдаст 0.

Для эффективности, реализованы функции and и or, принимающие произвольное количество аргументов. Соответствующие цепочки операторов AND и OR, преобразуются в один вызов этих функций.

Функции

Функции бывают как минимум* двух видов - обычные функции (называются просто, функциями) и агрегатные функции. Это совершенно разные вещи. Обычные функции работают так, как будто применяются к каждой строке по отдельности (для каждой строки, результат вычисления функции не зависит от других строк). Агрегатные функции аккумулируют множество значений из разных строк (то есть, зависят от целого множества строк).

В этом разделе речь пойдёт об обычных функциях. Для агрегатных функций, смотрите раздел "Агрегатные функции". * - есть ещё третий вид функций, к которым относится функция arrayJoin; также можно отдельно иметь ввиду табличные функции.

Строгая типизация

В ClickHouse, в отличие от стандартного SQL, типизация является строгой. То есть, не производится неявных преобразований между типами. Все функции работают для определённого набора типов. Это значит, что иногда вам придётся использовать функции преобразования типов.

Склейка одинаковых выражений

Все выражения в запросе, имеющие одинаковые AST (одинаковую запись или одинаковый результат синтаксического разбора), считаются имеющими одинаковые значения. Такие выражения склеиваются и исполняются один раз. Одинаковые подзапросы тоже склеиваются.

Типы результата

Все функции возвращают одно (не несколько, не ноль) значение в качестве результата. Тип результата обычно определяется только типами аргументов, но не значениями аргументов. Исключение - функция tupleElement (оператор a.N), а также функция toFixedString.

Константы

Для простоты, некоторые функции могут работать только с константами в качестве некоторых аргументов. Например, правый аргумент оператора LIKE должен быть константой. Почти все функции возвращают константу для константных аргументов. Исключение - функции генерации случайных чисел. Функция now возвращает разные значения для запросов, выполненных в разное время, но результат считается константой, так как константность важна лишь в пределах одного запроса. Константное выражение также считается константой (например, правую часть оператора LIKE можно сконструировать из нескольких констант).

Функции могут быть по-разному реализованы для константных и не константных аргументов (выполняется разный код). Но результат работы для константы и полноценного столбца, содержащего только одно такое же значение, должен совпадать.

Иммутабельность

Функции не могут поменять значения своих аргументов - любые изменения возвращаются в качестве результата. Соответственно, от порядка записи функций в запросе, результат вычислений отдельных функций не зависит.

Обработка ошибок

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

Вычисление выражений-аргументов

В почти всех языках программирования, для некоторых операторов может не вычисляться один из аргументов. Обычно - для операторов &&, ||, ?:. Но в ClickHouse, аргументы функций (операторов) вычисляются всегда. Это связано с тем, что вычисления производятся не по отдельности для каждой строки, а сразу для целых кусочков столбцов.

Выполнение функций при распределённой обработке запроса

При распределённой обработке запроса, как можно большая часть стадий выполнения запроса производится на удалённых серверах, а оставшиеся стадии (слияние промежуточных результатов и всё, что дальше) - на сервере-инициаторе запроса.

Это значит, что выполнение функций может производиться на разных серверах. Например, в запросе SELECT f(sum(g(x))) FROM distributed_table GROUP BY h(y), - если distributed_table имеет хотя бы два шарда, то функции g и h выполняются на удалённых серверах, а функция f - на сервере-инициаторе запроса; - если distributed_table имеет только один шард, то все функции f, g, h выполняются на сервере этого шарда.

Обычно результат выполнения функции не зависит от того, на каком сервере её выполнить. Но иногда это довольно важно. Например, функции, работающие со словарями, будут использовать словарь, присутствующий на том сервере, на котором они выполняются. Другой пример - функция hostName вернёт имя сервера, на котором она выполняется, и это можно использовать для служебных целей - чтобы в запросе SELECT сделать GROUP BY по серверам.

Если функция в запросе выполняется на сервере-инициаторе запроса, а вам нужно, чтобы она выполнялась на удалённых серверах, вы можете обернуть её в агрегатную функцию any или добавить в ключ в GROUP BY.

Арифметические функции

Для всех арифметических функций, тип результата вычисляется, как минимальный числовой тип, который может вместить результат, если такой тип есть. Минимум берётся одновременно по числу бит, знаковости и "плавучести". Если бит не хватает, то берётся тип максимальной битности.

Пример:

:) SELECT toTypeName(0), toTypeName(0 + 0), toTypeName(0 + 0 + 0), toTypeName(0 + 0 + 0 + 0)

┌─toTypeName(0)─┬─toTypeName(plus(0, 0))─┬─toTypeName(plus(plus(0, 0), 0))─┬─toTypeName(plus(plus(plus(0, 0), 0), 0))─┐
│ UInt8         │ UInt16                 │ UInt32                          │ UInt64                                   │
└───────────────┴────────────────────────┴─────────────────────────────────┴──────────────────────────────────────────┘

Арифметические функции работают для любой пары типов из UInt8, UInt16, UInt32, UInt64, Int8, Int16, Int32, Int64, Float32, Float64.

Переполнение производится также, как в C++.

plus(a, b), оператор a + b

Вычисляет сумму чисел.

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

minus(a, b), оператор a - b

Вычисляет разность чисел. Результат всегда имеет знаковый тип.

Также можно вычитать целые числа из даты и даты-с-временем. Смысл аналогичен - смотрите выше для plus.

multiply(a, b), оператор a * b

Вычисляет произведение чисел.

divide(a, b), оператор a / b

Вычисляет частное чисел. Тип результата всегда является типом с плавающей запятой. То есть, деление не целочисленное. Для целочисленного деления, используйте функцию intDiv. При делении на ноль получится inf, -inf или nan.

intDiv(a, b)

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

intDivOrZero(a, b)

Отличается от intDiv тем, что при делении на ноль или при делении минимального отрицательного числа на минус единицу, возвращается ноль.

modulo(a, b), оператор a % b

Вычисляет остаток от деления. Если аргументы - числа с плавающей запятой, то они предварительно преобразуются в целые числа, путём отбрасывания дробной части. Берётся остаток в том же смысле, как это делается в C++. По факту, для отрицательных чисел, используется truncated division. При делении на ноль или при делении минимального отрицательного числа на минус единицу, кидается исключение.

negate(a), оператор -a

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

abs(a)

Вычисляет абсолютное значение для числа a. То есть, если a < 0, то возвращает -a. Для беззнаковых типов ничего не делает. Для чисел типа целых со знаком, возвращает число беззнакового типа.

Битовые функции

Битовые функции работают для любой пары типов из UInt8, UInt16, UInt32, UInt64, Int8, Int16, Int32, Int64, Float32, Float64.

Тип результата - целое число, битность которого равна максимальной битности аргументов. Если хотя бы один аргумент знаковый, то результат - знаковое число. Если аргумент - число с плавающей запятой - оно приводится к Int64.

bitAnd(a, b)

bitOr(a, b)

bitXor(a, b)

bitNot(a)

bitShiftLeft(a, b)

bitShiftRight(a, b)

Функции сравнения

Функции сравнения возвращают всегда 0 или 1 (UInt8).

Сравнивать можно следующие типы: - числа; - строки и фиксированные строки; - даты; - даты-с-временем; внутри каждой группы, но не из разных групп.

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

Строки сравниваются побайтово. Более короткая строка меньше всех строк, начинающихся с неё и содержащих ещё хотя бы один символ.

Сравнение знаковых и беззнаковых целых чисел производится также, как в C++. То есть, вы можете получить неверный результат в некоторых случаях. Пример: SELECT 9223372036854775807 > -1

equals, оператор a = b и a == b

notEquals, оператор a != b и a <> b

less, оператор <

greater, оператор >

lessOrEquals, оператор <=

greaterOrEquals, оператор >=

Логические функции

Логические функции принимают любые числовые типы, а возвращают число типа UInt8, равное 0 или 1.

Ноль в качестве аргумента считается "ложью", а любое ненулевое значение - "истиной".

and, оператор AND

or, оператор OR

not, оператор NOT

xor

Функции преобразования типов

toUInt8, toUInt16, toUInt32, toUInt64

toInt8, toInt16, toInt32, toInt64

toFloat32, toFloat64

toDate, toDateTime

toString

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

При преобразовании в строку или из строки, производится форматирование или парсинг значения по тем же правилам, что и для формата TabSeparated (и почти всех остальных текстовых форматов). Если распарсить строку не удаётся - кидается исключение и выполнение запроса прерывается.

При преобразовании даты в число или наоборот, дате соответствует число дней от начала unix эпохи. При преобразовании даты-с-временем в число или наоборот, дате-с-временем соответствует число секунд от начала unix эпохи.

В качестве исключения, если делается преобразование из числа типа UInt32, Int32, UInt64, Int64 в Date, и если число больше или равно 65536, то число рассматривается как unix timestamp (а не как число дней) и округляется до даты. Это позволяет поддержать распространённый случай, когда пишут toDate(unix_timestamp), что иначе было бы ошибкой и требовало бы написания более громоздкого toDate(toDateTime(unix_timestamp))

Преобразование между датой и датой-с-временем производится естественным образом: добавлением нулевого времени или отбрасыванием времени.

Преобразование между числовыми типами производится по тем же правилам, что и присваивание между разными числовыми типами в C++.

toFixedString(s, N)

Преобразует аргумент типа String в тип FixedString(N) (строку фиксированной длины N). N должно быть константой. Если строка имеет меньше байт, чем N, то она дополняется нулевыми байтами справа. Если строка имеет больше байт, чем N - кидается исключение.

toStringCutToZero(s)

Принимает аргумент типа String или FixedString. Возвращает String, удаляя нулевые байты с конца строки.

reinterpretAsUInt8, reinterpretAsUInt16, reinterpretAsUInt32, reinterpretAsUInt64

reinterpretAsInt8, reinterpretAsInt16, reinterpretAsInt32, reinterpretAsInt64

reinterpretAsFloat32, reinterpretAsFloat64

reinterpretAsDate, reinterpretAsDateTime

Функции принимают строку и интерпретируют байты, расположенные в начале строки, как число в host order (little endian). Если строка имеет недостаточную длину, то функции работают так, как будто строка дополнена необходимым количеством нулевых байт. Если строка длиннее, чем нужно, то лишние байты игнорируются. Дата интерпретируется, как число дней с начала unix-эпохи, а дата-с-временем - как число секунд с начала unix-эпохи.

reinterpretAsString

Функция принимает число или дату или дату-с-временем и возвращает строку, содержающую байты, представляющие соответствующее значение в host order (little endian). При этом, отбрасываются нулевые байты с конца. Например, значение 255 типа UInt32 будет строкой длины 1 байт.

Функции для работы с датами и временем

toYear

- переводит дату или дату-с-временем в число типа UInt16, содержащее номер года (AD).

toMonth

- переводит дату или дату-с-временем в число типа UInt8, содержащее номер месяца (1-12).

toDayOfMonth

- переводит дату или дату-с-временем в число типа UInt8, содержащее номер дня в месяце (1-31).

toDayOfWeek

- переводит дату или дату-с-временем в число типа UInt8, содержащее номер дня в неделе (понедельник - 1, воскресенье - 7).

toHour

- переводит дату-с-временем в число типа UInt8, содержащее номер часа в сутках (0-23). Функция исходит из допущения, что перевод стрелок вперёд, если осуществляется, то на час, в два часа ночи, а перевод стрелок назад, если осуществляется, то на час, в три часа ночи (что, в общем, не верно - даже в Москве один раз перевод стрелок был осуществлён в другое время).

toMinute

- переводит дату-с-временем в число типа UInt8, содержащее номер минуты в часе (0-59).

toSecond

- переводит дату-с-временем в число типа UInt8, содержащее номер секунды в минуте (0-59). Секунды координации не учитываются.

toMonday

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

toStartOfMonth

- округляет дату или дату-с-временем вниз до первого дня месяца. Возвращается дата.

toStartOfQuarter

- округляет дату или дату-с-временем вниз до первого дня квартала. Первый день квартала - это одно из 1 января, 1 апреля, 1 июля, 1 октября. Возвращается дата.

toStartOfYear

- округляет дату или дату-с-временем вниз до первого дня года. Возвращается дата.

toStartOfMinute

- округляет дату-с-временем вниз до начала минуты.

toStartOfHour

- округляет дату-с-временем вниз до начала часа.

toTime

- переводит дату-с-временем на дату начала unix-эпохи, сохраняя при этом время.

toRelativeYearNum

- переводит дату-с-временем или дату в номер года, начиная с некоторого фиксированного момента в прошлом.

toRelativeMonthNum

- переводит дату-с-временем или дату в номер месяца, начиная с некоторого фиксированного момента в прошлом.

toRelativeWeekNum

- переводит дату-с-временем или дату в номер недели, начиная с некоторого фиксированного момента в прошлом.

toRelativeDayNum

- переводит дату-с-временем или дату в номер дня, начиная с некоторого фиксированного момента в прошлом.

toRelativeHourNum

- переводит дату-с-временем в номер часа, начиная с некоторого фиксированного момента в прошлом.

toRelativeMinuteNum

- переводит дату-с-временем в номер минуты, начиная с некоторого фиксированного момента в прошлом.

toRelativeSecondNum

- переводит дату-с-временем в номер секунды, начиная с некоторого фиксированного момента в прошлом.

now

Принимает ноль аргументов и возвращает текущее время на один из моментов выполнения запроса. Функция возвращает константу, даже если запрос выполнялся долго.

today

Принимает ноль аргументов и возвращает текущую дату на один из моментов выполнения запроса. То же самое, что toDate(now())

yesterday

Принимает ноль аргументов и возвращает вчерашнюю дату на один из моментов выполнения запроса. Делает то же самое, что today() - 1.

timeSlot

- округляет время до получаса. Эта функция является специфичной для Яндекс.Метрики, так как пол часа - минимальное время, для которого, если соседние по времени хиты одного посетителя на одном счётчике отстоят друг от друга строго более, чем на это время, визит может быть разбит на два визита. То есть, кортежи (номер счётчика, идентификатор посетителя, тайм-слот) могут использоваться для поиска хитов, входящий в соответствующий визит.

timeSlots(StartTime, Duration)

- для интервала времени, начинающегося в StartTime и продолжающегося Duration секунд, возвращает массив моментов времени, состоящий из округлений вниз до получаса точек из этого интервала. Например, timeSlots(toDateTime('2012-01-01 12:20:00'), 600) = [toDateTime('2012-01-01 12:00:00'), toDateTime('2012-01-01 12:30:00')]. Это нужно для поиска хитов, входящих в соответствующий визит.

Функции для работы со строками

empty

- возвращает 1 для пустой строки, и 0 для непустой строки. Тип результата - UInt8. Строка считается непустой, если содержит хотя бы один байт, пусть даже это пробел или нулевой байт. Функция также работает для массивов.

notEmpty

- возвращает 0 для пустой строки, и 1 для непустой строки. Тип результата - UInt8. Функция также работает для массивов.

length

- возвращает длину строки в байтах (не символах, не кодовых точках). Тип результата - UInt64. Функция также работает для массивов.

lengthUTF8

- возвращает длину строки в кодовых точках Unicode (не символах), при допущении, что строка содержит набор байт, являющийся текстом в кодировке UTF-8. Если допущение не выполнено - то возвращает какой-нибудь результат (не кидает исключение). Тип результата - UInt64.

lower

- переводит ASCII-символы латиницы в строке в нижний регистр.

upper

- переводит ASCII-символы латиницы в строке в верхний регистр.

lowerUTF8

- переводит строку в нижний регистр, при допущении, что строка содержит набор байт, представляющий текст в кодировке UTF-8. Не учитывает язык. То есть, для турецкого языка, результат может быть не совсем верным. Замечение: не помню, что делает, если строка не в UTF-8.

upperUTF8

- переводит строку в верхний регистр, при допущении, что строка содержит набор байт, представляющий текст в кодировке UTF-8. Не учитывает язык. То есть, для турецкого языка, результат может быть не совсем верным. Замечение: не помню, что делает, если строка не в UTF-8.

reverse

- разворачивает строку (как последовательность байт).

reverseUTF8

- разворачивает последовательность кодовых точек Unicode, при допущении, что строка содержит набор байт, представляющий текст в кодировке UTF-8. Иначе - что-то делает (не кидает исключение).

concat(s1, s2)

- склеивает две строки, без разделителей. Если нужно склеить больше двух строк - пишите concat несколько раз.

substring(s, offset, length)

- возвращает подстроку, начиная с байта по индексу offset, длины length байт. Индексация символов - начиная с единицы (как в стандартном SQL). Аргументы offset и length должны быть константами.

substringUTF8(s, offset, length)

Так же, как substring, но для кодовых точек Unicode. Работает при допущении, что строка содержит набор байт, представляющий текст в кодировке UTF-8. Если допущение не выполнено - то возвращает какой-нибудь результат (не кидает исключение).

appendTrailingCharIfAbsent(s, c)

Если строка s непустая и не содержит символ c на конце, то добавляет символ c в конец.

Функции поиска в строках

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

position(haystack, needle)

- поиск подстроки needle в строке haystack. Возвращает позицию (в байтах) найденной подстроки, начиная с 1, или 0, если подстрока не найдена.

positionUTF8(haystack, needle)

Так же, как position, но позиция возвращается в кодовых точках Unicode. Работает при допущении, что строка содержит набор байт, представляющий текст в кодировке UTF-8. Если допущение не выполнено - то возвращает какой-нибудь результат (не кидает исключение).

match(haystack, pattern)

- проверка строки на соответствие регулярному выражению pattern. Регулярное выражение re2. Возвращает 0 (если не соответствует) или 1 (если соответствует).

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

Регулярное выражение работает со строкой как с набором байт. Регулярное выражение не может содержать нулевые байты. Для шаблонов на поиск подстроки в строке, лучше используйте LIKE или position, так как они работают существенно быстрее.

extract(haystack, pattern)

Извлечение фрагмента строки по регулярному выражению. Если haystack не соответствует регулярному выражению pattern, то возвращается пустая строка. Если регулярное выражение не содержит subpattern-ов, то вынимается фрагмент, который подпадает под всё регулярное выражение. Иначе вынимается фрагмент, который подпадает под первый subpattern.

extractAll(haystack, pattern)

Извлечение всех фрагментов строки по регулярному выражению. Если haystack не соответствует регулярному выражению pattern, то возвращается пустая строка. Возвращается массив строк, состоящий из всех соответствий регулярному выражению. В остальном, поведение аналогично функции extract (по прежнему, вынимается первый subpattern, или всё выражение, если subpattern-а нет).

like(haystack, pattern), оператор haystack LIKE pattern

- проверка строки на соответствие простому регулярному выражению. Регулярное выражение может содержать метасимволы % и _. % обозначает любое количество любых байт (в том числе, нулевое количество символов). _ обозначает один любой байт.

Для экранирования метасимволов, используется символ \ (обратный слеш). Смотрите замечание об экранировании в описании функции match.

Для регулярных выражений вида %needle% действует более оптимальный код, который работает также быстро, как функция position. Для остальных регулярных выражений, код аналогичен функции match.

notLike(haystack, pattern), оператор haystack NOT LIKE pattern

То же, что like, но с отрицанием.

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

replaceOne(haystack, pattern, replacement)

Замена первого вхождения, если такое есть, подстроки pattern в haystack на подстроку replacement. Здесь и далее, pattern и replacement должны быть константами.

replaceAll(haystack, pattern, replacement)

Замена всех вхождений подстроки pattern в haystack на подстроку replacement.

replaceRegexpOne(haystack, pattern, replacement)

Замена по регулярному выражению pattern. Регулярное выражение re2. Заменяется только первое вхождение, если есть. В качестве replacement может быть указан шаблон для замен. Этот шаблон может включать в себя подстановки \0-\9. Подстановка \0 - вхождение регулярного выражения целиком. Подстановки \1-\9 - соответствующие по номеру subpattern-ы. Для указания символа \ в шаблоне, он должен быть экранирован с помощью символа \. Также помните о том, что строковый литерал требует ещё одно экранирование.

Пример 1. Переведём дату в американский формат:

SELECT DISTINCT
    EventDate,
    replaceRegexpOne(toString(EventDate), '(\\d{4})-(\\d{2})-(\\d{2})', '\\2/\\3/\\1') AS res
FROM test.hits
LIMIT 7
FORMAT TabSeparated

2014-03-17      03/17/2014
2014-03-18      03/18/2014
2014-03-19      03/19/2014
2014-03-20      03/20/2014
2014-03-21      03/21/2014
2014-03-22      03/22/2014
2014-03-23      03/23/2014

Пример 2. Размножить строку десять раз:

SELECT replaceRegexpOne('Hello, World!', '.*', '\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0') AS res

┌─res────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World! │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

replaceRegexpAll(haystack, pattern, replacement)

То же самое, но делается замена всех вхождений. Пример:

SELECT replaceRegexpAll('Hello, World!', '.', '\\0\\0') AS res

┌─res────────────────────────┐
│ HHeelllloo,,  WWoorrlldd!! │
└────────────────────────────┘

В качестве исключения, если регулярное выражение сработало на пустой подстроке, то замена делается не более одного раза. Пример:

SELECT replaceRegexpAll('Hello, World!', '^', 'here: ') AS res

┌─res─────────────────┐
│ here: Hello, World! │
└─────────────────────┘

Функции по работе с массивами

empty

- возвращает 1 для пустого массива, и 0 для непустого массива. Тип результата - UInt8. Функция также работает для строк.

notEmpty

- возвращает 0 для пустого массива, и 1 для непустого массива. Тип результата - UInt8. Функция также работает для строк.

length

- возвращает количество элементов в массиве. Тип результата - UInt64. Функция также работает для строк.

emptyArrayUInt8, emptyArrayUInt16, emptyArrayUInt32, emptyArrayUInt64

emptyArrayInt8, emptyArrayInt16, emptyArrayInt32, emptyArrayInt64

emptyArrayFloat32, emptyArrayFloat64

emptyArrayDate, emptyArrayDateTime

emptyArrayString

Принимает ноль аргументов и возвращает пустой массив соответствующего типа.

range(N)

- возвращает массив чисел от 0 до N-1. На всякий случай, если на блок данных, создаются массивы суммарной длины больше 100 000 000 элементов, то кидается исключение.

array(x1, ...), оператор [x1, ...]

- создаёт массив из аргументов функции. Аргументы должны быть константами и иметь типы, для которых есть наименьший общий тип. Должен быть передан хотя бы один аргумент, так как иначе непонятно, какого типа создавать массив. То есть, с помощью этой функции невозможно создать пустой массив (для этого используйте функции emptyArray*, описанные выше). Возвращает результат типа Array(T), где T - наименьший общий тип от переданных аргументов.

arrayElement(arr, n), оператор arr[n]

- достать элемент с индексом n из массива arr. n должен быть любым целочисленным типом. Индексы в массиве начинаются с единицы. Поддерживаются отрицательные индексы - в этом случае, будет выбран соответствующий по номеру элемент с конца. Например, arr[-1] - последний элемент массива.

Если индекс выходит за границы массива, то - если оба аргумента - константы, то кидается исключение; - иначе, возвращается некоторое значение по умолчанию (0 для чисел, пустая строка для строк и т. п.).

has(arr, elem)

- проверка наличия элемента elem в массиве arr. Возвращает 0, если элемента в массиве нет, или 1, если есть. elem должен быть константой.

indexOf(arr, x)

- возвращает индекс элемента x (начиная с 1), если он есть в массиве, или 0, если его нет.

countEqual(arr, x)

- возвращает количество элементов массива, равных x. Эквивалентно arrayCount(elem -> elem = x, arr).

arrayEnumerate(arr)

- возаращает массив [1, 2, 3, ..., length(arr)]

Эта функция обычно используется совместо с ARRAY JOIN. Она позволяет, после применения ARRAY JOIN, посчитать что-либо только один раз для каждого массива. Пример:

SELECT
    count() AS Reaches,
    countIf(num = 1) AS Hits
FROM test.hits
ARRAY JOIN
    GoalsReached,
    arrayEnumerate(GoalsReached) AS num
WHERE CounterID = 160656
LIMIT 10

┌─Reaches─┬──Hits─┐
│   95606 │ 31406 │
└─────────┴───────┘

В этом примере, Reaches - число достижений целей (строк, получившихся после применения ARRAY JOIN), а Hits - число хитов (строк, которые были до ARRAY JOIN). В данном случае, тот же результат можно получить проще:

SELECT
    sum(length(GoalsReached)) AS Reaches,
    count() AS Hits
FROM test.hits
WHERE (CounterID = 160656) AND notEmpty(GoalsReached)

┌─Reaches─┬──Hits─┐
│   95606 │ 31406 │
└─────────┴───────┘

Также эта функция может быть использована в функциях высшего порядка. Например, с её помощью можно достать индексы массива для элементов, удовлетворяющих некоторому условию.

arrayEnumerateUniq(arr, ...)

- возаращает массив, такого же размера, как исходный, где для каждого элемента указано, какой он по счету среди элементов с таким же значением. Например: arrayEnumerateUniq([10, 20, 10, 30]) = [1, 1, 2, 1].

Эта функция полезна при использовании ARRAY JOIN и агрегации по элементам массива. Пример:

SELECT
    Goals.ID AS GoalID,
    sum(Sign) AS Reaches,
    sumIf(Sign, num = 1) AS Visits
FROM test.visits
ARRAY JOIN
    Goals,
    arrayEnumerateUniq(Goals.ID) AS num
WHERE CounterID = 160656
GROUP BY GoalID
ORDER BY Reaches DESC
LIMIT 10

┌──GoalID─┬─Reaches─┬─Visits─┐
│   53225 │    3214 │   1097 │
│ 2825062 │    3188 │   1097 │
│   56600 │    2803 │    488 │
│ 1989037 │    2401 │    365 │
│ 2830064 │    2396 │    910 │
│ 1113562 │    2372 │    373 │
│ 3270895 │    2262 │    812 │
│ 1084657 │    2262 │    345 │
│   56599 │    2260 │    799 │
│ 3271094 │    2256 │    812 │
└─────────┴─────────┴────────┘

В этом примере, для каждого идентификатора цели, посчитано количество достижений целей (каждый элемент вложенной структуры данных Goals является достижением целей) и количество визитов. Если бы не было ARRAY JOIN, мы бы считали количество визитов как sum(Sign). Но в данном случае, строчки были размножены по вложенной структуре Goals, и чтобы после этого учесть каждый визит один раз, мы поставили условие на значение функции arrayEnumerateUniq(Goals.ID).

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

SELECT arrayEnumerateUniq([1, 1, 1, 2, 2, 2], [1, 1, 2, 1, 1, 2]) AS res

┌─res───────────┐
│ [1,2,1,1,2,1] │
└───────────────┘

Это нужно при использовании ARRAY JOIN с вложенной структурой данных и затем агрегации по нескольким элементам этой структуры.

arrayJoin(arr)

- особенная функция. Смотрите раздел "Функция arrayJoin".

Функции высшего порядка

Оператор ->, функция lambda(params, expr)

Позволяет описать лямбда-функцию для передачи в функцию высшего порядка. Слева от стрелочки стоит формальный параметр - произвольный идентификатор, или несколько формальных параметров - произвольные идентификаторы в кортеже. Справа от стрелочки стоит выражение, в котором могут использоваться эти формальные параметры, а также любые столбцы таблицы. Примеры: x -> 2 * x, str -> str != Referer. Функции высшего порядка, в качестве своего функционального аргумента могут принимать только лямбда-функции.

В функции высшего порядка может быть передана лямбда-функция, принимающая несколько аргументов. В этом случае, в функцию высшего порядка передаётся несколько массивов одинаковых длин, которым эти аргументы будут соответствовать.

Для всех функций кроме arrayMap, arrayFilter, первый аргумент (лямбда-функция) может отсутствовать. В этом случае, подразумевается тождественное отображение.

arrayMap(func, arr1, ...)

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

arrayFilter(func, arr1, ...)

Вернуть массив, содержащий только те элементы массива arr1, для которых функция func возвращает не 0.

Примеры:

SELECT arrayFilter(x -> x LIKE '%World%', ['Hello', 'abc World']) AS res

┌─res───────────┐
│ ['abc World'] │
└───────────────┘

SELECT
    arrayFilter(
        (i, x) -> x LIKE '%World%',
        arrayEnumerate(arr),
        ['Hello', 'abc World'] AS arr)
    AS res

┌─res─┐
│ [2] │
└─────┘

arrayCount([func,] arr1, ...)

Вернуть количество элементов массива arr, для которых функция func возвращает не 0. Если func не указана - вернуть количество ненулевых элементов массива.

arrayExists([func,] arr1, ...)

Вернуть 1, если существует хотя бы один элемент массива arr, для которого функция func возвращает не 0. Иначе вернуть 0.

arrayAll([func,] arr1, ...)

Вернуть 1, если для всех элементов массива arr, функция func возвращает не 0. Иначе вернуть 0.

arraySum([func,] arr1, ...)

Вернуть сумму значений функции func. Если функция не указана - просто вернуть сумму элементов массива.

arrayFirst(func, arr1, ...)

Вернуть первый элемент массива arr1, для которого функция func возвращает не 0.

arrayFirstIndex(func, arr1, ...)

Вернуть индекс первого элемента массива arr1, для которого функция func возвращает не 0.

Функции разбиения и слияния строк и массивов

splitByChar(separator, s)

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

splitByString(separator, s)

- то же самое, но использует строку из нескольких символов в качестве разделителя. Строка должна быть непустой.

alphaTokens(s)

- выделяет подстроки из подряд идущих байт из диапазонов a-z и A-Z. Возвращается массив выделенных подстрок.

Функции для работы с URL

Все функции работают не по RFC - то есть, максимально упрощены ради производительности.

Функции, извлекающие часть URL-а.

Если в URL-е нет ничего похожего, то возвращается пустая строка.

protocol

- выделить протокол. Примеры: http, ftp, mailto, magnet...

domain

- выделить домен.

domainWithoutWWW

- выделить домен, удалив не более одного 'www.' с начала, если есть.

topLevelDomain

- выделить домен верхнего уровня. Пример: .ru.

firstSignificantSubdomain

- выделить "первый существенный поддомен". Это понятие является нестандартным и специфично для Яндекс.Метрики. Первый существенный поддомен - это домен второго уровня, если он не равен одному из com, net, org, co, или домен третьего уровня, иначе. Например, firstSignificantSubdomain('https://news.yandex.ru/') = 'yandex', firstSignificantSubdomain('https://news.yandex.com.tr/') = 'yandex'. Список "несущественных" доменов второго уровня и другие детали реализации могут изменяться в будущем.

cutToFirstSignificantSubdomain

- выделяет часть домена, включающую поддомены верхнего уровня до "первого существенного поддомена" (см. выше). Например, cutToFirstSignificantSubdomain('https://news.yandex.com.tr/') = 'yandex.com.tr'.

path

- выделить путь. Пример: /top/news.html Путь не включает в себя query-string.

pathFull

- то же самое, но включая query-string и fragment. Пример: /top/news.html?page=2#comments

queryString

- выделить query-string. Пример: page=1&lr=213. query-string не включает в себя начальный знак вопроса, а также # и всё, что после #.

fragment

- выделить fragment identifier. fragment не включает в себя начальный символ решётки.

queryStringAndFragment

- выделить query-string и fragment identifier. Пример: страница=1#29390.

extractURLParameter(URL, name)

- достать значение параметра name в URL, если такой есть; или пустую строку, иначе; если параметров с таким именем много - вернуть первый попавшийся. Функция работает при допущении, что имя параметра закодировано в URL в точности таким же образом, что и в переданном аргументе.

extractURLParameters(URL)

- получить массив строк вида name=value, соответствующих параметрам URL. Значения никак не декодируются.

extractURLParameterNames(URL)

- получить массив строк вида name, соответствующих именам параметров URL. Значения никак не декодируются.

URLHierarchy(URL)

- получить массив, содержащий URL, обрезанный с конца по символам /, ? в пути и query-string. Подряд идущие символы-разделители считаются за один. Резка производится в позиции после всех подряд идущих символов-разделителей. Пример:

URLPathHierarchy(URL)

- то же самое, но без протокола и хоста в результате. Элемент / (корень) не включается. Пример:

Функция используется для реализации древовидных отчётов по URL в Яндекс.Метрике.

URLPathHierarchy('https://example.com/browse/CONV-6788') =
[
    '/browse/',
    '/browse/CONV-6788'
]

Функции, удаляющие часть из URL-а.

Если в URL-е нет ничего похожего, то URL остаётся без изменений.

cutWWW

- удалить не более одного 'www.' с начала домена URL-а, если есть.

cutQueryString

- удалить query-string. Знак вопроса тоже удаляется.

cutFragment

- удалить fragment identifier. Символ решётки тоже удаляется.

cutQueryStringAndFragment

- удалить query-string и fragment identifier. Знак вопроса и символ решётки тоже удаляются.

cutURLParameter(URL, name)

- удалить параметр URL с именем name, если такой есть. Функция работает при допущении, что имя параметра закодировано в URL в точности таким же образом, что и в переданном аргументе.

Функции для работы с IP-адресами

IPv4NumToString(num)

Принимает число типа UInt32. Интерпретирует его, как IPv4-адрес в big endian. Возвращает строку, содержащую соответствующий IPv4-адрес в формате A.B.C.D (числа в десятичной форме через точки).

IPv4StringToNum(s)

Функция, обратная к IPv4NumToString. Если IPv4 адрес в неправильном формате, то возвращает 0.

IPv4NumToStringClassC(num)

Похоже на IPv4NumToString, но вместо последнего октета используется xxx. Пример:

SELECT
    IPv4NumToStringClassC(ClientIP) AS k,
    count() AS c
FROM test.hits
GROUP BY k
ORDER BY c DESC
LIMIT 10

┌─k──────────────┬─────c─┐
│ 83.149.9.xxx   │ 26238 │
│ 217.118.81.xxx │ 26074 │
│ 213.87.129.xxx │ 25481 │
│ 83.149.8.xxx   │ 24984 │
│ 217.118.83.xxx │ 22797 │
│ 78.25.120.xxx  │ 22354 │
│ 213.87.131.xxx │ 21285 │
│ 78.25.121.xxx  │ 20887 │
│ 188.162.65.xxx │ 19694 │
│ 83.149.48.xxx  │ 17406 │
└────────────────┴───────┘

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

IPv6NumToString(x)

Принимает значение типа FixedString(16), содержащее IPv6-адрес в бинарном виде. Возвращает строку, содержащую этот адрес в текстовом виде. IPv6-mapped IPv4 адреса выводится в формате ::ffff:111.222.33.44. Примеры:

SELECT IPv6NumToString(toFixedString(unhex('2A0206B8000000000000000000000011'), 16)) AS addr

┌─addr─────────┐
│ 2a02:6b8::11 │
└──────────────┘
SELECT
    IPv6NumToString(ClientIP6 AS k),
    count() AS c
FROM hits_all
WHERE EventDate = today() AND substring(ClientIP6, 1, 12) != unhex('00000000000000000000FFFF')
GROUP BY k
ORDER BY c DESC
LIMIT 10

┌─IPv6NumToString(ClientIP6)──────────────┬─────c─┐
│ 2a02:2168:aaa:bbbb::2                   │ 24695 │
│ 2a02:2698:abcd:abcd:abcd:abcd:8888:5555 │ 22408 │
│ 2a02:6b8:0:fff::ff                      │ 16389 │
│ 2a01:4f8:111:6666::2                    │ 16016 │
│ 2a02:2168:888:222::1                    │ 15896 │
│ 2a01:7e00::ffff:ffff:ffff:222           │ 14774 │
│ 2a02:8109:eee:ee:eeee:eeee:eeee:eeee    │ 14443 │
│ 2a02:810b:8888:888:8888:8888:8888:8888  │ 14345 │
│ 2a02:6b8:0:444:4444:4444:4444:4444      │ 14279 │
│ 2a01:7e00::ffff:ffff:ffff:ffff          │ 13880 │
└─────────────────────────────────────────┴───────┘
SELECT
    IPv6NumToString(ClientIP6 AS k),
    count() AS c
FROM hits_all
WHERE EventDate = today()
GROUP BY k
ORDER BY c DESC
LIMIT 10

┌─IPv6NumToString(ClientIP6)─┬──────c─┐
│ ::ffff:94.26.111.111       │ 747440 │
│ ::ffff:37.143.222.4        │ 529483 │
│ ::ffff:5.166.111.99        │ 317707 │
│ ::ffff:46.38.11.77         │ 263086 │
│ ::ffff:79.105.111.111      │ 186611 │
│ ::ffff:93.92.111.88        │ 176773 │
│ ::ffff:84.53.111.33        │ 158709 │
│ ::ffff:217.118.11.22       │ 154004 │
│ ::ffff:217.118.11.33       │ 148449 │
│ ::ffff:217.118.11.44       │ 148243 │
└────────────────────────────┴────────┘

IPv6StringToNum(s)

Функция, обратная к IPv6NumToString. Если IPv6 адрес в неправильном формате, то возвращает строку из нулевых байт. HEX может быть в любом регистре.

Функции генерации псевдослучайных чисел

Используются некриптографические генераторы псевдослучайных чисел.

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

rand

- возвращает псевдослучайное число типа UInt32, равномерно распределённое среди всех чисел типа UInt32. Используется linear congruential generator.

rand64

- возвращает псевдослучайное число типа UInt64, равномерно распределённое среди всех чисел типа UInt64. Используется linear congruential generator.

Функции хэширования

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

halfMD5

- вычисляет MD5 от строки. Затем берёт первые 8 байт от хэша и интерпретирует их как UInt64 в big endian. Принимает аргумент типа String. Возвращает UInt64. Функция работает достаточно медленно (5 миллионов коротких строк в секунду на одном процессорном ядре). Если вам не нужен конкретно MD5, то используйте вместо этого функцию sipHash64.

MD5

- вычисляет MD5 от строки и возвращает полученный набор байт в виде FixedString(16). Если вам не нужен конкретно MD5, а нужен неплохой криптографический 128-битный хэш, то используйте вместо этого функцию sipHash128. Если вы хотите получить такой же результат, как выдаёт утилита md5sum, напишите lower(hex(MD5(s))).

sipHash64

- вычисляет SipHash от строки. Принимает аргумент типа String. Возвращает UInt64. SipHash - криптографическая хэш-функция. Работает быстрее чем MD5 не менее чем в 3 раза. Подробнее смотрите по ссылке: https://131002.net/siphash/

sipHash128

- вычисляет SipHash от строки. Принимает аргумент типа String. Возвращает FixedString(16). Отличается от sipHash64 тем, что финальный xor-folding состояния делается только до 128 бит.

cityHash64

- вычисляет CityHash64 от строки или похожую хэш-функцию для произвольного количества аргументов произвольного типа. Если аргумент имеет тип String, то используется CityHash. Это быстрая некриптографическая хэш-функция неплохого качества для строк. Если аргумент имеет другой тип, то используется implementation specific быстрая некриптографическая хэш-функция неплохого качества. Если передано несколько аргументов, то функция вычисляется по тем же правилам, с помощью комбинации по цепочке с использованием комбинатора из CityHash. Например, так вы можете вычислить чексумму всей таблицы с точностью до порядка строк: SELECT sum(cityHash64(*)) FROM table.

intHash32

- вычисляет 32-битный хэш-код от целого числа любого типа. Это сравнительно быстрая некриптографическая хэш-функция среднего качества для чисел.

intHash64

- вычисляет 64-битный хэш-код от целого числа любого типа. Работает быстрее, чем intHash32. Качество среднее.

SHA1

SHA224

SHA256

- вычисляет SHA-1, SHA-224, SHA-256 от строки и возвращает полученный набор байт в виде FixedString(20), FixedString(28), FixedString(32). Функция работает достаточно медленно (SHA-1 - примерно 5 миллионов коротких строк в секунду на одном процессорном ядре, SHA-224 и SHA-256 - примерно 2.2 миллионов). Рекомендуется использовать эти функции лишь в тех случаях, когда вам нужна конкретная хэш-функция и вы не можете её выбрать. Даже в этих случаях, рекомендуется применять функцию оффлайн - заранее вычисляя значения при вставке в таблицу, вместо того, чтобы применять её при SELECT-ах.

URLHash(url[, N])

Быстрая некриптографическая хэш-функция неплохого качества для строки, полученной из URL путём некоторой нормализации. URLHash(s) - вычислить хэш от строки без одного завершающего символа /, ? или # на конце, если такой там есть. URLHash(s, N) - вычислить хэш от строки до N-го уровня в иерархии URL, без одного завершающего символа /, ? или # на конце, если такой там есть. Уровни аналогичные URLHierarchy. Функция специфична для Яндекс.Метрики.

Функции кодирования

hex

Принимает строку, число, дату или дату-с-временем. Возвращает строку, содержащую шестнадцатиричное представление аргумента. Используются заглавные буквы A-F. Не используются префиксы 0x и суффиксы h. Для строк просто все байты кодируются в виде двух шестнадцатиричных цифр. Числа выводятся в big endian ("человеческом") формате. Для чисел вырезаются старшие нули, но только по целым байтам. Например, hex(1) = '01'. Даты кодируются как число дней с начала unix-эпохи. Даты-с-временем кодируются как число секунд с начала unix-эпохи.

unhex(str)

Принимает строку, содержащую произвольное количество шестнадцатиричных цифр, и возвращает строку, содержащую соответствующие байты. Поддерживаются как строчные, так и заглавные буквы A-F. Число шестнадцатиричных цифр не обязано быть чётным. Если оно нечётное - последняя цифра интерпретируется как младшая половинка байта 00-0F. Если строка-аргумент содержит что-либо кроме шестнадцатиричных цифр, то будет возвращён какой-либо implementation-defined результат (не кидается исключение). Если вы хотите преобразовать результат в число, то вы можете использовать функции reverse и reinterpretAsType.

bitmaskToList(num)

Принимает целое число. Возвращает строку, содержащую список степеней двойки, в сумме дающих исходное число; по возрастанию, в текстовом виде, через запятую, без пробелов.

bitmaskToArray(num)

Принимает целое число. Возвращает массив чисел типа UInt64, содержащий степени двойки, в сумме дающих исходное число; числа в массиве идут по возрастанию.

Функции округления

floor(x[, N])

Вернуть наибольшее круглое число, которое меньше или равно, чем x.

Круглым называется число, кратное 1 / 10N или ближайшее к нему число соответствующего типа данных, если 1 / 10N не представимо точно. N - целочисленная константа, не обязательный параметр. По умолчанию - ноль, что означает - округлять до целого числа. N может быть отрицательным. Примеры: floor(123.45, 1) = 123.4, floor(123.45, -1) = 120. x - любой числовой тип. Результат - число того же типа. Для целочисленных аргументов имеет смысл округление с отрицательным значением N (для неотрицательных N, функция ничего не делает). В случае переполнения при округлении (например, floor(-128, -1)), возвращается implementation specific результат.

ceil(x[, N])

Вернуть наименьшее круглое число, которое больше или равно, чем x. В остальном, аналогично функции floor, см. выше.

round(x[, N])

Вернуть ближайшее к num круглое число, которое может быть меньше или больше или равно x. Если x находится посередине от ближайших круглых чисел, то возвращается какое-либо одно из них (implementation specific). Число -0. может считаться или не считаться круглым (implementation specific). В остальном, аналогично функциям floor и ceil, см. выше.

roundToExp2(num)

Принимает число. Если число меньше единицы - возвращает 0. Иначе округляет число вниз до ближайшей (целой неотрицательной) степени двух.

roundDuration(num)

Принимает число. Если число меньше единицы - возвращает 0. Иначе округляет число вниз до чисел из набора: 1, 10, 30, 60, 120, 180, 240, 300, 600, 1200, 1800, 3600, 7200, 18000, 36000. Эта функция специфична для Яндекс.Метрики и предназначена для реализации отчёта по длительности визита.

roundAge(num)

Принимает число. Если число меньше 18 - возвращает 0. Иначе округляет число вниз до чисел из набора: 18, 25, 35, 45. Эта функция специфична для Яндекс.Метрики и предназначена для реализации отчёта по возрасту посетителей.

Условные функции

if(cond, then, else), оператор cond ? then : else

Возвращает then, если cond != 0 или else, если cond = 0. cond должно иметь тип UInt8, а then и else должны иметь тип, для которого есть наименьший общий тип.

Математические функции

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

e()

Принимает ноль аргументов, возвращает число типа Float64, близкое к числу e.

pi()

Принимает ноль аргументов, возвращает число типа Float64, близкое к числу π.

exp(x)

Принимает числовой аргумент, возвращает число типа Float64, близкое к экспоненте от аргумента.

log(x)

Принимает числовой аргумент, возвращает число типа Float64, близкое к натуральному логарифму от аргумента.

exp2(x)

Принимает числовой аргумент, возвращает число типа Float64, близкое к 2x.

log2(x)

Принимает числовой аргумент, возвращает число типа Float64, близкое к двоичному логарифму от аргумента.

exp10(x)

Принимает числовой аргумент, возвращает число типа Float64, близкое к 10x.

log10(x)

Принимает числовой аргумент, возвращает число типа Float64, близкое к десятичному логарифму от аргумента.

sqrt(x)

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

cbrt(x)

Принимает числовой аргумент, возвращает число типа Float64, близкое к кубическому корню от аргумента.

erf(x)

Если x неотрицательно, то erf(x / σ√2) - вероятность того, что случайная величина, имеющая нормальное распределение со среднеквадратичным отклонением σ, принимает значение, отстоящее от мат. ожидания больше чем на x.

Пример (правило трёх сигм):

SELECT erf(3 / sqrt(2))

┌─erf(divide(3, sqrt(2)))─┐
│      0.9973002039367398 │
└─────────────────────────┘

erfc(x)

Принимает числовой аргумент, возвращает число типа Float64, близкое к 1 - erf(x), но без потери точности для больших x.

lgamma(x)

Логарифм от гамма функции.

tgamma(x)

Гамма функция.

sin(x)

Синус.

cos(x)

Косинус.

tan(x)

Тангенс.

asin(x)

Арксинус.

acos(x)

Арккосинус.

atan(x)

Арктангенс.

pow(x, y)

xy.

Функции для работы со словарями Яндекс.Метрики

Чтобы указанные ниже функции работали, в конфиге сервера должны быть указаны пути и адреса для получения всех словарей Яндекс.Метрики. Словари загружаются при первом вызове любой из этих функций. Если справочники не удаётся загрузить - будет выкинуто исключение.

О том, как создать справочники, смотрите в разделе "Словари".

Множественные геобазы

ClickHouse поддерживает работу одновременно с несколькими альтернативными геобазами (иерархиями регионов), для того чтобы можно было поддержать разные точки зрения о принадлежности регионов странам.

В конфиге clickhouse-server указывается файл с иерархией регионов: <path_to_regions_hierarchy_file>/opt/geo/regions_hierarchy.txt</path_to_regions_hierarchy_file>

Кроме указанного файла, рядом ищутся файлы, к имени которых (до расширения) добавлен символ _ и какой угодно суффикс. Например, также найдётся файл /opt/geo/regions_hierarchy_ua.txt, если такой есть.

ua называется ключом словаря. Для словаря без суффикса, ключ является пустой строкой.

Все словари перезагружаются в рантайме (раз в количество секунд, заданное в конфигурационном параметре builtin_dictionaries_reload_interval, по умолчанию - раз в час), но перечень доступных словарей определяется один раз, при старте сервера.

Во все функции по работе с регионами, в конце добавлен один необязательный аргумент - ключ словаря. Далее он обозначен как geobase. Пример:

regionToCountry(RegionID) - использует словарь по умолчанию: /opt/geo/regions_hierarchy.txt;
regionToCountry(RegionID, '') - использует словарь по умолчанию: /opt/geo/regions_hierarchy.txt;
regionToCountry(RegionID, 'ua') - использует словарь для ключа ua: /opt/geo/regions_hierarchy_ua.txt;

regionToCity(id[, geobase])

Принимает число типа UInt32 - идентификатор региона из геобазы Яндекса. Если регион является городом или входит в некоторый город, то возвращает идентификатор региона - соответствующего города. Иначе возвращает 0.

regionToArea(id[, geobase])

Переводит регион в область (тип в геобазе - 5). В остальном, аналогично функции regionToCity.

SELECT DISTINCT regionToName(regionToArea(toUInt32(number), 'ua'))
FROM system.numbers
LIMIT 15

┌─regionToName(regionToArea(toUInt32(number), \'ua\'))─┐
│                                                      │
│ Москва и Московская область                          │
│ Санкт-Петербург и Ленинградская область              │
│ Белгородская область                                 │
│ Ивановская область                                   │
│ Калужская область                                    │
│ Костромская область                                  │
│ Курская область                                      │
│ Липецкая область                                     │
│ Орловская область                                    │
│ Рязанская область                                    │
│ Смоленская область                                   │
│ Тамбовская область                                   │
│ Тверская область                                     │
│ Тульская область                                     │
└──────────────────────────────────────────────────────┘

regionToDistrict(id[, geobase])

Переводит регион в федеральный округ (тип в геобазе - 4). В остальном, аналогично функции regionToCity.

SELECT DISTINCT regionToName(regionToDistrict(toUInt32(number), 'ua'))
FROM system.numbers
LIMIT 15

┌─regionToName(regionToDistrict(toUInt32(number), \'ua\'))─┐
│                                                          │
│ Центральный федеральный округ                            │
│ Северо-Западный федеральный округ                        │
│ Южный федеральный округ                                  │
│ Северо-Кавказский федеральный округ                      │
│ Приволжский федеральный округ                            │
│ Уральский федеральный округ                              │
│ Сибирский федеральный округ                              │
│ Дальневосточный федеральный округ                        │
│ Шотландия                                                │
│ Фарерские острова                                        │
│ Фламандский регион                                       │
│ Брюссельский столичный регион                            │
│ Валлония                                                 │
│ Федерация Боснии и Герцеговины                           │
└──────────────────────────────────────────────────────────┘

regionToCountry(id[, geobase])

Переводит регион в страну. В остальном, аналогично функции regionToCity. Пример: regionToCountry(toUInt32(213)) = 225 - преобразовали Москву (213) в Россию (225).

regionToContinent(id[, geobase])

Переводит регион в континент. В остальном, аналогично функции regionToCity. Пример: regionToContinent(toUInt32(213)) = 10001 - преобразовали Москву (213) в Евразию (10001).

regionToPopulation(id[, geobase])

Получает население для региона. Население может быть прописано в файлах с геобазой. Смотрите в разделе "Встроенные словари". Если для региона не прописано население, возвращается 0. В геобазе Яндекса, население может быть прописано для дочерних регионов, но не прописано для родительских.

regionIn(lhs, rhs[, geobase])

Проверяет принадлежность региона lhs региону rhs. Возвращает число типа UInt8, равное 1, если принадлежит и 0, если не принадлежит. Отношение рефлексивное - любой регион принадлежит также самому себе.

regionHierarchy(id[, geobase])

Принимает число типа UInt32 - идентификатор региона из геобазы Яндекса. Возвращает массив идентификаторов регионов, состоящий из переданного региона и всех родителей по цепочке. Пример: regionHierarchy(toUInt32(213)) = [213,1,3,225,10001,10000].

regionToName(id[, lang])

Принимает число типа UInt32 - идентификатор региона из геобазы Яндекса. Вторым аргументом может быть передана строка - название языка. Поддерживаются языки ru, en, ua, uk, by, kz, tr. Если второй аргумент отсутствует - используется язык ru. Если язык не поддерживается - кидается исключение. Возвращает строку - название региона на соответствующем языке. Если региона с указанным идентификатором не существует - возвращается пустая строка.

ua и uk обозначают одно и то же - украинский язык.

OSToRoot

Принимает число типа UInt8 - идентификатор операционной системы из словаря Яндекс.Метрики. Если переданному числу соответствует какая-либо операционная система - возвращает число типа UInt8 - идентификатор соответствующей корневой операционной системы (например, переводит Windows Vista в Windows). Иначе возвращает 0.

OSIn(lhs, rhs)

Проверяет принадлежность операционной системы lhs операционной системе rhs.

OSHierarchy

Принимает число типа UInt8 - идентификатор операционной системы из словаря Яндекс.Метрики. Возвращает массив с иерархией операционных систем. Аналогично функции regionHierarchy.

SEToRoot

Принимает число типа UInt8 - идентификатор поисковой системы из словаря Яндекс.Метрики. Если переданному числу соответствует какая-либо поисковая система - возвращает число типа UInt8 - идентификатор соответствующей корневой поисковой системы (например, переводит Яндекс.Картинки в Яндекс). Иначе возвращает 0.

SEIn(lhs, rhs)

Проверяет принадлежность поисковой системы lhs поисковой системе rhs.

SEHierarchy

Принимает число типа UInt8 - идентификатор поисковой системы из словаря Яндекс.Метрики. Возвращает массив с иерархией поисковых систем. Аналогично функции regionHierarchy.

Функции для работы с внешними словарями

Подробнее смотрите в разделе "Внешние словари".

dictGetUInt8, dictGetUInt16, dictGetUInt32, dictGetUInt64

dictGetInt8, dictGetInt16, dictGetInt32, dictGetInt64

dictGetFloat32, dictGetFloat64

dictGetDate, dictGetDateTime

dictGetString

dictGetT('dict_name', 'attr_name', id) - получить из словаря dict_name значение атрибута attr_name по ключу id. dict_name и attr_name - константные строки. id должен иметь тип UInt64. Если ключа id нет в словаре - вернуть значение по умолчанию, заданное в описании словаря.

dictIsIn

dictIsIn('dict_name', child_id, ancestor_id) - для иерархического словаря dict_name - узнать, находится ли ключ child_id внутри ancestor_id (или совпадает с ancestor_id). Возвращает UInt8.

dictGetHierarchy

dictGetHierarchy('dict_name', id) - для иерархического словаря dict_name - вернуть массив ключей словаря, начиная с id и продолжая цепочкой родительских элементов. Возвращает Array(UInt64).

Функции для работы с JSON.

В Яндекс.Метрике пользователями передаётся JSON в качестве параметров визитов. Для работы с таким JSON-ом, реализованы некоторые функции. (Хотя в большинстве случаев, JSON-ы дополнительно обрабатываются заранее, и полученные значения кладутся в отдельные столбцы в уже обработанном виде.) Все эти функции исходят из сильных допущений о том, каким может быть JSON, и при этом стараются почти ничего не делать.

Делаются следующие допущения: 1. Имя поля (аргумент функции) должно быть константой; 2. Считается, что имя поля в JSON-е закодировано некоторым каноническим образом. Например, visitParamHas('{"abc":"def"}', 'abc') = 1 , но visitParamHas('{"\\u0061\\u0062\\u0063":"def"}', 'abc') = 0 3. Поля ищутся на любом уровне вложенности, без разбора. Если есть несколько подходящих полей - берётся первое. 4. В JSON-е нет пробельных символов вне строковых литералов.

visitParamHas(params, name)

Проверить наличие поля с именем name.

visitParamExtractUInt(params, name)

Распарсить UInt64 из значения поля с именем name. Если поле строковое - попытаться распарсить число из начала строки. Если такого поля нет, или если оно есть, но содержит не число, то вернуть 0.

visitParamExtractInt(params, name)

Аналогично для Int64.

visitParamExtractFloat(params, name)

Аналогично для Float64.

visitParamExtractBool(params, name)

Распарсить значение true/false. Результат - UInt8.

visitParamExtractRaw(params, name)

Вернуть значение поля, включая разделители. Примеры: visitParamExtractRaw('{"abc":"\\n\\u0000"}', 'abc') = '"\\n\\u0000"' visitParamExtractRaw('{"abc":{"def":[1,2,3]}}', 'abc') = '{"def":[1,2,3]}'

visitParamExtractString(params, name)

Распарсить строку в двойных кавычках. Значение разэскейпливается. Если разэскейпить не удалось, то возвращается пустая строка. Примеры: visitParamExtractString('{"abc":"\\n\\u0000"}', 'abc') = '\n\0' visitParamExtractString('{"abc":"\\u263a"}', 'abc') = '☺' visitParamExtractString('{"abc":"\\u263"}', 'abc') = '' visitParamExtractString('{"abc":"hello}', 'abc') = '' На данный момент, не поддерживаются записанные в формате \uXXXX\uYYYY кодовые точки не из basic multilingual plane (они переводятся не в UTF-8, а в CESU-8).

Функции для реализации оператора IN.

in, notIn, globalIn, globalNotIn

Смотрите раздел "Операторы IN".

tuple(x, y, ...), оператор (x, y, ...)

- функция, позволяющая сгруппировать несколько столбцов. Для столбцов, имеющих типы T1, T2, ... возвращает кортеж типа Tuple(T1, T2, ...), содержащий эти столбцы. Выполнение функции ничего не стоит. Кортежи обычно используются как промежуточное значение в качестве аргумента операторов IN, или для создания списка формальных параметров лямбда-функций. Кортежи не могут быть записаны в таблицу.

tupleElement(tuple, n), оператор x.N

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

Прочие функции

hostName()

- возвращает строку - имя хоста, на котором эта функция была выполнена. При распределённой обработке запроса, это будет имя хоста удалённого сервера, если функция выполняется на удалённом сервере.

visibleWidth(x)

- вычисляет приблизительную ширину при выводе значения в текстовом (tab-separated) виде на консоль. Функция используется системой для реализации Pretty форматов.

toTypeName(x)

- получить имя типа. Возвращает строку, содержащую имя типа переданного аргумента.

blockSize()

- получить размер блока. В ClickHouse выполнение запроса всегда идёт по блокам (наборам кусочков столбцов). Функция позволяет получить размер блока, для которого её вызвали.

materialize(x)

- превратить константу в полноценный столбец, содержащий только одно значение. В ClickHouse полноценные столбцы и константы представлены в памяти по-разному. Функции по-разному работают для аргументов-констант и обычных аргументов (выполняется разный код), хотя результат почти всегда должен быть одинаковым. Эта функция предназначена для отладки такого поведения.

ignore(...)

- функция, принимающая любые аргументы, и всегда возвращающая 0. При этом, аргумент всё-равно вычисляется. Это может использоваться для бенчмарков.

sleep(seconds)

Спит seconds секунд на каждый блок данных. Можно указать как целое число, так и число с плавающей запятой.

currentDatabase()

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

isFinite(x)

Принимает Float32 или Float64 и возвращает UInt8, равный 1, если агрумент не бесконечный и не NaN, иначе 0.

isInfinite(x)

Принимает Float32 или Float64 и возвращает UInt8, равный 1, если агрумент бесконечный, иначе 0. Отметим, что в случае NaN возвращается 0.

isNaN(x)

Принимает Float32 или Float64 и возвращает UInt8, равный 1, если аргумент является NaN, иначе 0.

bar

Позволяет построить unicode-art диаграмму.

bar(x, min, max, width) - рисует полосу ширины пропорциональной (x - min) и равной width символов при x == max. min, max - целочисленные константы, значение должно помещаться в Int64. width - константа, положительное число, может быть дробным.

Полоса рисуется с точностью до одной восьмой символа. Пример:

SELECT
    toHour(EventTime) AS h,
    count() AS c,
    bar(c, 0, 600000, 20) AS bar
FROM test.hits
GROUP BY h
ORDER BY h ASC

┌──h─┬──────c─┬─bar────────────────┐
│  0 │ 292907 │ █████████▋         │
│  1 │ 180563 │ ██████             │
│  2 │ 114861 │ ███▋               │
│  3 │  85069 │ ██▋                │
│  4 │  68543 │ ██▎                │
│  5 │  78116 │ ██▌                │
│  6 │ 113474 │ ███▋               │
│  7 │ 170678 │ █████▋             │
│  8 │ 278380 │ █████████▎         │
│  9 │ 391053 │ █████████████      │
│ 10 │ 457681 │ ███████████████▎   │
│ 11 │ 493667 │ ████████████████▍  │
│ 12 │ 509641 │ ████████████████▊  │
│ 13 │ 522947 │ █████████████████▍ │
│ 14 │ 539954 │ █████████████████▊ │
│ 15 │ 528460 │ █████████████████▌ │
│ 16 │ 539201 │ █████████████████▊ │
│ 17 │ 523539 │ █████████████████▍ │
│ 18 │ 506467 │ ████████████████▊  │
│ 19 │ 520915 │ █████████████████▎ │
│ 20 │ 521665 │ █████████████████▍ │
│ 21 │ 542078 │ ██████████████████ │
│ 22 │ 493642 │ ████████████████▍  │
│ 23 │ 400397 │ █████████████▎     │
└────┴────────┴────────────────────┘

transform

Преобразовать значение согласно явно указанному отображению одних элементов на другие. Имеется два варианта функции:

1. transform(x, array_from, array_to, default)

x - что преобразовывать. array_from - константный массив значений для преобразования. array_to - константный массив значений, в которые должны быть преобразованы значения из from. default - константа, какое значение использовать, если x не равен ни одному из значений во from.

array_from и array_to - массивы одинаковых размеров.

Типы: transform(T, Array(T), Array(U), U) -> U

T и U - могут быть числовыми, строковыми, или Date или DateTime типами. При этом, где обозначена одна и та же буква (T или U), могут быть, в случае числовых типов, не совпадающие типы, а типы, для которых есть общий тип. Например, первый аргумент может иметь тип Int64, а второй - Array(UInt16).

Если значение x равно одному из элементов массива array_from, то возвращает соответствующий (такой же по номеру) элемент массива array_to; иначе возвращает default. Если имеется несколько совпадающих элементов в array_from, то возвращает какой-нибудь из соответствующих.

Пример:


SELECT
    transform(SearchEngineID, [2, 3], ['Яндекс', 'Google'], 'Остальные') AS title,
    count() AS c
FROM test.hits
WHERE SearchEngineID != 0
GROUP BY title
ORDER BY c DESC

┌─title─────┬──────c─┐
│ Яндекс    │ 498635 │
│ Google    │ 229872 │
│ Остальные │ 104472 │
└───────────┴────────┘

2. transform(x, array_from, array_to)

Отличается от первого варианта отсутствующим аргументом default. Если значение x равно одному из элементов массива array_from, то возвращает соответствующий (такой же по номеру) элемент массива array_to; иначе возвращает x.

Типы: transform(T, Array(T), Array(T)) -> T

Пример:


SELECT
    transform(domain(Referer), ['yandex.ru', 'google.ru', 'vk.com'], ['www.yandex', 'ввв.яндекс.рф', 'example.com']) AS s,
    count() AS c
FROM test.hits
GROUP BY domain(Referer)
ORDER BY count() DESC
LIMIT 10

┌─s──────────────┬───────c─┐
│                │ 2906259 │
│ www.yandex     │  867767 │
│ ███████.ru     │  313599 │
│ mail.yandex.ru │  107147 │
│ ввв.яндекс.рф  │  105668 │
│ ██████.ru      │  100355 │
│ █████████.ru   │   65040 │
│ news.yandex.ru │   64515 │
│ ██████.net     │   59141 │
│ example.com    │   57316 │
└────────────────┴─────────┘

Функция arrayJoin

Это совсем необычная функция.

Обычные функции не изменяют множество строк, а лишь изменяют значения в каждой строке (map). Агрегатные функции выполняют свёртку множества строк (fold, reduce). Функция arrayJoin выполняет размножение каждой строки в множество строк (unfold).

Функция принимает в качестве аргумента массив, и размножает исходную строку в несколько строк - по числу элементов массива. Все значения в столбцах просто копируются, кроме значения в столбце с применением этой функции - он заменяется на соответствующее значение массива.

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

Обратите внимание на синтаксис ARRAY JOIN в запросе SELECT, который предоставляет более широкие возможности.

Пример:

:) SELECT arrayJoin([1, 2, 3] AS src) AS dst, 'Hello', src

SELECT
    arrayJoin([1, 2, 3] AS src) AS dst,
    'Hello',
    src

┌─dst─┬─\'Hello\'─┬─src─────┐
│   1 │ Hello     │ [1,2,3] │
│   2 │ Hello     │ [1,2,3] │
│   3 │ Hello     │ [1,2,3] │
└─────┴───────────┴─────────┘

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

count()

Считает количество строк. Принимает ноль аргументов, возвращает UInt64. Единственный верный способ записи этой функции - count(). В отличие от стандартного SQL, нельзя писать count(*) - это парсится, как передача всех столбцов в качестве аргументов в функцию count. Также не имеет смысла писать count(column), так как NULL-ов в ClickHouse нет. Не поддерживается синтаксис COUNT(DISTINCT x) - для этого есть отдельная агрегатная функция uniq.

Запрос вида SELECT count() FROM table не оптимизируется, так как количество записей в таблице нигде не хранится отдельно - из таблицы будет выбран какой-нибудь достаточно маленький столбец, и будет посчитано количество значений в нём.

any(x)

Выбирает первое попавшееся значение. Порядок выполнения запроса может быть произвольным и даже каждый раз разным, поэтому результат данной функции недетерминирован. Для получения детерминированного результата, можно использовать функции min или max вместо any.

В некоторых случаях, вы всё-таки можете рассчитывать на порядок выполнения запроса. Это - случаи, когда SELECT идёт из подзапроса, в котором используется ORDER BY.

При наличии в запросе SELECT секции GROUP BY или хотя бы одной агрегатной функции, ClickHouse (в отличие от MySQL) требует, чтобы все выражения в секциях SELECT, HAVING, ORDER BY вычислялись из ключей или из агрегатных функций. То есть, каждый выбираемый из таблицы столбец, должен использоваться либо в ключах, либо внутри агрегатных функций. Чтобы получить поведение, как в MySQL, вы можете поместить остальные столбцы в агрегатную функцию any.

anyLast(x)

Выбирает последнее попавшееся значение. Результат так же недетерминирован, как и для функции any.

min(x)

Вычисляет минимум.

max(x)

Вычисляет максимум.

argMin(arg, val)

Вычисляет значение arg при минимальном значении val. Если есть несколько разных значений arg для минимальных значений val, то выдаётся первое попавшееся из таких значений.

argMax(arg, val)

Вычисляет значение arg при максимальном значении val. Если есть несколько разных значений arg для максимальных значений val, то выдаётся первое попавшееся из таких значений.

sum(x)

Вычисляет сумму. Работает только для чисел.

avg(x)

Вычисляет среднее. Работает только для чисел. Результат всегда - Float64.

uniq(x)

Приближённо вычисляет количество различных значений аргумента. Работает для чисел, строк, дат, дат-с-временем.

Используется алгоритм типа adaptive sampling: в качестве состояния вычислений используется выборка значений хэшей элементов, размером до 65535. По сравнению с широко известным алгоритмом HyperLogLog, этот алгоритм менее эффективен по соотношению точности и потребляемой памяти (даже с точностью до пропорциональности), зато является адаптивным, вследствие чего, при достаточно большой точности, потребляет меньше памяти при одновременном подсчёте кардинальности большого количества множеств, кардинальность которых имеет степенное распределение (то есть, в случае, когда подавляющее большинство множеств - маленькие). Также алгоритм является очень точным для множеств небольшой кардинальности (до 65536) и очень эффективным по CPU (при расчёте не слишком большого количества таких функций, использование uniq почти так же быстро, как использование других агрегатных функций).

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

Результат детерминирован (не зависит от порядка выполнения запроса).

uniqHLL12(x)

Приближённо вычисляет количество различных значений аргумента, используя алгоритм HyperLogLog. Используется 212 5-битовых ячеек. Размер состояния чуть больше 2.5 КБ.

Результат детерминирован (не зависит от порядка выполнения запроса).

В большинстве случаев, используйте функцию uniq. Данную функцию следует использовать, если вы точно представляете её преимущества.

uniqExact(x)

Вычисляет количество различных значений аргумента, точно. Не стоит бояться приближённых расчётов. Поэтому, используйте лучше функцию uniq. Функцию uniqExact следует использовать, если вам точно нужен точный результат.

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

groupArray(x)

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

В некоторых случаях, вы всё-таки можете рассчитывать на порядок выполнения запроса. Это - случаи, когда SELECT идёт из подзапроса, в котором используется ORDER BY.

groupUniqArray(x)

Составляет массив из различных значений аргумента. Расход оперативки такой же, как у функции uniqExact.

median(x)

Приближённо вычисляет медиану. Также смотрите аналогичную функцию quantile. Работает для чисел, дат, дат-с-временем. Для чисел возвращает Float64, для дат - дату, для дат-с-временем - дату-с-временем.

Используется reservoir sampling с размером резервуара до 8192. При необходимости, результат выдаётся с линейной аппроксимацией из двух соседних значений. Алгоритм показал себя более практичным, чем другой известный алгоритм - QDigest.

Результат зависит от порядка выполнения запроса, и является недетерминированным.

medianTiming(x)

Вычисляет медиану с фиксированной точностью. Работает для чисел. Предназначена для расчёта медианы от времени загрузки страницы в миллисекундах. Также смотрите аналогичную функцию quantileTiming.

Если значение больше 30 000 (соответствует времени загрузки страницы большем 30 секундам.) - результат приравнивается к 30 000. Если значение меньше 1024, то вычисление точное. Если значение от 1025 до 29 999, то вычисление идёт с округлением до числа, кратного 16.

Дополнительно, если всего в агрегатную функцию было передано меньше 32 значений, то вычисление точное.

При передаче в функцию отрицательных значений, поведение не определено.

Возвращаемое значение имеет тип Float32. Когда в функцию не было передано ни одного значения (при использовании medianTimingIf, quantileTimingIf), возвращается nan. Это сделано, чтобы отличать такие случаи от нулей. Смотрите замечание о сортировке NaN-ов в разделе "Секция ORDER BY".

Результат детерминирован (не зависит от порядка выполнения запроса).

Для своей задачи (расчёт квантилей времени загрузки страниц), использование этой функции эффективнее и результат точнее, чем для функции median/quantile.

medianDeterministic(x, determinator)

Работает аналогично функции median - приближённо вычисляет медиану. Но, в отличие от функции median, результат является детерминированным и не зависит от порядка выполнения запроса.

Для этого, функция принимает второй аргумент - "детерминатор". Это некоторое число, хэш от которого используется вместо генератора случайных чисел в алгоритме reservoir sampling. Для правильной работы функции, одно и то же значение детерминатора не должно встречаться слишком часто. В качестве детерминатора вы можете использовать идентификатор события, идентификатор посетителя и т. п.

Не используйте эту функцию для рассчёта таймингов. Для этого есть более подходящие функции - medianTiming, quantileTiming, quantilesTiming.

medianTimingWeighted(x, weight)

Отличается от функции medianTiming наличием второго аргумента - "веса". Вес - неотрицательное целое число. Результат считается так же, как если бы в функцию medianTiming значение x было передано weight количество раз.

varSamp(x)

Вычисляет величину Σ((x - x̅)2) / (n - 1), где n - размер выборки, x̅ - среднее значение x.

Она представляет собой несмещённую оценку дисперсии случайной величины, если переданные в функцию значения являются выборкой этой случайной величины.

Возвращает Float64. В случае, когда n <= 1, возвращается +∞.

varPop(x)

Вычисляет величину Σ((x - x̅)2) / n, где n - размер выборки, x̅ - среднее значение x.

То есть, дисперсию для множества значений. Возвращает Float64.

stddevSamp(x)

Результат равен квадратному корню от varSamp(x).

stddevPop(x)

Результат равен квадратному корню от varPop(x).

covarSamp(x, y)

Вычисляет величину Σ((x - x̅)(y - y̅)) / (n - 1).

Возвращает Float64. В случае, когда n <= 1, возвращается +∞.

covarPop(x, y)

Вычисляет величину Σ((x - x̅)(y - y̅)) / n.

corr(x, y)

Вычисляет коэффициент корреляции Пирсона: Σ((x - x̅)(y - y̅)) / sqrt(Σ((x - x̅)2) * Σ((y - y̅)2)).

Параметрические агрегатные функции

Некоторые агрегатные функции могут принимать не только столбцы-аргументы (по которым производится свёртка), но и набор параметров - констант для инициализации. Синтаксис - две пары круглых скобок вместо одной. Первая - для параметров, вторая - для аргументов.

quantile(level)(x)

Приближённо вычисляет квантиль уровня level. level - константа, число с плавающей запятой от 0 до 1. Рекомендуется использовать значения level в диапазоне 0.01 .. 0.99. Не используйте значения level, равные 0 или 1 - для таких случаев есть функции min и max.

Алгоритм такой же, как у функции median. На самом деле, quantile и median внутри - одна и та же функция. Функцию quantile можно использовать без параметров - в этом случае, она вычисляет медиану, а функцию median можно использовать с параметрами - в этом случае она вычисляет квантиль заданного уровня.

При использовании нескольких функций quantile и median с разными уровнями в запросе, внутренние состояния не объединяются (то есть, запрос работает менее эффективно, чем мог бы). В этом случае, используйте функцию quantiles.

quantiles(level1, level2, ...)(x)

Приближённо вычисляет квантили всех указанных уровней. Результат - массив, содержащий соответствующее количество значений.

quantileTiming(level)(x)

Вычисляет квантиль уровня level тем же алгоритмом, что и функция medianTiming.

quantilesTiming(level1, level2, ...)(x)

Вычисляет квантили всех указанных уровней тем же алгоритмом, что и функция medianTiming.

quantileTimingWeighted(level)(x, weight)

Вычисляет квантиль уровня level тем же алгоритмом, что и функция medianTimingWeighted.

quantilesTimingWeighted(level1, level2, ...)(x, weight)

Вычисляет квантили всех указанных уровней тем же алгоритмом, что и функция medianTimingWeighted.

quantileDeterministic(level)(x, determinator)

Вычисляет квантиль уровня level тем же алгоритмом, что и функция medianDeterministic.

quantilesDeterministic(level1, level2, ...)(x, determinator)

Вычисляет квантили всех указанных уровней тем же алгоритмом, что и функция medianDeterministic.

sequenceMatch(pattern)(time, cond1, cond2, ...)

Сопоставление с образцом для цепочки событий.

pattern - строка, содержащая шаблон для сопоставления. Шаблон похож на регулярное выражение. time - время события, тип DateTime cond1, cond2 ... - от одного до 32 аргументов типа UInt8 - признаков, было ли выполнено некоторое условие для события.

Функция собирает в оперативке последовательность событий. Затем производит проверку на соответствие этой последовательности шаблону. Возвращает UInt8 - 0, если шаблон не подходит и 1, если шаблон подходит.

Пример: sequenceMatch('(?1).*(?2)')(EventTime, URL LIKE '%company%', URL LIKE '%cart%') - была ли цепочка событий, в которой посещение страницы с адресом, содержащим company было раньше по времени посещения страницы с адресом, содержащим cart.

Это вырожденный пример. Его можно записать с помощью других агрегатных функций: minIf(EventTime, URL LIKE '%company%') < maxIf(EventTime, URL LIKE '%cart%'). Но в более сложных случаях, такого решения нет.

Синтаксис шаблонов: (?1) - ссылка на условие (вместо 1 - любой номер); .* - произвольное количество любых событий; (?t>=1800) - условие на время; за указанное время допускается любое количество любых событий; вместо >= могут использоваться операторы <, >, <=; вместо 1800 может быть любое число;

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

uniqUpTo(N)(x)

Вычисляет количество различных значений аргумента, если оно меньше или равно N. В случае, если количество различных значений аргумента больше N, возвращает N + 1.

Рекомендуется использовать для маленьких N - до 10. Максимальное значение N - 100.

Для состояния агрегатной функции используется количество оперативки равное 1 + N * размер одного значения байт. Для строк запоминается некриптографический хэш, имеющий размер 8 байт. То есть, для строк вычисление приближённое.

Работает максимально быстро за исключением паталогических случаев, когда используется большое значение N и количество уникальных значений чуть меньше N.

Пример применения: Задача: показывать в отчёте только поисковые фразы, по которым было хотя бы 5 уникальных посетителей. Решение: пишем в запросе GROUP BY SearchPhrase HAVING uniqUpTo(4)(UserID) >= 5

Комбинаторы агрегатных функций

К имени агрегатной функции может быть приписан некоторый суффикс. При этом, работа агрегатной функции некоторым образом модифицируется. Существуют комбинаторы If и Array. Смотрите разделы ниже.

Комбинатор -If. Условные агрегатные функции

К имени любой агрегатной функции может быть приписан суффикс -If. В этом случае, агрегатная функция принимает ещё один дополнительный аргумент - условие (типа UInt8). Агрегатная функция будет обрабатывать только те строки, для которых условие сработало. Если условие ни разу не сработало - возвращается некоторое значение по умолчанию (обычно - нули, пустые строки).

Примеры: countIf(cond), avgIf(x, cond), quantilesTimingIf(level1, level2)(x, cond), argMinIf(arg, val, cond) и т. п.

С помощью условных агрегатных функций, вы можете вычислить агрегаты сразу для нескольких условий, не используя подзапросы и JOIN-ы. Например, в Яндекс.Метрике, условные агрегатные функции используются для реализации функциональности сравнения сегментов.

Комбинатор -Array. Агрегатные функции для аргументов-массивов

К имени любой агрегатной функции может быть приписан суффикс -Array. В этом случае, агрегатная функция вместо аргументов типов T принимает аргументы типов Array(T) (массивы). Если агрегатная функция принимает несколько аргументов, то это должны быть массивы одинаковых длин. При обработке массивов, агрегатная функция работает, как исходная агрегатная функция по всем элементам массивов.

Пример 1: sumArray(arr) - просуммировать все элементы всех массивов arr. В данном примере можно было бы написать проще: sum(arraySum(arr)).

Пример 2: uniqArray(arr) - посчитать количество уникальных элементов всех массивов arr. Это можно было бы сделать проще: uniq(arrayJoin(arr)), но не всегда есть возможность добавить arrayJoin в запрос.

Комбинаторы -If и -Array можно сочетать. При этом, должен сначала идти Array, а потом If. Примеры: uniqArrayIf(arr, cond), quantilesTimingArrayIf(level1, level2)(arr, cond). Из-за такого порядка получается, что аргумент cond не должен быть массивом.

Комбинатор -State.

Комбинатор -Merge.

Словари

Словарь - это отображение (ключ -> атрибуты), которое можно использовать в запросе в виде функций. Это можно рассматривать как более удобный и максимально эффективный вариант JOIN-а с таблицами-справочниками (dimension tables).

Существуют встроенные и подключаемые (внешние) словари.

Встроенные словари

ClickHouse содержит встроенную возможность работы с геобазой.

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

Все функции поддерживают "транслокальность", то есть возможность использовать одновременно разные точки зрения на принадлежность регионов. Подробнее смотрите в разделе "Функции для работы со словарями Яндекс.Метрики".

В пакете по умолчанию, встроенные словари выключены. Для включения, раскомментируйте параметры path_to_regions_hierarchy_file и path_to_regions_names_files в конфигурационном файле сервера.

Геобаза загружается из текстовых файлов. Если вы работаете в Яндексе, то для их создания вы можете воспользоваться инструкцией: https://github.yandex-team.ru/raw/Metrika/ClickHouse_private/master/doc/create_embedded_geobase_dictionaries.txt

Положите файлы regions_hierarchy*.txt в директорию path_to_regions_hierarchy_file. Этот конфигурационный параметр должен содержать путь к файлу regions_hierarchy.txt (иерархия регионов по умолчанию), а другие файлы (regions_hierarchy_ua.txt) должны находиться рядом в той же директории.

Положите файлы regions_names_*.txt в директорию path_to_regions_names_files.

Также вы можете создать эти файлы самостоятельно. Формат файлов такой:

regions_hierarchy*.txt: TabSeparated (без заголовка), столбцы: - идентификатор региона (UInt32); - идентификатор родительского региона (UInt32); - тип региона (UInt8): 1 - континент, 3 - страна, 4 - федеральный округ, 5 - область, 6 - город; остальные типы не имеют значения; - население (UInt32) - не обязательный столбец.

regions_names_*.txt: TabSeparated (без заголовка), столбцы: - идентификатор региона (UInt32); - имя региона (String) - не может содержать табы или переводы строк, даже заэскейпленные.

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

Словари могут обновляться без перезапуска сервера. Но набор доступных словарей не обновляется. Для обновления проверяется время модификации файлов; если файл изменился, то словарь будет обновлён. Периодичность проверки настраивается конфигурационным параметром builtin_dictionaries_reload_interval. Обновление словарей (кроме загрузки при первом использовании) не блокирует запросы - во время обновления запросы используют старую версию словарей. Если при обновлении возникнет ошибка, то ошибка пишется в лог сервера, а запросы продолжат использовать старую версию словарей.

Рекомендуется периодически обновлять словари с геобазой. При обновлении, генерируйте новые файлы, записывая их в отдельное место, а только когда всё готово - переименовывайте в файлы, которые использует сервер.

Также имеются функции для работы с идентификаторами операционных систем и поисковых систем Яндекс.Метрики, пользоваться которыми не нужно.

Внешние словари

Существует возможность подключать свои собственные словари из различных источников данных. Источником данных для словаря может быть файл на локальной файловой системе, сервер ClickHouse или сервер MySQL. Словарь может полностью храниться в оперативке и периодически обновляться, или быть частично закэшированным в оперативке и динамически подгружать отсутствующие значения.

Конфигурация внешних словарей находится в отдельном файле или файлах, указанных в конфигурационном параметре dictionaries_config. Этот параметр содержит абсолютный или относительный путь к файлу с конфигурацией словарей. Относительный путь - относительно директории с конфигурационным файлом сервера. Путь может содержать wildcard-ы * и ? - тогда рассматриваются все подходящие файлы. Пример: dictionaries/*.xml.

Конфигурация словарей, а также множество файлов с конфигурацией, может обновляться без перезапуска сервера. Сервер проверяет обновления каждые 5 секунд. То есть, словари могут подключаться динамически.

Создание словарей может производиться при старте сервера или при первом использовании. Это определяется конфигурационном параметром dictionaries_lazy_load (в основном конфигурационном файле сервера). Параметр не обязателен, по умолчанию - true. Если true, то каждый словарь создаётся при первом использовании; если словарь не удалось создать - вызов функции, использующей словарь, кидает исключение. Если false, то все словари создаются при старте сервера, и в случае ошибки, сервер завершает работу.

Конфигурационный файл словарей имеет вид:

<dictionaries>
    <comment>Не обязательный элемент с любым содержимым; полностью игнорируется.</comment>

    <!-- Можно задать произвольное количество разных словарей. -->
    <dictionary>
        <!-- Имя словаря. Под этим именем словарь будет доступен для использования. -->
        <name>os</name>

        <!-- Источник данных. -->
        <source>

            <!-- Источник - файл на локальной файловой системе. -->
            <file>
                <!-- Путь на локальной файловой системе. -->
                <path>/opt/dictionaries/os.tsv</path>
                <!-- С помощью какого формата понимать файл. -->
                <format>TabSeparated</format>
            </file>

            <!-- или источник - таблица на сервере MySQL.
            <mysql>
                <!- - Эти параметры могут быть указаны как снаружи (общие для всех реплик), так и внутри конкретной реплики - ->
                <port>3306</port>
                <user>metrika</user>
                <password>qwerty</password>
                <!- - Можно указать от одной до произвольного количества реплик для отказоустойчивости. - ->
                <replica>
                    <host>example01-1</host>
                    <priority>1</priority> <!- - Меньше значение - больше приоритет. - ->
                </replica>
                <replica>
                    <host>example01-2</host>
                    <priority>1</priority>
                </replica>
                <db>conv_main</db>
                <table>counters</table>
            </mysql>
            -->

            <!-- или источник - таблица на сервере ClickHouse.
            <clickhouse>
                <host>example01-01-1</host>
                <port>9000</port>
                <user>default</user>
                <password></password>
                <db>default</db>
                <table>counters</table>
            </clickhouse>
            <!- - Если адрес похож на localhost, то запрос будет идти без сетевого взаимодействия.
                  Для отказоустойчивости, вы можете создать Distributed таблицу на localhost и прописать её. - ->
            -->
        </source>

        <!-- Периодичность обновления для полностью загружаемых словарей. 0 - никогда не обновлять. -->
        <lifetime>
            <min>300</min>
            <max>360</max>
            <!-- Периодичность обновления выбирается равномерно-случайно между min и max,
                 чтобы размазать по времени нагрузку при обновлении словарей на большом количестве серверов. -->
        </lifetime>

        <!-- или
        <!- - Периодичность обновления для полностью загружаемых словарей или время инвалидации для кэшируемых словарей.
              0 - никогда не обновлять. - ->
        <lifetime>300</lifetime>
        -->

        <layout>   <!-- Способ размещения в памяти. -->
            <flat />
            <!-- или
            <hashed />
            или
            <cache>
                <!- - Размер кэша в количестве ячеек; округляется вверх до степени двух. - ->
                <size_in_cells>1000000000</size_in_cells>
            </cache>
            -->
        </layout>

        <!-- Структура. -->
        <structure>
            <!-- Описание столбца, являющегося идентификатором (ключём) словаря. -->
            <id>
                <!-- Имя столбца с идентификатором. -->
                <name>Id</name>
            </id>

            <attribute>
                <!-- Имя столбца. -->
                <name>Name</name>
                <!-- Тип столбца. (Как столбец понимается при загрузке.
                     В случае MySQL, в таблице может быть TEXT, VARCHAR, BLOB, но загружается всё как String) -->
                <type>String</type>
                <!-- Какое значение использовать для несуществующего элемента. В примере - пустая строка. -->
                <null_value></null_value>
            </attribute>

            <!-- Может быть указано произвольное количество атрибутов. -->
            <attribute>
                <name>ParentID</name>
                <type>UInt64</type>
                <null_value>0</null_value>
                <!-- Определяет ли иерархию - отображение в идентификатор родителя (по умолчанию, false). -->
                <hierarchical>true</hierarchical>
                <!-- Можно считать отображение id -> attribute инъективным, чтобы оптимизировать GROUP BY. (по умолчанию, false) -->
                <injective>true</injective>
            </attribute>
        </structure>
    </dictionary>
</dictionaries>

Идентификатор (ключевой атрибут) словаря должен быть числом, помещающимся в UInt64. Составные и строковые ключи не поддерживаются. Впрочем, если в вашем словаре сложный ключ, вы можете захэшировать его и использовать хэш в качестве ключа. Для этого можно использовать View (как в ClickHouse, так и в MySQL).

Существует три способа размещения словаря в памяти.

1. flat - в виде плоских массивов. Самый эффективный способ. Он подходит, если все ключи меньше 500 000. Если при создании словаря обнаружен ключ больше, то кидается исключение и словарь не создаётся. Словарь загружается в оперативку целиком. Словарь использует количество оперативки, пропорциональное максимальному значению ключа. Ввиду ограничения на 500 000, потребление оперативки вряд ли может быть большим. Поддерживаются все виды источников. При обновлении, данные (из файла, из таблицы) читаются целиком.

2. hashed - в виде хэш-таблиц. Слегка менее эффективный способ. Словарь тоже загружается в оперативку целиком, и может содержать произвольное количество элементов с произвольными идентификаторами. На практике, имеет смысл использовать до десятков миллионов элементов, пока хватает оперативки. Поддерживаются все виды источников. При обновлении, данные (из файла, из таблицы) читаются целиком.

3. cache - наименее эффективный способ. Подходит, если словарь не помещается в оперативку. Представляет собой кэш из фиксированного количества ячеек, в которых могут быть расположены часто используемые данные. Поддерживается источник MySQL и ClickHouse; источник-файл не поддерживается. При поиске в словаре, сначала просматривается кэш. На каждый блок данных, все не найденные в кэше ключи (или устаревшие ключи) собираются в пачку, и с этой пачкой делается запрос к источнику вида SELECT attrs... FROM db.table WHERE id IN (k1, k2, ...). Затем полученные данные записываются в кэш.

Рекомендуется использовать способ flat, если возможно, или hashed. Скорость работы словарей с таким размещением в памяти является безупречной.

Способ cache следует использовать лишь если это неизбежно. Скорость работы кэша очень сильно зависит от правильности настройки и сценария использования. Словарь типа cache нормально работает лишь при достаточно больших hit rate-ах (рекомендуется 99% и выше). Посмотреть средний hit rate можно в таблице system.dictionaries. Укажите достаточно большой размер кэша. Количество ячеек следует подобрать экспериментальным путём - выставить некоторое значение, с помощью запроса добиться полной заполненности кэша, посмотреть на потребление оперативки (эта информация находится в таблице system.dictionaries); затем пропорционально увеличить количество ячеек так, чтобы расходовалось разумное количество оперативки. В качестве источника для кэша рекомендуется MySQL, так как ClickHouse плохо обрабатывает запросы со случайными чтениями.

Во всех случаях, производительность будет выше, если вызывать функцию для работы со словарём после GROUP BY, или если доставаемый атрибут помечен как инъективный. Для cache словарей, производительность будет лучше, если вызывать функцию после LIMIT-а - для этого можно использовать подзапрос с LIMIT-ом, и снаружи вызывать функцию со словарём.

Атрибут называется инъективным, если разным ключам соответствуют разные значения атрибута. Тогда при использовании в GROUP BY функции, достающей значение атрибута по ключу, эта функция автоматически выносится из GROUP BY.

При обновлении словарей из файла, сначала проверяется время модификации файла, и загрузка производится только если файл изменился. При обновлении из MySQL, для flat и hashed словарей, сначала делается запрос SHOW TABLE STATUS и смотрится время обновления таблицы. И если оно не NULL, то оно сравнивается с запомненным временем. Это работает для MyISAM таблиц, а для InnoDB таблиц время обновления неизвестно, поэтому загрузка из InnoDB делается при каждом обновлении.

Для cache-словарей может быть задано время устаревания (lifetime) данных в кэше. Если от загрузки данных в ячейке прошло больше времени, чем lifetime, то значение не используется, и будет запрошено заново при следующей необходимости его использовать.

Если словарь не удалось ни разу загрузить, то при попытке его использования, будет брошено исключение. Если при запросе к источнику cached словаря возникла ошибка, то будет брошено исключение. Обновление словарей (кроме загрузки при первом использовании) не блокирует запросы - во время обновления используется старая версия словаря. Если при обновлении возникнет ошибка, то ошибка пишется в лог сервера, а запросы продолжат использовать старую версию словарей.

Список внешних словарей и их статус можно посмотреть в таблице system.dictionaries.

Для использования внешних словарей, смотрите раздел "Функции для работы с внешними словарями".

Обратите внимание, что вы можете преобразовать значения по небольшому словарю, указав всё содержимое словаря прямо в запросе SELECT - смотрите раздел "Функция transform". Эта функциональность никак не связана с внешними словарями.

Настройки

Здесь будут рассмотрены настройки, которые можно задать с помощью запроса SET или в конфигурационном файле. Напомню, что эти настройки могут быть выставлены в пределах сессии или глобально. Настройки, которые можно задать только в конфигурационном файле сервера, здесь рассмотрены не будут.

max_block_size

Данные в ClickHouse обрабатываются по блокам (наборам кусочков столбцов). Внутренние циклы обработки одного блока достаточно эффективны, но при этом существуют заметные издержки на каждый блок. max_block_size - это рекомендация, какого размера блоки (в количестве строк) загружать из таблицы. Размер блока должен быть не слишком маленьким, чтобы издержки на каждый блок оставались незаметными, и не слишком большим, чтобы запрос с LIMIT-ом, который завершается уже после первого блока, выполнялся быстро; чтобы не использовалось слишком много оперативки при вынимании большого количества столбцов в несколько потоков; чтобы оставалась хоть какая-нибудь кэш-локальность.

По умолчанию - 65 536.

Из таблицы не всегда загружаются блоки размера max_block_size. Если ясно, что нужно прочитать меньше данных, то будет считан блок меньшего размера.

max_insert_block_size

Формировать блоки указанного размера, при вставке в таблицу. Эта настройка действует только в тех случаях, когда сервер сам формирует такие блоки. Например, при INSERT-е через HTTP интерфейс, сервер парсит формат данных, и формирует блоки указанного размера. А при использовании clickhouse-client, клиент сам парсит данные, и настройка max_insert_block_size на сервере не влияет на размер вставляемых блоков. При использованиии INSERT SELECT, настройка так же не имеет смысла, так как данные будут вставляться теми блоками, которые вышли после SELECT-а.

По умолчанию - 1 048 576.

Это намного больше, чем max_block_size. Это сделано, потому что некоторые движки таблиц (*MergeTree) будут на каждый вставляемый блок формировать кусок данных на диске, что является довольно большой сущностью. Также, в таблицах типа *MergeTree, данные сортируются при вставке, и достаточно большой размер блока позволяет отсортировать больше данных в оперативке.

max_threads

Максимальное количество потоков обработки запроса - без учёта потоков для чтения данных с удалённых серверов (смотрите параметр max_distributed_connections).

Этот параметр относится к потокам, которые выполняют параллельно одни стадии конвейера выполнения запроса. Например, если чтение из таблицы, вычисление выражений с функциями, фильтрацию с помощью WHERE и предварительную агрегацию для GROUP BY можно делать параллельно с использованием как минимум max_threads потоков, то будет использовано max_threads потоков.

По умолчанию - 8.

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

Для запросов, которые быстро завершаются из-за LIMIT-а, имеет смысл выставить max_threads поменьше. Например, если нужное количество записей находится в каждом блоке, то при max_threads = 8 будет считано 8 блоков, хотя достаточно было прочитать один.

Чем меньше max_threads, тем меньше будет использоваться оперативки.

max_compress_block_size

Максимальный размер блоков не сжатых данных перед сжатием при записи в таблицу. По умолчанию - 1 048 576 (1 MiB). При уменьшении размера, незначительно уменьшается коэффициент сжатия, незначительно возрастает скорость сжатия и разжатия за счёт кэш-локальности, и уменьшается потребление оперативки. Как правило, не имеет смысла менять эту настройку.

Не путайте блоки для сжатия (кусок памяти, состоящий из байт) и блоки для обработки запроса (пачка строк из таблицы).

min_compress_block_size

Для таблиц типа *MergeTree. В целях уменьшения задержек при обработке запросов, блок сжимается при записи следующей засечки, если его размер не меньше min_compress_block_size. По умолчанию - 65 536.

Реальный размер блока, если несжатых данных меньше max_compress_block_size, будет не меньше этого значения и не меньше объёма данных на одну засечку.

Рассмотрим пример. Пусть index_granularity, указанная при создании таблицы - 8192.

Пусть мы записываем столбец типа UInt32 (4 байта на значение). При записи 8192 строк, будет всего 32 КБ данных. Так как min_compress_block_size = 65 536, сжатый блок будет сформирован на каждые две засечки.

Пусть мы записываем столбец URL типа String (средний размер - 60 байт на значение). При записи 8192 строк, будет, в среднем, чуть меньше 500 КБ данных. Так как это больше 65 536 строк, то сжатый блок будет сформирован на каждую засечку. В этом случае, при чтении с диска данных из диапазона в одну засечку, не будет разжато лишних данных.

Как правило, не имеет смысла менять эту настройку.

max_query_size

Максимальный кусок запроса, который будет считан в оперативку для разбора парсером языка SQL. Запрос INSERT также содержит данные для INSERT-а, которые обрабатываются отдельным, потоковым парсером (расходующим O(1) оперативки), и не учитываются в этом ограничении.

По умолчанию - 64 KiB.

interactive_delay

Интервал в микросекундах для проверки, не запрошена ли остановка выполнения запроса, и отправки прогресса. По умолчанию - 100 000 (проверять остановку запроса и отправлять прогресс десять раз в секунду).

connect_timeout

receive_timeout

send_timeout

Таймауты в секундах на сокет, по которому идёт общение с клиентом. По умолчанию - 10, 300, 300.

poll_interval

Блокироваться в цикле ожидания запроса в сервере на указанное количество секунд. По умолчанию - 10.

max_distributed_connections

Максимальное количество одновременных соединений с удалёнными серверами при распределённой обработке одного запроса к одной таблице типа Distributed. Рекомендуется выставлять не меньше, чем количество серверов в кластере.

По умолчанию - 100.

Следующие параметры имеют значение только на момент создания таблицы типа Distributed (и при запуске сервера), поэтому их не имеет смысла менять в рантайме.

distributed_connections_pool_size

Максимальное количество одновременных соединений с удалёнными серверами при распределённой обработке всех запросов к одной таблице типа Distributed. Рекомендуется выставлять не меньше, чем количество серверов в кластере.

По умолчанию - 128.

connect_timeout_with_failover_ms

Таймаут в миллисекундах на соединение с удалённым сервером, для движка таблиц Distributed, если используются секции shard и replica в описании кластера. В случае неуспеха, делается несколько попыток соединений с разными репликами. По умолчанию - 50.

connections_with_failover_max_tries

Максимальное количество попыток соединения с каждой репликой, для движка таблиц Distributed. По умолчанию - 3

extremes

Считать ли экстремальные значения (минимумы и максимумы по столбцам результата запроса). Принимает 0 или 1. По умолчанию - 0 (выключено). Подробнее смотрите раздел "Экстремальные значения".

use_uncompressed_cache

Использовать ли кэш разжатых блоков. Принимает 0 или 1. По умолчанию - 0 (выключено). Кэш разжатых блоков (только для таблиц семейства MergeTree) позволяет существенно уменьшить задержки и увеличить пропускную способность при обработке большого количества коротких запросов. Включите эту настройку для пользователей, от которых идут частые короткие запросы. Также обратите внимание на конфигурационный параметр uncompressed_cache_size (настраивается только в конфигурационном файле) - размер кэша разжатых блоков. По умолчанию - 8 GiB. Кэш разжатых блоков заполняется по мере надобности; наиболее невостребованные данные автоматически удаляются.

Для запросов, читающих хоть немного приличный объём данных (миллион строк и больше), кэш разжатых блоков автоматически выключается, чтобы оставить место для действительно мелких запросов. Поэтому, можно держать настройку use_uncompressed_cache всегда выставленной в 1.

replace_running_query

При использовании HTTP-интерфейса, может быть передан параметр query_id - произвольная строка, являющаяся идентификатором запроса. Если в этот момент, уже существует запрос от того же пользователя с тем же query_id, то поведение определяется параметром replace_running_query.

0 - (по умолчанию) кинуть исключение (не давать выполнить запрос, если запрос с таким же query_id уже выполняется); 1 - отменить старый запрос и начать выполнять новый.

Эта настройка, выставленная в 1, используется в Яндекс.Метрике для реализации suggest-а значений для условий сегментации. После ввода очередного символа, если старый запрос ещё не выполнился, его следует отменить.

load_balancing

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

random (по умолчанию)

Для каждой реплики считается количество ошибок. Запрос отправляется на реплику с минимальным числом ошибок, а если таких несколько, то на случайную из них. Недостатки: не учитывается близость серверов; если на репликах оказались разные данные, то вы будете получать так же разные данные.

nearest_hostname

Для каждой реплики считается количество ошибок. Каждые 5 минут, число ошибок целочисленно делится на 2 - таким образом, обеспечивается расчёт числа ошибок за недавнее время с экспоненциальным сглаживанием. Если есть одна реплика с минимальным числом ошибок (то есть, на других репликах недавно были ошибки) - запрос отправляется на неё. Если есть несколько реплик с одинаковым минимальным числом ошибок, то запрос отправляется на реплику, имя хоста которой в конфигурационном файле минимально отличается от имени хоста сервера (по количеству отличающихся символов на одинаковых позициях, до минимальной длины обеих имён хостов).

Для примера, example01-01-1 и example01-01-2.yandex.ru отличаются в одной позиции, а example01-01-1 и example01-02-2 - в двух. Этот способ может показаться несколько дурацким, но он не использует внешние данные о топологии сети, и не сравнивает IP-адреса, что было бы сложным для наших IPv6-адресов.

Таким образом, если есть равнозначные реплики, предпочитается ближайшая по имени. Также можно сделать предположение, что при отправке запроса на один и тот же сервер, в случае отсутствия сбоев, распределённый запрос будет идти тоже на одни и те же серверы. То есть, даже если на репликах расположены разные данные, запрос будет возвращать в основном одинаковые результаты.

in_order

Реплики перебираются в таком порядке, в каком они указаны. Количество ошибок не имеет значения. Этот способ подходит для тех случаев, когда вы точно знаете, какая реплика предпочтительнее.

totals_mode

Каким образом вычислять TOTALS при наличии HAVING, а также при наличии max_rows_to_group_by и group_by_overflow_mode = 'any'. Смотрите раздел "Модификатор WITH TOTALS".

totals_auto_threshold

Порог для totals_mode = 'auto'. Смотрите раздел "Модификатор WITH TOTALS".

default_sample

Число с плавающей запятой от 0 до 1. По умолчанию - 1. Позволяет выставить коэффициент сэмплирования по умолчанию для всех запросов SELECT. (Для таблиц, не поддерживающих сэмплирование, будет кидаться исключение.) Если равно 1 - сэмплирование по умолчанию не делается.

Ограничения на сложность запроса

Ограничения на сложность запроса - часть настроек. Используются, чтобы обеспечить более безопасное исполнение запросов из пользовательского интерфейса. Почти все ограничения действуют только на SELECT-ы. При распределённой обработке запроса, ограничения действуют на каждом сервере по-отдельности.

Ограничения вида "максимальное количество чего-нибудь" могут принимать значение 0, которое обозначает "не ограничено". Для большинства ограничений также присутствует настройка вида overflow_mode - что делать, когда ограничение превышено. Оно может принимать одно из двух значений: throw или break; а для ограничения на агрегацию (group_by_overflow_mode) есть ещё значение any. throw - кинуть исключение (по умолчанию). break - прервать выполнение запроса и вернуть неполный результат, как будто исходные данные закончились. any (только для group_by_overflow_mode) - продолжить агрегацию по ключам, которые успели войти в набор, но не добавлять новые ключи в набор.

readonly

Если установлено в 1 - выполнять только запросы, которые не меняют данные и настройки. Для примера запросы SELECT и SHOW разрешены, а запросы INSERT и SET - запрещены. Написав SET readonly = 1, вы уже не сможете выключить readonly режим в текущей сессии.

При использовании метода GET HTTP интерфейса, автоматически выставляется readonly = 1. То есть, для запросов, модифицирующие данные, можно использовать только метод POST. Сам запрос при этом можно отправлять как в теле POST-а, так и в параметре URL.

max_memory_usage

Максимальное количество потребляемой памяти при выполнении запроса на одном сервере. По умолчанию - 10 GB.

Настройка не учитывает объём свободной памяти или общий объём памяти на машине. Ограничение действует на один запрос, в пределах одного сервера. Текущее потребление оперативки для каждого запроса можно посмотреть с помощью SHOW PROCESSLIST. Также отслеживается пиковое потребление оперативки для каждого запроса, и выводится в лог.

Некоторые случаи потребления оперативки не отслеживаются: - большие константы (например, очень длинная константная строка); - состояния агрегатных функций groupArray, а также quantile (для quantileTiming - отслеживается);

Потребление оперативки не полностью учитывается для состояний агрегатных функций min, max, any, anyLast, argMin, argMax от аргументов String и Array.

max_rows_to_read

Следующие ограничения могут проверяться на каждый блок (а не на каждую строку). То есть, ограничения могут быть немного нарушены. При выполнении запроса в несколько потоков, следующие ограничения действуют в каждом потоке по-отдельности.

Максимальное количество строчек, которое можно прочитать из таблицы при выполнении запроса.

max_bytes_to_read

Максимальное количество байт (несжатых данных), которое можно прочитать из таблицы при выполнении запроса.

read_overflow_mode

Что делать, когда количество прочитанных данных превысило одно из ограничений: throw или break. По умолчанию: throw.

max_rows_to_group_by

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

group_by_overflow_mode

Что делать, когда количество уникальных ключей при агрегации превысило ограничение: throw, break или any. По умолчанию: throw. Использование значения any позволяет выполнить GROUP BY приближённо. Качество такого приближённого вычисления сильно зависит от статистических свойств данных.

max_rows_to_sort

Максимальное количество строк до сортировки. Позволяет ограничить потребление оперативки при сортировке.

max_bytes_to_sort

Максимальное количество байт до сортировки.

sort_overflow_mode

Что делать, если количество строк, полученное перед сортировкой, превысило одно из ограничений: throw или break. По умолчанию: throw.

max_result_rows

Ограничение на количество строк результата. Проверяются также для подзапросов и на удалённых серверах при выполнении части распределённого запроса.

max_result_bytes

Ограничение на количество байт результата. Аналогично.

result_overflow_mode

Что делать, если объём результата превысил одно из ограничений: throw или break. По умолчанию: throw. Использование break по смыслу похоже на LIMIT.

max_execution_time

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

timeout_overflow_mode

Что делать, если запрос выполняется дольше max_execution_time: throw или break. По умолчанию: throw.

min_execution_speed

Минимальная скорость выполнения запроса в строчках в секунду. Проверяется на каждый блок данных по истечении timeout_before_checking_execution_speed. Если скорость выполнения запроса оказывается меньше, то кидается исключение.

timeout_before_checking_execution_speed

Проверять, что скорость выполнения запроса не слишком низкая (не меньше min_execution_speed), после прошествия указанного времени в секундах.

max_columns_to_read

Максимальное количество столбцов, которых можно читать из таблицы в одном запросе. Если запрос требует чтения большего количества столбцов - кинуть исключение.

max_temporary_columns

Максимальное количество временных столбцов, которых необходимо одновременно держать в оперативке, в процессе выполнения запроса, включая константные столбцы. Если временных столбцов оказалось больше - кидается исключение.

max_temporary_non_const_columns

То же самое, что и max_temporary_columns, но без учёта столбцов-констант. Стоит заметить, что столбцы-константы довольно часто образуются в процессе выполнения запроса, но расходуют примерно нулевое количество вычислительных ресурсов.

max_subquery_depth

Максимальная вложенность подзапросов. Если подзапросы более глубокие - кидается исключение. По умолчанию: 100.

max_pipeline_depth

Максимальная глубина конвейера выполнения запроса. Соответствует количеству преобразований, которое проходит каждый блок данных в процессе выполнения запроса. Считается в пределах одного сервера. Если глубина конвейера больше - кидается исключение. По умолчанию: 1000.

max_ast_depth

Максимальная вложенность синтаксического дерева запроса. Если превышена - кидается исключение. На данный момент, проверяются не во время парсинга а уже после парсинга запроса. То есть, во время парсинга может быть создано слишком глубокое синтаксическое дерево, но запрос не будет выполнен. По умолчанию: 1000.

max_ast_elements

Максимальное количество элементов синтаксического дерева запроса. Если превышено - кидается исключение. Аналогично, проверяется уже после парсинга запроса. По умолчанию: 10 000.

max_rows_in_set

Максимальное количество строчек для множества в секции IN, создаваемого из подзапроса.

max_bytes_in_set

Максимальное количество байт (несжатых данных), занимаемое множеством в секции IN, создаваемым из подзапроса.

set_overflow_mode

Что делать, когда количество данных превысило одно из ограничений: throw или break. По умолчанию: throw.

max_rows_in_distinct

Максимальное количество различных строчек при использовании DISTINCT.

max_bytes_in_distinct

Максимальное количество байт, занимаемых хэш-таблицей, при использовании DISTINCT.

distinct_overflow_mode

Что делать, когда количество данных превысило одно из ограничений: throw или break. По умолчанию: throw.

max_rows_to_transfer

Максимальное количество строчек, которых можно передать на удалённый сервер или сохранить во временную таблицу, при использовании GLOBAL IN.

max_bytes_to_transfer

Максимальное количество байт (несжатых данных), которых можно передать на удалённый сервер или сохранить во временную таблицу, при использовании GLOBAL IN.

transfer_overflow_mode

Что делать, когда количество данных превысило одно из ограничений: throw или break. По умолчанию: throw.

Профили настроек

Профили настроек - это множество настроек, сгруппированных под одним именем. Для каждого пользователя ClickHouse указывается некоторый профиль. Все настройки профиля можно применить, установив настройку с именем profile. Пример:

SET profile = 'web'

- установить профиль web - то есть, установить все настройки, относящиеся к профилю web.

Профили настроек объявляются в конфигурационном файле пользователей. Обычно это - users.xml. Пример:

<!-- Профили настроек. -->
<profiles>
    <!-- Настройки по умолчанию -->
    <default>
        <!-- Максимальное количество потоков при выполнении одного запроса. -->
        <max_threads>8</max_threads>
    </default>

    <!-- Настройки для запросов из пользовательского интерфейса -->
    <web>
        <max_rows_to_read>1000000000</max_rows_to_read>
        <max_bytes_to_read>100000000000</max_bytes_to_read>

        <max_rows_to_group_by>1000000</max_rows_to_group_by>
        <group_by_overflow_mode>any</group_by_overflow_mode>

        <max_rows_to_sort>1000000</max_rows_to_sort>
        <max_bytes_to_sort>1000000000</max_bytes_to_sort>

        <max_result_rows>100000</max_result_rows>
        <max_result_bytes>100000000</max_result_bytes>
        <result_overflow_mode>break</result_overflow_mode>

        <max_execution_time>600</max_execution_time>
        <min_execution_speed>1000000</min_execution_speed>
        <timeout_before_checking_execution_speed>15</timeout_before_checking_execution_speed>

        <max_columns_to_read>25</max_columns_to_read>
        <max_temporary_columns>100</max_temporary_columns>
        <max_temporary_non_const_columns>50</max_temporary_non_const_columns>

        <max_subquery_depth>2</max_subquery_depth>
        <max_pipeline_depth>25</max_pipeline_depth>
        <max_ast_depth>50</max_ast_depth>
        <max_ast_elements>100</max_ast_elements>

        <readonly>1</readonly>
    </web>
</profiles>

В примере задано два профиля: default и web. Профиль default имеет специальное значение - он всегда обязан присуствовать и применяется при запуске сервера. То есть, профиль default содержит настройки по умолчанию. Профиль web - обычный профиль, который может быть установлен с помощью запроса SET или с помощью параметра URL при запросе по HTTP. Профили настроек могут наследоваться от друг-друга - это реализуется указанием настройки profile перед остальными настройками, перечисленными в профиле.

Конфигурационные файлы

Основной конфигурационный файл сервера - config.xml. Он расположен в директории /etc/clickhouse-server/.

Отдельные настройки могут быть переопределены в файлах *.xml и *.conf из директорий conf.d и config.d рядом с конфигом. У элементов этих конфигурационных файлов могут быть указаны атрибуты replace или remove. Если ни один не указан - объединить содержимое элементов рекурсивно с заменой значений совпадающих детей. Если указано replace - заменить весь элемент на указанный. Если указано remove - удалить элемент.

Также в конфиге могут быть указаны "подстановки". Если у элемента присутствует атрибут incl, то в качестве значения будет использована соответствующая подстановка из файла. По умолчанию, путь к файлу с подстановками - /etc/metrika.xml. Он может быть изменён в конфиге в элементе include_from. Значения подстановок указываются в элементах /yandex/имя_подстановки этого файла.

В config.xml может быть указан отдельный конфиг с настройками пользователей, профилей и квот. Относительный путь к нему указывается в элементе users_config. По умолчанию - users.xml. Если users_config не указан, то настройки пользователей, профилей и квот, указываются непосредственно в config.xml. Сервер следит за изменениями users_config и перезагружает его в рантайме. То есть, вы можете добавлять или изменять пользователей и их настройки без перезапуска сервера.

Для users_config могут также существовать переопределения в файлах из директории users_config.d (например, users.d) и подстановки. Стоит заметить, что сервер следит за обновлениями только непосредственно файла users.xml, поэтому всевозможные переопределения не обновляются в рантайме.

Для каждого конфигурационного файла, сервер при запуске генерирует также файлы file-preprocessed.xml. Эти файлы содержат все выполненные подстановки и переопределения, и предназначены для информационных целей. Сам сервер эти файлы не использует, и редактировать их не нужно.

Права доступа

Пользователи и права доступа настраиваются в конфиге пользователей. Обычно это users.xml.

Пользователи прописаны в секции users. Рассмотрим фрагмент файла users.xml:

<!-- Пользователи и ACL. -->
<users>
    <!-- Если имя пользователя не указано, используется пользователь default. -->
    <default>
        <!-- Пароль (в открытом виде). Может быть пустым. -->
        <password></password>

        <!-- Список сетей, из которых разрешён доступ.
            Каждый элемент списка имеет одну из следующих форм:
            <ip> IP-адрес или маска подсети. Например, 222.111.222.3 или 10.0.0.1/8 или 2a02:6b8::3 или 2a02:6b8::3/64.
            <host> Имя хоста. Например: example01. Для проверки делается DNS-запрос, и все полученные адреса сравниваются с адресом клиента.
            <host_regexp> Регексп для имён хостов. Например, ^example\d\d-\d\d-\d\.yandex\.ru$
                Для проверки, для адреса клиента делается DNS PTR-запрос и к результату применяется регексп.
                Потом для результата PTR-запроса делается снова DNS-запрос, и все полученные адреса сравниваются с адресом клиента.
                Настоятельно рекомендуется, чтобы регексп заканчивался на \.yandex\.ru$.

            Если вы устанавливаете ClickHouse самостоятельно, укажите здесь:
                <networks>
                        <ip>::/0</ip>
                </networks>
        -->
        <networks incl="networks" />

        <!-- Профиль настроек, использующийся для пользователя. -->
        <profile>default</profile>

        <!-- Квота, использующаяся для пользователя. -->
        <quota>default</quota>
    </default>

    <!-- Для запросов из пользовательского интерфейса Метрики через API для данных по отдельным счётчикам. -->
    <web>
        <password></password>
        <networks incl="networks" />
        <profile>web</profile>
        <quota>default</quota>
    </web>

Здесь видно объявление двух пользователей - default и web. Пользователя web мы добавили самостоятельно. Пользователь default выбирается в случаях, когда имя пользователя не передаётся, поэтому такой пользователь должен присутствовать в конфигурационном файле обязательно. Также пользователь default используется при распределённой обработки запроса - система ходит на удалённые серверы под ним. Поэтому, у пользователя default должен быть пустой пароль и не должно быть выставлено существенных ограничений или квот - иначе распределённые запросы сломаются.

Пароль указывается в открытом виде, прямо в конфиге. В связи с этим, не следует рассматривать такие пароли, как защиту от потенциального злоумышленника. Скорее, они нужны для защиты от сотрудников Яндекса.

Указывается список сетей, из которых разрешён доступ. В этом примере, список сетей для обеих пользователей, загружается из отдельного файла (/etc/metrika.xml), содержащего подстановку networks. Вот его фрагмент:

<yandex>
    ...
    <networks>
        <ip>::/64</ip>
        <ip>93.111.222.128/26</ip>
        <ip>2a02:6b8:0:111::/64</ip>
        ...
    </networks>
</yandex>

Можно было бы указать этот список сетей непосредственно в users.xml, или в файле в директории users.d (подробнее смотрите раздел "Конфигурационные файлы").

В конфиге приведён комментарий, указывающий, как можно открыть доступ отовсюду.

Для продакшен использования, указывайте только элементы вида ip (IP-адреса и их маски), так как использование host и host_regexp может вызывать лишние задержки.

Далее указывается используемый профиль настроек пользователя (смотрите раздел "Профили настроек"). Вы можете указать профиль по умолчанию - default. Профиль может называться как угодно; один и тот же профиль может быть указан для разных пользователей. Наиболее важная вещь, которую вы можете прописать в профиле настроек - настройку readonly, равную 1, что обеспечивает доступ только на чтение.

Затем указывается используемая квота (смотрите раздел "Квоты"). Вы можете указать квоту по умолчанию - default. Она настроена в конфиге по умолчанию так, что только считает использование ресурсов, но никак их не ограничивает. Квота может называться как угодно; одна и та же квота может быть указана для разных пользователей - в этом случае, подсчёт использования ресурсов делается для каждого пользователя по отдельности.

Квоты

Квоты позволяют ограничить использование ресурсов за некоторый интервал времени, или просто подсчитывать использование ресурсов. Квоты настраиваются в конфиге пользователей. Обычно это users.xml.

В системе есть возможность ограничить сложность одного запроса. Для этого смотрите раздел "Ограничения на сложность запроса". В отличие от них, квоты: - ограничивают не один запрос, а множество запросов, которые могут быть выполнены за интервал времени; - при распределённой обработке запроса, учитывают ресурсы, потраченные на всех удалённых серверах.

Рассмотрим фрагмент файла users.xml, описывающего квоты.

<!-- Квоты. -->
<quotas>
    <!-- Имя квоты. -->
    <default>
        <!-- Ограничения за интервал времени. Можно задать много интервалов с разными ограничениями. -->
        <interval>
            <!-- Длина интервала. -->
            <duration>3600</duration>

            <!-- Без ограничений. Просто считать соответствующие данные за указанный интервал. -->
            <queries>0</queries>
            <errors>0</errors>
            <result_rows>0</result_rows>
            <read_rows>0</read_rows>
            <execution_time>0</execution_time>
        </interval>
    </default>

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

    <statbox>
        <!-- Ограничения за интервал времени. Можно задать много интервалов с разными ограничениями. -->
        <interval>
            <!-- Длина интервала. -->
            <duration>3600</duration>

            <queries>1000</queries>
            <errors>100</errors>
            <result_rows>1000000000</result_rows>
            <read_rows>100000000000</read_rows>
            <execution_time>900</execution_time>
        </interval>

        <interval>
            <duration>86400</duration>

            <queries>10000</queries>
            <errors>1000</errors>
            <result_rows>5000000000</result_rows>
            <read_rows>500000000000</read_rows>
            <execution_time>7200</execution_time>
        </interval>
    </statbox>

Для квоты с именем statbox заданы ограничения за каждый час и за каждые 24 часа (86 400 секунд). Интервал времени считается начиная от некоторого implementation defined фиксированного момента времени. То есть, интервал длины 24 часа начинается не обязательно в полночь.

Когда интервал заканчивается, все накопленные значения сбрасываются. То есть, в следующий час, расчёт квоты за час, начинается заново.

Рассмотрим величины, которые можно ограничить:

queries - общее количество запросов; errors - количество запросов, при выполнении которых было выкинуто исключение; result_rows - суммарное количество строк, отданных в виде результата; read_rows - суммарное количество исходных строк, прочитанных из таблиц, для выполнения запроса, на всех удалённых серверах; execution_time - суммарное время выполнения запросов, в секундах (wall time);

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

Для квоты может быть включена возможность указывать "ключ квоты", чтобы производить учёт ресурсов для многих ключей независимо. Рассмотрим это на примере:

    <!-- Для глобального конструктора отчётов. -->
    <web_global>
        <!-- keyed - значит в параметре запроса передаётся "ключ" quota_key,
                и квота считается по отдельности для каждого значения ключа.
            Например, в качестве ключа может передаваться логин пользователя в Метрике,
                и тогда квота будет считаться для каждого логина по отдельности.
            Имеет смысл использовать только если quota_key передаётся не пользователем, а программой.

            Также можно написать <keyed_by_ip /> - тогда в качестве ключа квоты используется IP-адрес.
            (но стоит учесть, что пользователь может достаточно легко менять IPv6-адрес)
        -->
        <keyed />

Квота прописывается для пользователей в секции users конфига. Смотрите раздел "Права доступа".

При распределённой обработке запроса, накопленные величины хранятся на сервере-инициаторе запроса. То есть, если пользователь пойдёт на другой сервер - там квота будет действовать "с нуля".

При перезапуске сервера, квоты сбрасываются.

Яндекс.Метрика