SSD Nodes Learn 8GB RAM — $66/год
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-01

DuckDB или SQLite: что выбрать для сервера

Узнайте, почему SQLite и DuckDB эффективно работают вместе на одном VPS. Разбираем разницу между OLTP и OLAP, а также примеры использования для транзакций и аналитики.

DuckDB против SQLite на сервере: краткий ответ

SQLite — это движок OLTP (online transaction processing): он хранит данные в виде строк и предназначен для безопасного и быстрого чтения или записи небольшого их количества за раз. DuckDB — это движок OLAP (online analytical processing): он хранит данные в виде столбцов и предназначен для сканирования миллионов строк с последующим вычислением одного агрегата. Оба решения являются встраиваемыми библиотеками, оба работают с обычными файлами, и ни одно из них не требует запуска серверного процесса, за которым нужно следить.

Поэтому честный ответ на вопрос «что выбрать» почти всегда звучит как «и то, и другое на одном 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 сообщает о режиме, на который переключилась база данных; это самая полезная настройка на сервере. В режиме журнала отката по умолчанию процесс записи блокирует всех читателей. В режиме 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';. На сервере, где работают другие службы, это ограничение не позволит случайному запросу вытеснить ваше приложение из оперативной памяти.

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 не изменяются после записи, поэтому достаточно просто скопировать каталог.

#duckdb#sqlite#database#analytics#parquet#self-hosting