Сканеры баз: postgres, mysql, sqlite
DuckDB умеет читать не только файлы, но и другие базы данных. Расширения postgres, mysql и sqlite подключают живые PostgreSQL, MySQL и SQLite-базы прямо в SQL-сессию DuckDB. После этого таблицы внешней СУБД становятся таблицами DuckDB: их можно запрашивать, джойнить с локальными данными, джойнить с Parquet-файлами на S3. Это превращает DuckDB в federated query engine — движок, исполняющий один запрос поверх нескольких разнородных источников. Этот урок про то, как сканеры баз устроены, что они проталкивают во внешнюю СУБД, а что нет, и где это полезно.
Зачем DuckDB ходить в другую базу
Trino: федеративные запросы к PostgreSQL и MySQL Trino: pushdown в источник — что отдаётся базе, что считается самТипичная ситуация: транзакционные данные приложения живут в PostgreSQL, а аналитика, исторические снапшоты и большие выгрузки лежат в Parquet на S3. Чтобы построить отчёт, нужно соединить «свежее состояние из Postgres» с «историей из Parquet». Классический путь — выгрузить Postgres в файл, потом всё обработать. Это лишний шаг, и данные на момент анализа уже устаревают.
Сканер базы убирает этот шаг. DuckDB подключается к PostgreSQL напрямую, видит его таблицы как свои, и джойн «Postgres + Parquet» становится одним обычным SQL-запросом. При этом DuckDB остаётся аналитическим движком: тяжёлую агрегацию и джойн он выполняет своим векторизованным движком, а PostgreSQL используется как источник строк. OLTP-база не нагружается аналитической работой — она лишь отдаёт данные.
ATTACH: подключение внешней базы
Внешняя база подключается командой ATTACH с указанием типа. После этого она появляется в каталоге DuckDB как отдельная база со своими схемами и таблицами.
-- Подключить PostgreSQL
ATTACH 'host=10.0.0.5 port=5432 dbname=shop user=analyst password=...' AS pg (TYPE postgres);
-- Подключить SQLite-файл
ATTACH 'app.sqlite' AS lite (TYPE sqlite);
-- Подключить MySQL
ATTACH 'host=10.0.0.7 user=root database=orders' AS my (TYPE mysql);
Расширения postgres, mysql, sqlite — core и autoloadable: явный INSTALL/LOAD обычно не нужен, ATTACH с соответствующим TYPE подтянет расширение сам. Учётные данные для PostgreSQL и MySQL лучше держать не в строке подключения, а в secret типа postgres/mysql — тогда строка ATTACH не содержит пароля.
После ATTACH таблицы внешней базы адресуются через её псевдоним: pg.public.customers. Они ведут себя как обычные таблицы DuckDB:
ATTACH 'host=10.0.0.5 dbname=shop user=analyst' AS pg (TYPE postgres);
-- Запрос к таблице PostgreSQL прямо из DuckDB
SELECT country, COUNT(*) AS customers
FROM pg.public.customers
GROUP BY country
ORDER BY customers DESC;
┌─────────┬───────────┐
│ country │ customers │
│ varchar │ int64 │
├─────────┼───────────┤
│ DE │ 14820 │
│ FR │ 9310 │
│ PL │ 6105 │
└─────────┴───────────┘
И главное — джойн внешней таблицы с локальными данными или файлами:
-- Свежие клиенты из Postgres + история заказов из Parquet на S3
SELECT c.country, SUM(o.amount) AS revenue
FROM pg.public.customers c
JOIN 's3://bucket/orders/*.parquet' o ON o.customer_id = c.id
GROUP BY c.country;
Один запрос — два источника, разные технологии. Это и есть federated-запрос.
Pushdown в сканерах баз: что уходит во внешнюю СУБД
Здесь — самая важная для производительности часть. Когда DuckDB читает из PostgreSQL, он не обязан тянуть всю таблицу к себе. Он умеет протолкнуть часть работы в саму PostgreSQL: DuckDB генерирует SQL-запрос к внешней базе и включает в него фильтры и список нужных колонок.
- Projection pushdown: если запрос трогает 3 колонки из 30, DuckDB отправит в PostgreSQL
SELECT col1, col2, col3 FROM ..., а неSELECT *. По сети придут только 3 колонки. - Filter pushdown: предикат
WHERE country = 'DE'уходит вWHERE-часть запроса к PostgreSQL. Фильтрацию делает PostgreSQL своим индексом, а по сети передаются только подходящие строки.
А вот чего сканер не проталкивает: сложную агрегацию, оконные функции, многотабличные джойны между внешними таблицами в общем случае выполняет сам DuckDB своим векторизованным движком. Это осознанный дизайн: DuckDB как аналитический движок считает агрегаты быстрее типичной OLTP-базы. Роль PostgreSQL в federated-запросе — отдать отфильтрованные и спроецированные строки; тяжёлую аналитику берёт на себя DuckDB.
Проверить, что именно ушло в PostgreSQL, можно через EXPLAIN: в плане у оператора сканирования postgres виден сгенерированный SQL-текст. Если фильтр в нём есть — pushdown сработал.
Pushdown ломается, когда фильтр использует выражение, которого нет в SQL внешней СУБД, или функцию, специфичную для DuckDB. Тогда DuckDB не может включить предикат в запрос к PostgreSQL, тянет таблицу целиком и фильтрует у себя. На большой внешней таблице это превращается в перекачку всей таблицы по сети. Всегда проверяйте EXPLAIN: если ожидаемого фильтра в сгенерированном SQL нет — переформулируйте предикат проще, ближе к стандартному SQL.
Перенос данных между базами
Сканеры работают в обе стороны. Раз внешняя база видна как обычная, в неё можно и писать, и из неё можно копировать. Это делает DuckDB удобным инструментом миграции и ETL между СУБД.
-- Скопировать таблицу из PostgreSQL в локальную таблицу DuckDB
CREATE TABLE local_customers AS SELECT * FROM pg.public.customers;
-- Целиком скопировать одну базу в другую
COPY FROM DATABASE pg TO my;
-- Записать результат запроса в таблицу MySQL
INSERT INTO my.orders_summary
SELECT customer_id, COUNT(*), SUM(amount)
FROM pg.public.orders GROUP BY customer_id;
Типичные сценарии: одноразовая миграция PostgreSQL -> MySQL, регулярная выгрузка горячих таблиц OLTP в Parquet для аналитики, перенос SQLite-базы мобильного приложения в аналитическое хранилище.
| Расширение | Внешняя СУБД | Подключение | Запись |
|---|---|---|---|
postgres | PostgreSQL | Строка подключения (host/port/db) | Да |
mysql | MySQL / MariaDB | Строка подключения | Да |
sqlite | SQLite | Путь к файлу базы | Да |
Расширение sqlite особенно удобно тем, что SQLite-база — это один файл, без сервера. ATTACH к SQLite не требует ни сети, ни учётных данных. Это делает связку «DuckDB читает SQLite» отличным способом аналитики над данными приложений, которые хранят состояние в SQLite-файле, — мобильные приложения, десктопные программы, браузеры.
Транзакционная семантика и ограничения
Внешняя база остаётся внешней — со своей моделью конкурентности и своими блокировками. DuckDB не управляет транзакциями PostgreSQL так, как своими. При чтении это незаметно, но при записи важно помнить: DuckDB отправляет во внешнюю СУБД команды, а изоляция и блокировки — на стороне той СУБД.
Производительность тоже своя. Чтение из PostgreSQL ограничено скоростью PostgreSQL и сети — это не колоночное хранилище DuckDB с его сжатием и zonemap. Если одна и та же внешняя таблица читается в аналитике многократно, разумнее один раз скопировать её в нативную таблицу DuckDB или в Parquet и дальше работать с быстрой локальной копией. Federated-запрос хорош для свежести данных, не для повторяющейся тяжёлой аналитики поверх одних и тех же строк.
Попробуй сам
Для задания удобен SQLite — он не требует сервера.
- Создайте SQLite-файл с парой таблиц (можно прямо из DuckDB:
ATTACH 'test.sqlite' AS lite (TYPE sqlite); CREATE TABLE lite.users AS SELECT range AS id, 'user' || range AS name FROM range(1000);). - Откройте новую сессию DuckDB, выполните
ATTACH 'test.sqlite' AS lite (TYPE sqlite)и запроситеSELECT COUNT(*) FROM lite.users. Убедитесь, что таблица SQLite видна как обычная. - Создайте локальную таблицу или Parquet-файл и сделайте джойн его с таблицей из SQLite в одном запросе. Это ваш первый federated-запрос.
- Если есть доступный PostgreSQL: подключите его через ATTACH, выполните
EXPLAIN SELECT col1, col2 FROM pg.schema.table WHERE col1 = 'X'и найдите в плане сгенерированный SQL-текст для PostgreSQL. Убедитесь, что фильтр и список колонок в него проброшены (pushdown).
Trino: федерация как первоклассная функция