Представим таблицу с детализацией звонков у оператора связи. В каждой строке from_number, to_number, время и длительность звонка, партиции по дням. Для детализации абонента за месяц нужны две выгрузки, исходящие по from_number и входящие по to_number. Сколько файлов прочитает движок зависит от того, как дата-инженер разложит строки по файлам.
🔸 Как движок определяет, какие файлы можно пропустить
Для каждого Parquet-файла Iceberg хранит в манифестах минимум и максимум по каждому столбцу. Trino или Spark сравнивают эти границы с условием WHERE ещё до чтения и пропускают файлы, куда нужный номер попасть не может. Если звонки записывались в порядке поступления, в каждом файле окажутся номера со всей страны. Границы у всех файлов совпадут, и ради пары десятков строк запрос прочитает партицию целиком.
🔸 Обычная сортировка помогает только одному столбцу
Отсортируем партицию по from_number. Каждый файл теперь хранит узкий диапазон звонящих, и исходящие лежат в одном файле. Эти люди звонят на какие угодно номера, поэтому to_number в каждом файле занимает весь диапазон, и поиск входящих читает всё. Сортировка по паре (from_number, to_number) этого не исправит, потому что to_number упорядочен лишь внутри одного звонящего.
🔸 Как z-order делит порядок между столбцами
Для примера разобьём номера на 4 диапазона по коду после +7: 900..924, 925..949, 950..974 и 975..999. Z-order по очереди отмечает, в какой половине кодов звонящий и в какой вызываемый, а затем в какой четверти внутри своей половины каждый из них. Ответы записываются нулём или единицей и склеиваются в ключ. Звонок с +7 931 на +7 962 получает ключ 0110, потому что звонящий в первой половине кодов, вызываемый во второй, а внутри своих половин звонящий во второй четверти, вызываемый в первой. После сортировки по ключу каждый файл хранит звонки из одной группы номеров в другую, и границы у него узкие сразу по обоим столбцам. На схеме видно, что поиск исходящих теперь читает два файла вместо одного, а поиск входящих - два вместо четырёх.
На четырёх файлах выигрыш скромный, но он отлично масштабируется. При z-order файлы образуют квадратную сетку, и поиск по одному столбцу проходит через одну её строку, то есть примерно через sqrt(N) файлов. В партиции из 1024 файлов обе выгрузки при сортировке по from_number прочитают 1025 файлов, а при z-order в идеальном случае около 64.
🔸 Где z-order не нужен
• запросы фильтруют только по одному столбцу, например биллинг исходящих. Обычная сортировка по этому столбцу отсекает точнее
• время звонка уже разложено по дневным партициям, в ключе оно ничего не добавит
• столбцы с малой кардинальностью, например тип звонка или тариф, в ключе бесполезны, их значения всё равно попадут почти в каждый файл
• с каждым новым столбцом области в файлах растягиваются и выигрыш тает, обычно берут два-четыре столбца.
🔸 Как включить
В Iceberg z-order делает процедура Spark
CALL catalog.system.rewrite_data_files(
table => 'cdr.calls',
strategy => 'sort',
sort_order => 'zorder(from_number, to_number)');
Новые звонки загружаются без порядка, поэтому процедуру нужно запускать регулярно. Параметр where ограничивает её закрытой вчерашней партицией. В Trino z-order пока нет. Свойство sorted_by сортирует строки внутри каждого файла по обычному правилу, а optimize только склеивает мелкие файлы. Отсечение проверяется и в Trino через системную таблицу $files, где у каждого файла видны lower_bounds и upper_bounds. Если они почти одинаковые у всех файлов, фильтр по этому столбцу читает всё)
🔸 А что ещё умеет Iceberg?
Открыл бесплатное демо по Lakehouse (Iceberg, Trino, S3) на 30 минут. Оно про
time travel, возможность через метаданные переключаться между состояниями данных. Вы почистите таблицу поездок велошеринга от мусора, получите ошибочный DELETE от коллеги и восстановите таблицу из прошлого снимка. Стенд тот же, что в полной версии лабы, все инструменты в одном окне браузера.p.s. Следующая лаба уже в работе, анонс будет скоро. На выбор следующей темы можно повлиять в голосовании на сайте