DuckDB или SQLite: что выбрать для сервера
SQLite подходит для транзакций, а DuckDB для аналитики по Parquet и CSV. Узнайте, почему на одном VPS стоит использовать обе базы данных и как настроить их совместную работу.
DuckDB против SQLite на сервере: ответ в одном предложении
SQLite — это движок OLTP (онлайн-обработка транзакций): он хранит данные в виде строк и предназначен для быстрого и безопасного чтения или записи небольшого количества записей за раз. DuckDB — это движок OLAP (онлайн-аналитическая обработка): он хранит данные в виде столбцов и предназначен для сканирования миллионов строк с последующим вычислением одного агрегата. Оба решения являются встраиваемыми библиотеками, оба работают с обычными файлами, и ни одно из них не требует запуска серверного процесса, за которым нужно следить.
Поэтому честный ответ на вопрос «что выбрать» почти всегда звучит как «и то, и другое на одном VPS». Ваше приложение хранит текущее состояние в SQLite. Ваша система отчетности считывает файлы Parquet и CSV с помощью DuckDB. Они не конкурируют, так как выполняют разные задачи.
Почему строковое и колоночное хранение меняют ответ
SQLite записывает строку как единый непрерывный фрагмент страницы. Получение одного заказа по первичному ключу затрагивает одну страницу индекса и одну страницу данных, что составляет два чтения. Именно это приложение выполняет тысячи раз в секунду: прочитать пользователя, обновить сессию, вставить заказ.
DuckDB записывает каждый столбец отдельно и сжимает его. Вычисление суммы amount_cents по пяти миллионам строк считывает только столбец amount_cents, пропускает все остальные байты в файле и выполняет суммирование с помощью векторизованного кода над пакетами значений. Остальные столбцы никогда не считываются с диска, что и обеспечивает высокую скорость.
Теперь запустите каждый движок с нагрузкой, не предназначенной для него. SQLite при суммировании столбца вынужден обходить каждую строку и извлекать её целиком со страницы, чтобы добраться до одного поля, поэтому он считывает с диска гораздо больше данных, чем необходимо. DuckDB при вставке одного заказа вынужден обращаться к хранилищу каждого столбца ради одного значения и накладывать блокировку записи на весь файл базы данных. Ни один из движков не является неисправным. Каждый из них отвечает на вопрос, для которого он не был спроектирован.
Когда SQLite эффективен: транзакционное состояние приложения
Выбирайте SQLite, если операции записи небольшие, частые и должны быть гарантированно сохранены. Это сессии, заказы, строки очередей, настройки — всё, что создаётся в ходе веб-запроса.
sudo apt update && sudo apt install -y sqlite3
sudo install -d -o "$USER" -g "$USER" /srv/appСоздайте таблицу и включите режим журналирования с опережением записи (write-ahead logging) одним действием.
sqlite3 /srv/app/app.db <<'SQL'
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY,
customer TEXT NOT NULL,
placed_at TEXT NOT NULL,
amount_cents INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS orders_customer ON orders (customer);
INSERT INTO orders (customer, placed_at, amount_cents)
VALUES ('ana', '2026-07-30T09:14:00Z', 4200);
SQLПервая строка вывода — wal. Это PRAGMA сообщает о режиме, в который переключилась база данных; это самая полезная настройка на сервере. В режиме rollback journal по умолчанию пишущий процесс блокирует всех читателей. В режиме WAL читатели продолжают считывать последнее зафиксированное состояние, пока один процесс записи добавляет данные. В результате медленный отчёт больше не блокирует веб-запрос, идущий следом.
Проверьте, что строка была добавлена:
sqlite3 /srv/app/app.db "SELECT * FROM orders WHERE customer = 'ana';"Вы получите 1|ana|2026-07-30T09:14:00Z|4200. Рядом с файлом базы данных появились ещё два файла: app.db-wal и app.db-shm; оба они относятся к ней. Копирование только app.db во время работы приложения приведёт к созданию повреждённой резервной копии, что будет рассмотрено далее.
SQLite по-прежнему допускает только одного пишущего пользователя одновременно. Это ограничение является блокировкой, а не очередью, поэтому второй процесс записи, который ждёт слишком долго, завершится с ошибкой database is locked вместо бесконечного ожидания. Увеличьте время ожидания с помощью PRAGMA busy_timeout = 5000; для каждого соединения, которое открывает ваше приложение. Пять секунд ожидания устраняют большинство подобных ошибок при стандартной веб-нагрузке.
В чем преимущество DuckDB: аналитика по имеющимся файлам
Выбирайте DuckDB, если вопрос начинается с «сколько», «какова сумма» или «какие десять лучших», а входными данными является набор файлов CSV или Parquet. Установите клиент командной строки, версия 1.5.5 на июль 2026 года:
curl https://install.duckdb.org | shСкрипт устанавливает бинарный файл в ~/.duckdb/cli/latest/duckdb и выводит строку, добавляющую его в ваш PATH. Убедитесь, что он работает:
~/.duckdb/cli/latest/duckdb :memory: "SELECT version();"Создайте реалистичный файл для запроса. Эта команда записывает пять миллионов строк заказов в формат Parquet со сжатием zstd:
mkdir -p /srv/data
duckdb :memory: "
COPY (
SELECT i AS id,
'cust_' || (i % 5000) AS customer,
TIMESTAMP '2026-01-01 00:00:00' + INTERVAL (i) MINUTE AS placed_at,
(i * 37) % 20000 AS amount_cents
FROM range(5000000) t(i)
) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd);"Теперь задайте аналитический вопрос. Откройте оболочку, включите таймер и выполните запрос к файлу напрямую, без этапа импорта:
.timer on
SELECT customer,
count(*) AS orders,
sum(amount_cents)/100.0 AS revenue
FROM '/srv/data/orders.parquet'
GROUP BY customer
ORDER BY revenue DESC
LIMIT 5;Считайте свое число из .timer, а не доверяйте опубликованным данным, так как результат зависит от вашего диска и количества ядер. Важна сама структура процесса. Здесь не было CREATE TABLE, не было INSERT и не было этапа загрузки: DuckDB прочитала заголовок Parquet, определила, какие фрагменты столбцов нужны для запроса, и прочитала только их. Весь каталог обрабатывается аналогичным образом с помощью шаблона FROM '/srv/data/orders-*.parquet', что позволяет превратить месячный объем ежедневных выгрузок в один запрос.
Скорость диска является фундаментом для всего этого, а сканирование столбцов — это длинное последовательное чтение, поэтому разница между NVMe и старыми SATA-накопителями на VPS здесь проявляется гораздо отчетливее, чем при небольших случайных операциях чтения в SQLite.
Чтение базы данных SQLite из DuckDB
Эти два движка взаимодействуют через расширение sqlite в DuckDB. Подключите базу данных приложения в режиме только для чтения, чтобы аналитический запрос не мог изменить текущие данные:
INSTALL sqlite;
LOAD sqlite;
ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY);
SELECT customer, count(*) AS orders
FROM app.orders
GROUP BY customer
ORDER BY orders DESC;Этот метод считывает строки из файла SQLite непосредственно во время выполнения запроса без создания копий. Это удобно, но не быстро, так как данные на диске хранятся в строковом формате, и DuckDB приходится их перебирать. Используйте этот способ для экспорта, а не для дашбордов, которые обновляются каждые тридцать секунд:
COPY (SELECT * FROM app.orders)
TO '/srv/data/orders-2026-07.parquet' (FORMAT parquet, COMPRESSION zstd);Этот единственный оператор и есть вся схема работы. SQLite хранит актуальные записи. Запланированный экспорт преобразует закрытые периоды в формат Parquet. DuckDB отвечает на любые вопросы, охватывающие месяцы, а база данных приложения остается компактной, что обеспечивает высокую скорость записи.
Выполняйте экспорт по расписанию, а не вручную. Для этого лучше всего подходит пара из службы и таймера systemd: один юнит, который запускает COPY, и один таймер, который активирует его каждую ночь.
Запуск обоих на одном VPS
Здесь ничего не требует контейнера и ничего не требует порта. Оба движка являются библиотеками, поэтому установка сводится к пакету и пути к файлу. Если остальная часть вашего стека уже работает через Docker Compose на том же VPS, смонтируйте каталог с данными в нужный контейнер вместо добавления сервиса базы данных, так как добавлять нечего.
Два правила позволяют избежать проблем при такой конфигурации.
Выделите каждому движку свой каталог: /srv/app для файла SQLite, в который пишет приложение, и /srv/data для файлов Parquet, которые считывает аналитика. Если они используют один каталог, задача резервного копирования, создающая снапшот одного, вступит в конфликт с другим.
Не направляйте два процесса на один файл базы данных DuckDB в режиме чтения-записи. Только один процесс может удерживать файл DuckDB для записи, а второй не сможет его открыть. Множество читающих процессов работают корректно, если каждый из них устанавливает access_mode = 'READ_ONLY'. Это удивляет тех, кто переходит с SQLite, где несколько процессов обычно совместно используют файл. Если ваша аналитика только считывает файлы Parquet, этот вопрос не возникает, что является еще одной причиной хранить постоянное состояние в SQLite.
Резервные копии различаются, и эти различия критичны
Работающая база данных SQLite состоит из трех файлов, и их копирование с помощью cp во время записи приведет к созданию файла, который откроется, но будет содержать некорректные данные. Используйте встроенную команду резервного копирования движка, которая создает согласованный снимок, пока приложение продолжает запись:
sqlite3 /srv/app/app.db ".backup '/srv/backup/app-$(date -u +%Y%m%dT%H%M%SZ).db'"
sqlite3 /srv/backup/app-20260730T091400Z.db "PRAGMA integrity_check;"integrity_check выводит ok при успешном копировании. Любой другой результат означает, что этот снимок следует удалить и создать новый.
Файлы Parquet никогда не изменяются после записи, поэтому они не требуют специальной обработки: просто копируйте каталог. Отправляйте оба пути с сервера с помощью резервного копирования restic с вашего VPS, и весь уровень данных будет представлен двумя каталогами в одной задаче резервного копирования.
Типичные ошибки и сообщения, которые вы увидите
Error: database is locked от SQLite означает, что другое соединение удерживало блокировку записи дольше, чем позволяет ваш таймаут. Это не повреждение данных. Установите PRAGMA busy_timeout для каждого соединения, а затем найдите длительную транзакцию, которую следовало разбить на несколько коротких.
Error: unable to open database file после изменения прав доступа обычно означает, что процесс может записывать файл, но не имеет прав на запись в его директорию. SQLite создает app.db-wal и app.db-shm рядом с базой данных, поэтому директория должна быть доступна для записи, а не только сам файл .db.
IO Error: Could not set lock on file от DuckDB означает, что другой процесс уже открыл эту базу данных для записи. Закройте другую оболочку или откройте свою базу в режиме только для чтения.
Out of Memory Error от DuckDB на небольшом VPS означает, что запросу потребовалось больше оперативной памяти, чем было доступно. DuckDB сбрасывает данные на диск, когда это возможно, поэтому предоставьте ей место для временных файлов, открыв файл базы данных на диске вместо :memory:, и ограничьте потребление ресурсов с помощью SET memory_limit = '2GB';. На сервере, где работают другие службы, это ограничение не даст случайному запросу вытеснить ваше приложение из RAM.
Binder Error: Referenced column "amount" not found при выполнении запросов к Parquet почти всегда означает, что схема файла отличается от той, которую вы ожидаете. Выполните DESCRIBE SELECT * FROM '/srv/data/orders.parquet'; и проверьте реальные имена столбцов.
Как сделать выбор на практике
Определите шаблон записи. Если требуется множество мелких операций записи, которые должны сохраняться при отключении питания, используйте SQLite. Определите шаблон чтения. Если требуются полные сканирования с агрегацией по длительной истории, используйте DuckDB. Большинство реальных систем требуют и того, и другого. Правильный подход — предоставить каждому движку ту часть работы, с которой он справляется лучше всего, вместо того чтобы заставлять один инструмент выполнять задачи другого.
Избегайте миграции рабочего состояния приложения в DuckDB только из-за того, что отчет работал медленно. Медленная работа отчета вызвана структурой хранения данных. Решением является экспорт данных, а не переписывание пути записи.
FAQ
Может ли DuckDB заменить SQLite в качестве базы данных для моего приложения?
Не в том случае, если приложение часто выполняет запись. DuckDB накладывает блокировку на запись на весь файл базы данных, разрешает только один процесс записи за раз и оптимизирована для пакетных изменений, а не для вставки отдельных строк. Храните транзакционное состояние в SQLite, а DuckDB используйте для чтения данных с помощью ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY);, когда требуется сформировать отчет.
Действительно ли DuckDB быстрее, чем SQLite, при выполнении аналитики?
Для сканирования и агрегации больших таблиц — да, и причина кроется в структуре хранения, а не в настройках. DuckDB считывает только те столбцы, которые указаны в запросе, и обрабатывает значения пакетами, тогда как SQLite вынуждена просматривать всю строку целиком, чтобы получить доступ к одному полю. При выборке одной строки по первичному ключу ситуация меняется на противоположную, так как SQLite обращается к двум страницам, а DuckDB — к хранилищу каждого столбца.
Нужно ли много оперативной памяти для работы DuckDB на VPS?
Нет, но ограничьте потребление ресурсов и используйте диск. Открывайте файл базы данных, а не :memory:, чтобы DuckDB могла выгружать промежуточные результаты на диск, затем установите SET memory_limit = '2GB'; в значение, которое может выделить ваш VPS. Без ограничений один крупный запрос GROUP BY может вызвать Out of Memory Error или привести к вытеснению других сервисов из оперативной памяти.
Как перенести данные из SQLite в формат Parquet?
Подключите файл SQLite из DuckDB и скопируйте результат запроса напрямую с помощью COPY (SELECT * FROM app.orders) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd);. Запускайте этот процесс по расписанию для закрытых периодов, например, для строк за прошлый месяц, а недавние записи оставьте в SQLite, куда их продолжает записывать приложение.
Что именно нужно резервировать и как?
Оба типа файлов, но разными способами. Создавайте снимки SQLite с помощью sqlite3 app.db ".backup '/srv/backup/app.db'", а не cp, так как работающая база данных — это также -wal и -shm, и простое копирование может привести к повреждению данных. Файлы Parquet не меняются после записи, поэтому достаточно просто скопировать директорию.