Введение
- чем индексирование в ClickHouse отличается от традиционных систем управления реляционными базами данных
- как ClickHouse формирует и использует разреженный первичный индекс таблицы
- какие рекомендации по индексированию в ClickHouse считаются лучшими практиками
Набор данных
- Мы будем использовать подмножество из 8,87 миллиона строк (событий) из этого набора данных.
- Размер данных в несжатом виде составляет 8,87 миллиона событий и около 700 МБ. При хранении в ClickHouse они сжимаются до 200 МБ.
- В нашем подмножестве каждая строка содержит три столбца: идентификатор интернет-пользователя (столбец
UserID), URL, по которому он щёлкнул (столбецURL), и время события (столбецEventTime).
- “Какие 10 URL пользователь открывал чаще всего?”
- “Какие 10 пользователей чаще всего открывали определённый URL?”
- “В какое время (например, в какие дни недели) пользователь чаще всего открывает определённый URL?”
Тестовая машина
Полное сканирование таблицы
Проектирование индексов в ClickHouse
Проектирование индексов для огромных объёмов данных
B(+)-Tree имеет среднюю временную сложность O(log n); точнее, log_b n = log_2 n / log_2 b, где b — коэффициент ветвления B(+)-Tree, а n — число индексированных строк. Поскольку b обычно находится в диапазоне от нескольких сотен до нескольких тысяч, B(+)-Tree представляют собой очень неглубокие структуры, и для поиска записей требуется небольшое число операций позиционирования на диске. При 8,87 миллиона строк и коэффициенте ветвления 1000 в среднем требуется 2,3 таких обращения к диску. За это приходится платить: дополнительными затратами дискового пространства и памяти, более высокими издержками на вставку при добавлении новых строк в таблицу и записей в индекс, а также иногда необходимостью перебалансировки B-Tree.
С учётом ограничений, связанных с индексами B-Tree, движки таблиц в ClickHouse используют другой подход. Семейство движков MergeTree в ClickHouse спроектировано и оптимизировано для работы с огромными объёмами данных. Эти таблицы рассчитаны на приём миллионов вставок строк в секунду и хранение очень больших объёмов данных (сотни PB). Данные быстро записываются в таблицу по частям, а затем к этим частям в фоне применяются правила слияния. В ClickHouse у каждой части есть собственный первичный индекс. Когда части сливаются, первичные индексы слитой части тоже объединяются. В тех очень больших масштабах, на которые рассчитан ClickHouse, крайне важно эффективно расходовать диск и память. Поэтому вместо индексации каждой строки первичный индекс части содержит одну запись индекса (так называемую «метку») на каждую группу строк (называемую «гранулой») — этот приём называется разреженным индексом.
Разреженная индексация возможна потому, что ClickHouse хранит строки каждой части на диске упорядоченными по столбцам первичного ключа. Вместо прямого поиска отдельных строк (как в индексе на основе B-Tree) разреженный первичный индекс позволяет быстро — с помощью двоичного поиска по записям индекса — определить группы строк, которые потенциально могут соответствовать запросу. Найденные группы потенциально подходящих строк (гранулы) затем параллельно передаются в движок ClickHouse для поиска совпадений. Такое проектирование индексов позволяет сделать первичный индекс небольшим (он может и должен полностью помещаться в оперативную память), при этом всё равно значительно ускоряя выполнение запросов — особенно диапазонных запросов, типичных для аналитических сценариев работы с данными.
Ниже подробно показано, как ClickHouse строит и использует свой разреженный первичный индекс. Далее в статье мы обсудим некоторые рекомендации по выбору, исключению и упорядочиванию столбцов таблицы, которые используются для построения индекса (столбцов первичного ключа).
Таблица с первичным ключом
Подробности DDL-оператора
Подробности DDL-оператора
Чтобы упростить дальнейшее изложение в этом руководстве, а также сделать диаграммы и результаты воспроизводимыми, этот DDL-оператор:
- Задаёт составной ключ сортировки таблицы с помощью предложения
ORDER BY. - Явно задаёт, сколько записей будет в первичном индексе, с помощью следующих настроек:
index_granularity: явно установлено значение по умолчанию 8192. Это означает, что на каждую группу из 8192 строк в первичном индексе будет одна запись. Например, если таблица содержит 16384 строки, в индексе будет две записи.index_granularity_bytes: установлено в 0, чтобы отключить адаптивную гранулярность индекса. Адаптивная гранулярность индекса означает, что ClickHouse автоматически создаёт одну запись индекса для группы из n строк, если выполняется хотя бы одно из следующих условий:- Если
nменьше 8192, а суммарный размер данных этихnстрок больше или равен 10 МБ (значение по умолчанию дляindex_granularity_bytes). - Если суммарный размер данных
nстрок меньше 10 МБ, ноnравно 8192.
- Если
compress_primary_key: установлено в 0, чтобы отключить сжатие первичного индекса. Это позволит нам при необходимости позже просмотреть его содержимое.
Далее вставьте данные:
Затем оптимизируйте таблицу:
Мы можем использовать следующий запрос, чтобы получить метаданные о нашей таблице:
- Данные таблицы хранятся в широком формате в определённом каталоге на диске, то есть внутри этого каталога для каждого столбца таблицы будет один файл данных (и один файл меток).
- В таблице 8,87 миллиона строк.
- Общий размер несжатых данных всех строк составляет 733,28 МБ.
- Общий сжатый размер всех строк на диске составляет 206,94 МБ.
- У таблицы есть первичный индекс с 1083 записями (так называемыми ‘метками’), а размер индекса составляет 96,93 КБ.
- В сумме данные таблицы, файлы меток и файл первичного индекса занимают на диске 207,07 МБ.
Данные хранятся на диске в порядке столбцов первичного ключа
- составной первичный ключ
(UserID, URL)и - составной ключ сортировки
(UserID, URL, EventTime).
- Если бы мы указали только ключ сортировки, первичный ключ был бы неявно задан равным ключу сортировки.
- Для более эффективного использования памяти мы явно указали первичный ключ, содержащий только те столбцы, по которым наши запросы выполняют фильтрацию. Первичный индекс, построенный на основе первичного ключа, полностью загружается в оперативную память.
- Чтобы сохранить единообразие диаграмм в руководстве и максимально увеличить коэффициент сжатия, мы определили отдельный ключ сортировки, включающий все столбцы таблицы (если похожие данные в столбце расположены близко друг к другу, например благодаря сортировке, они сжимаются лучше).
- Если заданы оба ключа, первичный ключ должен быть префиксом ключа сортировки.
EventTime из ключа сортировки).
EventTime.- в представлении на диске для каждого столбца таблицы существует отдельный файл данных (*.bin), в котором все значения этого столбца хранятся в сжатом формате, и
- 8,87 миллиона строк хранятся на диске в лексикографическом порядке по возрастанию по столбцам первичного ключа (и дополнительным столбцам ключа сортировки), то есть в данном случае
- сначала по
UserID, - затем по
URL, - и наконец по
EventTime:
- сначала по
UserID.bin, URL.bin и EventTime.bin — это файлы данных на диске, в которых хранятся значения столбцов UserID, URL и EventTime.
- Поскольку первичный ключ задает лексикографический порядок строк на диске, у таблицы может быть только один первичный ключ.
- Мы нумеруем строки, начиная с 0, чтобы соответствовать внутренней схеме нумерации строк в ClickHouse, которая также используется в сообщениях журналирования.
Данные организуются в гранулы для параллельной обработки
index_granularity (со значением по умолчанию 8192).
Первые 8192 строки (их значения столбцов) — в физическом порядке на диске — логически относятся к грануле 0, затем следующие 8192 строки (их значения столбцов) относятся к грануле 1 и так далее.
- Последняя гранула (гранула 1082) “содержит” менее 8192 строк.
- В начале этого руководства, в разделе “Подробности DDL-оператора”, мы упоминали, что отключили адаптивную гранулярность индекса (чтобы упростить изложение в этом руководстве, а также сделать диаграммы и результаты воспроизводимыми). Поэтому все гранулы (кроме последней) в нашей таблице-примере имеют одинаковый размер.
- Для таблиц с адаптивной гранулярностью индекса (по умолчанию гранулярность индекса является адаптивной) размер некоторых гранул может быть меньше 8192 строк в зависимости от размера данных в строках.
-
Мы выделили оранжевым цветом некоторые значения из столбцов первичного ключа (
UserID,URL). Эти выделенные оранжевым значения столбцов — значения столбцов первичного ключа из первой строки каждой гранулы. Как мы увидим ниже, именно эти выделенные оранжевым значения будут записями в первичном индексе таблицы. - Мы нумеруем гранулы, начиная с 0, чтобы соответствовать внутренней схеме нумерации ClickHouse, которая также используется в сообщениях журналирования.
У первичного индекса одна запись на гранулу
- первая запись индекса (‘метка 0’ на диаграмме ниже) хранит значения столбцов ключа первой строки гранулы 0 с диаграммы выше,
- вторая запись индекса (‘метка 1’ на диаграмме ниже) хранит значения столбцов ключа первой строки гранулы 1 с диаграммы выше, и так далее.
- Для таблиц с адаптивной гранулярностью индекса в первичном индексе также хранится ещё одна, дополнительная “финальная” метка, в которой записываются значения столбцов первичного ключа последней строки таблицы. Но поскольку мы отключили адаптивную гранулярность индекса (чтобы упростить объяснение в этом руководстве, а также сделать диаграммы и результаты воспроизводимыми), индекс нашей таблицы в примере не содержит этой финальной метки.
- Файл первичного индекса полностью загружается в оперативную память. Если размер файла превышает объём доступной свободной памяти, ClickHouse выдаст ошибку.
Просмотр содержимого первичного индекса
Просмотр содержимого первичного индекса
В самоуправляемом кластере ClickHouse мы можем использовать табличную функцию file, чтобы просмотреть содержимое первичного индекса нашей таблицы-примера.Для этого сначала нужно скопировать файл первичного индекса в user_files_path одного из узлов работающего кластера:
- Шаг 1: Получить путь к части, содержащей файл первичного индекса
- Шаг 2: Получить user_files_path Значение user_files_path по умолчанию в Linux:
- Шаг 3: Скопировать файл первичного индекса в user_files_path
SELECT path FROM system.parts WHERE table = 'hits_UserID_URL' AND active = 1на тестовой машине возвращает /Users/tomschreiber/Clickhouse/store/85f/85f4ee68-6e28-4f08-98b1-7d8affa1d88c/all_1_9_4./var/lib/clickhouse/user_files/В Linux можно проверить, не было ли оно изменено: $ grep user_files_path /etc/clickhouse-server/config.xmlНа тестовой машине этот путь — /Users/tomschreiber/Clickhouse/user_files/cp /Users/tomschreiber/Clickhouse/store/85f/85f4ee68-6e28-4f08-98b1-7d8affa1d88c/all_1_9_4/primary.idx /Users/tomschreiber/Clickhouse/user_files/primary-hits_UserID_URL.idxТеперь мы можем просмотреть содержимое первичного индекса через SQL:
- Получить количество записей
- Получить первые две метки индекса
- Получить последнюю метку индекса
SELECT count( )<br/>FROM file('primary-hits_UserID_URL.idx', 'RowBinary', 'UserID UInt32, URL String');
возвращает 1083SELECT UserID, URL<br/>FROM file('primary-hits_UserID_URL.idx', 'RowBinary', 'UserID UInt32, URL String')<br/>LIMIT 0, 2;возвращает240923, http://showtopics.html%3...<br/> 4073710, http://mk.ru&pos=3_0SELECT UserID, URL FROM file('primary-hits_UserID_URL.idx', 'RowBinary', 'UserID UInt32, URL String')<br/>LIMIT 1082, 1;
возвращает
4292714039 │ http://sosyal-mansetleri...Это в точности соответствует нашей диаграмме содержимого первичного индекса для таблицы-примера:
-
Метки индекса UserID:
Значения
UserID, хранящиеся в первичном индексе, отсортированы по возрастанию.
Поэтому «метка 1» на диаграмме выше указывает, что значенияUserIDвсех строк таблицы в грануле 1 и во всех следующих гранулах гарантированно больше либо равны 4.073.710.
-
Метки индекса для URL:
Довольно близкая мощность столбцов первичного ключа
UserIDиURLозначает, что метки индекса для всех столбцов ключа после первого, как правило, задают диапазон данных только до тех пор, пока значение предыдущего столбца ключа остаётся одинаковым для всех строк таблицы хотя бы в пределах текущей гранулы.
Например, поскольку значения UserID для метки 0 и метки 1 на диаграмме выше различаются, ClickHouse не может предположить, что все значения URL во всех строках таблицы в грануле 0 больше или равны'http://showtopics.html%3...'. Однако если бы значения UserID для метки 0 и метки 1 на диаграмме выше были одинаковыми (то есть значение UserID оставалось бы одним и тем же для всех строк таблицы в пределах гранулы 0), ClickHouse мог бы предположить, что все значения URL во всех строках таблицы в грануле 0 больше или равны'http://showtopics.html%3...'. Позже мы подробнее обсудим, как это влияет на производительность выполнения запросов.
Первичный индекс используется для отбора гранул
749927693 в столбце UserID. Для этого требуется 19 шагов при средней временной сложности O(log2 n):
Подробности журнала трассировки
Подробности журнала трассировки
Была определена метка 176 (‘найденная левая граничная метка’ включается, а ‘найденная правая граничная метка’ не включается), и поэтому все 8192 строки из гранулы 176 (которая начинается со строки 1.441.792 — мы увидим это позже в этом руководстве) затем передаются в ClickHouse, чтобы найти фактические строки со значением 749927693 в столбце UserID.
Как обсуждалось выше, ClickHouse использует свой разреженный первичный индекс, чтобы быстро (с помощью двоичного поиска) выбирать гранулы, которые потенциально могут содержать строки, соответствующие запросу. Это первый этап (выбор гранул) выполнения запроса к ClickHouse. На втором этапе (чтение данных) ClickHouse определяет расположение выбранных гранул, чтобы передать все их строки в движок ClickHouse и найти строки, которые действительно соответствуют запросу. Этот второй этап подробнее рассматривается в следующем разделе.
Файлы меток используются для определения расположения гранул
UserID в индексе была найдена метка 176. Следовательно, соответствующая ей гранула 176 потенциально может содержать строки со значением 749.927.693 в столбце UserID.
Подробности выбора гранулы
Подробности выбора гранулы
Приведённая выше диаграмма показывает, что метка 176 — это первая запись индекса, для которой минимальное значение UserID связанной гранулы 176 меньше 749.927.693, а минимальное значение UserID гранулы 177 для следующей метки (метки 177) больше этого значения. Поэтому только гранула 176, соответствующая метке 176, потенциально может содержать строки со значением 749.927.693 в столбце UserID.
UserID, все 8192 строки этой гранулы нужно передать в ClickHouse потоком.
Для этого ClickHouse должен знать физическое расположение гранулы 176.
В ClickHouse физические расположения всех гранул нашей таблицы хранятся в файлах меток. Как и в случае с файлами данных, для каждого столбца таблицы существует отдельный файл меток.
Следующая диаграмма показывает три файла меток UserID.mrk, URL.mrk и EventTime.mrk, в которых хранятся физические расположения гранул для столбцов таблицы UserID, URL и EventTime.
Мы уже обсуждали, что первичный индекс — это плоский несжатый файл-массив (primary.idx), содержащий метки индекса, нумерация которых начинается с 0.
Аналогично, файл меток тоже представляет собой плоский несжатый файл-массив (*.mrk), содержащий метки, нумерация которых начинается с 0.
После того как ClickHouse определяет и выбирает метку индекса для гранулы, которая потенциально может содержать строки, соответствующие запросу, в файлах меток можно выполнить позиционный поиск по массиву, чтобы получить физические расположения этой гранулы.
Каждая запись в файле меток для конкретного столбца хранит два расположения в виде смещений:
-
Первое смещение (
'block_offset'на диаграмме выше) указывает на блок в сжатом файле данных столбца, который содержит сжатую версию выбранной гранулы. Такой сжатый блок потенциально может содержать несколько сжатых гранул. При чтении найденный сжатый блок распаковывается в оперативную память. -
Второе смещение (
'granule_offset'на диаграмме выше) из файла меток указывает расположение гранулы внутри несжатых данных блока.
- Для таблиц с широким форматом и без адаптивной гранулярности индекса ClickHouse использует файлы меток
.mrk, как показано выше; они содержат записи с двумя 8-байтными адресами в каждой записи. Эти записи представляют собой физические расположения гранул, и все такие гранулы имеют одинаковый размер.
-
Для таблиц с широким форматом и адаптивной гранулярностью индекса ClickHouse использует файлы меток
.mrk2, которые содержат записи, аналогичные файлам.mrk, но с дополнительным третьим значением в каждой записи: числом строк в грануле, с которой связана текущая запись. -
Для таблиц с компактным форматом ClickHouse использует файлы меток
.mrk3.
EventTime.Для нашего примера запроса ClickHouse нужны только два смещения физического расположения для гранулы 176 в файле данных UserID (UserID.bin) и два смещения физического расположения для гранулы 176 в файле данных URL (URL.bin).Дополнительный уровень косвенности, обеспечиваемый файлами меток, позволяет не хранить непосредственно в первичном индексе записи о физических расположениях всех 1083 гранул для всех трёх столбцов и тем самым избежать хранения ненужных (и потенциально неиспользуемых) данных в оперативной памяти.Использование нескольких индексов первичного ключа
Второстепенные столбцы ключа могут быть (не)эффективны
Мы используем запрос, который вычисляет 10 пользователей, чаще всего переходивших по URL “http://public_search”:
Универсальный алгоритм поиска с исключением
- запрос ищет строки, где URL = “W3”.
- используется абстрактная версия нашей таблицы hits с упрощёнными значениями UserID и URL.
- для индекса используется один и тот же составной первичный ключ (UserID, URL). Это означает, что строки сначала упорядочиваются по значениям UserID. Затем строки с одинаковым значением UserID упорядочиваются по URL.
- размер гранулы равен двум, то есть каждая гранула содержит две строки.
- Метка индекса 0, для которой значение URL меньше W3 и значение URL у непосредственно следующей метки индекса тоже меньше W3, может быть исключена, потому что метки 0 и 1 имеют одинаковое значение UserID. Обратите внимание: это предварительное условие исключения гарантирует, что гранула 0 целиком состоит из значений UserID U1, поэтому ClickHouse может предположить, что максимальное значение URL в грануле 0 тоже меньше W3, и исключить эту гранулу.
- Метка индекса 1, для которой значение URL меньше (или равно) W3, а значение URL у непосредственно следующей метки индекса больше (или равно) W3, выбирается, потому что это означает, что гранула 1 потенциально может содержать строки с URL W3.
- Метки индекса 2 и 3, для которых значение URL больше W3, могут быть исключены, поскольку метки первичного индекса хранят значения ключевых столбцов для первой строки таблицы в каждой грануле, а строки таблицы на диске отсортированы по значениям ключевых столбцов, поэтому гранулы 2 и 3 не могут содержать значение URL W3.
Примечание об индексе пропуска данных
GRANULARITY 4 в операторе ALTER TABLE выше) — минимальное и максимальное значения URL:
Первая запись индекса (‘mark 0’ на диаграмме выше) хранит минимальное и максимальное значения URL для строк, относящихся к первым 4 гранулам нашей таблицы.
Вторая запись индекса (‘mark 1’) хранит минимальное и максимальное значения URL для строк, относящихся к следующим 4 гранулам нашей таблицы, и так далее.
(ClickHouse также создал специальный файл меток для индекса пропуска данных, чтобы определять расположение групп гранул, связанных с метками индекса.)
Из-за столь же высокой мощности UserID и URL этот вторичный индекс пропуска данных не помогает исключать гранулы при выполнении нашего запроса с фильтрацией по URL.
Конкретное значение URL, которое ищет запрос (то есть ‘http://public_search'), с большой вероятностью находится между минимальным и максимальным значениями, сохранёнными в индексе для каждой группы гранул, из-за чего ClickHouse вынужден выбирать эти группы гранул (поскольку они могут содержать строки, соответствующие запросу).
Необходимость использовать несколько первичных индексов
Варианты создания дополнительных первичных индексов
- Создать вторую таблицу с другим первичным ключом.
- Создать materialized view для существующей таблицы.
- Добавить проекцию в существующую таблицу.
Вариант 1: Вторичные таблицы
UserIDs тоже будет работать с новой дополнительной таблицей не слишком эффективно, потому что UserID теперь является вторым ключевым столбцом в первичном индексе этой таблицы, и поэтому ClickHouse будет использовать универсальный алгоритм поиска с исключением для выбора гранул, который не очень эффективен при столь же высокой мощности UserID и URL.
Откройте блок с подробностями.
Запрос с фильтрацией по UserIDs теперь работает медленно
Запрос с фильтрацией по UserIDs теперь работает медленно
UserIDs и по URL:
Вариант 2: Materialized Views
- мы меняем порядок столбцов ключа в первичном ключе представления по сравнению с нашей исходной таблицей
- materialized view опирается на неявно созданную таблицу, порядок строк и первичный индекс которой определяются заданным первичным ключом
- неявно созданная таблица выводится запросом
SHOW TABLESи имеет имя, начинающееся с.inner - также можно сначала явно создать базовую таблицу для materialized view, а затем представление сможет использовать эту таблицу через предложение
TO [db].[table] - мы используем ключевое слово
POPULATE, чтобы сразу заполнить неявно созданную таблицу всеми 8,87 миллиона строк из исходной таблицы hits_UserID_URL - если в исходную таблицу hits_UserID_URL вставляются новые строки, эти строки автоматически вставляются и в неявно созданную таблицу
- Фактически неявно созданная таблица имеет тот же порядок строк и тот же первичный индекс, что и вторичная таблица, которую мы создали явно:
Вариант 3: Проекции
- проекция создает скрытую таблицу, порядок строк и первичный индекс которой определяются указанным предложением
ORDER BYпроекции - скрытая таблица не отображается запросом
SHOW TABLES - мы используем ключевое слово
MATERIALIZE, чтобы сразу заполнить скрытую таблицу всеми 8,87 миллиона строк из исходной таблицы hits_UserID_URL - если в исходную таблицу hits_UserID_URL вставляются новые строки, они также автоматически вставляются в скрытую таблицу
- запрос всегда (синтаксически) обращен к исходной таблице hits_UserID_URL, но если порядок строк и первичный индекс скрытой таблицы позволяют выполнить запрос эффективнее, вместо исходной будет использована скрытая таблица
- обратите внимание, что проекции не делают более эффективными запросы с ORDER BY, даже если ORDER BY совпадает с ORDER BY проекции (см. https://github.com/ClickHouse/ClickHouse/issues/47333)
- фактически неявно созданная скрытая таблица имеет тот же порядок строк и тот же первичный индекс, что и вторичная таблица, которую мы явно создали:
Краткое резюме
Эффективный порядок столбцов ключа
- эффективность фильтрации по вторичным столбцам ключа в запросах, так и на
- коэффициент сжатия файлов данных таблицы.
UserID) к URL (столбец URL) помечен как трафик бота (столбец IsRobot).
Мы будем использовать составной первичный ключ, включающий все три вышеупомянутых столбца. Его можно использовать для ускорения типичных запросов веб-аналитики, вычисляющих:
- какая доля (в процентах) трафика к определённому URL приходится на ботов, или
- насколько мы уверены, что конкретный пользователь является (или не является) ботом (какой процент трафика от этого пользователя считается трафиком бота или, наоборот, не считается)
clickhouse client:
URL и IsRobot, поэтому порядок этих столбцов в составном первичном ключе важен как для эффективного ускорения запросов с фильтрацией по этим столбцам, так и для достижения оптимальных коэффициентов сжатия файлов с данными столбцов таблицы.
Чтобы продемонстрировать это, мы создадим две версии таблицы для наших данных анализа бот-трафика:
- таблицу
hits_URL_UserID_IsRobotс составным первичным ключом(URL, UserID, IsRobot), где столбцы ключа упорядочены по мощности в порядке убывания - таблицу
hits_IsRobot_UserID_URLс составным первичным ключом(IsRobot, UserID, URL), где столбцы ключа упорядочены по мощности в порядке возрастания
hits_URL_UserID_IsRobot с составным первичным ключом (URL, UserID, IsRobot):
hits_IsRobot_UserID_URL с составным первичным ключом (IsRobot, UserID, URL):
Эффективная фильтрация по вторичным столбцам ключа
UserID таблицы, в которой мы упорядочили столбцы ключа (URL, UserID, IsRobot) по мощности в порядке убывания:
(IsRobot, UserID, URL) по мощности в порядке возрастания:
Оптимальный коэффициент сжатия файлов данных
UserID в двух таблицах, которые мы создали выше:
UserID значительно выше у таблицы, в которой столбцы ключа (IsRobot, UserID, URL) упорядочены по мощности по возрастанию.
Хотя в обеих таблицах хранится абсолютно одинаковый набор данных (мы вставили в обе таблицы одни и те же 8,87 миллиона строк), порядок столбцов ключа в составном первичном ключе существенно влияет на то, сколько места на диске занимают сжатые файлы данных столбцов таблицы:
- в таблице
hits_URL_UserID_IsRobotс составным первичным ключом(URL, UserID, IsRobot), где столбцы ключа упорядочены по мощности по убыванию, файл данныхUserID.binзанимает 11.24 MiB места на диске - в таблице
hits_IsRobot_UserID_URLс составным первичным ключом(IsRobot, UserID, URL), где столбцы ключа упорядочены по мощности по возрастанию, файл данныхUserID.binзанимает всего 877.47 KiB места на диске
cl, а строки с одинаковым значением cl — по значению ch. И поскольку первый столбец ключа cl имеет низкую мощность, велика вероятность, что существуют строки с одинаковым значением cl. А значит, велика и вероятность того, что значения ch окажутся упорядоченными (локально — для строк с одинаковым значением cl).
Если в столбце похожие данные расположены близко друг к другу, например благодаря сортировке, такие данные будут сжиматься лучше.
В целом алгоритм сжатия выигрывает от длины последовательностей данных (чем больше данных он видит, тем лучше для сжатия)
и локальности (чем более похожи данные, тем выше коэффициент сжатия).
В отличие от диаграммы выше, на диаграмме ниже схематично показан порядок строк на диске для первичного ключа, в котором столбцы ключа упорядочены по мощности по убыванию:
Теперь строки таблицы сначала упорядочиваются по значению ch, а строки с одинаковым значением ch — по значению cl.
Но поскольку первый ключевой столбец ch имеет высокую мощность, строки с одинаковым значением ch встречаются редко. А значит, значения cl тоже вряд ли будут упорядочены (локально — в пределах строк с одинаковым значением ch).
Поэтому значения cl, скорее всего, расположены в случайном порядке и, соответственно, имеют низкую локальность и плохой коэффициент сжатия.
Кратко
Эффективная идентификация отдельных строк
Конкретный пример
- порядок вставки строк при изменении содержимого (например, из-за нажатий клавиш при вводе текста в текстовое поле) и
- порядок данных на диске из вставленных строк при использовании
PRIMARY KEY (hash):
hash используется в качестве столбца первичного ключа,
- конкретные строки можно получать очень быстро, но
- строки таблицы (то есть их данные в столбцах) хранятся на диске в порядке возрастания (уникальных и случайных) значений hash. Поэтому значения столбца content тоже хранятся в случайном порядке, без локальности данных, что приводит к неоптимальному коэффициенту сжатия файла данных столбца content.
- хеш содержимого, как обсуждалось выше, который различается для разных данных, и
- locality-sensitive hash (fingerprint), который не меняется при небольших изменениях данных.
- порядок вставки строк при изменении содержимого (например, из-за нажатий клавиш при вводе текста в текстовое поле) и
- порядок данных на диске из вставленных строк при использовании составного
PRIMARY KEY (fingerprint, hash):
fingerprint, а для строк с одинаковым значением fingerprint итоговый порядок определяется значением hash.
Поскольку данные, различающиеся лишь незначительно, получают одно и то же значение fingerprint, похожие данные теперь хранятся на диске рядом друг с другом в столбце content. А это очень хорошо влияет на коэффициент сжатия столбца content, поскольку алгоритмы сжатия в целом выигрывают от локальности данных (чем больше данные похожи, тем выше коэффициент сжатия).
Компромисс заключается в том, что для получения конкретной строки требуются два поля (fingerprint и hash), чтобы оптимально использовать первичный индекс, который получается из составного PRIMARY KEY (fingerprint, hash).