C# ABP 프레임워크에서 데이터베이스별 순차적 GUID 생성을 통한 인덱스 단편화 최소화

ABP(Abp.io) 기반 애플리케이션에서 GUID를 기본 키로 사용할 경우, 무작위 GUID는 인덱스 단편화를 유발하여 성능 저하를 초래할 수 있다. 이를 해결하기 위해 데이터베이스 종류에 따라 최적화된 순차적(Sequential) GUID를 생성하는 구현 방법을 소개한다.

순차 GUID 생성기 인터페이스

public interface IGuidGenerator
{
    Guid Create();
}

데이터베이스별 순차 GUID 구현

using System;
using System.Security.Cryptography;

public class SequentialGuidGenerator : IGuidGenerator
{
    public static readonly SequentialGuidGenerator Instance = new();

    private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create();

    public DatabaseType DbType { get; set; } = DatabaseType.SqlServer;

    private SequentialGuidGenerator() { }

    public Guid Create() => Create(DbType);

    public Guid Create(DatabaseType dbType)
    {
        return dbType switch
        {
            DatabaseType.SqlServer => Generate(SequenceType.AtEnd),
            DatabaseType.Oracle => Generate(SequenceType.AsBinary),
            DatabaseType.MySql or DatabaseType.PostgreSql => Generate(SequenceType.AsString),
            _ => throw new NotSupportedException($"Unsupported database type: {dbType}")
        };
    }

    private Guid Generate(SequenceType seqType)
    {
        byte[] randomPart = new byte[10];
        _rng.Locking(r => r.GetBytes(randomPart));

        long timestamp = DateTime.UtcNow.Ticks / 10_000L; // 밀리초 단위
        byte[] timeBytes = BitConverter.GetBytes(timestamp);

        if (BitConverter.IsLittleEndian)
            Array.Reverse(timeBytes);

        byte[] guidBytes = new byte[16];

        switch (seqType)
        {
            case SequenceType.AsString:
            case SequenceType.AsBinary:
                Buffer.BlockCopy(timeBytes, 2, guidBytes, 0, 6);
                Buffer.BlockCopy(randomPart, 0, guidBytes, 6, 10);

                if (seqType == SequenceType.AsString && BitConverter.IsLittleEndian)
                {
                    Array.Reverse(guidBytes, 0, 4);
                    Array.Reverse(guidBytes, 4, 2);
                }
                break;

            case SequenceType.AtEnd:
                Buffer.BlockCopy(randomPart, 0, guidBytes, 0, 10);
                Buffer.BlockCopy(timeBytes, 2, guidBytes, 10, 6);
                break;
        }

        return new Guid(guidBytes);
    }

    public enum DatabaseType
    {
        SqlServer,
        Oracle,
        MySql,
        PostgreSql
    }

    private enum SequenceType
    {
        AsString,
        AsBinary,
        AtEnd
    }
}

락 확장 메서드

public static class LockExtensions
{
    public static void Locking<T>(this T obj, Action<T> action) where T : class
    {
        lock (obj)
        {
            action(obj);
        }
    }
}

사용 예시

for (int i = 0; i < 100; i++)
{
    var id = SequentialGuidGenerator.Instance.Create(SequentialGuidGenerator.DatabaseType.SqlServer);
    Console.WriteLine($"{id}   {i}   {DateTime.Now.ToFileTime()}");
    Thread.Sleep(1);
}

주의할 점은 SQL Server의 경우, GUID가 문자열 형태로 출력될 때 순차적으로 보이지 않더라도, 내부적으로 uniqueidentifier 타입으로 저장 및 정렬 시 순차성을 유지한다는 것이다. 이는 SQL Server의 고유한 정렬 규칙 때문이며, 다음과 같은 특징을 가진다:

  • 1~8바이트: 왼쪽에서 오른쪽으로 정렬
  • 9~10바이트: 오른쪽에서 왼쪽으로 정렬
  • 11~16바이트: 오른쪽에서 왼쪽으로 정렬

반면, Oracle은 RAW(16) 타입으로 바이트 배열을 저장하며 .NET의 기본 GUID 정렬과 동일하게 동작하고, MySQL 및 PostgreSQL은 CHAR(36) 형식의 문자열로 저장되므로 문자열 사전순 정렬 규칙을 따른다. 따라서 각 데이터베이스의 저장 및 정렬 방식에 맞춰 GUID의 순차 부분 위치를 조정해야 인덱스 성능을 극대화할 수 있다.

태그: C# ABP SequentialGUID SQLServer Oracle

10월 4일 10:34에 게시됨