الفرق بين UUID وULID وNano ID والمعرّفات ذاتية الزيادة
مقارنة بين خمسة أنظمة شائعة للمعرّفات فيما يهم عمليًا: الحجم، والترتيب، واحتمال التصادم، وسلوك قاعدة البيانات، وما يكشفه المعرّف عن بياناتك.
اختيار صيغة المعرّف يتلخّص في ثلاثة أسئلة. هل يجب أن تترتّب المعرّفات بحسب وقت الإنشاء؟ هل ستظهر في الروابط أو في واجهات API عامة؟ وهل ستخزّنها قاعدة البيانات في عمود أصلي بطول 128 بت أم كنص؟ وقد غيّرت UUID v7، التي صارت معيارًا في RFC 9562 في مايو 2024، الإجابة لكثير من المشاريع: فهي تمنح الترتيب الزمني الذي جعل ULID شائعًا، بصيغة يقبلها أصلًا كل عمود UUID وكل مكتبة.
نظرة سريعة
| الترقيم التلقائي | UUID v4 | UUID v7 | ULID | Nano ID | |
|---|---|---|---|---|---|
| مثال | 1042 | f47ac10b-58cc-4372-a567-0e02b2c3d479 | 01926f3a-7c2b-7d4e-9a1f-3b5c8d2e6f10 | 01J9QKMZ1BFN79M7SVBJ6JWVRG | V1StGXR8_Z5jdHi6B-myT |
| الحجم الثنائي | 4 أو 8 بايت | 16 بايت | 16 بايت | 16 بايت | يُخزَّن كنص |
| طول النص | حتى 10 أو 19 رقمًا | 36 | 36 | 26 | 21 افتراضيًا |
| البتات العشوائية | لا يوجد | 122 | حتى 74 | 80 | 126 افتراضيًا |
| يترتّب بوقت الإنشاء | نعم | لا | نعم، حتى المللي ثانية | نعم، حتى المللي ثانية | لا |
| يُولَّد دون قاعدة البيانات | لا | نعم | نعم | نعم | نعم |
| المواصفة | ميزة في قاعدة البيانات | RFC 9562 | RFC 9562 | مواصفة مجتمعية | مكتبة |
| يكشف | العدد والترتيب | لا شيء | وقت الإنشاء | وقت الإنشاء | لا شيء |
كيف يُبنى كل واحد منها
- الترقيم التلقائي عدّاد تحتفظ به قاعدة البيانات. ينتهي
INTذو الإشارة بطول 32 بت عند 2,147,483,647، وينتهيBIGINTذو الإشارة عند 9,223,372,036,854,775,807. ولا يعرف التطبيق المعرّف إلا بعد الإدراج، ما لم يحجز قيمًا من تسلسل (sequence) مسبقًا. - UUID v4 مكوّن من 122 بت عشوائية، إضافة إلى 6 بتات ثابتة تحدّد النسخة والنمط (variant).
- UUID v7 يبدأ بختم زمني يونكس بطول 48 بت بالمللي ثانية، يليه رقم النسخة، ثم 12 بت تُسمّى
rand_a، ثم النمط، ثم 62 بت تُسمّىrand_b. ويسمح RFC 9562 للمولّد باستخدام بعض هذه البتات كعدّاد، حتى تخرج المعرّفات المُنشأة في المللي ثانية نفسها مرتّبة. ومولّد هذا الموقع يفعل ذلك بالبتات الاثنتي عشرة فيrand_a. - ULID ختم زمني بالمللي ثانية بطول 48 بت مع 80 بت عشوائية، يُكتب في 26 محرفًا بترميز Base32 الخاص بـ Crockford: 10 محارف للوقت و16 للعشوائية. تستبعد أبجديته الحروف I وL وO وU، ولا تفرّق بين الأحرف الكبيرة والصغيرة. وفي الوضع الرتيب (monotonic)، يأخذ ULID المُنشأ في المللي ثانية نفسها التي أُنشئ فيها سابقه الجزءَ العشوائي من ذلك السابق ويضيف إليه واحدًا.
- Nano ID سلسلة من المحارف العشوائية، عددها افتراضيًا 21 محرفًا من الأبجدية ذات 64 محرفًا
A-Z a-z 0-9 _ -. يحمل كل محرف 6 بتات، فيكون المجموع 126 بت. ويمكن تغيير الطول والأبجدية كليهما، ويتغيّر عدد البتات العشوائية تبعًا لذلك.
ULID وUUID كلاهما رقم بطول 128 بت يُكتب بطريقة مختلفة. فـ 01J9QKMZ1BFN79M7SVBJ6JWVRG و01926f3a-7c2b-7d4e-9a1f-3b5c8d2e6f10 قيمة واحدة، الأولى بترميز Base32 والثانية بالنظام الست عشري. ولهذا يمكن تخزين ULID في عمود UUID أصلي، مثل النوع uuid في PostgreSQL، ويحتفظ هناك بترتيبه. لكن ULID الذي لم يبدأ حياته كـ UUID من النسخة 7 يحمل بتات اعتباطية في موضعَي النسخة والنمط، لذا قد يرفضه مدقّق UUID صارم.
احتمالات التصادم بالحساب
ينطبق تقريب مفارقة عيد الميلاد (birthday approximation) على المعرّفات العشوائية بالكامل: مع n معرّفًا مسحوبًا من N = 2 مرفوعًا لقوة عدد البتات العشوائية، يكون احتمال وجود تكرار واحد على الأقل نحو 1 − e^(−n²/2N)، ويبلغ 50% عند n = 1.18 × √N تقريبًا.
| النظام | البتات العشوائية | عدد المعرّفات لاحتمال تكرار 50% | احتمال التكرار بين مليار معرّف |
|---|---|---|---|
| UUID v4 | 122 | 2.7 × 10^18 | 9.4 × 10^-20 |
| Nano ID، 21 محرفًا | 126 | 1.1 × 10^19 | 5.9 × 10^-21 |
| Nano ID، 10 محارف | 60 | 1.3 × 10^9 | نحو 35% |
الصف الأخير هو ما يجب أن تتذكّره: تقصير Nano ID من أجل روابط أجمل يستنزف مقاومة التصادم بسرعة، لذا أجرِ هذا الحساب قبل أن تختار الطول.
أما في UUID v7 وULID، فلا يمكن أن يتصادم معرّفان إلا إن حملا المللي ثانية نفسها، لذا فإن n هنا هو عدد المعرّفات المُنشأة في مللي ثانية واحدة بواسطة مولّدات لا تتشارك عدّادًا. ومع 1,000 معرّف من هذا النوع في المللي ثانية نفسها، يكون احتمال التكرار نحو 2.6 × 10^-17 مع 74 بت عشوائية، و4.1 × 10^-19 مع بتات ULID الثمانين. وعمليًا، الخطر الحقيقي في كل هذه الأنظمة هو مصدر العشوائية الضعيف، مثل Math.random، لا الحساب الرياضي.
سلوك فهارس قاعدة البيانات
تعيش المفاتيح الأساسية عادةً في فهرس من نوع B-tree. وحين يكون كل مفتاح جديد أكبر من سابقه، كما في الترقيم التلقائي وUUID v7 وULID، تقع عمليات الإدراج على الحافة اليمنى للفهرس، فتمتلئ الصفحات بالكامل وتبقى الصفوف المكتوبة حديثًا متقاربة. أما المفاتيح العشوائية، أي UUID v4 وNano ID، فتقع في أي مكان: تنقسم الصفحات، وتبقى فارغة جزئيًا، ومتى لم يعد الفهرس يتّسع في الذاكرة قد يعني كل إدراج عملية قراءة عشوائية.
ويكون الأثر أقوى في MySQL وMariaDB مع محرك InnoDB، لأنهما يخزّنان الصفوف نفسها بترتيب المفتاح الأساسي، فتبعثر المفاتيح العشوائية الجدول كله لا الفهرس وحده. أما PostgreSQL فيحتفظ بالصفوف في كومة (heap) منفصلة، فيقتصر الفرق على الفهرس.
وصيغة التخزين لا تقل أهمية عن نظام المعرّف نفسه:
- خزّن UUID وULID في نوع أصلي بطول 16 بايت حيثما وُجد:
uuidفي PostgreSQL، وUUIDفي MariaDB 10.7 وما بعده، وBINARY(16)في MySQL. أماCHAR(36)فيشغل أكثر من ضعف المساحة ويكبّر كل فهرس. - في MySQL، لا تمرّر علم التبديل إلى
UUID_TO_BIN(uuid, 1)مع قيم v7. فهذا العلم يعيد ترتيب حقول الوقت لصالح UUID من النسخة 1، وسيُفسد ترتيب v7. - يقارن النوع
uniqueidentifierفي SQL Server البايتات الستة الأخيرة أولًا، لذا لا تترتّب قيم v7 المخزّنة فيه بحسب الوقت. راجع التفاصيل في دليل الفرق بين UUID وGUID. - معرّفات Nano ID نصوص، وتُخزَّن في
CHARأوVARCHAR، مع ترتيب مقارنة ثنائي (binary collation) إن كان يجب التمييز بين الأحرف الكبيرة والصغيرة.
الروابط وما يكشفه المعرّف
الصيغ الخمس كلها آمنة في الرابط دون ترميز بالنسبة المئوية: الأرقام الست عشرية والشرطات وأبجدية Crockford Base32 وأبجدية Nano ID كلها محارف غير محجوزة في RFC 3986. والفرق بينها في الطول (36 أو 26 أو 21 محرفًا) وفي حالة الأحرف. فـ UUID وULID لا يفرّقان بين الأحرف الكبيرة والصغيرة، لذا يصمدان أمام الأنظمة التي تغيّر حالة الأحرف؛ أما Nano ID فلا.
وكثيرًا ما يكون ما يكشفه المعرّف هو العامل الحاسم في الاختيار:
- الترقيم التلقائي يكشف الحجم. فإن كان رقم فاتورتك اليوم 1042 وصار 1342 بعد أسبوع، فقد أصدرت الشركة نحو 300 فاتورة في ذلك الأسبوع. كما تجعل المعرّفات التسلسلية تجربة السجلات المجاورة أمرًا في غاية السهولة، وهذا يحوّل أي فحص صلاحيات ناقص إلى تسريب بيانات.
- UUID v7 وULID يكشفان وقت إنشاء السجل حتى المللي ثانية. في الطلبات نادرًا ما يكون ذلك مشكلة؛ أما في حسابات المستخدمين أو السجلات الطبية فقد يكون. الصق معرّف v7 في أداة الفحص في مولّد UUID v7 وستعرض لك الختم الزمني.
- UUID v4 وNano ID لا يكشفان شيئًا عن زمان إنشائهما أو مكانه.
ولا يُعدّ أيٌّ من هذه المعرّفات سرًّا. فالمعرّف الذي يصعب تخمينه يجعل محاولات التخمين والتجربة أصعب، لكنه لا يغني عن التحقق من أن المستخدم مسموح له برؤية السجل.
أيها تختار
- الجداول الداخلية في قاعدة بيانات واحدة التي لا تُعرض للمستخدمين أبدًا: الترقيم التلقائي بنوع
BIGINT. إنه أصغر المفاتيح وأبسطها. - المفاتيح الأساسية التي ينشئها التطبيق أو تتوزّع عبر خدمات متعددة، حين يتوفر عمود UUID أصلي: UUID v7.
- المعرّفات العامة التي يجب أن يبقى وقت إنشائها خاصًا، ومعرّفات الربط (correlation IDs)، وأسماء الملفات: UUID v4، من مولّد UUID v4 أو
crypto.randomUUID(). - المعرّفات القصيرة في الروابط حيث لا يهم الترتيب: Nano ID بطوله الافتراضي أو بطول قريب منه.
- ULID حين تستخدمه بيئتك التقنية أصلًا، أو حين تريد شكله النصي الأقصر الذي لا يفرّق بين الأحرف الكبيرة والصغيرة. أما في المشاريع الجديدة فتغطي UUID v7 الاحتياجات نفسها بصيغة قياسية.
وهناك تركيبة شائعة تجمع أفضل ما في الطريقتين: مفتاح أساسي من نوع BIGINT أو UUID v7 للربط بين الجداول وللفهارس، مع معرّف عام عشوائي منفصل عليه فهرس فريد لاستخدامه في الروابط وواجهات API.