
Última atualização em 17 de novembro de 2025 por Willian Tuttoilmondo
Quando escrevi este artigo sobre multi-tenacidade, bancos de dados e chaves primárias, usei como exemplo um modelo que está disponível em quase todos os bancos de dados mais usados do mercado: o UUID. Como tudo evolui quando se fala em desenvolvimento e arquitetura, o UUID acabou revelando suas desvantagens e para que elas fossem superadas, outro modelo foi criado: o ULID. Mas, o que é o ULID e como usá-lo no meu dia a dia?
Desvendando o ULID
ULID, do Inglês Universally Unique Lexicographically Sortable Identifier, é um identificador único e ordenável formado por duas informações distintas: 48 bits que indicam os milissegundos de um Unix Epoch Timestamp e 80 bits de dados aleatórios, codificadas como uma string de 26 caracteres em Base32 Crockford, tornando-a mais compacta e eficiente para armazenamento e leitura do que um UUID tradicional. Num exemplo muito simpes, vamos criar sete identificadores ULID e ver como eles se ordenam.

Como é possível ver, todos os identificadores gerados exatamente no mesmo milissegundo possuem o mesmo início, o que nos permite ordenar o registro pelo momento de sua criação. Ao compararmos com o UUID veremos que essa vantagem não se mantém.

A primeira vantagem já se mostra aí. Enquanto o UUID não pode ser ordenado, o ULID nos permite fazer esta ordenação. Isso garante que os registros, independentemente da origem, possam ser visualmente ordenados pela chave primária quando nenhum outro ordenador for usado. Vale lembrar que, assim como o UUID, o ULID pode ser gerado pela aplicação.
A receita do bolo
A ideia por trás do ULID é simples: usar o momento da criação do valor para codificar seu valor. Os dez primeiros caracteres compõem a parcela codificada do timestamp do momento da geração. Por isso, no exemplo, como vários identificadores foram gerados no mesmo milissegundo, todos eles têm o mesmo início: 01K9SFSMXV. Vale lembrar que esta codificação é feita em Base32 Crockford, que utiliza apenas os caracteres 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, G, H, J, K, M, N, P, Q, R, S, T, V, W, X, Y e Z. Isso garante que o caractere O não seja confundido com o caracter 0, por exemplo. O restante, aleatório, é gerado da mesma forma, usando a mesma base.
O segredo, no entanto, está na codificação da data.
Para efetuarmos a codificação, vamos precisar definir duas constantes: um vetor com os caracteres permitidos e a largura deste vetor. Para isso, vamos defini-las da seguinte forma:
1 2 3 4 5 6 | const BASE_32: array [0..31] of Char = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z'); ENCODING_LENGTH = 32; |
Depois disso, precisamos codificar o valor em uma string de 10 bytes com base no Unix Epoch Timestamp que definirmos como base. Para garantir a ordenação pelo horário da criação do valor, usaremos como base o valor de data e hora correntes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | function GetEncodedTime: string; var Epoch: Int64; i: Byte; Module: Integer; const TIME_LENGTH = 10; begin Epoch := MilliSecondsBetween(TTimeZone.Local.ToUniversalTime(Now), UnixDateDelta); Result := EmptyStr; for i := TIME_LENGTH downto 1 do begin Module := Epoch mod ENCODING_LENGTH; Result := BASE_32[Module] + Result; Epoch := Trunc((Epoch - Module) / ENCODING_LENGTH); end; end; |
Já para o restante, basta que usemos dados aleatórios determinados pela constante de caracteres válidos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | const KEY_LENGTH = 16; function GenerateRandom: string; var i: Byte; begin Result := EmptyStr; Randomize; for i := 1 to KEY_LENGTH do begin Result := Result + BASE_32[Random(Pred(ENCODING_LENGTH))]; end; end; |
A desvantagem que temos, nesse caso, é que não existe uma estrutura nativa na linguagem Delphi – na verdade, em nenhuma linguagem de programação – que trate ULID. Para isso, precisamos criar uma estrutura e implementá-la. Para resolver esta questão, criei um tipo chamado TULID que cria e manipula ULIDs de maneira simples e rápida.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | unit Framework.Types.ULID; interface uses System.DateUtils, System.SysUtils; type TULID = packed record private const BASE_32: array [0..31] of Char = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z'); ENCODING_LENGTH = 32; KEY_ALLOWED_CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; KEY_FIRST_CHAR: Char = '0'; KEY_LAST_CHAR: Char = 'Z'; KEY_LENGTH = 16; KEY_START_POSITION = 11; ULID_LENGTH = 26; class function GenerateRandom(const ASeries: string = ''): string; static; inline; class function GetEncodedTime: string; static; inline; class function GetEpoch: Int64; static; inline; class function ValidateSeries(const ASeries: string): Boolean; static; inline; public class function New: string; overload; static; inline; class function New(const ASeries: string): string; overload; static; inline; class function Next(const AULID: string): string; static; inline; class function Prior(const AULID: string): string; static; inline; end; EULIDException = class(Exception); implementation { TULID } class function TULID.New: string; begin Result := TULID.GetEncodedTime + TULID.GenerateRandom; end; class function TULID.New(const ASeries: string): string; begin Result := TULID.GetEncodedTime + TULID.GenerateRandom(ASeries); end; class function TULID.Next(const AULID: string): string; var i: Byte; const LAST_ULID = 'ZZZZZZZZZZZZZZZZ'; LAST_ULID_ERROR_MESSAGE = '"%s" is the last ULID value for its millisecond'; begin Result := AULID; if Copy(Result, KEY_START_POSITION, KEY_LENGTH) = LAST_ULID then begin raise EULIDException.Create(Format(LAST_ULID_ERROR_MESSAGE, [AULID])); end; for i := ULID_LENGTH downto KEY_START_POSITION do begin case Result[i] = KEY_LAST_CHAR of True : Result[i] := KEY_FIRST_CHAR; False: begin Result[i] := KEY_ALLOWED_CHARS[Succ(Pos(Result[i], KEY_ALLOWED_CHARS))]; Break; end; end; end; end; class function TULID.Prior(const AULID: string): string; var i: Byte; const FIRST_ULID = '0000000000000000'; FIRST_ULID_ERROR_MESSAGE = '"%s" is the first ULID value for its millisecond'; begin Result := AULID; if Copy(Result, KEY_START_POSITION, KEY_LENGTH) = FIRST_ULID then begin raise EULIDException.Create(Format(FIRST_ULID_ERROR_MESSAGE, [AULID])); end; for i := ULID_LENGTH downto KEY_START_POSITION do begin case Result[i] = KEY_FIRST_CHAR of True : Result[i] := KEY_LAST_CHAR; False: begin Result[i] := KEY_ALLOWED_CHARS[Pred(Pos(Result[i], KEY_ALLOWED_CHARS))]; Break; end; end; end; end; class function TULID.ValidateSeries(const ASeries: string): Boolean; var i: Byte; Temp: string; const FORBBIDEN_CHARS = ['O', 'I', 'L', 'U']; begin Result := True; Temp := UpperCase(ASeries); if ASeries = EmptyStr then begin Exit; end; for i := 1 to Length(ASeries) do begin Result := Result and (not CharInSet(Temp[i], FORBBIDEN_CHARS)); end; end; class function TULID.GetEncodedTime: string; var Epoch: Int64; i: Byte; Module: Integer; const TIME_LENGTH = 10; begin Epoch := TULID.GetEpoch; Result := EmptyStr; for i := TIME_LENGTH downto 1 do begin Module := Epoch mod ENCODING_LENGTH; Result := BASE_32[Module] + Result; Epoch := Trunc((Epoch - Module) / ENCODING_LENGTH); end; end; class function TULID.GenerateRandom(const ASeries: string): string; var i: Byte; const INVALID_SERIES_MESSAGE = '"%s" is not a valid series'; begin if not ValidateSeries(ASeries) then begin raise EULIDException.Create(Format(INVALID_SERIES_MESSAGE, [ASeries])); end; Result := ASeries; Randomize; for i := 1 to KEY_LENGTH - Length(ASeries) do begin Result := Result + BASE_32[Random(Pred(ENCODING_LENGTH))]; end; end; class function TULID.GetEpoch: Int64; begin Result := MilliSecondsBetween(TTimeZone.Local.ToUniversalTime(Now), UnixDateDelta); end; end. |
Implementando ULID no banco de dados
Assim como as linguagens de programação, os bancos de dados também não suportam nativamente o tipo ULID. Contudo, graças à comunidade, as plataformas livres vão sendo abastecidas com extensões que ajudam a tratar os dados de maneira simples e rápida.
Com gosto muito de usar o PostgreSQL para meus projetos, encontrei uma extensão muito boa, que me permite trabalhar com ULIDs de maneira simples e natural: a pgx_ulid. É ela quem me permite fazer o exercício lá do início, gerando as chaves em lote. Sua instalação é simples e rápida e pode ser feita tranquilamente. Como meu servidor PostgreSQL está instalado em uma máquina Linux, baixei o pacote para a versão do banco de dados – 18, no meu caso – aqui e fiz a instalação. Depois disso é só criar a extensão com o comando abaixo e voilà, teremos o tipo de dado e as funções básicas para operação disponíveis.
1 2 3 | CREATE EXTENSION IF NOT EXISTS ulid SCHEMA public VERSION "0.2.2"; |
Isso cria as funções abaixo:

Para mais informações, a página do projeto no GitHUb conta com uma boa documentação.
Mas, enfim, quais são as vantagens mesmo?
Além da vantagem de ser uma chave única, como já vimos aqui, se compararmos com o UUID, temos o seguinte:
- Classificável por tempo – Os primeiros 10 bytes contêm o Unix Epoch, que é classificado automaticamente;
- Base32 de Crockford – Codificação mais eficiente, sem distinção entre maiúsculas e minúsculas, sem caracteres especiais;
- Opção de monotonicidade – Pode gerar valores monotonicamente crescentes ou decrescentes para o mesmo timestamp e;
- Legível por humanos – Mais fácil de depurar e ler em logs e bancos de dados.
Beleza, e agora?
ULID é algo novo e que muita gente ainda não conhece. Como sempre gosto de ressaltar, é importante ter em mente que, independente do projeto, as escolhas feitas no seu início determinam o rumo que ele toma e como podemos agir para ampliar e manter sua escalabilidade. Aplicar ULID em um novo projeto não requer muito, porém, na maioria dos casos, se a equipe não for madura o suficiente ou não estiver disposta a entender coisas novas, de nada adinatará.
Mas, confiem em mim quando digo, este é um caminho sem volta. Uma vez que dominamos essa técnica para criar chaves primárias e utiliza-las para manter nossos dados com chaves únicas, independente de sua origem, nunca mais usaremos sequences…


Faça um comentário