UUID و GUID: واقعاً چه فرقی دارند؟
GUID نامی است که مایکروسافت روی UUID گذاشته. تفاوتهایی که در عمل با آنها روبهرو میشوید قالب متنی، ترتیب بایتها هنگام ذخیره به شکل ۱۶ بایت و ترتیب مرتبسازی SQL Server است.
اگر فقط جواب کوتاه را میخواهید: GUID و UUID یک نوع مقدارند؛ یک شناسه ۱۲۸ بیتی که معمولاً به شکل ۳۲ رقم هگز در پنج گروه نوشته میشود. Guid.NewGuid() در .NET و crypto.randomUUID() در JavaScript هر دو UUID v4 تصادفی میسازند. با این حال مشکلات ناسازگاری همچنان پیش میآیند و از سه جا سرچشمه میگیرند: اینکه مقدار چطور به شکل متن نوشته میشود، ۱۶ بایت آن به چه ترتیبی قرار میگیرند، و SQL Server چطور آن را مقایسه میکند.
دو نام برای یک استاندارد
UUIDها از Network Computing System شرکت Apollo آمدهاند و بعد به OSF Distributed Computing Environment (DCE) راه یافتند. مایکروسافت طراحی DCE را برای COM به کار گرفت و این مقدارها را شناسههای یکتای سراسری (globally unique identifier) نامید. IETF بعدتر این قالب را در ژوئیه ۲۰۰۵ بهعنوان RFC 4122 منتشر کرد که در مه ۲۰۲۴ جایش را به RFC 9562 داد؛ همان نسخهای که UUID v6، v7 و v8 را اضافه کرد. هر دو RFC صراحتاً میگویند که UUIDها GUID هم نامیده میشوند.
یک فیلد، تاریخچه این نامگذاری را در خود دارد. بیتهای variant در ابتدای گروه چهارم، UUIDهای RFC را که آن گروه در آنها با 8، 9، a یا b شروع میشود، از بازهای که برای سازگاری با سیستمهای قدیمی مایکروسافت رزرو شده و با c یا d شروع میشود جدا میکنند. GUIDهایی که ویندوز و .NET امروزی میسازند از variant استاندارد RFC استفاده میکنند، پس در عمل این دو از هم قابل تشخیص نیستند.
قالبهای متنی
متد Guid.ToString() در .NET یک مشخصکننده قالب (format specifier) میگیرد. خروجیهای زیر برای یک مقدار یکسان در .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}، از رجیستری ویندوز، شناسههای کلاس COM و فایلهای پروژه Visual Studio میآید، و SQL Server هم مقدارهای uniqueidentifier را با حروف بزرگ نمایش میدهد. RFC 4122 میخواهد خروجی با حروف کوچک باشد و ورودی بدون حساسیت به بزرگی و کوچکی حروف پذیرفته شود، و Guid.Parse همه شکلهای این جدول را میپذیرد.
قاعده عملی: هیچوقت GUIDها را به شکل رشته مقایسه نکنید. "{F47AC10B-...}" و "f47ac10b-..." دو رشته متفاوتاند اما یک مقدار. یا هر دو طرف را parse کنید، یا پیش از ذخیره یا مقایسه، مقدار را به حروف کوچک و بدون آکولاد یکسانسازی کنید.
ترتیب بایتها: تفاوتی که دردسر میسازد
یک UUID استاندارد RFC وقتی به شکل ۱۶ بایت ذخیره میشود big-endian است: بایتها به همان ترتیب رقمهای هگز میآیند. ساختار GUID مایکروسافت متفاوت است. این ساختار به شکل یک عدد صحیح ۳۲ بیتی، دو عدد صحیح ۱۶ بیتی و ۸ بایت تکی تعریف شده و روی ماشینهای 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 هم از همین چیدمان mixed-endian استفاده میکند: تبدیل یک uniqueidentifier به binary(16) همان بایتهای ToByteArray() را میدهد.
باگ وقتی ظاهر میشود که یک طرف بایتها را با یک چیدمان بنویسد و طرف دیگر با چیدمان دیگر بخواند. یک سرویس .NET خروجی ToByteArray() را در یک ستون BINARY(16) یا یک پیام باینری مینویسد؛ یک سرویس Java، Go یا Python بایتها را بهعنوان UUID استاندارد RFC میخواند و 0bc17af4-cc58-7243-a567-0e02b2c3d479 را تحویل میگیرد. مقدار همچنان یکتاست، پس هیچ خطایی رخ نمیدهد، اما جستوجوها بین دو سیستم هیچوقت نتیجهای نمیدهند. رقم نسخه هم جابهجا شده است: در این مثال، مقدار بههمریخته ادعا میکند UUID v7 است.
یک چیدمان را انتخاب کنید و در مرز بین سیستمها صریحاً تبدیل کنید:
- .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 ممکن است ردیفهایی را جا بیندازد یا تکرار کند، اگر اپلیکیشن خودش GUIDها را مرتب کند و بعد ردیفها را با
WHERE id > @lastاز SQL Server بخواهد. بگذارید هر دو کار را پایگاه داده انجام دهد. - UUID v7 در SQL Server کمکی نمیکند. مقدار v7 با یک برچسب زمانی شروع میشود، اما SQL Server اول انتهای تصادفی آن را مقایسه میکند؛ پس کلیدهای v7، از جمله کلیدهایی که
Guid.CreateVersion7()در .NET 9 میسازد، درست مثل کلیدهای v4 در جاهای تصادفی یک ایندکسuniqueidentifierمینشینند.
NEWID یا NEWSEQUENTIALID
NEWID() | NEWSEQUENTIALID() | |
|---|---|---|
| نحوه ساخت | تصادفی، v4 | تابع UuidCreateSequential ویندوز، یک UUID مبتنی بر زمان، با بایتهای جابهجاشده |
| کجا میتوان استفاده کرد | هر جای T-SQL | فقط در یک قید DEFAULT روی ستونی از نوع uniqueidentifier |
| ترتیب | تصادفی | در ترتیب مرتبسازی SQL Server روی همان ماشین صعودی است؛ ممکن است بعد از راهاندازی مجدد ویندوز از مقدار پایینتری شروع شود |
| قابل پیشبینی | خیر | بله: مقدار بعدی را میتوان حدس زد |
| رفتار ایندکس | درجها هر جایی مینشینند و باعث تقسیم صفحه (page split) میشوند | درجها به انتهای ایندکس میروند |
NEWSEQUENTIALID() به این دلیل وجود دارد که کلیدهای تصادفی ایندکسهای clustered را پراکنده (fragmented) میکنند. جابهجایی بایتها همان چیزی است که باعث میشود مقدارهای این تابع در مقایسه غیرمعمول SQL Server صعودی باشند، و به همین دلیل مرتب کردن آنها به شکل متن، ترتیب ساختشان را بازتولید نمیکند. چون مقدارها قابل حدساند، مستندات مایکروسافت استفاده از آن را در جاهایی که حریم خصوصی مهم است توصیه نمیکند؛ چنین شناسههایی را در URLها نیاورید. اگر کلیدها بهجای پایگاه داده در اپلیکیشن ساخته میشوند، برای اینکه مثل SQL Server مرتب شوند باید بایتهای زمانی را در گروه آخر قرار دهید؛ یعنی دقیقاً برعکس کاری که UUID v7 انجام میدهد.
ساختن هرکدام
چون مقدارها یکساناند، هر تولیدکننده UUID v4 یک تولیدکننده GUID هم هست. تولیدکننده GUID مقدارهای تصادفی v4 را به سبک رجیستری، یعنی با حروف بزرگ و آکولاد، میسازد و تولیدکننده UUID v4 همین نوع مقدار را به شکل استاندارد RFC و با حروف کوچک. هر دو میتوانند آکولاد، بزرگی و کوچکی حروف و خط تیرهها را تغییر دهند، که قالبهای N، D و B بالا را پوشش میدهد.