بررسی عملکرد و چالشهای مرتبسازی Guid.CreateVersion7 در Microsoft SQL Server
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۷/۰۴ ۰۸:۳۰
آدرس: www.dntips.ir
چکیده: با معرفی استاندارد RFC 9562 و افزوده شدن متدGuid.CreateVersion7()در .NET 9، بسیاری از توسعهدهندگان بر این باورند که مشکل دیرینه مرتبسازی کلیدهای اصلی از نوع شناسه یکتا (UUID / GUID) و عارضه قطعهقطعهشدن شاخصها (Index Fragmentation) بهطور کامل حل شده است. با این حال، رفتار داخلی موتور پایگاهداده Microsoft SQL Server در مرتبسازی نوع دادهuniqueidentifierاز استاندارد لغتنامهای و چپبهراست پیروی نمیکند. در این مقاله، معماری مقایسه بایتها در SQL Server را کالبدشکافی کرده و نشان میدهیم که چراGuid.CreateVersion7()در SQL Server رفتاری تقریباً معادل یک GUID تصادفی (Random GUID) دارد. در ادامه، پیامدهای عملکردی ناشی از شکست صفحات (Page Split)، راهکارهای جایگزین در سطح پایگاهداده و لایه ORM (مانند EF Core)، و روش بازآرایی بایتها (Byte Reshuffling) به همراه شبیهسازی عملکردی مورد بررسی قرار میگیرد.
INT یا BIGINT با ویژگی IDENTITY): دارای ترتیبی خطی، فشرده و بهینه برای ساختارهای B-Tree، اما نامناسب برای معماریهای توزیعشده، سناریوهای Multi-Tenant و سامانههای نیازمند تولید کلید در سمت کلاینت پیش از ذخیرهسازی.Guid.CreateVersion7()، انتظار میرفت که چالشهای ترتیبی در SQL Server نیز برطرف شود. با این وجود، به دلیل سازوکار خاص مرتبسازی داده در SQL Server، این فرض از اساس نادرست است.درج ترتیبی (Append):
[ صفحه ۱ (پر) ] --> [ صفحه ۲ (پر) ] --> [ صفحه ۳ (در حال پر شدن...) ]
درج تصادفی (Random Insert):
[ صفحه ۱ (پر) ] + مقدار تصادفی جدید ==> [ شکاف صفحه (Page Split) ]
|-> [ صفحه ۱ (۵۰٪) ]
|-> [ صفحه جدید (۵۰٪) ]01a0ba47-e8d0-7198-9f36-28a04dcb9a21 01a0ba47-e8d6-771b-979a-a1ccc7581c77 01a0ba47-e8d9-7621-8d8b-bf27581ec544
uniqueidentifier در SQL Server بر اساس ساختار حافظهای تاریخچهای کتابخانه OLE DB / RPC ویندوز مرتب میشود. پایگاه داده SQL Server ساختار بایتهای آرایه GUID را با ترتیب زیر (از باارزشترین به کمارزشترین برای مقایسه و سورت) ارزیابی میکند:Guid.CreateVersion7() رفتاری دقیقاً مشابه یک GUID کاملاً تصادفی دارد.SqlGuid (واقع در فضاینام System.Data.SqlTypes) استفاده کرد که منطق مقایسهای دقیقاً همگام با موتور SQL Server را پیادهسازی میکند. آزمایشی شبیهسازی شد که طی آن ۵٬۰۰۰ کلید با فاصله زمانی ۱ میلیثانیه درج شده و نرخ قرارگیری کلید در انتهای شاخص سنجیده شد:using System.Data.SqlTypes;
static double PercentAtEnd(Func<Guid> generate, int count = 5_000)
{
var max = new SqlGuid(generate());
var atEnd = 0;
for (var i = 0; i < count; i++)
{
Thread.Sleep(1);
var next = new SqlGuid(generate());
if (next > max)
{
max = next;
atEnd++;
}
}
return 100.0 * atEnd / count;
}
Console.WriteLine($"Guid.NewGuid(): {PercentAtEnd(Guid.NewGuid):F1}%");
Console.WriteLine($"Guid.CreateVersion7(): {PercentAtEnd(Guid.CreateVersion7):F1}%");
Console.WriteLine($"Reshuffled version 7: {PercentAtEnd(CreateVersion7ForSqlServer):F1}%");Guid.NewGuid(): ۰.۱٪ درج در انتها (کاملاً تصادفی)Guid.CreateVersion7(): ۰.۲٪ درج در انتها (رفتار مشابه تصادفی)Reshuffled version 7: ۱۰۰.۰٪ درج در انتها (کاملاً ترتیبی)public static Guid CreateVersion7ForSqlServer()
{
Span<byte> rfc = stackalloc byte[16];
Guid.CreateVersion7().TryWriteBytes(rfc, bigEndian: true, out _);
Span<byte> sql = stackalloc byte[16];
// انتقال برچسب زمانی ۴۸ بیتی به باارزشترین بخش مقایسه در SQL Server
rfc[0..6].CopyTo(sql[10..]);
rfc[6..8].CopyTo(sql[8..]);
rfc[8..10].CopyTo(sql[6..]);
rfc[10..12].CopyTo(sql[4..]);
rfc[12..16].CopyTo(sql[0..]);
return new Guid(sql);
}نکته معماری مهم: این کار باعث میشود شناسه نهایی دیگر یک استاندارد معتبر RFC 9562 نباشد. اگر سیستمهای خارجی یا سرویسهای جانبی بر مبنای متن استاندارد UUIDv7 وابسته به استخراج برچسب زمانی باشند، این روش پروتکل آنها را دچار خطا میکند.
SequentialGuidValueGenerator برای تولید شناسهها استفاده میکند که بایتها را مطابق با ساختار مقایسهای SQL Server مرتب میسازد. انتساب دستی شناسه از طریق Guid.CreateVersion7() در کد دامنه، این ویژگی بهینهساز را نادیده گرفته و غیرفعال میکند.NEWSEQUENTIALID()CREATE TABLE dbo.Orders
(
Id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID() PRIMARY KEY CLUSTERED,
OrderDate DATETIME2 NOT NULL
);SELECT
i.name AS IndexName,
ps.avg_fragmentation_in_percent,
ps.avg_page_space_used_in_percent,
ps.page_count
FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID('dbo.Orders'), NULL, NULL, 'SAMPLED') AS ps
JOIN sys.indexes AS i
ON i.object_id = ps.object_id AND i.index_id = ps.index_id;ALTER INDEX ALL ON dbo.Orders REBUILD;
Guid.CreateVersion7() در داتنت پیشرفتی چشمگیر برای پایگاههای داده استاندارد (نظیر PostgreSQL) به شمار میروند؛ با این حال، تفاوت تاریخی SQL Server در تقدم ارزیابی بایتها باعث میشود استفاده مستقیم از آن به افت عملکرد و شکست صفحات بینجامد. برای پروژههای متکی بر SQL Server، انتخاب هوشمندانه بین NEWSEQUENTIALID()، مولد پیشفرض EF Core یا شناسههای اختصاصی بازآراییشده، کلید حفظ مقیاسپذیری و بهرهوری سیستم خواهد بود.