JSONB: הדרך הכי טובה להרוס את מסד הנתונים שלך
JSONB הוא עוצמתי, שימושי, וקל מאוד לנצל אותו לרעה כשאתה נותן ל-blob להפוך לסכמה האמיתית שלך.
PostgreSQL הוסיפה את JSONB כדי לאפשר אחסון נתונים חצי-מובנים מבלי להגדיר סכמות נוקשות מראש. הרעיון היה נכון: לפעמים אתה באמת לא יודע איך הנתונים ייראו, או שהם משתנים בתדירות גבוהה מדי מכדי שעמודות מסורתיות יהיו הגיוניות.
זה חשוב כי JSONB אינה טעות. במערכות רבות היא הייצוג הנקי ביותר של מרחב הבעיה. אם אתה מאחסן מטענים של webhooks מצד שלישי, גופי אירועים עם גרסאות, דגלי תכונה, או אובייקטי תצורה של LLM שבהם כל ספק ומודל חושפים סט אפשרויות שונה ומשתנה תדיר, כפיית הכל לעמודות מדרגה ראשונה יכולה להיות מביכה יותר מאשר מועילה.
הבעיה היא ש-JSONB היא גם הדרך הקלה ביותר לדחות החלטות סכמה מבלי להודות שאתה דוחה אותן. איפשהו בין הכוונה לביצוע, היא הפכה למקבילה המסד-נתונים של “אני אנקה את החדר שלי אחר כך.” הפתרון הזמני שהגעת אליו לפני שישה חודשים? הוא עדיין שם, ועכשיו הייצור תלוי בו.
אני רואה את אותו הדפוס שוב ושוב. צוות מוסיף עמודת JSONB כי הם לא בטוחים לגבי הדרישות. הם מבטיחים לעצמם שינמלו אותה ברגע שהדברים יתייצבו. שלוש שנים אחר כך, העמודה הזו מכילה ארבעים גרסאות שונות של מה שהיה אמור להיות פרופיל משתמש, שנשאלות על ידי חמישה עשר שירותים שכל אחד מהם מניח הנחות שונות לגבי מה שיש בפנים.
החוב הטכני אינו JSONB עצמה. זה הפער בין מה שאמרת לעצמך שאתה בונה לבין מה שבנית בפועל: מערכת סכמה-בזמן-קריאה לא מתועדת.
מה בדרך כלל קורה
אתה מוסיף תכונה ואתה לא בטוח אם משתמשים צריכים twitter_handle או bluesky_handle או משהו אחר לגמרי. במקום לחשוב על הסכמה, אתה עושה את זה:
CREATE TABLE users ( id SERIAL PRIMARY KEY, profile JSONB);זה עובד. אתה משחרר את התכונה, עובר לבאה, ואז לבאה אחריה. עמודת JSONB גדלה בשקט ברקע.
זו נקודת הפיצול. אם profile נשאר בלוק אטום שנשלף לפי user.id, כנראה שאתה בסדר. אם הוא מתחיל להפוך למקום העיקרי שבו נתונים עסקיים חיים, יחסי העלות-תועלת משתנים מהר.
המוצר שואל: “כמה משתמשים נמצאים בניו יורק?”
אתה כותב:
SELECT count(*) FROM users WHERE profile->>'location' = 'New York';פוסטגרס מבצע סריקת טבלה מלאה. כל שורה.
אז אתה מוסיף אינדקס GIN. אולי זה עדיין מקובל. לפעמים זה כן. אבל עכשיו אתה משלם על מורכבות ועלות אחסון אמיתיים כי שדה שמתנהג כמו נתונים רלציוניים מהשורה הראשונה מעולם לא הפך לעמודה מהשורה הראשונה.
שנה 1: סחיפת סכמה
יש לך שלוש גרסאות של נתונים באותה עמודה.
- שורה 1:
{"city": "NYC"} - שורה 1000:
{"location": "NYC"} - שורה 5000:
{"address": {"city": "New York"}}
קוד האפליקציה שלך נראה כעת כך:
const city = user.location || user.city || user.address?.city || "Unknown";לא הסרת את הסכמה. פשוט העברת את בדיקות התקינות והעקביות ממסד הנתונים לקוד אפליקציה מפוזר.
מתי באמת להשתמש ב-JSONB
ל-JSONB יש מקרי שימוש תקפים. פעמים רבות זה בסדר גמור, ולפעמים זו הבחירה הטובה ביותר הזמינה.
ההבחנה הקריטית אינה “מובנה טוב, JSON רע.” היא קרובה יותר לזה:
- האם הנתונים נשלפים בעיקר כיחידה שלמה על ידי מפתח ראשי יציב?
- האם המפתחות משתנים מהותית בין ספקים, גרסאות, דיירים או זמנים?
- האם אתה שואל על כמה שדות ידועים, או ממציא שאילתות נתיב חדשות בכל ספרינט?
- האם האפליקציה מנהלת גרסאות ואימות בכוונה, או שהיא פשוט מאלתרת?
מקרי שימוש לגיטימיים ב-JSONB
-
מטעני Webhook: אתה מקבל נתונים מ-Stripe, Slack או GitHub. אין לך שליטה על הסכמה. ייתכן שלעולם לא תשאל עליהם. אתה רק צריך לאחסן אותם לצורך ניפוי באגים או הפעלה חוזרת. מושלם ל-JSONB.
-
רישום (Logging) וזרמי אירועים: לוגים של אפליקציה, ביקורות (audit trails), הקשרי שגיאות. אלה כבדי כתיבה, לעתים רחוקות נשאלים לפי שדות ספציפיים, ולעיתים קרובות מנותחים בכמות גדולה או מיוצאים לפלטפורמות אנליטיקה. JSONB בסדר כאן.
-
העדפות והגדרות משתמש: אובייקטי הגדרות שבהם יש לך 100+ דגלים בוליאניים, רובם false, ואתה תמיד שולף את כל הבלוב לפי מזהה משתמש. אתה לא מריץ
WHERE preferences->>'theme' = 'dark'. JSONB עובד. -
תצורת ספק/מודל LLM: זו אחת הדוגמאות המודרניות הברורות ביותר. OpenAI, Anthropic, Gemini, מודלים מקומיים במשקל פתוח, ושערים ספציפיים לספק – כולם חושפים פרמטרים חופפים אך שונים. אפילו בתוך ספק אחד, יכולות המודל ושמות האפשרויות מתפתחים. בלוב תצורה ב-JSONB הוא לעתים קרובות כנה הרבה יותר מאשר להעמיד פנים ש-
temperature,top_p,reasoning_effort,json_schema,tool_choiceועוד עשרים כפתורים צריכים להיות כולם עמודות אוניברסליות. JSONB הוא לעתים קרובות האבסטרקציה הנכונה כאן. -
שמירת תגובות API במטמון: אתה שומר במטמון תגובות API שלמות. מסד הנתונים הוא פשוט Redis מהיר יותר. אתה שולף לפי מפתח מטמון, לעולם לא לפי מאפיינים מקוננים. JSONB מתאים.
-
Event Sourcing: אתה מאחסן מטעני אירועים בלתי ניתנים לשינוי. השאילתות שלך הן תמיד “תן לי את כל האירועים עבור אגרגט X” ממוינים לפי זמן. אתה אף פעם לא מריץ סעיפי
WHEREעל מאפייני אירוע. JSONB מתאים. -
משטחי הרחבה (Extensibility Surfaces): אינטגרציות, הגדרות תוספים, עקיפות (overrides) לפי דייר, מטא-דאטה של מרקטפלייס, יכולות ספק, או שדות “תוספות” שבהם אתה מצפה במפורש שהצורה תשתנה לפי תת-סוג. JSONB יכול להיות החוזה הנכון, לא פשרה.
כלל אצבע: אם האפליקציה שולפת את המסמך לפי מפתח ידוע ומבינה איך לאמת/לגרסן אותו, JSONB יכול להיות מצוין. אם העסק ממשיך לשאול שאלות רלציוניות על מפתחות מקוננים, השדות האלה מנסים להפוך לעמודות.
הדפוס הטוב ביותר הוא לרוב היברידי
הרבה מערכות בוגרות מגיעות לכאן:
CREATE TABLE llm_requests ( id UUID PRIMARY KEY, provider TEXT NOT NULL, model TEXT NOT NULL, status TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), config JSONB NOT NULL);זה בדרך כלל טוב יותר משני הקצוות.
provider,model,statusו-created_atהם עמודות מדרגה ראשונה כי תסנן, תצטרף, תצבור ותבנה עליהן אינדקסים.configנשאר JSONB כי משטח האפשרויות המדויק הוא ספציפי למודל, ספציפי לספק, וצפוי להתפתח.
זו לא “כישלון בנורמליזציה”. זה לצייר את הקו במקום הנכון.
בקנה מידה: גירסון אובייקטים > נורמליזציה
כאן זה נהיה מעניין. בקנה מידה גדול מספיק, הפתרון ה”נכון” אינו נורמליזציה – אלא גירסון אובייקטים.
אם יש לך מיליארדי שורות ואבולוציית סכמה תכופה, העברת עמודות הופכת ליקרה. חברות כמו Stripe, GitHub ו-Netflix לא מנמלות הכל. במקום זאת:
CREATE TABLE entities ( id UUID PRIMARY KEY, version INT NOT NULL, data JSONB NOT NULL);האפליקציה שלך יודעת לקרוא version: 1, version: 2, version: 3. אין צורך בהגירות מסד נתונים עבור שדות חדשים. הקוד מטפל בתאימות לאחור.
זו החלטה ארכיטקטונית, לא עצלנות. היא מחליפה מורכבות מסד נתונים במורכבות אפליקציה. לפעמים זה בדיוק הטרייד-אוף הנכון, במיוחד כשהמסך מנוהל בגרסאות באופן טבעי והאפליקציה היא המפרשת הקנונית.
מצב הכשל אינו “שימוש ב-JSONB”. מצב הכשל הוא שימוש ב-JSONB ללא גירסון, ולידציה, חוקי קידום, או גבול ברור בין נתוני מסמך לנתונים רלציוניים.
השאלות שבאמת חשובות
לפני שתוסיף עמודת JSONB, שאל:
- האם נבצע שאילתות על שדות מקוננים ב-
WHERE,JOIN,GROUP BY, אוORDER BYבאופן קבוע? - האם אנחנו שולטים בסכמה הזו, או שהיא מוגדרת חיצונית ומשתנה?
- האם הצורה היא הטרוגנית במכוון בין רשומות?
- האם יש לנו ולידציה וגירסון ברמת האפליקציה?
- אילו שדות צפויים להפוך לממדים תפעוליים מאוחר יותר?
אם התשובה ל-#1 היא “כן, כל הזמן”, זה סימן חזק לעמודות.
אם התשובות ל-#2 ו-#3 הן “כן”, כנראה ש-JSONB עושה עבודה אמיתית עבורך.
בריחה מהמלכודת
אם אתה כבר בבור הזה, תפסיק לחפור.
- ביקורת: הרץ את
jsonb_object_keysובחן את השינוי בפועל בצורת הנתונים, לא את הצורה שאתה מניח שקיימת. - קידום: זהה את השדות שאתה מסנן, מצרף, ממיין או מדווח עליהם בתדירות הגבוהה ביותר. הפוך אותם לעמודות אמיתיות.
- ולידציה: הוסף ולידציה ברמת האפליקציה או מסד הנתונים עבור כל מה שנשאר ב-JSONB.
- גירסון: אם הבלוב הוא נתוני דומיין אמיתיים, בצע לו גירסון מפורש.
- קיצוץ: הסר מפתחות כפולים מהבלוב לאחר שהעמודות שקודמו הוקמו.
אל תספר לעצמך שכל בלוב חייב להיות מנורמל. גם אל תספר לעצמך שבלוב עם סמנטיקה עסקית קבועה הוא “זמני”.
JSONB הוא מצוין כשהמסך הוא באמת בצורת מסמך. הוא מסוכן כשזו סכמה רלציונית עם שפם מזויף.
משאבים
- תיעוד PostgreSQL JSONB
- אסטרטגיות אינדקס ל-JSONB
- מתי להשתמש ב-JSONB לעומת עמודות רלציוניות
- שיטות עבודה מומלצות לעיצוב סכמה ב-PostgreSQL