DanLevy.net

מפתחות זרים: אל תשאלו אם הם מהירים

שאל במה אתה באמת מייעל.

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

לא כי הם מדדו צוואר בקבוק. לא כי הכתיבה הייתה באמת איטית. אלא כי הם קראו איפשהו ש”מפתחות זרים לא מתקשקשים.” שישה חודשים לאחר מכן, היו להם 2 מיליארד רשומות יתומות, מערכת חיוב שחייבה משתמשים שנמחקו, וניתוחים שהיו שגויים ב-40%.

כשניסו להוסיף בחזרה את האילוצים? מסד הנתונים נתקע בניסיון לאמת נתונים קיימים שכבר היו מושחתים.

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

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

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

במה אתה באמת סוחר

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

האם אתה מקשר הכל עם מפתחות זרים? בוא נחשוב מה באמת משתנה ומה ההשלכות:

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

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

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

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

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

כשכתיבה מהירה באמת חשובה

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

יש לך כמה אפשרויות. אתה יכול לשנות את רמת בידוד הטרנזקציות מ-SERIALIZABLE ל-READ COMMITTED, שהיא מהירה יותר אבל מוותרת על ערבויות עקביות מסוימות. אתה יכול לקבץ את ההתחייבויות שלך, להכניס 1000 שורות לטרנזקציה במקום אחת בכל פעם כדי לפזר את תקורת מפתח הזר. או שאתה יכול לנרמל למבנה לוג של הוספה בלבד שבו אתה אפילו לא מנסה לאמת הפניות.

האפשרות השלישית הזו אינה רמאות, דרך אגב. זה פשוט עיצוב אחר:

CREATE TABLE sensor_log (
id BIGSERIAL PRIMARY KEY,
recorded_at TIMESTAMPTZ NOT NULL,
data JSONB NOT NULL -- { station_id, sensor_id, temp, humidity, ... }
);
CREATE INDEX ON sensor_log USING GIN (data);
CREATE INDEX ON sensor_log (recorded_at);

אין חיבורים. אין בדיקות מפתח זר. פשוט צרף נתונים ושאל לפי טווח זמן או אינדקס GIN על ה-JSONB. האם זה “best practice”? כנראה שלא במובן שספרי לימוד של מסדי נתונים מלמדים. האם זה עובד כשאתה מכניס 50,000 שורות בדקה על Raspberry Pi? בהחלט.

הניתוק קורה כשאנשים מתייחסים ל-”best practice” כאל ציווי מוסרי במקום כתבנית שעובדת היטב בתרחישים נפוצים אבל אולי לא מתאימה לשלך.

מלכודת הנורמליזציה

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

אז בסוף אתה מקבל משהו כמו: OrdersOrderItemsProductsVariantsColorsSizes

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

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

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

אחסן {"color": "blue", "size": "M"} ישירות על ההזמנה. סיימת.

זיהוי נתוני תמונת מצב

איך אתה יודע מתי משהו צריך להיות תמונת מצב? שאל את עצמך האם זה רשומה של נקודת זמן:

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

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

בלובים אטומים

יש קטגוריה נוספת מעבר לתמונות מצב: נתונים שאף פעם לא שואלים עליהם. פשוט מאחסנים אותם ומביאים אותם בשלמותם.

תצורות מודל של LLM כמו {"model": "gpt-4", "temperature": 0.7, "max_tokens": 2000} הן לא משהו שאתה שואל לפי temperature. אתה מביא את כל התצורה לפי מזהה בקשה כשאתה צריך אותה. מטעני JWT אחרי פענוח, יומני בקשה/תגובה של API לצורך ניפוי שגיאות, אובייקטי העדפות משתמש עם הגדרות ערכת נושא ודגלי התראות. כל אלה בלובים אטומים. אתה לא צריך נורמליזציה. אתה לא צריך מפתחות זרים. תדחוף אותם ל-JSONB ותמשיך בחייך.

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

(אבל היזהר: זה יכול להתפוצץ בצורה מרהיבה אם מאוחר יותר תצטרך לשאול על הנתונים האלה. ראה פיתוי ה-JSONB למקרה שבו גישה זו יוצרת סיוט משלה.)

קנה מידה הוא הקשר

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

Raspberry Pi שמתעד 10,000 קריאות חיישן בדקה לכרטיס microSD? זה לגיטימי קנה מידה גבוה עבור החומרה הזו. AWS Aurora עם IOPS מוקצה שמטפל במיליארדי שורות? אתה יכול להשתמש במפתחות זרים דרכם בלי להזיע.

המגבלה הקשיחה האמיתית היא לא על מספר שורות או נפח כתיבה. זה sharding.

כשטבלת Users שלך נמצאת על שרת A וטבלת Orders על שרת B, מפתחות זרים לא יכולים לעבוד פיזית. למסד הנתונים אין מנגנון לאכוף אילוץ על פני גבולות רשת. בשלב הזה, אתה כבר מריץ ג’ובים ברקע כדי למצוא רשומות יתומות ומיישם תבניות של עקביות בסופו של דבר.

זה קורה ב-SaaS רב-דיירים שבו כל דייר מקבל מסד נתונים מבודד משלו לצורכי תאימות, או בפריסות IoT שבהן יש לך 50,000 התקני קצה שכל אחד מריץ SQLite מקומית. ברגע שאתה שם, מפתחות זרים יורדים מהשולחן (תרתי משמע) בלי קשר לשיקולי ביצועים.

אבל עד שאתה מגיע לגבול הארכיטקטוני הזה, אולי אל תייעל בטרם עת לבעיות של נטפליקס כשאתה בונה כלי פנימי ל-10 משתמשים.

איך זה נראה בפועל

במקום לשאול “האם עלי להשתמש במפתחות זרים?”, נסה לשאול את שלושת הדברים האלה:

מה נשבר אם ההפניה הזו שגויה? האם זו תביעה, חיוב מושחת, הפרת רגולציה? או סתם חיבור חסר שמחזיר null בלוח המחוונים האנליטי שלך?

מה נשבר אם האימות איטי? האם אתה מאבד נתוני זמן-אמת שאין להם תחליף? או שהשאילתות שלך פשוט לוקחות עוד 50 מילישניות?

האם הנתונים האלה הם צילום מצב או הפניה? האם אתה מתעד איך משהו נראה ברגע מסוים, או שאתה מצביע לערך הנוכחי הסמכותי?

משם, התבניות צצות די באופן טבעי:

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

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

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

אבל שים לב שאמרתי “כנראה” ו”לעתים קרובות”. כי ההקשר חשוב, וההקשר שלך שונה משלי.

מחשבות אחרונות

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

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

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

בשביל מה אתה מייעל?

משאבים