Ответ есть, а цены у него нет: чем мерить без секундомера
Чему научишься
- измерять цену ответа тремя числами: сколько кусков, строк и засечек пришлось прочитать
- сравнивать два запроса с одинаковым ответом, но разной ценой, и объяснять причину разницы
- не полагаться на секундомер, если вместе с запросом он измеряет создание базы и загрузку данных
- читать список колонок и типов чужой таблицы, даже если показать её полное определение нельзя
10:15. Три числа за март
27 марта 2184 года. Читальный зал Академии на станции «Хранилище-9»: высокий светлый ярус, длинные столы, окно выдачи.
Три дня назад ты сдал смену на приёмном уровне. Лента ночной смены работает без тебя: данные доезжают, повторный запуск ничего не ломает. Наверху вопросы уже другие.
Смотритель зала кладёт перед тобой лист с тремя вопросами за март: сколько было визитов, сколько разных посетителей, сколько раз материал дошёл до рук. Сами вопросы простые, ответы найдутся быстро.
Проблема в другом. Один и тот же ответ можно получить двумя запросами, но один обойдётся залу примерно в двадцать раз дороже другого. Значит, мало получить правильное число — нужно ещё понять цену, которую движок заплатил за ответ.
Секундомер здесь не поможет. Таймер ячейки запускается ДО того, как база вообще появилась: в эти миллисекунды входят создание базы и схемы, а также загрузка всех семи таблиц. Мерить им отдельный запрос — всё равно что взвешивать материал вместе с тележкой, на которой его привезли.
Поэтому с этой смены ты отвечаешь не только за ответ, но и за то, сколько данных пришлось прочитать, чтобы его получить.
КВЕРИ: С возвращением, архивист. Внизу спрашивали, доехало ли. Здесь спрашивают, сколько, — а потом сразу: сколько ты прочитал, чтобы это узнать. Второй вопрос мой. Без числа не приму.
Над столом разворачивается панель выдачи: восемь пустых ячеек — по одной на главу — и журнал в три колонки: вопрос → приём → цена. Пока пусто и там, и там.
К концу этого урока в панели появится первая цена, а к концу главы — первый ответ.

Три числа вместо миллисекунд
Оба запроса дали 174. Но цена ответа оказалась разной: первый прочитал 1172 строки, второй — 23 565. Тот же вопрос обошёлся движку примерно в двадцать раз дороже.
Почему так произошло? Дело не в результате, а в том, смог ли ClickHouse заранее отбросить ненужные данные или был вынужден их читать.
rr_events разбита на дневные партиции: события каждого дня лежат в отдельном куске на диске. Фильтр по event_date совпадает с ключом партиционирования. Поэтому движок берёт один кусок из двадцати четырёх, а остальные даже не открывает.
Фильтр по toDate32(event_ts) задаёт тот же вопрос иначе — через вычисление над колонкой. Чтобы понять, подходит ли строка, движку сначала приходится её прочитать. Поэтому он открывает двадцать четыре куска, затрагивает двадцать четыре засечки и читает все 23 565 строк.
Разница в цене появилась ещё до вычисления ответа: первый запрос помог движку сразу сузить чтение, второй — нет.
Четыре знакомых слова. Ты уже встречал их в главе про колоночное хранилище, и дальше курс использует их в том же смысле:
- кусок — отдельная порция данных таблицы на диске;
- гранула — минимальная порция строк внутри куска, которую движок умеет прочитать (у большинства таблиц зала это 8192 строки, у витрины
dm_vydacha— 1024); - засечка — метка на границе гранулы: по засечкам движок прыгает внутрь куска, не читая его с начала;
- ключ сортировки — порядок, в котором строки лежат внутри куска; засечки расставлены по нему, поэтому вопрос «в порядке ключа» стоит дёшево, а поперёк — дорого.
Отсюда и мера, которой будем пользоваться дальше: цена ответа — это три числа: кусков / строк / засечек.
Они удобнее времени ещё и потому, что не зависят от загрузки станции и от того, кто ещё читает зал в эту минуту. Два прогона одного запроса дадут одинаковые числа, а одинаковое время — не обязательно.
Обязательная форма
Цену показывает EXPLAIN ESTIMATE, но напрямую в уроке его не вызывают.
Причина техническая: в первой колонке результата стоит имя базы, где исполнялась ячейка. При каждом запуске создаётся новая база с новым именем, поэтому сырой результат менялся бы от прогона к прогону.
Чтобы оставить только устойчивую часть результата, цену всегда снимаем через обёртку и явно перечисляем нужные колонки:
SELECT parts, rows, marks FROM viewExplain('EXPLAIN ESTIMATE', '', ( ... ваш запрос ... ))
В результате остаются именно три числа, которые нужны для сравнения запросов: куски, строки и засечки.
Есть ещё одна ловушка. Иногда движку вообще не приходится читать данные — например, если ты запросил только количество строк за один день, а таблица разбита на партиции по дням. Тогда обёртка вернёт пусто.
Это не ошибка. Ответ взялся из служебных записей о кусках, поэтому сами строки читать не понадобилось. Если хочешь увидеть цену чтения, добавь в вопрос хотя бы одну настоящую колонку.
SHOW CREATE TABLE песочница не пропустит — это правило песочницы, а не ограничение ClickHouse. Поэтому используем доступную, но неполную замену.Что в песочнице закрыто и чем оно заменено
В курсе ты работаешь с одиночным сервером через браузер, без администраторских прав. Поэтому часть привычных инструментов здесь недоступна.
Это не мешает разбирать механику ClickHouse: для каждой закрытой возможности есть рабочая замена. Именно её курс использует до последнего урока.
| закрыто | чем заменено | что теряется |
|---|---|---|
SHOW CREATE TABLE | DESCRIBE TABLE — 12 строк по числу колонок и 7 колонок описания, включая способ сжатия и срок жизни | не видно движка таблицы, ключа сортировки и настроек; их придётся спрашивать у того, кто таблицу заводил |
| служебная база с данными о кусках | uniqExact(_part) и список кусков массивом прямо в запросе | не видно размеров на диске и истории слияний |
| несколько машин | две локальные таблицы и UNION ALL | нет сети между машинами, а значит и её цены — самой дорогой части настоящего распределённого ответа |
Важно понимать границу этой замены. Она меняет способ наблюдения за системой, но не сам движок.
Всё остальное настоящее: тот же движок, та же версия, те же ошибки дословно. Если урок использует замену, он говорит об этом прямо — как здесь.
Вопрос с собеседования
Как это спрашивают на собеседовании
Типичный вопрос: «Как вы поймёте, что запрос читает лишнее?»
Ответ «замерил время» обычно не устраивает. Время зависит от кэша, соседей по серверу и даже от того, какой это запуск запроса. Оно показывает, сколько занял конкретный прогон, но плохо объясняет, почему один запрос читает больше другого.
Сильный ответ опирается на объём чтения: назови три числа из плана — сколько кусков, строк и засечек пришлось прочитать — и объясни, как они изменятся после фильтра по ключу партиционирования.
В вакансиях и документации встретишь индустриальные названия этих терминов: кусок — part, засечка — mark, гранула — granule. На собеседовании почти наверняка назовут именно их.
Главное из урока
| вопрос | приём | цена |
|---|---|---|
| Сколько разных посетителей было 5 марта? | фильтр по event_date — ключу партиционирования таблицы | 1 кусок / 1172 строки / 1 засечка |
| То же самое | фильтр по вычислению над отметкой времени | 24 куска / 23 565 строк / 24 засечки |
Ответ в обоих случаях один — 174 посетителя. Но цена разошлась примерно в двадцать раз, потому что первый фильтр совпал с ключом партиционирования таблицы, а второй заставил движок прочитать все куски.
Это и есть главный вывод урока: правильного ответа недостаточно. Нужно понимать, сколько данных ClickHouse прочитал по дороге к нему.
Первая ячейка панели выдачи больше не пустует. В ней стоит не ответ, а мера, которой курс будет оценивать все остальные семь.
КВЕРИ: Принято. Не потому, что число красивое, — потому что рядом написано, во что оно обошлось. Дальше сложнее: ответы станут длиннее, а цена научится прятаться.