בחירת מבנה נתונים ב-MongoDB: Embed או Reference?

Cube with a MongoDB leaf logo, representing the MongoDB database.

בעת תכנון סכמה ומודל נתונים עם MongoDB, אחת ההחלטות החשובות ביותר שתקבלו היא האם להשתמש ב-Embedded Document או Reference Document. הבחירה הזו יכולה להשפיע באופן משמעותי על הביצועים והגמישות של המערכת שלכם.

במאמר הזה אנחנו רוצים לחקור איך עדיף לשמור את כתובת המגורים של האזרחים ביחד עם הנתונים האישיים שלהם. אם ניקח דוגמה, בישראל יש כ-10 מיליון תושבים, ברמת גן מתגוררים 174,974 תושבים וברחוב מסוים יכולים להתגורר מאות ואלפים של אנשים. איך אתם הייתם שומרים מידע כזה ב-Reference או ב-Embedded?

מה זה Embedded Documents?

זו טכניקה שבה המידע נשמר בתוך השני ממש כמו אובייקט ב-JavaScript, הנה דוגמה ל-Embedded Documents בו אני שומר את הנתונים של המשתמש ובנוסף אובייקט שמאגד את כל הנתונים של מיקום המגורים של המשתמש:

Screenshot of embedded MongoDB code.

מה זה Reference Documents?

זו טכניקה שבה נתונים מאוחסנים באופן נפרד תחת ישויות שונות ומקושרים זה לזה דרך מזהים ייחודיים. בדוגמה הזו אתם יכולים לראות שחילקתי את המידע לשני סוגים: מידע אישי של משתמש וכתובת מגוריו. במקרה זה, מזהה הכתובת (id_) מקשר בין הישויות:

Screenshot of reference MongoDB code.

הבנת נתונים סטטיים בסכמות MongoDB:

הכוונה לנתונים סטטיים זה שהמידע משתנה לעתים רחוקות (אם בכלל). במקרה שלנו, אנחנו דנים בשמות ארצות, ערים ורחובות. אפשר לטעון שבגלל שנתונים כאלה מתעדכנים לעתים רחוקות (אם בכלל), נוכל להשתמש ב-Embedded ונהנה משאילתות מהירות ופשוטות יותר. אבל מצד שני יהיו לנו כפילויות שזה פחות טוב, ואז היתרונות של Reference (מניעת כפילויות, ואחסון טוב יותר) אולי יהיו עדיפים. אז איך אנחנו יכולים לבחור מה עדיף? אנחנו, בתור Database Engineers או Backend Developers, צריכים לקחת את כל האפשריות כדי לנסות לעצב את הסכמה והמבנה הנכונים ביותר.

נתונים סטטיים עם Reference:

הפניה (Reference), כוללת יצירת מסמכים (documents) נפרדים עבור ישויות שונות, והקישור בניהם מתבצע על ידי foreign keys. אומנם המונח foreign keys מתייחס יותר למסדי נתונים מבוססי SQL, אבל הוא מתאר בדיוק את אותו הדבר גם ב-NoSQL כמו MongoDB. כדי ליצור הפניות ב-MongoDB, אנחנו יכולים להשתמש ב-lookup$ או בשיטת ()populate ב-Mongoose. אז בואו ננתח את הגישה הזו (Reference Document) עם הנתונים הסטטיים שלנו - (שמות ארצות, ערים ורחובות):

  • Data integrity & maintenance:

    תחשבו שיש לנו document אחד של מדינה ישראל ויש בתוכו רק filed אחד - 'countryName', וביצענו הפנייה מה-document הזה, לכל אזרחי ישראל במאגר שלנו (10 מיליון). עכשיו תחשבו שיש משימה חדשה, להוסיף לאותו document גם מידע היסטורי ונתונים דמוגרפיים.

    במקרה הזה אנחנו לא צריכים לשנות 10 מיליון documents של אזרחים, אנחנו פשוט משנים במקום אחד, ב-collection של countries ב-document של מדינת ישראל, וזהו, אין כפילויות וחסכנו מקום ב-database. לעומת זאת, אם היינו משתמשים ב-Embedded Document, היינו צריכים להוסיף את הנתונים החדשים (מידע היסטורי ונתונים דמוגרפיים) ב-documents של כל אחד מהאזרחים במדינת ישראל (10 מיליון) מה שהיה יוצר לנו כפילויות ופחות מקום ב-database.

  • Modular & Separation of concerns:

    אז נמשיך עם הדוגמה של מדינת ישראל ו-10 מיליון אזרחים. אם נשתמש בהפניה (Reference Document) אנחנו נוסיף את השדות (מידע היסטורי ונתונים דמוגרפיים) רק על document אחד בלבד - (מדינת ישראל) ולא על 10 מיליון documents (אזרחים). כלומר השיטה הזו עוזרת לנו לשמר עקביות על הנתונים וגם מאגדת במקום אחד את כל הנתונים של המדינה (Separation of concerns).

נתונים סטטיים עם Embedded:

אז אחרי שניתחנו את הסיטואציה שלנו עם Reference, הגיע הזמן לנתח את האפשרות של Embedded.

  • Query performance:

    טכניקת Embedded הרבה יותר מהירה מאשר Reference, כי כשאנחנו משתמשים בהפניות (Reference) אנחנו למעשה מבצעים Join כמו ב-SQL. אנחנו הולכים ל-collection אחד, אחר כך הולכים ל-collection שני, מחברים את המידע ומקבלים אותו בתוצאה הסופית, וזו פעולה כבדה יותר מאשר Embedded, ולמה? כי Embedded Document מאפשרת לקרוא את המידע באופן מהיר יותר, מכיוון שכל המידע נמצא במקום אחד, כלומר ב-document אחד ולא בכמה. לכן, אנחנו צריכים לבצע שאילתה אחד ל-database שלנו כדי להשיג את כל הנתונים (מידע מלא על האזרח עם נתוני כתובת מגורים).

  • Minimized Document Growth:

    השיקול שנתונים כמו שמות ארצות, ערים ורחובות נחשבים לנתונים סטטיים, אומר לנו שהם כמעט ולא ישתנו (אם בכלל). בהתחשב בעובדה הזו אנחנו לא צריכים לדאוג שהמידע ישתנה, ולכן אנחנו יכולים לייעל את המערכת שלנו, ולשמור את אותם הנתונים בצורה של Embedded. היתרונות הם כמובן שאילתות פשוטות ומהירות יותר מאשר Reference.

ההחלטה בין Embedded לבין Reference אינה חד-משמעית, ואתם צריכים להבין את המטרות של המערכת שלכם, ולכן הכנתי עבורכם כללי אצבע שתוכלו ללכת לפיהם:

מתי להשתמש ב-Embedded Document?

  • Frequent Read Together:

    כאשר נדרשת קריאת מידע באופן תדיר, ביחד עם נתונים נוספים. למשל, פרטי אזרח + כתובת מגוריו. במקרה הזה, השימוש ב-Embedded Document יהיה הפתרון המועדף. שימוש זה מאפשר גישה מהירה ויעילה לכל הנתונים הרלוונטיים שאנחנו צריכים בשאילתה אחת, מבלי הצורך לבצע מספר שאילתות לאיחוד נתונים מטבלאות או מסמכים שונים כמו Join.

  • Rarely Changes:

    כשהמידע אינו משתנה והוא מצומצם, כלומר מורכב ממספר קטן של שדות, הכפילות של הנתונים לא אמורה להוות בעיה גדולה. אבל כמובן שצריך לקחת כל מקרה לגופו; כמו כמה documents יהיו, או האם המשאבים שלכם מאפשרים כפילויות וכדומה. כלומר, אם המסמכים כוללים כמות גדולה של נתונים, או ששטח האחסון שלכם קטן, כפל מידע עשוי לגרום לבעיות וכנראה גם לעלויות גבוהות יותר של אחסון.

  • Query Performance:

    כאשר המהירות היא ערך עליון, השימוש ב-Embedded Document יכול להיות בעל יתרון משמעותי, כי כל המידע נמצא תחת קורת גג אחת, כלומר ב-document אחד. זה מאפשר גישה מהירה וישירה לכל הנתונים הנדרשים מבלי הצורך לבצע פעולות נוספות כמו Join, מה שמקצר את זמני העיבוד של הנתונים.

  • Document Size:

    חשוב לוודא שהנתונים המאוחסנים במסמך לא יחרגו מהגבול המרבי שנקבע על ידי MongoDB, העומד על 16 מגה-בייט למסמך. אם הוא חורג, תצטרכו למצוא פתרון אחר, לדוגמה, ליצור Reference.

מתי להשתמש ב-Reference Document?

  • Independent Update Patterns:

    אם הנתונים משתנים לעיתים קרובות ואתם לא קוראים את המידע של ה-document שמבצע את ההפניה בתדירות גבוהה ואתם מעדיפים לשמור על סדר - Separation of concerns, השימוש ב-Reference Document יהיה מתאים יותר.

  • Reduce Duplication:

    אם אתם רוצים לנצל את שטח האחסון שלכם (למנוע כפילות של נתונים), ומהירות השאילתות היא לא המטרה המרכזית שלכם, שימוש ב-Reference Document יהיה מתאים יותר.

  • Large, Variable Data Sets:

    אם מסמכים יכולים לגדול באופן משמעותי או בכלל, ללא הגבלה, כלומר מלא שדות, עדיף להשתמש ב-Reference Document ולהפנות אליו כל פעם שצריך מאשר להעתיק את המידע ל-documents אחרים, שבמקרה שלנו זה 10 מיליון (לא מעט).

ניתוח Dataset של 500 מיליון אזרחים

בואו נבחן את האסטרטגיות לניתוח Dataset של 500 מיליון אזרחים, תוך דיון ביתרונות וחסרונות של שימוש ב-Reference Document Embedded Documen ב-MongoDB.

  • Duplication Concerns:

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

  • Update Management:

    אם נשתמש ב-Embedded, כל עדכון לפרטי מידע משוכפל (כמו שינוי שם מדינה) ידרוש עדכון של 500 מיליון documents, אחד לכל אזרח. אם נשתמש ב-Reference, אנחנו נשנה את אותו מסמך (document) של אותה מדינה, כלומר פעם אחת בלבד, והעדכון יכלול על כל ה-500 מיליון documents. אבל בגלל שהסיכוי ששם המדינה או העיר או הרחוב ישתנו הוא אפסי, ובהנחה שהמהירות חשובה לנו ויש לנו את המשאבים, אולי זה עדיף.

  • Frequent Read Together:

    אם אנחנו צריכים לקרוא את כל המידע של האזרח באופן תדיר, ובהנחה שהמהירות חשובה לנו מאוד, ואין לנו בעיה עם משאבים, והנתונים לא משתנים לעיתים קרובות ,(אם בכלל) השימוש ב-Embedded Document יהיה הפתרון המועדף. שימוש זה מאפשר גישה מהירה ויעילה לכל הנתונים הרלוונטיים שאנחנו צריכים בשאילתה אחת, מבלי הצורך לבצע מספר שאילתות לאיחוד נתונים מטבלאות או מסמכים שונים כמו Join.

אז כמו שאתם רואים, ההחלטה האם להשתמש ב-Embedded או ב-Reference, תלויה בצורכי המערכת שלכם, כלומר, איך אתם מבצעים שאילתות? האם יש לכם שטח אחסון קטן או גדול? האם המהירות חשובה לכם או שאתם יכולים להסתדר עם טיפה איטיות? כלומר, זה השלב שאתם צריכים לשבת וללמוד את צורכי ומטרות המערכת מקרוב ולנסות לנתח את אופן השימוש בה.