DuckDB czy SQLite na serwerze? Kiedy używać obu
SQLite przechowuje stan transakcyjny aplikacji, a DuckDB analizuje Parquet i CSV. Zobacz, dlaczego na jednym VPS zwykle warto uruchomić oba oraz poznaj przykłady.
DuckDB a SQLite na serwerze: odpowiedź w jednym zdaniu
SQLite to silnik OLTP (przetwarzanie transakcji online): przechowuje dane w wierszach i został zaprojektowany do bezpiecznego oraz szybkiego odczytu i zapisu kilku wierszy naraz. DuckDB to silnik OLAP (analityczne przetwarzanie online): przechowuje dane w kolumnach i został zaprojektowany do skanowania milionów wierszy oraz zwracania jednego wyniku agregacji. Oba są bibliotekami osadzanymi, oba otwierają zwykły plik i żaden z nich nie uruchamia procesu serwera, który wymagałby stałego nadzoru.
Dlatego uczciwa odpowiedź na pytanie „który?” brzmi niemal zawsze: „oba, na tym samym VPS”. Aplikacja przechowuje bieżący stan w SQLite. Raportowanie odczytuje pliki Parquet i CSV za pomocą DuckDB. Te rozwiązania nie konkurują ze sobą, ponieważ nie wykonują tego samego zadania.
Dlaczego sposób przechowywania wierszowego i kolumnowego zmienia wynik
SQLite zapisuje wiersz jako jeden spójny fragment strony. Pobranie jednego zamówienia według klucza podstawowego wymaga odczytu jednej strony indeksu i jednej strony danych, czyli 2 odczytów. Dokładnie to robi aplikacja tysiące razy na sekundę: odczytuje tego użytkownika, aktualizuje tę sesję, wstawia to zamówienie.
DuckDB zapisuje każdą kolumnę osobno i ją kompresuje. Sumowanie amount_cents dla 5 milionów wierszy odczytuje tylko kolumnę amount_cents, pomija każdy pozostały bajt w pliku i wykonuje sumowanie za pomocą kodu wektoryzowanego na partiach wartości. Pozostałe kolumny nie są nigdy odczytywane z dysku. To właśnie zapewnia tę szybkość.
Następnie należy uruchomić każdy silnik dla obciążenia właściwego dla drugiego silnika. Sumowanie kolumny w SQLite wymaga przejścia przez każdy wiersz i pobrania całego wiersza ze strony, aby uzyskać dostęp do jednego pola. W rezultacie odczytywanych jest znacznie więcej danych z dysku, niż jest potrzebne. Wstawienie jednego zamówienia w DuckDB wymaga zmodyfikowania magazynu każdej kolumny dla jednej wartości. Wymaga także zablokowania zapisu dla całego pliku bazy danych. Żaden z tych silników nie jest uszkodzony. Każdy odpowiada na pytanie, do którego nie został zaprojektowany.
Gdzie SQLite sprawdza się najlepiej: transakcyjny stan aplikacji
Należy wybrać SQLite, gdy zapisy są małe, częste i nie mogą zostać utracone. Dotyczy to sesji, zamówień, wierszy kolejki, ustawień oraz wszystkiego, co tworzy żądanie sieciowe.
sudo apt update && sudo apt install -y sqlite3
sudo install -d -o "$USER" -g "$USER" /srv/appTabelę należy utworzyć i włączyć rejestrowanie write-ahead logging w tym samym kroku.
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);
SQLPierwszy wiersz wyniku to wal. Jest to PRAGMA informujący o przełączeniu trybu. To najbardziej użyteczne ustawienie na serwerze. W domyślnym trybie rollback journal zapisujący blokuje każdego czytelnika. W trybie WAL czytelnicy nadal odczytują ostatni zatwierdzony stan, podczas gdy jeden zapisujący dopisuje dane. Dzięki temu wolny raport nie wstrzymuje żądania sieciowego.
Należy sprawdzić, czy wiersz został zwrócony:
sqlite3 /srv/app/app.db "SELECT * FROM orders WHERE customer = 'ana';"Zostanie zwrócone 1|ana|2026-07-30T09:14:00Z|4200. Obok bazy danych pojawiły się dwa dodatkowe pliki: app.db-wal i app.db-shm. Oba należą do tej bazy. Skopiowanie samego app.db podczas działania aplikacji tworzy niespójną kopię zapasową. Ten przypadek opisano niżej.
SQLite nadal pozwala na zapis tylko jednemu zapisującemu naraz. To ograniczenie jest blokadą, a nie kolejką. Dlatego drugi zapisujący, który czeka zbyt długo, kończy działanie błędem database is locked zamiast blokować się bez końca. Należy zwiększyć czas oczekiwania za pomocą PRAGMA busy_timeout = 5000; na każdym połączeniu otwieranym przez aplikację. Pięć sekund oczekiwania eliminuje większość tych błędów przy typowym obciążeniu serwera WWW.
Gdzie DuckDB sprawdza się najlepiej: analityka plików, które już istnieją
DuckDB należy wybrać, gdy pytanie zaczyna się od „ile”, „jak dużo” lub „które dziesięć najlepszych”, a dane wejściowe stanowi zbiór plików CSV lub Parquet. Należy zainstalować klienta wiersza poleceń, w wersji 1.5.5 na lipiec 2026:
curl https://install.duckdb.org | shSkrypt instaluje plik binarny w ~/.duckdb/cli/latest/duckdb i wyświetla wiersz, który dodaje tę lokalizację do PATH. Należy potwierdzić poprawne działanie:
~/.duckdb/cli/latest/duckdb :memory: "SELECT version();"Należy utworzyć realistyczny plik do wykonywania zapytań. Poniższe polecenie zapisuje pięć milionów wierszy zamówień w formacie Parquet, z kompresją 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);"Następnie należy wykonać zapytanie analityczne. Należy otworzyć powłokę, włączyć pomiar czasu i wykonać zapytanie bezpośrednio na pliku, bez etapu importu:
.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;Należy odczytać własny wynik z .timer, zamiast ufać opublikowanemu wynikowi, ponieważ rezultat zależy od używanego dysku i liczby rdzeni. Istotny jest sposób jego uzyskania. Nie było CREATE TABLE, INSERT ani etapu ładowania: DuckDB odczytał stopkę Parquet, określił, które fragmenty kolumn są wymagane przez zapytanie, i odczytał tylko te fragmenty. Cały katalog działa tak samo z użyciem wzorca glob, FROM '/srv/data/orders-*.parquet', dzięki czemu miesięczny zbiór codziennych eksportów staje się jednym zapytaniem.
Szybkość dysku wyznacza dolną granicę wydajności, a skanowanie kolumny jest długim odczytem sekwencyjnym. Dlatego różnica między NVMe a starszymi nośnikami SATA na VPS jest tutaj wyraźniejsza niż w przypadku niewielkich odczytów losowych SQLite.
Odczytywanie bazy SQLite z DuckDB
Oba silniki współpracują za pośrednictwem rozszerzenia sqlite DuckDB. Dołącz bazę danych aplikacji tylko do odczytu, aby zapytanie analityczne nigdy nie mogło zapisywać do aktywnego stanu:
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;Dane są odczytywane z pliku SQLite w momencie wykonywania zapytania, bez kopiowania. Jest to wygodne, ale nie jest szybkie, ponieważ dane na dysku nadal są przechowywane w postaci wierszy, a DuckDB musi je przetworzyć. Używaj tego rozwiązania do eksportu, a nie do pulpitu przeładowywanego co trzydzieści sekund:
COPY (SELECT * FROM app.orders)
TO '/srv/data/orders-2026-07.parquet' (FORMAT parquet, COMPRESSION zstd);To pojedyncze polecenie przedstawia cały schemat. SQLite przechowuje najnowsze aktywne wiersze. Zaplanowany eksport konwertuje zamknięte okresy do formatu Parquet. DuckDB odpowiada na wszystkie zapytania obejmujące wiele miesięcy, a baza aplikacji pozostaje niewielka, dzięki czemu operacje zapisu są szybkie.
Uruchamiaj eksport zgodnie z harmonogramem, a nie ręcznie. Para usługi i reguły czasomierza systemd ma tu odpowiedni zakres: jedna jednostka uruchamia COPY, a jeden czasomierz uruchamia ją co noc.
Uruchamianie obu silników na jednym VPS
Żaden z tych elementów nie wymaga kontenera ani portu. Oba silniki są bibliotekami, dlatego instalacja obejmuje pakiet i ścieżkę do pliku. Jeśli pozostała część stosu działa już za pomocą Docker Compose na tym samym VPS, należy zamontować katalog danych w kontenerze, który go potrzebuje, zamiast dodawać usługę bazy danych, ponieważ nie ma usługi, którą można dodać.
Dwie zasady pozwalają uniknąć problemów w tej konfiguracji.
Należy przydzielić każdemu silnikowi osobny katalog: /srv/app dla pliku SQLite zapisywanego przez aplikację oraz /srv/data dla plików Parquet odczytywanych przez system analityczny. Gdy używają wspólnego katalogu, zadanie tworzenia kopii zapasowej, które wykonuje migawkę jednego z nich, zaczyna kolidować z działaniem drugiego.
Nie należy kierować dwóch procesów do jednego pliku bazy danych DuckDB w trybie odczytu i zapisu. Tylko jeden proces może otworzyć plik DuckDB z prawem zapisu. Drugi proces nie może go w ogóle otworzyć. Wielu czytelników może korzystać z pliku, jeśli każdy z nich ustawi access_mode = 'READ_ONLY'. Może to zaskakiwać osoby przyzwyczajone do SQLite, gdzie kilka procesów regularnie współdzieli jeden plik. Jeśli system analityczny tylko odczytuje pliki Parquet, problem ten nie występuje. To kolejny powód, aby przechowywać trwały stan w SQLite.
Kopie zapasowe różnią się, a ta różnica ma znaczenie
Działająca baza danych SQLite składa się z trzech plików. Skopiowanie ich za pomocą cp w trakcie zapisu tworzy plik, który można otworzyć, ale którego zawartość jest nieprawidłowa. Należy użyć wbudowanego polecenia kopii zapasowej silnika. Tworzy ono spójny migawkowy obraz, gdy aplikacja nadal zapisuje dane:
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 wyświetla ok w przypadku poprawnej kopii. Każdy inny wynik oznacza, że należy usunąć tę migawkę i utworzyć kolejną.
Pliki Parquet nie zmieniają się po zapisaniu, więc nie wymagają specjalnego postępowania. Wystarczy wykonać kopię zapasową katalogu. Obie ścieżki należy przesłać poza serwer za pomocą kopii zapasowych restic z VPS, a cała warstwa danych obejmuje dwa katalogi w ramach jednego zadania tworzenia kopii zapasowej.
Tryby awarii i dokładne komunikaty, które zostaną wyświetlone
Error: database is locked z SQLite oznacza, że inne połączenie utrzymywało blokadę zapisu dłużej, niż wynosił ustawiony limit czasu. Nie oznacza to uszkodzenia danych. Ustaw PRAGMA busy_timeout dla każdego połączenia, a następnie wyszukaj długą transakcję, która powinna zostać podzielona na kilka krótkich transakcji.
Error: unable to open database file po zmianie uprawnień zwykle oznacza, że proces może zapisywać plik, ale nie może zapisywać w jego katalogu. SQLite tworzy app.db-wal i app.db-shm obok bazy danych, dlatego katalog musi mieć uprawnienia zapisu. Nie wystarczy nadanie ich tylko plikowi .db.
IO Error: Could not set lock on file z DuckDB oznacza, że drugi proces ma już otwartą tę bazę danych w trybie zapisu. Zamknij drugą powłokę albo otwórz bazę w trybie tylko do odczytu.
Out of Memory Error z DuckDB na małym VPS oznacza, że zapytanie wymagało więcej pamięci roboczej, niż było dostępne. DuckDB może zapisywać dane tymczasowe na dysku, dlatego należy zapewnić na to miejsce, otwierając plik bazy danych na dysku zamiast :memory:, a następnie ograniczyć zużycie pamięci za pomocą SET memory_limit = '2GB';. Na serwerze obsługującym inne usługi ten limit zapobiega wyparciu aplikacji z pamięci RAM przez zapytanie ad hoc.
Binder Error: Referenced column "amount" not found podczas wykonywania zapytań dotyczących Parquet niemal zawsze oznacza, że schemat pliku różni się od zapamiętanego. Uruchom DESCRIBE SELECT * FROM '/srv/data/orders.parquet'; i odczytaj zwrócone rzeczywiste nazwy kolumn.
Jak dokonać wyboru w praktyce
Należy określić charakter operacji zapisu. Wiele małych zapisów, które muszą przetrwać awarię zasilania, wskazuje na SQLite. Należy określić charakter operacji odczytu. Pełne skanowanie z agregacjami obejmującymi długą historię wskazuje na DuckDB. W większości rzeczywistych systemów odpowiedź na oba pytania brzmi „tak”. Właściwym rozwiązaniem jest powierzenie każdemu silnikowi tej części zadań, do której najlepiej się nadaje, zamiast zmuszać jeden z nich do zastępowania drugiego.
Należy unikać przenoszenia bieżącego stanu aplikacji do DuckDB tylko dlatego, że raport działał wolno. Raport działał wolno z powodu układu danych w pamięci masowej. Właściwym rozwiązaniem jest więc eksport, a nie przepisanie ścieżki zapisu.
FAQ
Czy DuckDB może zastąpić SQLite jako baza danych aplikacji?
Nie w przypadku bazy, w której często wykonywane są operacje zapisu. DuckDB blokuje cały plik bazy danych na czas zapisu, zezwala tylko jednemu procesowi na jednoczesny odczyt i zapis oraz jest zoptymalizowany pod kątem operacji zbiorczych, a nie wstawiania pojedynczych wierszy. Stan transakcyjny należy przechowywać w SQLite, a DuckDB powinien odczytywać go za pomocą ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY);, gdy jest potrzebny raport.
Czy DuckDB rzeczywiście działa szybciej niż SQLite w zadaniach analitycznych?
Tak, w przypadku skanowania dużej tabeli i wykonywania agregacji. Wynika to z układu danych, a nie ze sztuczki optymalizacyjnej. DuckDB odczytuje tylko kolumny wskazane w zapytaniu i przetwarza wartości partiami, natomiast SQLite musi przejść przez całe wiersze, aby uzyskać dostęp do jednego pola. W przypadku pobierania pojedynczego wiersza według klucza głównego sytuacja się odwraca, ponieważ SQLite odczytuje dwie strony, a DuckDB odczytuje dane przechowywane we wszystkich kolumnach.
Czy do uruchomienia DuckDB na VPS potrzebuję dużo pamięci RAM?
Nie, ale należy określić limit i zapewnić miejsce na dysku. Należy otworzyć plik bazy danych zamiast :memory:, aby DuckDB mógł zapisywać pośrednie wyniki na dysku, a następnie ustawić SET memory_limit = '2GB'; na wartość, którą VPS może udostępnić. Bez limitu jedno duże GROUP BY może zwiększyć Out of Memory Error lub spowodować usunięcie innych usług z pamięci RAM.
Jak przenieść dane z SQLite do Parquet?
Należy dołączyć plik SQLite z poziomu DuckDB i bezpośrednio skopiować wyniki zapytania za pomocą COPY (SELECT * FROM app.orders) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd);. Należy uruchamiać tę operację zgodnie z harmonogramem dla zamkniętych okresów, na przykład dla wierszy z ubiegłego miesiąca, a najnowsze wiersze pozostawić w SQLite, gdzie aplikacja nadal je zapisuje.
Którą bazę należy uwzględnić w kopii zapasowej i jak to zrobić?
Obie, ale w różny sposób. Migawki SQLite należy tworzyć za pomocą sqlite3 app.db ".backup '/srv/backup/app.db'" zamiast cp, ponieważ działająca baza danych jest również -wal i plikiem -shm, a zwykła kopia może być niespójna. Pliki Parquet nie zmieniają się po zapisaniu, dlatego wystarczy skopiować katalog.