ULID默认不按时间排序是因为其字符串比较依赖Base32编码后的字典序,而MySQL/PostgreSQL若使用非二进制类型或不兼容排序规则(如utf8mb4_unicode_ci)会破坏字典序一致性;正确做法是MySQL用BINARY(16)、PostgreSQL用BYTEA存储toBinary()结果,并避免UUID字段或大小写归一化排序规则。

Ulid 可以直接用作时间有序的数据库主键,但必须配合正确的存储类型和索引策略,否则排序优势会失效。
Ulid 生成后为什么默认不按时间排序?
Ulid 本质是 Base32 编码的 128 位值(10 字符),其二进制结构包含时间戳前缀,但字符串比较依赖编码后的字典序。MySQL 或 PostgreSQL 默认对 VARCHAR 字段做字节序比较,而 Base32 编码保证了时间戳高位在前 —— 所以只要字段类型不破坏原始顺序,ORDER BY ulid ASC 就能天然按生成时间排序。
常见错误是把 Ulid::toBase32() 存进 CHAR(10) 但用了 utf8mb4_unicode_ci 排序规则,它会对大小写、连字符等做归一化处理,破坏字典序一致性。应改用 utf8mb4_bin 或直接用 BINARY(10) 类型。
- MySQL 中推荐:用
BINARY(10)存储,避免字符集干扰 - PostgreSQL 中推荐:用
BYTEA或CHAR(10)+COLLATE "C" - 别用
UUID类型字段存 ULID —— 它是为 RFC 4122 UUID 设计的,长度和校验逻辑都不匹配
Doctrine 实体里怎么正确映射 Ulid 主键?
Doctrine 不原生支持 ULID,不能像 @ORM\GeneratedValue(strategy="UUID") 那样自动处理。你得手动控制生成和赋值时机,否则 ORM 可能尝试用数据库自增或序列,导致冲突。
关键点是:禁用 Doctrine 的 ID 生成策略,改用构造时生成,并确保主键字段可写:
- 实体类中去掉
@ORM\GeneratedValue,只保留@ORM\Id和@ORM\Column(type="binary", length=10) - 在
__construct()里调用new Ulid()并赋值给主键属性 - 如果用 Symfony 表单,记得在
UlidType上设disabled=true,防止用户篡改
示例片段:
use Symfony\Component\Uid\Ulid;
class Post
{
#[ORM\Id]
#[ORM\Column(type: 'binary', length: 10)]
private string $id;
public function __construct()
{
$this->id = (new Ulid())->toBinary(); // 注意:不是 toString() 或 toBase32()
}
}
Ulid 的 toBinary() 和 toBase32() 哪个该存进数据库?
选 toBinary()。Ulid 内部是 128 位整数,toBinary() 返回原始 16 字节二进制串(PHP string),而 toBase32() 是人类可读的 10 字符字符串,多一层编码开销且占更多空间(即使存 CHAR(10),也比 BINARY(10) 多字符集处理负担)。
性能差异明显:BINARY 类型索引更紧凑,比较更快;Base32 字符串在 WHERE 查询中需隐式转换,尤其跨数据库迁移时容易出错。
- MySQL:用
BINARY(16)存toBinary()结果(注意长度是 16,不是 10) - PostgreSQL:用
BYTEA,对应 PHP 的toBinary() - 千万别用
toBase32()存进UUID字段 —— 校验会失败,Ulid::fromString()解析也会报InvalidUlidException
为什么 Ulid 比 UUID v7 更适合某些场景?
Ulid 和 UUID v7 都是时间优先设计,但 Ulid 更轻量:它没有版本号、变体位、保留字段这些 RFC 9562 规定的元信息,纯时间+随机熵,生成快、解析快、存储省。
实际影响在于:
- Ulid 的
toBinary()是固定 16 字节,UUID v7 的二进制表示含额外标志位,长度虽同为 16 字节但结构更复杂 - Ulid 的 Base32 编码无分隔符、无大小写混用,URL 和日志中更干净;UUID v7 默认格式仍带横线
- 如果你不需要 RFC 兼容性(比如对接外部系统要求 UUID v7 标准),Ulid 就是更纯粹的时间有序 ID
真正容易被忽略的是时钟精度:Ulid 时间戳精度为毫秒,同一毫秒内靠随机部分区分;若业务并发极高(如每毫秒插入数千条),仍需确认应用层是否做了重试或降级兜底 —— 这不是 Ulid 的缺陷,而是所有时间基 ID 的共性约束。


















