Производительность запроса складывается из чтения данных, распаковки, вычисления и передачи результата. Для объяснения медленного SQL нужно выяснить, какая из этих работ велика. Одного времени в клиенте недостаточно: оно может включать сеть и вывод большого результата.
Сначала схема и запрос
Пусть events содержит event_type, event_time и duration_ms. Для суммы по одному типу важны фильтр и порядок данных:
EXPLAIN indexes = 1
SELECT sum(duration_ms)
FROM events
WHERE event_type = 'click';Если ORDER BY начинается с event_type, похожие строки находятся рядом. Индекс может сократить число читаемых гранул. При сортировке только по уникальному ID тот же тип может быть рассеян по всей таблице. Оба запроса могут возвращать одинаковую сумму, но выполнять разный объём чтения.
PREWHERE позволяет сначала прочитать колонки фильтра, а остальные — для прошедших отбор строк. ClickHouse может переносить подходящие условия из WHERE автоматически. Этот механизм сокращает чтение столбцов после отбора; он не создаёт новый индекс и не исправляет неудачный сортировочный ключ.
SHOW CREATE TABLE показывает фактические типы, ключи, партиции и движок проверяемой таблицы.
План и pipeline
EXPLAIN показывает план: источник, фильтры, агрегацию, сортировку и другие этапы. indexes = 1 добавляет сведения об отсечении частей и гранул. Нужно различать число выбранных частей и долю строк внутри них: большая часть может быть прочитана лишь частично.
EXPLAIN PIPELINE показывает потоки выполнения и места объединения работы. max_threads ограничивает параллелизм; маленький вход, блокирующая стадия и доступные ресурсы определяют фактическое число потоков.
Кардинальность результата этапа — число строк, которые он выдаёт. Это другое употребление термина, чем число различных значений колонки. Оценка числа строк и фактическая кратность помогают понять, насколько велики будут JOIN и промежуточная агрегация.
План не является измерением времени. Имена узлов и подробности EXPLAIN меняются между версиями. Условия могут не отсекать данные из-за распределения значений или формы выражения, даже если индекс существует.
Метрики завершённого запроса
system.query_log хранит события выполнения, если журналирование включено. По query_id можно найти завершение нужного запроса:
SELECT
query_duration_ms,
read_rows,
read_bytes,
memory_usage
FROM system.query_log
WHERE type = 'QueryFinish'
AND is_initial_query = 1
AND query_id = 'идентификатор-нужного-запроса';read_rows и read_bytes описывают логическое чтение; read_bytes не равен числу физических байтов, полученных с диска. memory_usage помогает оценить память запроса. В распределённом выполнении существуют начальный и дочерние запросы на узлах; их метрики нельзя бездумно складывать дважды.
Записи query_log появляются с задержкой. Для сравнения вариантов запросы можно пометить log_comment и выбрать их события завершения.
Spill и временные данные
Spill — перенос промежуточных данных вычисления из памяти во временные файлы на диске. Внешняя сортировка обрабатывает порции и затем сливает их; внешняя агрегация и grace hash JOIN тоже могут использовать диск, чтобы выполнить работу при ограниченной RAM. Это дополнительный ввод-вывод и задержка, но иногда приемлемая цена за выполнение большого запроса без ошибки нехватки памяти.
Spill сам по себе не доказывает плохой план. Для небольшого ожидаемого результата внезапный spill — повод проверить число групп, ширину строк, кратность JOIN и перекос. В ClickHouse внешнюю агрегацию и сортировку регулируют max_bytes_before_external_group_by и max_bytes_before_external_sort; для JOIN важен выбранный алгоритм. Порог spill не равен полному лимиту памяти запроса. Требуются также свободное место для временных файлов и подходящая скорость диска.
Как сравнивать изменения
Сначала проверяют равенство результата. Затем сравнивают чтение, память и время на одинаковых данных и при одинаковых настройках. Для ключей это означает не менять одновременно типы, количество строк и фильтр. Для JOIN — сохранять кратность и правила NULL.
Повторные запуски чередуют, чтобы один вариант не получал преимущество прогретого кэша. Размер данных, одновременно работающие запросы, диск и число потоков влияют на ответ. Полезно описать условия измерения и диапазон наблюдений, а не объявлять одно случайное время свойством продукта.
Причины постепенного замедления
Если прежний запрос стал медленнее, сравнивают его текст, схему, план, настройки и метрики с прежним запуском. Затем проверяют рост входа и промежуточных наборов, число частей, фоновые слияния, конкурентную нагрузку, чтение диска и состояние кэша. В распределённом запросе нужен также поиск узла, на котором сосредоточилась работа. Изменение версии сервера или настроек способно изменить выполнение при том же SQL.
Рост входа увеличивает чтение, память и очереди; размножение строк в JOIN усиливает нагрузку. К концу месяца текущая партиция может вырасти. Более короткий период партиционирования рассматривают после проверки фильтров, отсечения и числа parts.
Где искать лишнюю работу
Большие read_rows указывают на широкое чтение: проверьте фильтры, ключ, pruning и возможность подходящего представления. Малое чтение при высокой памяти может объясняться большим JOIN или множеством уникальных групп. Сортировка огромного результата и SELECT * добавляют работу после отбора.
По установленной причине сокращают столбцы и диапазоны, меняют порядок ключа, готовят агрегат, уменьшают стороны JOIN или ограничивают выдачу. Затем повторяют сравнение результата и метрик.