In-process (embedded) OLAP-СУБД: что значит «база внутри процесса»
В первом уроке вводного модуля мы сказали, что DuckDB — это in-process OLAP-СУБД, и сравнили его с SQLite. Теперь разберём, что именно стоит за словами «in-process» и «embedded», на уровне устройства процесса операционной системы. Это не терминологическая придирка: модель встраивания определяет производительность, развёртывание и границы применимости DuckDB. Не понимая её, легко принять неверное архитектурное решение — и потом дорого его исправлять.
В этом уроке мы разберём, как клиент-серверная СУБД и embedded-СУБД устроены на уровне процессов ОС, что физически происходит при запросе, и почему «база внутри процесса» — это и сила DuckDB, и его ограничение одновременно.
Сразу зафиксируем терминологию, чтобы дальше не путаться. «In-process» и «embedded» — практически синонимы, два названия одного свойства. «Embedded» («встроенный») подчёркивает форму поставки: СУБД встроена в приложение как библиотека. «In-process» («внутри процесса») подчёркивает место исполнения: движок работает в том же процессе операционной системы, что и приложение. Противоположность обоих — «клиент-серверная» (или «standalone») СУБД, которая работает как отдельный процесс-сервер. Когда в курсе встретится любой из этих терминов применительно к DuckDB, речь об одном и том же: движок-библиотека внутри процесса хоста.
Анатомия клиент-серверной СУБД
Возьмём PostgreSQL. Когда вы «работаете с базой», в системе на самом деле есть как минимум два разных процесса операционной системы.
Первый — процесс СУБД-сервера (демон postgres). Он стартует при загрузке машины, открывает TCP-порт 5432, имеет собственное адресное пространство, собственный пул памяти (shared buffers), собственные рабочие потоки и процессы. Он — владелец данных: только он читает и пишет файлы базы на диске.
Второй — процесс вашего приложения (Python-скрипт, например). У него своё адресное пространство, изолированное от сервера операционной системой. Напрямую дотянуться до памяти сервера он не может — это запрещено на уровне ядра ОС.
Чтобы приложение получило данные, оно открывает соединение с сервером и обменивается с ним сообщениями по протоколу. Даже если сервер на той же машине, общение идёт через сетевой стек (loopback-интерфейс) или Unix-сокет.
Что физически происходит при запросе SELECT ... в этой модели:
- Приложение формирует текст запроса и сериализует его в байты протокола.
- Байты идут через сокет — это системный вызов и переключение контекста.
- Сервер десериализует запрос, парсит, планирует, исполняет.
- Результат — набор строк — сервер сериализует обратно в байты протокола.
- Байты идут через сокет к приложению.
- Приложение десериализует строки в свои структуры данных.
Шаги 1, 2, 4, 5, 6 — это чистые накладные расходы транспорта. На больших результатах сериализация и десериализация могут стоить дороже самого вычисления. Подчеркнём: эти расходы возникают не из-за плохой реализации какой-то конкретной СУБД, а из-за самой клиент-серверной модели. Раз сервер и приложение — разные процессы, данные обязаны пройти через границу между ними, а пройти её можно только сериализованными. Это структурная цена архитектуры, и заплатит её любая клиент-серверная СУБД, как бы хорошо она ни была написана.
Анатомия embedded-СУБД
Теперь DuckDB. Когда вы пишете import duckdb в Python и вызываете запрос, нового процесса операционной системы не появляется. Нет демона. Нет порта. Нет сокета.
DuckDB — это разделяемая библиотека (.so на Linux, .dylib на macOS, .dll на Windows), которую интерпретатор Python загружает в своё собственное адресное пространство. С точки зрения операционной системы DuckDB-движок — это просто часть кода вашего Python-процесса, ровно как numpy или код самого интерпретатора.
«Embedded» («встроенный») и означает именно это: СУБД встроена в процесс хоста как библиотека. «In-process» — синоним, подчёркивающий, что движок исполняется внутри того же процесса.
Что физически происходит при запросе в этой модели:
- Приложение вызывает функцию DuckDB и передаёт ей текст запроса — это обычный вызов функции в пределах процесса, как
len(x). - Движок парсит, планирует, исполняет запрос прямо здесь же.
- Результат отдаётся как структура данных в той же памяти — указатель, а не сериализованные байты.
Шагов сериализации, сокетов и переключений контекста нет вообще. Запрос — это вызов функции, результат — указатель на память. Разница с клиент-серверной моделью здесь не количественная («чуть быстрее»), а качественная: целого этапа транспорта данных просто не существует.
Прямое следствие: zero-copy обмен данными
Самое важное практическое следствие in-process модели — обмен данными с хост-средой почти бесплатен.
Допустим, у вас в Python есть pandas DataFrame sales на 50 миллионов строк, и вы хотите выполнить по нему SQL. В клиент-серверной СУБД пришлось бы загрузить эти 50 миллионов строк в сервер: сериализовать, передать по сокету, сервер их десериализует и материализует у себя. Объём данных физически прошёл бы границу процессов — дважды (туда данные, обратно результат).
DuckDB находится в том же адресном пространстве, что и DataFrame sales. Движок может получить указатель на буферы, где pandas уже хранит данные, и читать их прямо оттуда. Никакого копирования, никакой сериализации. Это и называется zero-copy.
import duckdb
import pandas as pd
sales = pd.DataFrame({
"region": ["EU", "US", "EU", "ASIA", "US"],
"amount": [100, 250, 70, 300, 180],
})
# DuckDB видит DataFrame sales по имени — он в той же памяти процесса
result = duckdb.sql("SELECT region, SUM(amount) AS total FROM sales GROUP BY region ORDER BY total DESC")
print(result)
Вывод:
┌─────────┬───────┐
│ region │ total │
│ varchar │ int64 │
├─────────┼───────┤
│ US │ 430 │
│ ASIA │ 300 │
│ EU │ 170 │
└─────────┴───────┘
Обратите внимание: мы нигде не «загружали» sales в DuckDB и не регистрировали его. DuckDB просто увидел переменную sales в области видимости Python — потому что она в той же памяти. Механику этого (replacement scans) курс разберёт отдельно; здесь важно одно: это работает только потому, что движок и DataFrame физически в одном процессе.
Zero-copy — не магия DuckDB, а следствие in-process архитектуры. Любая embedded-СУБД в одном процессе с приложением имеет к его памяти прямой доступ. Клиент-серверная СУБД не имеет принципиально: граница процессов, которую держит ядро ОС, пересекается только сериализованными данными.
Стоит оценить, насколько крупный это выигрыш. Представьте аналитический пайплайн, который несколько раз перекладывает данные между шагами: что-то посчитали, передали дальше, ещё посчитали. В клиент-серверной модели каждая такая передача — это сериализация на одной стороне и десериализация на другой, и на больших объёмах суммарное время на эту «упаковку-распаковку» может превысить время самих вычислений. В in-process модели этих шагов просто нет: данные остаются на месте, операторы движка читают их по указателю. Получается, что in-process архитектура убирает не отдельную медленную операцию, а целый класс накладных расходов — транспорт данных между процессами. Для аналитики, где данных много и их часто перекладывают, это решающее преимущество.
Полезно также понять, почему граница процессов в принципе непреодолима для zero-copy. Изоляция адресных пространств — это не «настройка», а фундаментальное свойство, которое ядро операционной системы обеспечивает ради безопасности и стабильности: один процесс не должен иметь возможности читать или портить память другого. Поэтому два процесса физически не могут просто «поделиться» структурой данных по указателю — указатель одного процесса в адресном пространстве другого бессмыслен. Любой обмен данными между процессами обязан пройти через явный механизм — сокет, файл, разделяемую память — и в общем случае это означает сериализацию. Embedded-СУБД обходит проблему радикально: она вообще не пересекает границу процессов, потому что находится по ту же сторону, что и приложение.
Embedded — это и ограничение
Apache DataFusion: альтернативный embeddable-движок на RustIn-process модель даёт скорость и простоту, но у неё есть оборотная сторона, и понимать её обязательно.
Раз DuckDB живёт внутри процесса, у базы нет независимого от процесса существования. Из этого следуют ограничения.
Нет сетевого доступа. К DuckDB нельзя «подключиться» с другой машины или из другого процесса по сети — подключаться не к чему, демона нет. Если данные нужны нескольким сервисам одновременно по сети — embedded-модель не подходит (для облачного сценария существует MotherDuck, отдельный managed-сервис).
Один процесс — один писатель. Persistent-файл DuckDB в каждый момент может быть открыт на запись только одним процессом. Внутри этого процесса много потоков и много читателей — но не много пишущих процессов. Это делает DuckDB непригодным для роли разделяемой транзакционной БД многих сервисов.
Жизненный цикл базы привязан к процессу. In-memory база существует, пока жив процесс; завершился процесс — база исчезла. Persistent-файл переживает процесс, но активная сессия — нет.
| Аспект | Клиент-сервер (PostgreSQL) | Embedded (DuckDB) |
|---|---|---|
| Процессов ОС | Сервер + клиент(ы) | Один |
| Передача запроса | Сериализация + сокет | Вызов функции |
| Обмен данными с приложением | Сериализация через границу процессов | Zero-copy, общая память |
| Сетевой доступ | Да, много клиентов | Нет |
| Конкурентная запись | Много процессов-писателей | Один процесс-писатель |
| Администрирование | Демон, порты, пользователи | Нет, библиотека |
Это не «недостатки» DuckDB — это границы его класса. DuckDB и клиент-серверная СУБД решают разные задачи. DuckDB — встраиваемый аналитический движок: быстрый, простой, для аналитики внутри одного процесса. Когда нужен сетевой многопользовательский доступ к общим данным, берут клиент-серверную СУБД. Выбор инструмента — это выбор под задачу, и in-process модель прямо очерчивает, для каких задач DuckDB создан.
Почему ограничения in-process — это разумный размен
Может показаться, что отсутствие сетевого доступа и модель «один писатель» — серьёзная плата. Но стоит посмотреть на это как на осознанный размен, а не как на упущение разработчиков.
Клиент-серверная архитектура даёт сетевой многопользовательский доступ — но платит за это сложностью. Сервер нужно развернуть, настроить, держать запущенным, мониторить, обновлять, защищать. Между приложением и данными встаёт сетевой слой со своими задержками и со стоимостью сериализации. Эта цена оправдана, когда вам действительно нужно, чтобы много клиентов с разных машин одновременно работали с общими данными — например, для операционной БД сервиса.
Но огромный класс задач этого не требует. Аналитик исследует датасет в своём ноутбуке. ETL-скрипт обрабатывает данные в одном процессе. Аналитический модуль приложения считает отчёты по локальным данным. Во всех этих случаях многопользовательский сетевой доступ не нужен вообще — а значит, и платить за него сложностью незачем. DuckDB сделал ровно этот размен: отказался от того, что этому классу задач не нужно (сервер, сеть, многопроцессная запись), и в обмен получил то, что этому классу задач критично важно (нулевой транспорт данных, тривиальное развёртывание, отсутствие администрирования).
То есть ограничения in-process модели — это не «DuckDB чего-то не умеет», а «DuckDB не несёт стоимость того, что его целевым задачам не нужно». Для аналитики в одном процессе это не компромисс, а точное попадание в задачу. А когда задача меняется — нужен именно сетевой доступ — это сигнал, что нужен другой класс инструмента, и в этом нет ничего плохого: инструмент выбирают под задачу.
Попробуй сам
Убедитесь, что DuckDB действительно не создаёт отдельного процесса. Запустите интерактивную Python-сессию и в ней выполните:
import os
print("PID процесса:", os.getpid())
import duckdb
con = duckdb.connect()
con.sql("SELECT 1").fetchall()
print("PID после работы с DuckDB:", os.getpid())
PID до и после — один и тот же, и нового процесса в системе не появилось. Параллельно откройте системный монитор (ps, top или Task Manager) и поищите процесс с именем duckdb — вы его не найдёте, потому что движок исполняется внутри процесса python. Сравните это с PostgreSQL: там ps aux | grep postgres покажет демон-процесс, существующий отдельно от любого приложения. Эта разница — наблюдаемое доказательство того, что значит «in-process». Запишите для себя вывод одним предложением: у DuckDB нет процесса, отдельного от приложения, — и именно из этого факта вытекают и zero-copy обмен данными, и простота развёртывания, и ограничения вроде модели «один писатель».