Persistent-файл: ATTACH/DETACH, несколько баз в одной сессии
До сих пор мы говорили об одной базе: запустили CLI или создали соединение — работаем с одним файлом или с одной in-memory базой. Но реальная задача часто требует большего: иметь под рукой одновременно несколько баз — например, читать из одной, писать в другую, или копировать данные между файлами.
DuckDB решает это через ATTACH и DETACH — операторы, которые подключают и отключают базы данных к текущей сессии. Подключив несколько баз, вы можете обращаться к таблицам из любой из них и даже соединять данные между файлами в одном запросе.
В этом уроке: как устроен оператор ATTACH, как работает каталог из нескольких баз, как адресовать таблицы по имени базы, и как DETACH отключает базу. Версия — DuckDB 1.5.2.
Зачем это вообще нужно — стоит понять с самого начала. В клиент-серверной СУБД «несколько баз» — это несколько баз на одном сервере, и переключение между ними тяжеловесно. В DuckDB база — это файл, и ATTACH делает работу с несколькими файлами такой же лёгкой, как работа с одним. Это открывает способ организации данных, который в серверных СУБД неудобен: разложить логически разные наборы данных по отдельным файлам и при этом свободно запрашивать их вместе. Сырые данные — в одном файле, справочники — в другом, витрины — в третьем; а запрос видит их все как единое целое. Именно эту возможность ATTACH и даёт.
ATTACH: подключить базу к сессии
ATTACH подключает файл базы данных к текущей сессии и даёт ему имя (alias), по которому к нему дальше обращаются:
ATTACH 'sales.duckdb' AS sales;
После этого база из файла sales.duckdb доступна в сессии под именем sales. Файл может уже существовать — тогда подключается готовая база; если файла нет, DuckDB создаст новый.
Имя можно не указывать — DuckDB выведет его из имени файла:
ATTACH 'warehouse.duckdb';
-- база доступна под именем warehouse
Подключать можно базу только для чтения — полезно, когда вы не хотите случайно изменить исходные данные:
ATTACH 'reference.duckdb' AS ref (READ_ONLY);
В режиме READ_ONLY любая попытка записи в базу ref завершится ошибкой — это страховка для справочных и исходных данных.
Каталог: несколько баз одновременно
Ключевая возможность — ATTACH можно вызвать несколько раз, подключив сразу несколько баз. Все они существуют в сессии одновременно, образуя каталог подключённых баз.
ATTACH 'raw.duckdb' AS raw;
ATTACH 'staging.duckdb' AS staging;
ATTACH 'marts.duckdb' AS marts;
Теперь в одной сессии живут три отдельных файла-базы. Посмотреть полный список подключённых баз можно так:
SHOW DATABASES;
┌──────────────┐
│ database_name│
│ varchar │
├──────────────┤
│ marts │
│ memory │
│ raw │
│ staging │
└──────────────┘
Обратите внимание на memory — это всегда присутствующая in-memory база сессии. И заметьте, что сессия — это не «одна база», а набор баз: транзиентная memory плюс всё, что вы подключили через ATTACH.
ATTACH из Python
ATTACH и DETACH — это обычные SQL-операторы, поэтому из Python они работают точно так же, как в CLI: их просто исполняют через соединение.
import duckdb
con = duckdb.connect()
con.execute("ATTACH 'raw.duckdb' AS raw")
con.execute("ATTACH 'marts.duckdb' AS marts")
# запрос между подключёнными базами
con.execute("""
CREATE TABLE marts.clean AS
SELECT id, amount FROM raw.events WHERE amount > 0
""")
con.execute("DETACH raw")
con.close()
Никакого специального API для подключения нескольких баз не нужно — соединение Python это та же сессия, и ATTACH наполняет её каталог. Это типичная форма пайплайна: одно соединение, несколько подключённых баз-слоёв, запросы между ними. Заметьте, что соединение здесь создано как in-memory (connect() без имени файла) — оно служит лишь «рабочим столом», к которому подключены реальные persistent-базы. Так часто и делают: in-memory соединение как точка входа, а данные — в attach-нутых файлах.
Адресация: имя базы перед таблицей
Раз баз несколько, DuckDB нужно понимать, к таблице какой именно базы вы обращаетесь. Адрес таблицы строится из частей через точку. Полная форма:
имя_базы.имя_схемы.имя_таблицы
Схема по умолчанию называется main, поэтому на практике чаще пишут двухчастную форму имя_базы.имя_таблицы:
-- таблица orders из базы raw
SELECT count(*) FROM raw.orders;
-- таблица orders из базы staging — это другая таблица в другом файле
SELECT count(*) FROM staging.orders;
raw.orders и staging.orders — две разные таблицы в двух разных файлах, и их легко различить именем базы. Когда имя базы не указано, DuckDB ищет таблицу в текущей базе — её можно сменить оператором USE:
USE staging;
-- теперь "orders" без префикса означает staging.orders
SELECT count(*) FROM orders;
USE задаёт базу по умолчанию для неполных имён — удобно, когда вы какое-то время работаете в одной базе и не хотите писать префикс каждый раз. Но у удобства USE есть оборотная сторона: после USE смысл неполного имени orders зависит от того, какая база сейчас текущая, а это легко упустить из виду. Поэтому в скриптах, которые должны работать однозначно и предсказуемо, безопаснее писать полные имена с префиксом базы, а USE оставлять для интерактивной работы, где текущая база у вас перед глазами.
Когда в сессии несколько баз, пишите имена таблиц с явным префиксом базы — raw.orders, marts.revenue. Это убирает двусмысленность: запрос читается однозначно, и нет риска случайно обратиться не к той таблице из-за того, что текущая база оказалась не той, что вы думали.
Запросы между базами
Раз все подключённые базы видны в одной сессии, в одном запросе можно работать с таблицами из разных файлов — DuckDB соединит их прозрачно.
Соединение таблиц из двух разных баз:
ATTACH 'raw.duckdb' AS raw;
ATTACH 'dict.duckdb' AS dict;
-- JOIN между таблицей из raw и таблицей из dict
SELECT o.order_id, c.country_name
FROM raw.orders AS o
JOIN dict.countries AS c ON o.country_code = c.code;
Движку всё равно, что orders и countries физически в разных файлах — для запроса это просто две таблицы каталога. И оптимизатор работает с таким запросом так же полноценно, как с запросом внутри одной базы: pushdown, выбор порядка соединения, всё применяется обычным образом — граница файлов оптимизатору не мешает.
Это даёт удобный способ копировать данные между базами — обычным CREATE TABLE AS SELECT или INSERT:
-- скопировать (с трансформацией) таблицу из raw в marts
CREATE TABLE marts.clean_orders AS
SELECT order_id, amount, order_date
FROM raw.orders
WHERE amount > 0;
Источник — база raw, приёмник — база marts, всё в одном операторе. Так удобно строить пайплайн, где каждый слой данных живёт в своём файле. Заметьте, что это та же модель, что в ELT-подходе: данные перетекают со слоя на слой через SQL-трансформации, только здесь слои — это отдельные файлы DuckDB, а трансформации — обычные запросы между ними.
Стоит остановиться на том, почему запрос между файлами вообще работает так прозрачно, без явного «открыть файл, прочитать, закрыть». Дело в том, что после ATTACH база перестаёт быть «файлом» с точки зрения запроса — она становится частью каталога сессии, то есть пространства имён, в котором живут все объекты. Когда оптимизатор строит план запроса JOIN между raw.orders и dict.countries, ему не нужно знать, что это разные файлы: для него обе таблицы — это записи каталога с известной схемой и известным способом чтения. Физический слой (какой файл, какие блоки) скрыт за слоем каталога. Именно поэтому соединение между двумя файлами в DuckDB выглядит и стоит примерно так же, как соединение двух таблиц внутри одного файла — разница лишь в том, откуда физически читаются данные, а логика плана одинакова.
Это объясняет и одну практическую тонкость. Транзакции в DuckDB работают в пределах подключённых баз: изменение нескольких таблиц можно сделать согласованно. Но ATTACH подключает именно базы DuckDB (а также, через расширения, и другие источники — это тема модуля про расширения). Чисто файловый источник вроде отдельного Parquet-файла вы не «attach-аете» — его запрашивают напрямую (SELECT * FROM 'file.parquet', тема модуля про внешние данные). Разница принципиальная: ATTACH — это про подключение полноценной базы со своим каталогом и транзакционностью, а прямой запрос файла — про чтение данных без понятия базы.
DETACH: отключить базу
DETACH отключает базу от сессии — она перестаёт быть видимой:
DETACH sales;
Важно понимать, что DETACH делает и чего не делает. Он отключает базу от текущей сессии: после DETACH sales обращение к sales.orders даст ошибку — такой базы в сессии больше нет. Но сам файл на диске остаётся нетронутым: DETACH не удаляет файл, его легко подключить снова через ATTACH. Отключение — это про видимость в сессии, а не про удаление данных.
DETACH полезен, чтобы освободить ресурсы, связанные с базой, когда она больше не нужна в текущей работе, или чтобы корректно «закрыть» базу перед тем, как, например, скопировать её файл.
| Оператор | Что делает |
|---|---|
ATTACH 'file' AS name | Подключает базу из файла к сессии под именем name |
ATTACH 'file' AS name (READ_ONLY) | Подключает базу только для чтения |
SHOW DATABASES | Показывает все подключённые базы сессии |
USE name | Делает базу name текущей для неполных имён таблиц |
DETACH name | Отключает базу от сессии; файл на диске сохраняется |
Зачем разносить данные по нескольким файлам
Возникает резонный вопрос: если можно держать всё в одной базе, зачем вообще раскладывать данные по нескольким файлам и соединять их через ATTACH? У этого есть несколько практических причин.
Разделение слоёв пайплайна. В типичном аналитическом пайплайне данные проходят стадии: сырые данные, очищенные промежуточные, финальные витрины. Дать каждой стадии свой файл (raw.duckdb, staging.duckdb, marts.duckdb) — значит сделать структуру пайплайна видимой прямо на уровне файловой системы. Каждый файл можно копировать, версионировать, передавать независимо от остальных.
Изоляция исходных данных через READ_ONLY. Подключив базу с исходными данными в режиме READ_ONLY, вы получаете гарантию: пайплайн физически не может их повредить. Любая попытка записи в такую базу завершится ошибкой. Это страховка, которую дорого получить иначе.
Управление жизненным циклом. Разные файлы можно подключать и отключать по мере надобности. Если справочная база нужна лишь на одном шаге пайплайна — подключите её перед этим шагом и отключите после, освободив связанные ресурсы. Сессия не обязана всё время держать открытым всё.
Гибкость переноса. Single-file формат DuckDB означает, что база — это один файл. Разнеся данные по файлам, вы можете перенести, забэкапить или отдать коллеге ровно тот срез, который нужен — например, только витрины, без сырых данных.
Несколько баз в одной сессии — это не «обходной приём», а штатный способ организации работы в DuckDB. Каталог сессии спроектирован так, чтобы держать произвольное число баз и прозрачно работать с ними как с единым пространством имён. Не бойтесь раскладывать данные по файлам там, где это делает структуру проекта яснее.
Попробуй сам
Постройте мини-пайплайн из нескольких баз и попрактикуйте запросы между ними.
- Запустите CLI в in-memory режиме. Создайте две persistent-базы и подключите их:
ATTACH 'raw.duckdb' AS raw;иATTACH 'marts.duckdb' AS marts;. - Выполните
SHOW DATABASES— убедитесь, что видныraw,martsиmemory. - В базе
rawсоздайте таблицу:CREATE TABLE raw.events AS SELECT i AS id, i % 3 AS kind FROM range(30) AS r(i);. - Сделайте запрос между базами: скопируйте отфильтрованные данные из
rawвmarts—CREATE TABLE marts.kind0 AS SELECT id FROM raw.events WHERE kind = 0;. Проверьте результат черезSELECT count(*) FROM marts.kind0;. - Выполните
USE marts;и убедитесь, что теперьSELECT count(*) FROM kind0;без префикса работает. - Отключите базу:
DETACH raw;. Попробуйте обратиться кraw.events— получите ошибку. Затем убедитесь, что файлraw.duckdbвсё ещё на диске, и подключите его снова черезATTACH.
Шаг 6 показывает суть DETACH: база уходит из сессии, но файл остаётся.