Hive connector: HMS и Glue, ORC и Parquet, партиции и бакеты
В прошлом модуле мы детально разобрали Iceberg — современный формат таблиц. Этот модуль закрывает остальные форматы lakehouse, и начать логично с самого старого — Hive. Hive — это не просто «ещё один коннектор»: понимание его модели объясняет, почему Iceberg и Delta устроены именно так, какие проблемы они исправляют, и почему в продакшене 2026 года всё ещё полно Hive-таблиц. Этот урок разбирает анатомию Hive-таблицы, роль Metastore, форматы файлов и механику партиций и бакетов в Trino.
Что такое Hive-таблица: метаданные отдельно от данных
Hive появился в эпоху Hadoop как способ дать SQL поверх файлов в HDFS. Его модель проще модели Iceberg, и важно её ухватить, потому что Trino-коннектор hive работает именно с ней.
Hive-таблица состоит из двух разделённых частей. Первая — данные: файлы в директории object storage или HDFS. Просто файлы в папке, без слоя метаданных рядом с ними. Вторая — метаданные в Hive Metastore: отдельный сервис, который хранит описание таблицы — её схему, формат файлов, расположение (LOCATION) и список партиций.
Ключевое отличие от Iceberg вытекает прямо отсюда. У Iceberg метаданные — это дерево файлов рядом с данными, и в нём перечислен каждый файл данных поимённо со статистикой. У Hive метаданные в Metastore описывают таблицу до уровня директории партиции, но не до уровня отдельного файла.
Apache Iceberg: иерархия метаданных vs Hive Metastore Metastore знает: «партиция dt=2026-05-19 лежит в такой-то директории». Какие конкретно файлы внутри этой директории — Metastore не знает. Чтобы это выяснить, Trino делает list директории во время запроса.
Это «list во время запроса» — корень многих особенностей Hive, к которым мы вернёмся. А пока запомните формулу: Hive = Metastore (схема и партиции) + директории с файлами. Никакого слоя метаданных уровня файла, никаких снапшотов.
Metastore: HMS или AWS Glue
Коннектору hive обязательно нужен Metastore — иначе негде взять схему таблицы. Trino поддерживает два варианта.
Hive Metastore Service (HMS) — классический. Это отдельный Java-сервис с реляционной БД (PostgreSQL/MySQL) под капотом, общающийся по Thrift. В конфигурации каталога Trino — hive.metastore=thrift и hive.metastore.uri=thrift://.... HMS проверен годами, его понимают все движки экосистемы Hadoop. Минус — это лишний сервис, который надо разворачивать и обслуживать.
AWS Glue Data Catalog — managed-аналог от AWS. Тот же реестр схем и партиций, но как сервис AWS, который не нужно поднимать самому. В конфигурации — hive.metastore=glue. Естественный выбор, если инфраструктура в AWS.
# etc/catalog/hive.properties — вариант с HMS
connector.name=hive
hive.metastore=thrift
hive.metastore.uri=thrift://hms:9083
fs.native-s3.enabled=true
s3.endpoint=http://minio:9000
s3.path-style-access=true
Заметьте: это тот же HMS, что используется как один из вариантов каталога для Iceberg в прошлом модуле. Один сервис Metastore может обслуживать и Hive-таблицы, и Iceberg-таблицы — но это разные форматы таблиц, и записи в Metastore у них устроены по-разному.
В актуальных релизах Trino из коннекторов удалён legacy-слой доступа к object storage (старые Hadoop-based реализации). Используется только нативная файловая система Trino — fs.native-s3.enabled для S3 и S3-совместимых хранилищ, и аналогичные свойства для Azure и GCS. Старые конфигурации с Hadoop-плагинами в свежем Trino не работают — это одно из ломающих изменений последних версий.
Форматы файлов: ORC и Parquet
Hive исторически поддерживает много форматов файлов — ORC, Parquet, Avro, RCFile, SequenceFile, JSON, CSV, текстовые. Но для аналитики в Trino реально важны два колоночных: ORC и Parquet. Текстовые и CSV-форматы строчные, без сжатия и статистики — для больших таблиц они непригодны, годятся лишь для приёма сырых данных.
ORC и Parquet — оба колоночные, оба сжимают данные по столбцам и хранят внутреннюю статистику. Parquet — фактический отраслевой стандарт, его использует почти вся экосистема (Spark, Iceberg, Delta по умолчанию). ORC исторически глубоко связан с Hive и нужен для одной конкретной вещи — ACID-таблиц Hive, о которых речь в следующем уроке.
Формат файлов Hive-таблицы задаётся при создании:
CREATE TABLE hive.logs.events (
event_id BIGINT,
user_id BIGINT,
amount DECIMAL(12,2),
dt DATE
)
WITH (
format = 'PARQUET',
partitioned_by = ARRAY['dt']
);
Важная деталь о статистике. Колоночные форматы хранят статистику внутри файлов — min/max по row group. Trino использует её для пропуска row group при чтении уже открытого файла. Но это статистика уровня файла, и чтобы Trino её увидел, файл нужно открыть. Статистика уровня таблицы для cost-based optimizer (общее число строк, NDV) у Hive живёт в Metastore и обновляется отдельно — командой ANALYZE. Hive исторически не предоставляет некоторые виды статистики так полно, как Iceberg, — это одна из причин, по которой CBO на Hive-таблицах работает менее точно.
Партиции в Hive: директория = значение колонки
Партиция в Hive — это физическая директория, имя которой кодирует значение партиционирующей колонки. Таблица из примера выше, партиционированная по dt, разложится в object storage так:
s3://lake/logs/events/
dt=2026-05-18/ 00001.parquet 00002.parquet
dt=2026-05-19/ 00001.parquet 00002.parquet
dt=2026-05-20/ 00001.parquet
Партиционирующая колонка dt — это отдельная колонка в схеме, и её значение не хранится в самих файлах: оно закодировано в имени директории. Это и есть та модель, против которой проектировали hidden partitioning Iceberg (вспомните прошлый модуль). Запрос обязан фильтровать именно по партиционирующей колонке:
-- Фильтр по dt: Metastore знает директории партиций,
-- Trino делает list ТОЛЬКО нужных директорий — это partition pruning
SELECT count(*) FROM hive.logs.events
WHERE dt = DATE '2026-05-19';
-- _col0
-- -------
-- 48210
Здесь работает partition pruning. Metastore хранит список партиций с их директориями. Получив WHERE dt = '2026-05-19', Trino спрашивает Metastore про подходящие партиции и делает list только их директорий — папки dt=2026-05-18 и dt=2026-05-20 вообще не трогаются. Если же фильтра по dt нет, Trino вынужден перечислить все партиции и сделать list всех директорий — на таблице с десятками тысяч партиций одно только планирование станет дорогим.
Если запрос к большой Hive-таблице неожиданно медленный на этапе планирования — проверьте, есть ли в нём фильтр по партиционирующей колонке. Без него Trino перечисляет все партиции и делает list всех директорий. Это самая частая причина медленных запросов к Hive: не объём данных, а перечисление партиций.
Бакеты: предсказуемое разбиение внутри партиции
Партиции хорошо работают для колонок с умеренным числом значений — дата, регион. Для колонки высокой кардинальности вроде user_id партиции не годятся: миллионы значений дали бы миллионы директорий. Hive решает это бакетингом (bucketing).
Бакетинг разбивает данные по хешу колонки на фиксированное число файлов-бакетов. Таблица с bucketed_by = ARRAY['user_id'] и bucket_count = 32 раскладывает строки по 32 бакетам: бакет строки определяется как хеш user_id по модулю 32. Число бакетов фиксировано и предсказуемо.
CREATE TABLE hive.logs.events_bucketed (
event_id BIGINT,
user_id BIGINT,
amount DECIMAL(12,2),
dt DATE
)
WITH (
format = 'PARQUET',
partitioned_by = ARRAY['dt'],
bucketed_by = ARRAY['user_id'],
bucket_count = 32
);
Бакетинг даёт два выигрыша. Первый — bucket pruning: запрос WHERE user_id = 12345 позволяет Trino вычислить хеш и прочитать только один из 32 бакетов вместо всех. Второй — ускорение join: если две таблицы забакетены по ключу join одинаково, соответствующие строки уже лежат в соответствующих бакетах, и join можно сделать без дорогого перераспределения данных по сети.
Партиции и бакеты дополняют друг друга: партиции отсекают по колонке умеренной кардинальности (дата), бакеты — упорядочивают данные по колонке высокой кардинальности (id) внутри каждой партиции. Это аналог day(ts) плюс bucket(id, N) из мира Iceberg, только в Hive — на уровне физических директорий и файлов, а не метаданных.
Попробуй сам
В песочнице курса (Trino + HMS + MinIO) создайте Hive-таблицу events, партиционированную по dt, в формате Parquet. Загрузите данные за три-четыре разных дня. Упражнение первое: выполните SELECT count(*) с фильтром по одному dt, оберните в EXPLAIN, найдите в выводе число затронутых партиций — должна быть одна. Затем выполните тот же запрос без фильтра по dt и сравните. Упражнение второе: посмотрите, как таблица разложена в MinIO — найдите директории вида dt=... и убедитесь, что значение dt в файлах не хранится, оно только в имени папки. Упражнение третье, концептуальное: создайте таблицу с bucketed_by по user_id и bucket_count = 16, загрузите данные. Письменно объясните: чем partition pruning по dt принципиально отличается от bucket pruning по user_id, и почему для user_id нельзя было просто использовать партиционирование.