SSD Nodes Learn 8GB RAM — $66/שנה
מדריכים Matt Connorמאת Matt Connor · עודכן 2026-08-01

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 באותו שלב.

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, כותב חוסם כל קורא. במצב 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 עונה על כל שאלה המשתרעת על פני חודשים, ומסד הנתונים של היישום נשאר קטן, וכך הכתיבות אליו נשארות מהירות.

הריצו את הייצוא לפי לוח זמנים ולא ידנית. זוג של שירות ו-timer של systemd מתאים בדיוק למטרה זו: יחידה אחת שמריצה את COPY, ו-timer אחד שמפעיל אותה מדי לילה.

הפעלת שניהם ב-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 פירושו שחיבור אחר החזיק בנעילת הכתיבה זמן רב יותר מפרק הזמן שהוגדר ב־timeout. אין זו שחיתות נתונים. הגדירו את 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 נוגע באחסון של כל עמודה.

האם דרוש לי זיכרון RAM רב כדי להפעיל את DuckDB ב־VPS?

לא, אך יש להקצות לו מגבלה ונפח אחסון. פתח קובץ מסד נתונים במקום :memory:, כדי ש־DuckDB יוכל לשפוך תוצאות ביניים לדיסק. לאחר מכן הגדר את SET memory_limit = '2GB'; לערך שה־VPS יכול להקצות. ללא מגבלה, GROUP BY גדול עלול להגדיל את Out of Memory Error או לגרום לשירותים אחרים להידחק מזיכרון ה־RAM.

כיצד מעבירים נתוני 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