SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-10-28

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. Установите клиент командной строки версии 2.0.0 (релиз LTS) по состоянию на 27 октября 2026 года:

curl https://install.duckdb.org | sh

Скрипт устанавливает последний стабильный релиз, если вы не зафиксировали конкретную версию, например curl https://install.duckdb.org | DUCKDB_VERSION=1.5.6 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 прочитала нижний колонтитул (footer) 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, и весь уровень данных будет представлен двумя каталогами в одной задаче резервного копирования.

Режимы сбоев и точные сообщения, которые вы увидите

database is locked из SQLite, независимо от того, приходит ли оно через драйвер вашего приложения или оболочку sqlite3, означает, что другое соединение удерживало блокировку записи дольше, чем позволял ваш тайм-аут. Это не повреждение данных. Установите PRAGMA busy_timeout для каждого соединения, а затем найдите длительную транзакцию, которую следовало разбить на несколько коротких.

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:, поэтому открытие файла базы данных само по себе не обеспечивает работу механизма сброса. Она записывает эти временные файлы в каталог .tmp рядом с файлом базы данных или, для сессии в оперативной памяти, в каталог .tmp относительно места запуска, поэтому запускайте процесс там, где есть права на запись и свободное место на диске, либо укажите другое расположение с помощью SET temp_directory = '/var/tmp/duckdb';. Важный параметр — это ограничение: SET memory_limit = '2GB';. По умолчанию оно составляет 80% оперативной памяти машины, поэтому на сервере, где работают другие службы, этот лимит предотвращает вытеснение вашего приложения из памяти при выполнении произвольного запроса. Некоторые агрегатные функции, такие как string_agg(), не могут использовать сброс на диск, как и некоторые виды агрегации на основе сортировки, поэтому запрос, построенный на них, все еще может исчерпать память.

Binder Error: Referenced column "amount" not found при запросе Parquet почти всегда означает, что схема файла отличается от той, которую вы помните. Выполните DESCRIBE SELECT * FROM '/srv/data/orders.parquet'; и проверьте реальные имена столбцов.

Как сделать выбор на практике

Определите шаблон записи. Если требуется множество мелких операций записи, которые должны сохраняться при отключении питания, используйте SQLite. Определите шаблон чтения. Если требуются полные сканирования с агрегацией по длительной истории, используйте DuckDB. Большинство реальных систем требуют и того, и другого. Правильный подход — предоставить каждому движку ту часть работы, с которой он справляется лучше всего, вместо того чтобы заставлять один инструмент выполнять задачи другого.

Избегайте миграции рабочего состояния приложения в DuckDB только из-за того, что отчет работал медленно. Медленная работа отчета вызвана структурой хранения данных. Решением является экспорт данных, а не переписывание пути записи.

FAQ

Может ли DuckDB заменить SQLite в качестве базы данных для моего приложения?

Не в том случае, если приложение часто выполняет запись. DuckDB накладывает блокировку записи на весь файл базы данных, разрешает только один процесс записи одновременно и оптимизирована для пакетных изменений, а не для вставки отдельных строк. Храните транзакционное состояние в SQLite, а для формирования отчетов считывайте его с помощью ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY);.

Действительно ли DuckDB быстрее, чем SQLite, при выполнении аналитики?

Для сканирования и агрегации больших таблиц — да, и причина кроется в структуре хранения, а не в настройках. DuckDB считывает только те столбцы, которые указаны в запросе, и обрабатывает значения пакетами, тогда как SQLite вынуждена просматривать строки целиком, чтобы получить доступ к одному полю. При выборке одной строки по первичному ключу ситуация меняется на противоположную, так как SQLite обращается к двум страницам, а DuckDB — к хранилищу каждого столбца.

Нужно ли много оперативной памяти для работы DuckDB на VPS?

Нет, но установите лимит и оставьте место для сброса данных на диск. DuckDB по умолчанию сбрасывает промежуточные результаты на диск, даже в сессии :memory:, если во временном каталоге есть место для записи. Установите SET memory_limit = '2GB'; в значение, которое может выделить ваш VPS. Если оставить значение по умолчанию (80% от объема RAM), один крупный 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 не меняются после записи, поэтому достаточно просто скопировать каталог.

Была ли эта статья полезной?