UUID أم GUID: ما الذي يختلف فعلًا؟
GUID هو الاسم الذي تطلقه مايكروسوفت على UUID. والفروق التي تواجهها عمليًا هي تنسيق النص، وترتيب البايتات حين تُخزَّن القيمة في 16 بايت، وترتيب الفرز في SQL Server.
إن كنت تحتاج إلى الإجابة المختصرة فقط: الـ GUID والـ UUID نوع واحد من القيم، معرّف بطول 128 بت يُكتب عادةً في 32 رقمًا ست عشريًا موزّعة على خمس مجموعات. فالدالة Guid.NewGuid() في .NET والدالة crypto.randomUUID() في JavaScript تُنتج كلتاهما UUID عشوائيًا من النسخة 4. ومع ذلك تقع مشكلات في التوافق بين الأنظمة، ومصدرها ثلاثة أماكن: طريقة كتابة القيمة كنص، وترتيب بايتاتها الستة عشر، وطريقة مقارنتها في SQL Server.
اسمان لمعيار واحد
تعود أصول UUID إلى نظام Network Computing System من شركة Apollo، ثم انتقلت إلى بيئة OSF Distributed Computing Environment. وتبنّت مايكروسوفت تصميم DCE في تقنية COM وسمّت هذه القيم «المعرّفات الفريدة عالميًا» (globally unique identifiers). ثم نشرت IETF الصيغة لاحقًا في RFC 4122 في يوليو 2005، الذي حلّ محلّه في مايو 2024 المعيار RFC 9562، وهو الإصدار الذي أضاف UUID v6 وv7 وv8. وينص المعياران صراحةً على أن UUID تُعرف أيضًا باسم GUID.
وهناك حقل واحد يحفظ تاريخ هذه التسمية. فبتات النمط (variant) في بداية المجموعة الرابعة تميّز UUID الخاضعة للـ RFC، التي تبدأ فيها تلك المجموعة بـ 8 أو 9 أو a أو b، عن نطاق محجوز للتوافق مع أنظمة مايكروسوفت القديمة يبدأ بـ c أو d. والـ GUID التي تنشئها إصدارات Windows و.NET الحالية تستخدم نمط الـ RFC، لذا لا يمكن عمليًا التمييز بين الاثنين.
صيغ النص
تقبل الدالة Guid.ToString() في .NET محدّد صيغة. وهذه مخرجاتها للقيمة نفسها في .NET 8:
| المحدّد | الناتج |
|---|---|
N | f47ac10b58cc4372a5670e02b2c3d479 |
D (الافتراضي) | f47ac10b-58cc-4372-a567-0e02b2c3d479 |
B | {f47ac10b-58cc-4372-a567-0e02b2c3d479} |
P | (f47ac10b-58cc-4372-a567-0e02b2c3d479) |
X | {0xf47ac10b,0x58cc,0x4372,{0xa5,0x67,0x0e,0x02,0xb2,0xc3,0xd4,0x79}} |
كلها بأحرف صغيرة. أما الأسلوب بالأحرف الكبيرة والأقواس المعقوفة، {F47AC10B-58CC-4372-A567-0E02B2C3D479}، فمصدره سجل Windows ومعرّفات أصناف COM وملفات مشاريع Visual Studio، كما يعرض SQL Server قيم uniqueidentifier بأحرف كبيرة أيضًا. ويطلب RFC 4122 أن تكون المخرجات بأحرف صغيرة وأن تُقبل المدخلات دون تمييز بين الكبيرة والصغيرة، والدالة Guid.Parse تقبل كل الأشكال الواردة في الجدول.
القاعدة العملية: لا تقارن قيم GUID كنصوص أبدًا. فـ "{F47AC10B-...}" و"f47ac10b-..." نصّان مختلفان لقيمة واحدة. حلّل الطرفين، أو وحّد الصيغة إلى أحرف صغيرة بلا أقواس قبل التخزين أو المقارنة.
ترتيب البايتات: الفرق الذي يوقعك في المشكلات
حين يُخزَّن UUID الخاضع للـ RFC في 16 بايت يكون بترتيب big-endian: تأتي البايتات بترتيب الأرقام الست عشرية نفسه. أما بنية GUID في مايكروسوفت فمختلفة. فهي معرّفة كعدد صحيح بطول 32 بت، وعددين صحيحين بطول 16 بت، و8 بايتات مفردة، وعلى الأجهزة ذات ترتيب little-endian تُخزَّن الأعداد الصحيحة الثلاثة ببايتات معكوسة. والدالة Guid.ToByteArray() تعيد هذا التخطيط بالضبط، والمُنشئ new Guid(byte[]) يتوقعه:
Text: f47ac10b-58cc-4372-a567-0e02b2c3d479
RFC bytes: f4 7a c1 0b 58 cc 43 72 a5 67 0e 02 b2 c3 d4 79
ToByteArray(): 0b c1 7a f4 cc 58 72 43 a5 67 0e 02 b2 c3 d4 79
البايتات الثمانية الأخيرة متطابقة، والمجموعات الثلاث الأولى معكوسة. ويستخدم SQL Server التخطيط المختلط نفسه: تحويل uniqueidentifier إلى binary(16) يعطي بايتات ToByteArray().
ويظهر الخلل حين يكتب طرف البايتات بتخطيط ويقرؤها الطرف الآخر بالتخطيط الآخر. تكتب خدمة .NET ناتج ToByteArray() في عمود BINARY(16) أو في رسالة ثنائية، فتقرأ خدمة مكتوبة بـ Java أو Go أو Python البايتات على أنها UUID قياسي وتحصل على 0bc17af4-cc58-7243-a567-0e02b2c3d479. القيمة ما زالت فريدة، لذا لا يفشل شيء، لكن عمليات البحث بين النظامين لا تتطابق أبدًا. وقد انتقل رقم النسخة أيضًا: في هذا المثال تدّعي القيمة المشوّشة أنها من النسخة 7.
اختر تخطيطًا واحدًا وحوّل صراحةً عند الحدود بين الأنظمة:
- .NET 8 وما بعده:
guid.ToByteArray(bigEndian: true)وnew Guid(bytes, bigEndian: true)تقرآن وتكتبان بترتيب الـ RFC. - Python:
uuid.UUID(bytes_le=data)تقرأ تخطيط مايكروسوفت، وu.bytes_leتكتبه. - في أي مكان: إن خزّنت نصًا أو نوع UUID أصليًا بدل البايتات الخام، فلن يُطرح هذا السؤال أصلًا.
كيف يرتّب SQL Server قيم uniqueidentifier
لا يرتّب SQL Server قيم GUID بالطريقة التي يُقرأ بها نصها. فهو يقارن مجموعة البايتات الستة الأخيرة أولًا، ثم المجموعة الرابعة، ثم الثالثة فالثانية فالأولى، وداخل هذه المجموعات الثلاث الأولى يكون البايت الأيمن هو الأعلى وزنًا. القائمة التالية مرتّبة تصاعديًا بترتيب uniqueidentifier، كما ينتجها النوع SqlGuid في .NET الذي ينفّذ المقارنة نفسها:
01000000-0000-0000-0000-000000000000
ff000000-0000-0000-0000-000000000000
00000001-0000-0000-0000-000000000000
00000000-0100-0000-0000-000000000000
00000000-0001-0000-0000-000000000000
00000000-0000-0100-0000-000000000000
00000000-0000-0001-0000-000000000000
00000000-0000-0000-0001-000000000000
00000000-0000-0000-0100-000000000000
00000000-0000-0000-0000-000000000002
00000000-0000-0000-0000-010000000000
أما فرز القيم نفسها كـ System.Guid في .NET، أو كنصوص في أي لغة، فيعطي ترتيبًا مختلفًا: تأتي 00000000-0000-0000-0000-000000000002 أولًا وff000000-0000-0000-0000-000000000000 أخيرًا. ويترتّب على ذلك أمران:
- قد يتخطّى الترقيم بمجموعة المفاتيح (keyset pagination) صفوفًا أو يكرّرها إن فرز التطبيق قيم GUID بنفسه ثم طلب من SQL Server الصفوف
WHERE id > @last. دع قاعدة البيانات تتولّى الأمرين معًا. - UUID v7 لا تساعد في SQL Server. تبدأ قيمة v7 بختم زمني، لكن SQL Server يقارن الذيل العشوائي أولًا، لذا فإن مفاتيح v7، بما فيها تلك الصادرة عن
Guid.CreateVersion7()في .NET 9، تقع في مواضع عشوائية داخل فهرسuniqueidentifierتمامًا كمفاتيح v4.
NEWID أم NEWSEQUENTIALID
NEWID() | NEWSEQUENTIALID() | |
|---|---|---|
| طريقة الإنشاء | عشوائي، النسخة 4 | الدالة UuidCreateSequential في Windows، أي UUID مبني على الوقت، مع إعادة ترتيب بايتاته |
| أين يمكن استخدامها | في أي مكان في T-SQL | فقط في قيد DEFAULT على عمود من نوع uniqueidentifier |
| الترتيب | عشوائي | متزايد بحسب ترتيب SQL Server على ذلك الجهاز، وقد يبدأ من قيمة أدنى بعد إعادة تشغيل Windows |
| قابلية التنبؤ | لا | نعم: يمكن تخمين القيمة التالية |
| سلوك الفهرس | تقع عمليات الإدراج في أي مكان فتسبّب انقسام الصفحات | تذهب عمليات الإدراج إلى نهاية الفهرس |
وُجدت NEWSEQUENTIALID() لأن المفاتيح العشوائية تجزّئ الفهارس العنقودية (clustered indexes). وإعادة ترتيب البايتات هي ما يجعل قيمها تتزايد وفق طريقة المقارنة غير المألوفة في SQL Server، وهي أيضًا السبب في أن فرزها كنص لا يعيد ترتيب إنشائها. ولأن قيمها قابلة للتخمين، تنصح وثائق مايكروسوفت بعدم استخدامها حيث تهم الخصوصية؛ فأبقِ هذه المعرّفات خارج الروابط. وإن كانت المفاتيح تُولَّد في التطبيق بدلًا من ذلك، فإن الفرز على طريقة SQL Server يتطلب وضع البايتات المبنية على الوقت في المجموعة الأخيرة، وهذا عكس ما تفعله UUID v7 تمامًا.
توليد أيٍّ منهما
لأن القيم واحدة، فإن أي مولّد لـ UUID v4 هو مولّد GUID أيضًا. يُنتج مولّد GUID قيمًا عشوائية من النسخة 4 بأسلوب سجل Windows، أي بأحرف كبيرة وأقواس معقوفة، بينما يُنتج مولّد UUID v4 النوع نفسه من القيم بأحرف صغيرة وبصيغة الـ RFC. ويستطيع كلاهما التبديل بين وجود الأقواس وحالة الأحرف والشرطات، وهذا يغطي الصيغ N وD وB المذكورة أعلاه.