מחולל UUID

יצירת מזהי UUID בודדים או חמישים בבת אחת: v4 אקראי, או v7 ממוין לפי זמן, שחותם הזמן שבתחילתו שומר על סדר המנה ועושה ממנו מפתח טוב יותר למסד נתונים.

גרסה

122 ביט אקראיים. לא חושף דבר על מועד היצירה.

כמה

יוצר…

מהו UUID ולמה הוא קיים

UUID — ראשי תיבות של Universally Unique Identifier, ובתיעוד של מיקרוסופט מכונה GUID — הוא ערך בן 128 סיביות שנכתב כ־32 ספרות הקסדצימליות בקיבוץ המוכר 8-4-4-4-12. כל תכליתו היא לאפשר למערכות נפרדות לייצר מזהים באופן עצמאי, בלי שום תיאום ביניהן, ועדיין להיות בטוחות שהתוצאות לא יתנגשו. זה מה שמבדיל אותו מעמודת auto-increment במסד נתונים: שני שרתים, שני לקוחות ניידים במצב לא-מקוון במטוס, ומשימת רקע — כולם יכולים ליצור רשומות באותו רגע בלי לבקש רשות מאיש.

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

גרסה 4 מול גרסה 7 — ההחלטה שבאמת חשובה

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

גרסה 7, שתוקננה ב־RFC 9562 ב־2024, מחליפה את 48 הסיביות הראשונות בחותמת זמן Unix במילישניות וממלאת את השאר באקראיות. מכיוון שהזמן בא ראשון והערך נקרא משמאל לימין, מיון של מזהי v7 כטקסט רגיל ממיין אותם גם כרונולוגית.

זה אינו הבדל קוסמטי. מסדי נתונים שומרים מפתחות ראשיים באינדקס B-tree ממוין. הכנס מפתחות v7 וכל שורה חדשה נוחתת בקצה הימני של העץ, ליד קודמתה — הדפים שאליהם כותבים נשארים בזיכרון והאינדקס גדל בצורה מסודרת. הכנס מפתחות v4 וכל כתיבה נוחתת במיקום אקראי, כך שהמסד נוגע בדף אחר בכל פעם, שיעורי הפגיעה במטמון יורדים והאינדקס מתפצל. בטבלה גדולה ועמוסה ההבדל בקצב ההכנסה משמעותי, ולכן v7 אומץ כל כך מהר למפתחות ראשיים.

  • בחר v7 למפתחות ראשיים במסד נתונים, למזהי אירועים ולוגים, ולכל דבר שתרצה למיין או לתשאל לפי טווח זמן יצירה.
  • בחר v4 כשהמזהה מופיע במקום לא מהימן ואסור שידלוף ממנו דבר — כולל העובדה שרשומה אחת נוצרה רגע לפני אחרת.
  • שתיהן מתאימות ל־correlation ID, למפתח אידמפוטנטיות או לשם קובץ, שם הסדר אינו משנה.

התמורה היא בדיוק הדליפה הזאת: מזהה v7 מספר לכל מי שרואה אותו, ברזולוציה של מילישנייה, מתי הוא נוצר. עבור מזהה שורה זה בדרך כלל לא מזיק ולעיתים קרובות מועיל. עבור אסימון איפוס סיסמה או קישור שיתוף ציבורי זה מידע שלא התכוונת לפרסם — ואלה בכלל לא צריכים להיות UUID, אלא אסימונים אקראיים ממחולל סודות ייעודי.

סדר בתוך אותה מילישנייה

דקוּת שתופסת מימושי v7 רבים: חומרה מודרנית מייצרת הרבה יותר ממזהה אחד למילישנייה. אם שני ערכים חולקים חותמת זמן, הסדר ביניהם נקבע על ידי הסיביות האקראיות שאחריה — כלומר, באקראי. צור חמישים בלולאה צפופה ותקבל חמישים ערכים שמסודרים בקירוב בלבד, ותאבד בדיוק את התכונה שבגללה בחרת ב־v7.

RFC 9562 פותר זאת באמצעות מונה מונוטוני, והכלי הזה מממש אותו: 12 הסיביות שמיד אחרי חותמת הזמן סופרות כלפי מעלה בתוך המילישנייה, כך שמנה עולה בהחלט ולא רק בקירוב. אם המונה מתמלא — יותר מ־4096 ערכים באותה מילישנייה — המחולל לווה את המילישנייה הבאה במקום לגלוש ולייצר ערך שממוין לפני קודמו. הוא מטפל באותה רוח גם בשעון שנע אחורה, מה שקורה בתיקוני NTP.

קריאה של ערך v7 משמאל לימין, כשהדוגמה היא 0190a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b:

  • 0190a1b2-c3d4 — חותמת זמן Unix בת 48 סיביות במילישניות. מכיוון שהיא באה ראשונה, סדר הטקסט הוא סדר הזמן.
  • 7 — ניבל הגרסה, שהוא מה שהופך את זה ל־v7 ולא ל־v4.
  • e5f — המונה המונוטוני בן 12 הסיביות, שעולה בתוך מילישנייה אחת.
  • 8 — סיביות ה־variant, שנקבעו ב־RFC 9562 לכל UUID מודרני (תמיד ‎8, 9, a או b).
  • a9b-0c1d2e3f4a5b — 62 הסיביות הנותרות, אקראיות לחלוטין.

אחסון ושימוש ב־UUID

הצורה הקנונית היא אותיות קטנות עם מקפים, ו־RFC 9562 קובע שמחוללים צריכים להוציא בדיוק את זה — ולכן הכלי הזה אינו מציע אפשרויות פורמט. עדיין תפגוש כתיבים אחרים בשטח: אותיות גדולות עטופות בסוגריים מסולסלים בכלים של מיקרוסופט, וללא מקפים היכן שמישהו רצה עמודה קצרה יותר. כולם אותן 128 סיביות, וההשוואה ביניהם צריכה להתעלם מגודל האותיות.

המקום שבו זה באמת נחשב הוא האחסון. UUID הוא 16 בתים, אבל צורת הטקסט שלו היא 36 תווים — כך שאחסון כמחרוזת יותר מכפיל את הנפח בשורה, וחשוב מכך בכל אינדקס שכולל אותה. השתמש בטיפוס מובנה היכן שקיים כזה:

uuid                      -- PostgreSQL: טיפוס מובנה של 16 בתים
BINARY(16)                -- MySQL: קומפקטי; CHAR(36) מבזבז 20 בתים לשורה
uniqueidentifier          -- SQL Server
crypto.randomUUID()       // ג׳אווהסקריפט: v4 בלבד, דורש הקשר מאובטח
uuid.uuid4() / uuid7()    # פייתון: v4 בספרייה התקנית; v7 דרך ספרייה

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

איך לדעת אם UUID הוא v4 או v7

בדרך כלל מקבלים מזהה בלי הערה שמסבירה מאיפה הוא הגיע, ואין בכך צורך: הגרסה כתובה בתוך הערך עצמו. שתיהן 32 ספרות הקסדצימליות באותו קיבוץ 8-4-4-4-12, ולכן ההבדל אינו בצורה — הוא שתי ספרות בודדות במקומות קבועים, וכל השאר נגזר מהן.

  • הספרה הראשונה בקבוצה השלישית היא ניבל הגרסה: ספרת 4 פירושה גרסה 4, וספרת 7 פירושה גרסה 7. לשום חלק אחר בערך אין בכך מילה.
  • הספרה הראשונה בקבוצה הרביעית נושאת את סיביות ה־variant, והיא ‎8, 9, a או b בשתי הגרסאות — ולכן היא לעולם אינה מגלה באיזו מהן אתה מחזיק. מה שהיא כן מגלה הוא שהערך תואם ל־RFC 9562 בכלל; כל דבר אחר במקום הזה הוא מבנה ישן יותר או לא UUID.
  • אם ניבל הגרסה הוא 7, שתים-עשרה הספרות הראשונות הן זמן היצירה: מונה בן 48 סיביות של מילישניות מאז תחילת 1970, כתוב הקסדצימלית.
  • אם היא 4, אין מה לקרוא הלאה. v4 אינו מקודד זמן, לא מכונה ולא סדר, וזו התכונה שבגללה בחרת בו.

קח את הערך שהחלק שלמעלה מפענח, 0190a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b. הקבוצה השלישית שלו נפתחת ב־7, ולכן זו גרסה 7, ושתים-עשרה הספרות הראשונות שלו הן 0190a1b2c3d4 — המר את המספר ההקסדצימלי הזה לעשרוני, מסור אותו לממיר Unix timestamp, ותקבל אחר צהריים ביולי 2024. לצידו, 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d פותח את הקבוצה השלישית שלו ב־4, ושתים-עשרה הספרות הראשונות שלו הן נתונים אקראיים שאינם מפענחים לכלום. השניים מסכימים על דבר אחד בדיוק: שניהם פותחים את הקבוצה הרביעית באחת מארבע הספרות שסיביות ה־variant מתירות.

האם v7 עמיד בפני התנגשויות כמו v4?

זו שאלה הוגנת, מפני שב־v7 באמת יש פחות אקראיות. גרסה 4 מקדישה 122 מתוך 128 הסיביות שלה לנתונים אקראיים. גרסה 7 מקדישה 48 לחותמת הזמן, וכאן עוד 12 למונה המונוטוני, ונשארות 62 סיביות אקראיות — קצת יותר ממחצית. כמספר יבש זה נראה כמו נסיגה רצינית.

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

ולכן ההשוואה הכנה אינה 122 סיביות מול 62. היא הגרלה אחת שנערכת שוב ושוב לאורך כל חיי המערכת מול הגרלה נפרדת וקטנה בהרבה שנערכת בתוך כל מילישנייה ונזרקת כשהמילישנייה נגמרת. הסידור השני הוא החזק מבין השניים, והסיבה להעדיף v4 נשארת זו שהחלק על ההשוואה נותן — לא התנגשויות, אלא ש־v7 אומר בקול רם מתי נוצר.

מעבר של טבלה קיימת מ־v4 ל־v7

השאלה שבאה אחרי הבחירה ב־v7 היא מה לעשות עם השורות שכבר נמצאות בטבלה, והחלק המרגיע הוא שהעמודה עצמה אינה צריכה להשתנות כלל. שתי הגרסאות הן אותן 128 סיביות באותה צורת טקסט בת 36 תווים, ולכן עמודת uuid ב־PostgreSQL, BINARY(16) או CHAR(36) מכילה תערובת בלי לשים לב. מתחילים לייצר v7 לשורות חדשות ועוצרים שם; אין שלב הגירה ואין מילוי לאחור.

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

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

שאלות נפוצות

להשתמש ב־v4 או ב־v7?
השתמש ב־v7 למפתחות ראשיים במסד נתונים ולכל דבר שתמיין לפי זמן יצירה: חותמת הזמן המובילה שומרת על הכנסות מקובצות בקצה האינדקס במקום מפוזרות עליו. השתמש ב־v4 כשהמזהה לא אמור לחשוף דבר, כולל מתי נוצר.
האם שני UUID יכולים לצאת זהים?
זה אפשרי אבל בלתי סביר עד אפסיות. לגרסה 4 יש 122 סיביות אקראיות, כך שגם אחרי ייצור מיליארדי ערכים ההסתברות להתנגשות נשארת הרחק מתחת להסתברות שהאחסון ישבש אותם בשקט במקום.
מה קרה לגרסאות ‎1, 3 ו־5?
גרסה 1 מקודדת חותמת זמן ואת כתובת ה־MAC של המכונה, מה שמדליף זהות חומרה ואי אפשר לייצר אותו בדפדפן כלל. גרסאות 3 ו־5 גוזרות UUID דטרמיניסטית ממרחב שמות ומשם באמצעות MD5 או SHA-1 — שימושי כשאותו קלט חייב תמיד לייצר את אותו מזהה, אבל זו משימה אחרת מיצירת מזהה חדש.
האם UUID מאובטח מספיק כדי לשמש כאסימון סודי?
UUID מגרסה 4 אינו ניתן לניחוש, אבל גרסה 7 מקודדת בגלוי את זמן היצירה, ואף אחת מהן לא נועדה לשמש כאישור גישה. לאיפוס סיסמאות, לאסימוני סשן או לקישורי שיתוף — צור סוד אקראי ייעודי ובדוק הרשאות בשרת, במקום להסתמך על כך שקשה לנחש את המזהה.
למה מנת v7 שלי אינה ממוינת לגמרי בכלים אחרים?
מפני שמימושים רבים מדלגים על המונה המונוטוני. כששני ערכים או יותר חולקים מילישנייה, הסדר ביניהם נופל לסיביות האקראיות שאחריה. הכלי הזה מממש את המונה של RFC 9562, ולכן מנה שנוצרה כאן עולה בהחלט.
איך כדאי לאחסן UUID במסד נתונים?
בטיפוס מובנה של 16 בתים היכן שקיים כזה — uuid ב־PostgreSQL, uniqueidentifier ב־SQL Server, BINARY(16) ב־MySQL. אחסון צורת הטקסט בת 36 התווים במקום זאת יותר מכפיל את הנפח בשורה ובכל אינדקס שכולל את העמודה.
האם הם נוצרים בשרתים שלכם?
לא. הם נוצרים בדפדפן שלך באמצעות crypto.getRandomValues, מקור האקראיות הקריפטוגרפי של הפלטפורמה — אותו מקור ש־crypto.randomUUID משתמש בו. שום ערך אינו נשלח לשום מקום, וטעינה מחדש של העמוד מייצרת מערכת חדשה לגמרי.
אפשר לדעת מתי נוצר UUID מגרסה 7?
כן, והערך עצמו הוא כל מה שצריך. שתים-עשרה הספרות ההקסדצימליות הראשונות שלו הן מונה של מילישניות מאז תחילת 1970: המר אותן לעשרוני ומסור את התוצאה לממיר Unix timestamp. ל־v4 אין שדה כזה, ולכן אותן שתים-עשרה ספרות שם אקראיות ואינן מפענחות לכלום.
האם ל־v7 יש פחות סיביות אקראיות מאשר ל־v4?
כן — 62 כאן מול 122 של v4, מפני שחותמת הזמן והמונה תופסים את המקום. זה אינו הופך התנגשות לסבירה יותר בפועל: שני ערכי v7 יכולים להתנגש רק אם נוצרו באותה מילישנייה, ולכן 62 הסיביות האלה מושקעות בתוך מילישנייה אחת ולא לאורך כל חיי המערכת, ובתוך מנה אחת מהכלי הזה המונה הופך חזרה לבלתי אפשרית ולא רק לבלתי סבירה.
האם UUID מגרסה 4 ומגרסה 7 יכולים לשבת באותה עמודה?
כן. הם אותן 128 סיביות באותה צורת טקסט, ולכן שום דבר בסכמה אינו משתנה ואין הגירה — מייצרים v7 מעכשיו ומשאירים את השורות הישנות בשקט. הדבר היחיד שכדאי לשים לב אליו הוא המיון: השורות החדשות נמצאות בסדר יצירה ביניהן, אבל הישנות והאקראיות מפוזרות סביבן, ולכן מיון לפי המפתח אינו סדר זמן עבור הטבלה כולה.
מה הן גרסאות 6 ו־8 של UUID?
RFC 9562 מגדיר את שתיהן לצד v7. גרסה 6 היא גרסה 1 עם שדות חותמת הזמן שלה מסודרים מחדש כך שהערך ממוין כרונולוגית, ונועדה למערכות שכבר מחויבות ל־v1 — התקן אומר שכל דבר אחר צריך להשתמש ב־v7 במקום. גרסה 8 היא משבצת פתוחה בכוונה למבנים מותאמים אישית, שבה רק סיביות הגרסה וה־variant קבועות ו־122 הנותרות שלך להגדרה. הכלי הזה אינו מייצר אף אחת מהן.

כלים קשורים