JDBC-коннекторы: маппинг типов, метаданные, connection pooling
В прошлом уроке мы узнали, что PostgreSQL, MySQL, SQL Server и Oracle — reference-коннекторы общего семейства JDBC-коннекторов. Теперь заглянем внутрь этого семейства. Что общего у всех JDBC-коннекторов, как тип numeric из PostgreSQL становится типом Trino, откуда Trino знает, какие в источнике таблицы, и почему между Trino и базой стоит пул соединений. Понимание этого слоя — фундамент: оно объясняет и поведение типов, и метаданные, и нагрузку на источник, а в следующем уроке — и pushdown.
Что такое JDBC и что общего у JDBC-коннекторов
JDBC (Java Database Connectivity) — стандартный Java-интерфейс доступа к реляционным базам. Любая реляционная СУБД поставляет JDBC-драйвер — библиотеку, которая умеет общаться с этой базой по её протоколу, но наружу выставляет единый стандартный API: «открой соединение», «выполни SQL», «верни строки результата», «опиши таблицу».
Trino этим и пользуется. JDBC-коннектор Trino — это, по сути, обёртка вокруг JDBC-драйвера конкретной базы. Все JDBC-коннекторы — PostgreSQL, MySQL, Oracle, ClickHouse, Redshift и прочие — построены на общем каркасе и разделяют единый набор задач:
- Подключаться к источнику через его JDBC-драйвер по
connection-url. - Читать метаданные — список схем, таблиц, колонок — через стандартные JDBC-вызовы.
- Маппить типы — превращать типы данных источника в типы Trino и обратно.
- Транслировать запрос — превращать (часть) запроса Trino в SQL, понятный источнику, и отправлять его драйверу.
- Управлять соединениями — переиспользовать дорогие сетевые соединения через пул.
Именно потому, что каркас общий, изучив один JDBC-коннектор, вы понимаете всё семейство. Различия конкретных коннекторов — это в основном различия в маппинге типов и в наборе поддерживаемого pushdown, а не в устройстве.
Маппинг типов: типы источника становятся типами Trino
У каждой СУБД своя система типов, и она не совпадает с системой типов Trino. PostgreSQL имеет numeric, text, timestamptz; MySQL — tinyint, datetime, text; Oracle — NUMBER, VARCHAR2, CLOB. Trino оперирует своими типами: DECIMAL, VARCHAR, TIMESTAMP, BIGINT. Чтобы данные источника стали данными Trino, нужен маппинг типов (type mapping) — таблица соответствий «тип источника -> тип Trino» для чтения и «тип Trino -> тип источника» для записи.
Этот маппинг — встроенная часть каждого JDBC-коннектора, и он работает в обе стороны. При чтении: коннектор узнаёт через JDBC тип колонки в источнике и подбирает соответствующий тип Trino. При записи (INSERT, CREATE TABLE AS): коннектор подбирает тип источника под тип Trino.
Несколько примеров маппинга PostgreSQL-коннектора при чтении:
| Тип PostgreSQL | Тип Trino |
|---|---|
bigint | BIGINT |
integer | INTEGER |
numeric(p,s) | DECIMAL(p,s) |
real | REAL |
double precision | DOUBLE |
varchar(n), text | VARCHAR |
boolean | BOOLEAN |
date | DATE |
timestamp | TIMESTAMP |
Большинство маппингов прямолинейны. Но есть подводные камни, и о них важно знать заранее.
SQL Internals: как СУБД хранит числовые типы SQL: система типов и преобразованияНесовпадение диапазонов и точности. Системы типов разных баз не изоморфны. Числовой тип источника может иметь точность или диапазон, которым в Trino нет точного аналога. Коннектор обычно подбирает ближайший подходящий тип Trino, но при экзотических значениях возможна потеря точности или ошибка.
Типы без аналога. У источника бывают типы, которым в Trino аналога нет вовсе, — специфические геометрические, сетевые, собственные пользовательские типы базы. Коннектор может либо отобразить такой тип на VARCHAR (как текстовое представление), либо не поддержать его — поведение зависит от коннектора.
Управление маппингом. Для пограничных случаев JDBC-коннекторы дают свойства, влияющие на маппинг, — например, как трактовать «неотображаемые» типы. Когда видите неожиданное поведение типа из источника — первым делом смотрите раздел type mapping на странице этого коннектора в документации.
Маппинг типов — первое, что стоит проверять при странностях федеративного запроса. Если число из источника пришло в Trino с неожиданной точностью, дата сдвинулась, а собственный тип базы вообще не читается — причина почти всегда в маппинге типов конкретного коннектора, а не в данных. Раздел type mapping в документации коннектора описывает все соответствия и доступные свойства настройки.
Метаданные: откуда Trino знает структуру источника
Чтобы выполнить SHOW TABLES FROM postgresql.public или просто разрешить имя таблицы в запросе, Trino должен знать структуру источника — какие в нём схемы, таблицы, колонки и каких типов. Эти метаданные JDBC-коннектор получает через стандартные метаданные JDBC: драйвер умеет по запросу вернуть список схем, список таблиц схемы, описание колонок таблицы.
Здесь — фундаментальное отличие от lakehouse-форматов. У Iceberg метаданные — это дерево файлов со снапшотами и статистикой; у Hive — записи в Metastore. А у JDBC-источника владелец метаданных — сама база. Trino не хранит копию структуры PostgreSQL; он каждый раз спрашивает её у самого PostgreSQL через JDBC. Создали в базе новую таблицу — Trino увидит её, потому что спросит у базы заново.
Чтобы не дёргать источник метаданными на каждый запрос, JDBC-коннекторы кэшируют ответы на короткое время — кэш метаданных. Это разумный компромисс между свежестью и нагрузкой на источник. Поэтому только что созданная в базе таблица может появиться в Trino не мгновенно, а с задержкой кэша.
Со статистикой для cost-based optimizer ситуация аналогичная: JDBC-коннектор берёт статистику из самого источника — реляционные базы ведут собственную статистику для своих оптимизаторов. Насколько полную статистику отдаёт коннектор, зависит от конкретной СУБД и коннектора. Если статистики мало, CBO Trino работает по федеративному запросу менее точно.
SQL: EXPLAIN и статистика планировщика PostgreSQLConnection pooling: почему соединения переиспользуют
Последний элемент JDBC-слоя — управление соединениями. Сетевое соединение с реляционной базой — дорогой ресурс. Открыть его означает: установить TCP-сессию, пройти аутентификацию, согласовать параметры сессии. Это занимает заметное время, и каждое открытое соединение потребляет ресурсы и на стороне Trino, и — что важнее — на стороне базы: у любой СУБД есть лимит на число одновременных соединений.
Теперь вспомните, как Trino исполняет запрос: распределённо, множеством задач на множестве воркеров. Если бы каждая задача, которой нужны данные из PostgreSQL, открывала собственное соединение и закрывала его после — источник захлёбывался бы в шторме открытий-закрытий, а его лимит соединений быстро бы исчерпался.
Решение — connection pool, пул соединений. Коннектор держит набор уже открытых, переиспользуемых соединений к источнику. Задаче, которой нужны данные, выдаётся свободное соединение из пула; закончив, задача не закрывает его, а возвращает в пул для следующей. Соединения переиспользуются вместо постоянного пересоздания.
Пул даёт две вещи. Производительность — нет затрат на открытие соединения под каждую операцию. И защита источника — размер пула ограничивает число одновременных соединений к базе сверху, и Trino не может случайно превысить её лимит. Параметры пула (его размер, тайм-ауты) настраиваются в каталоге.
Размер пула соединений — это рычаг управления нагрузкой на источник. Слишком маленький пул — задачи Trino простаивают в очереди за свободным соединением, федеративный запрос замедляется. Слишком большой — Trino может приблизиться к лимиту соединений базы и помешать другим её потребителям, включая боевое приложение. Подбирают, исходя из лимита соединений источника и числа конкурентных федеративных запросов.
Попробуй сам
В песочнице с подключённым каталогом PostgreSQL выполните серию упражнений. Первое, метаданные: создайте в самой базе PostgreSQL новую таблицу напрямую (через psql, в обход Trino), затем выполните SHOW TABLES в Trino — появилась ли таблица сразу или с задержкой, и почему. Затем DESCRIBE этой таблицы в Trino — сверьте, в какие типы Trino отобразились типы PostgreSQL. Второе, маппинг типов: создайте в PostgreSQL таблицу с колонками типов numeric(10,2), text, timestamp, boolean, выполните DESCRIBE через Trino и выпишите соответствие «тип PostgreSQL -> тип Trino». Третье, концептуальное: ответьте письменно на три вопроса. Кто является владельцем метаданных при работе Trino с PostgreSQL — и чем это отличается от Iceberg-таблицы? Зачем JDBC-коннектор кэширует метаданные? Что произойдёт с боевой базой, если у пула соединений снять ограничение размера, а к Trino придёт сотня одновременных федеративных запросов к этой базе?