Learning Platform
Глоссарий Troubleshooting
Урок 03.03 · 22 мин
Средний
attachdetachpersistentmulti-database

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 подключает базу к сессии
Файл sales.duckdbPersistent-база на диске в single-file формате — самостоятельный файл
ATTACH ... AS sales
Сессия DuckDBТекущее соединение: после ATTACH видит базу под именем sales и может адресовать её таблицы
DETACH sales
База отключенаПосле DETACH сессия больше не видит базу sales; сам файл на диске остаётся нетронутым

Каталог: несколько баз одновременно

Ключевая возможность — 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 оставлять для интерактивной работы, где текущая база у вас перед глазами.

TIP

Когда в сессии несколько баз, пишите имена таблиц с явным префиксом базы — 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 — это про подключение полноценной базы со своим каталогом и транзакционностью, а прямой запрос файла — про чтение данных без понятия базы.

Запрос между несколькими базами в одной сессии
raw.ordersТаблица из подключённой базы raw — сырые данные в одном файле
dict.countriesТаблица из подключённой базы dict — справочник в другом файле
JOIN в одном запросе
Результат / marts.clean_ordersDuckDB прозрачно соединяет таблицы из разных файлов; результат можно записать в третью базу

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 означает, что база — это один файл. Разнеся данные по файлам, вы можете перенести, забэкапить или отдать коллеге ровно тот срез, который нужен — например, только витрины, без сырых данных.

NOTE

Несколько баз в одной сессии — это не «обходной приём», а штатный способ организации работы в DuckDB. Каталог сессии спроектирован так, чтобы держать произвольное число баз и прозрачно работать с ними как с единым пространством имён. Не бойтесь раскладывать данные по файлам там, где это делает структуру проекта яснее.


Попробуй сам

Постройте мини-пайплайн из нескольких баз и попрактикуйте запросы между ними.

  1. Запустите CLI в in-memory режиме. Создайте две persistent-базы и подключите их: ATTACH 'raw.duckdb' AS raw; и ATTACH 'marts.duckdb' AS marts;.
  2. Выполните SHOW DATABASES — убедитесь, что видны raw, marts и memory.
  3. В базе raw создайте таблицу: CREATE TABLE raw.events AS SELECT i AS id, i % 3 AS kind FROM range(30) AS r(i);.
  4. Сделайте запрос между базами: скопируйте отфильтрованные данные из raw в martsCREATE TABLE marts.kind0 AS SELECT id FROM raw.events WHERE kind = 0;. Проверьте результат через SELECT count(*) FROM marts.kind0;.
  5. Выполните USE marts; и убедитесь, что теперь SELECT count(*) FROM kind0; без префикса работает.
  6. Отключите базу: DETACH raw;. Попробуйте обратиться к raw.events — получите ошибку. Затем убедитесь, что файл raw.duckdb всё ещё на диске, и подключите его снова через ATTACH.

Шаг 6 показывает суть DETACH: база уходит из сессии, но файл остаётся.

Trino: федерация источников через коннекторы
Проверка знанийKnowledge check
Что делает оператор ATTACH, как DuckDB различает таблицы с одинаковым именем из разных подключённых баз, и что именно происходит при DETACH?
ОтветAnswer
ATTACH подключает файл базы данных к текущей сессии и присваивает ему имя (alias), по которому к базе дальше обращаются; если файла нет, DuckDB создаёт новый, а с опцией READ_ONLY база подключается только для чтения. ATTACH можно вызвать несколько раз, и тогда в сессии одновременно существует несколько баз, образующих каталог (плюс всегда присутствует транзиентная in-memory база memory). Таблицы из разных баз DuckDB различает по префиксу: полный адрес таблицы это имя_базы.имя_схемы.имя_таблицы, на практике чаще двухчастная форма имя_базы.имя_таблицы, так как схема по умолчанию main. Поэтому raw.orders и staging.orders — однозначно разные таблицы в разных файлах. Если префикс не указан, DuckDB ищет таблицу в текущей базе, которую можно сменить оператором USE. DETACH отключает базу от текущей сессии — после этого обращение к её таблицам даёт ошибку, потому что в сессии такой базы больше нет. При этом DETACH не удаляет файл с диска: файл остаётся нетронутым и его можно снова подключить через ATTACH. То есть DETACH управляет видимостью базы в сессии и освобождением связанных с ней ресурсов, а не сохранностью данных.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что делает оператор `ATTACH 'sales.duckdb' AS sales`?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6