SSD Nodes Learn Hosting plans →
מדריכים Matt Connorמאת Matt Connor · עודכן 2026-08-03

DuckDB לעומת SQLite בשרת: למה צריך את שניהם

SQLite שומר מצב עסקאות, ו-DuckDB מנתח קובצי Parquet ו-CSV. גלו מדוע VPS אחד מריץ לרוב את שניהם, עם דוגמה מעשית לכל מנוע.

DuckDB לעומת SQLite בשרת: התשובה במשפט אחד

SQLite הוא מנוע OLTP ‏(עיבוד עסקאות מקוון): הוא מאחסן נתונים בשורות, ונועד לקרוא ולכתוב כמה שורות בכל פעם, באופן בטוח ומהיר. DuckDB הוא מנוע OLAP ‏(עיבוד אנליטי מקוון): הוא מאחסן נתונים בעמודות, ונועד לסרוק מיליוני שורות ולהחזיר תוצאת צבירה אחת. שניהם ספריות משובצות, שניהם פותחים קובץ רגיל, ואף אחד מהם אינו מפעיל תהליך שרת שעליכם לנטר ולתחזק.

לכן התשובה הכנה לשאלה "באיזה מהם לבחור" היא כמעט תמיד "בשניהם, על אותו VPS". היישום שלכם שומר את המצב השוטף שלו ב־SQLite. הדוחות שלכם קוראים קובצי Parquet ו־CSV באמצעות DuckDB. הם אינם מתחרים זה בזה, משום שהם אינם מבצעים את אותה משימה.

מדוע אחסון לפי שורות ואחסון לפי עמודות משנים את התשובה

SQLite כותב שורה כיחידה רציפה אחת בתוך עמוד. שליפת הזמנה אחת לפי המפתח הראשי נוגעת בעמוד אינדקס אחד ובעמוד נתונים אחד, כלומר בשתי פעולות קריאה. זה בדיוק מה שיישום עושה אלפי פעמים בשנייה: קורא משתמש זה, מעדכן את ה־session הזה ומוסיף את ההזמנה הזו.

DuckDB כותב כל עמודה בנפרד ומבצע דחיסה. חישוב הסכום של amount_cents על פני חמישה מיליון שורות קורא רק את העמודה amount_cents, מדלג על כל בית אחר בקובץ ומריץ את החישוב באמצעות קוד וקטורי על אצוות ערכים. שאר העמודות אינן נקראות מהדיסק כלל, וזה מקור המהירות.

כעת הפעילו כל מנוע מול עומס העבודה של המנוע האחר. SQLite, בעת חישוב סכום של עמודה, צריך לעבור על כל השורות ולשלוף את השורה כולה מהעמוד כדי להגיע לשדה אחד. לכן הוא קורא מהדיסק הרבה יותר נתונים מהנדרש. DuckDB, בעת הוספת הזמנה אחת, צריך לגעת באחסון של כל עמודה כדי לכתוב ערך יחיד, והוא נועל לכתיבה את קובץ מסד הנתונים כולו כדי לעשות זאת. אף אחד מהמנועים אינו פגום. כל אחד מהם מתמודד עם שאלה שלשמה לא תוכנן.

היכן SQLite מתאים במיוחד: מצב יישומי טרנזקציוני

בחרו ב־SQLite כאשר הכתיבות קטנות, תכופות ואסור לאבד אותן. הפעלות, הזמנות, רשומות בתור, הגדרות וכל נתון שבקשת web יוצרת.

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

בדקו שהשורה התקבלה:

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; בכל חיבור שהיישום פותח. המתנה של חמש שניות מונעת את רוב השגיאות האלה בעומס web רגיל.

היכן 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);"

כעת הציגו את השאלה האנליטית. פתחו את ה־shell, הפעילו את המדידה, ושאבו את הנתונים ישירות מהקובץ בלי שלב import:

.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 ולא היה שלב load:‏ DuckDB קרא את ה־footer של Parquet, זיהה אילו מקטעי עמודות נדרשים לשאילתה, וקרא רק אותם. ספרייה שלמה פועלת באותו אופן באמצעות glob,‏ 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

שום דבר כאן אינו דורש מכולה, ושום דבר אינו דורש פורט. שני המנועים הם ספריות, ולכן ההתקנה מסתכמת בחבילה ובנתיב לקובץ. אם שאר ה־stack שלך כבר פועל באמצעות Docker Compose באותו VPS, בצע mount של תיקיית הנתונים לתוך המכולה שזקוקה לה, במקום להוסיף שירות מסד נתונים, משום שאין שירות להוסיף.

שני כללים מונעים בעיות בתצורה הזו.

הקצה לכל מנוע תיקייה משלו: /srv/app עבור קובץ SQLite שהיישום כותב אליו, ו־/srv/data עבור קובצי Parquet ש־analytics קורא. כאשר הם משתפים תיקייה, משימת גיבוי שיוצרת snapshot של אחד מהם עלולה להתנגש בפעילות של השני.

אל תפנה שני תהליכים לאותו קובץ מסד נתונים של DuckDB במצב קריאה-כתיבה. רק תהליך אחד יכול להחזיק קובץ DuckDB לכתיבה, והתהליך השני ייכשל כבר בניסיון לפתוח אותו. קוראים רבים יכולים לפעול במקביל, אם כל אחד מהם מגדיר את access_mode = 'READ_ONLY'. הדבר מפתיע משתמשים שמגיעים מ־SQLite, שבו כמה תהליכים משתפים קובץ באופן שגרתי. אם שכבת ה־analytics שלך רק קוראת קובצי Parquet, השאלה אינה מתעוררת כלל. זו סיבה נוספת לשמור את המצב העמיד ב־SQLite.

גיבויים שונים זה מזה, וההבדל עלול לגרום נזק

מסד נתונים פעיל של SQLite מורכב משלושה קבצים. העתקה שלהם באמצעות cp במהלך כתיבה עלולה ליצור קובץ שנפתח, אך תוכנו שגוי. השתמשו בפקודת הגיבוי של המנוע עצמו. הפקודה יוצרת snapshot עקבי בזמן שהיישום ממשיך לכתוב:

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

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

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