PHP Symfony ULID与UUID的排序特性有什么区别
来源:17golang原创
时间:2026-10-13 19:29:57 213浏览 收藏
Symfony ULID 可以按时间前缀排序,但“UUID 无法排序”并不准确:UUIDv4 的随机值不表达生成先后,UUIDv6 和 UUIDv7 则为时间排序设计。真正需要比较的是版本、编码表示和生成器范围,而不是只比较 ULID 与 UUID 两个名字。
官方地址:https://symfony.com/doc/current/components/uid.html
下面以订单写入服务的设计案例讨论选择,不包含实际线上改造或压测结果。PHP 示例面向支持 Uuid::v7() 的 Symfony UID 组件;旧项目应以已经安装的组件版本为准。
订单增多后,为什么按 ID 排序不等于按下单时间排序
假设订单由多个 PHP worker 创建,列表需要展示最近下单的订单。原方案使用 UUIDv4,并直接按 ID 降序取数据。这会得到一个确定的字符串顺序,却不是用户期待的时间顺序。增加索引只能让数据库更有效地执行这个错误的排序要求,不能给随机标识补上业务时间。
这里应拆开两个目标:标识负责唯一定位记录;业务时间负责表达事件发生时刻。即使换成带时间前缀的 ID,生成时刻也可能与客户端下单时刻、服务接收时刻、事务提交时刻不同。延迟提交和补录订单都会暴露这种差异。
ULID、UUIDv4 和 UUIDv7,排序依据分别是什么
| 标识 | 排序依据 | 不能推导的结论 |
|---|---|---|
| ULID | 时间前缀在前,尾部区分同一时间内的值 | 不同进程的同毫秒值不代表统一生成顺序 |
| UUIDv4 | 随机内容形成值顺序 | 值越大不代表生成越晚 |
| UUIDv6 | 重新排列时间字段,使高位时间优先 | 不是事务提交序号 |
| UUIDv7 | Unix 毫秒时间位于高位,其余部分按生成器实现分配 | 不能保证多个节点严格按业务事件先后排列 |
ULID 规范定义 48 位毫秒时间和 80 位尾部,并采用固定长度的 Crockford Base32 表示。规范的一般排序规则不保证同毫秒内的先后;单调生成器可以递增尾部。Symfony 文档进一步说明其同一进程、同一毫秒生成的 ULID 通过递增尾部保持单调性。不要把这个局部条件扩展成整个集群的承诺。
UUIDv7 的标准布局同样把 48 位 Unix 毫秒时间放在高位;版本和变体占用固定位置,余下部分允许采用随机数据、子毫秒精度或计数等方式。Symfony 的具体实现行为与 UUIDv7 标准允许的全部行为不是一回事;不能把某个生成器的额外保证归给所有 UUIDv7 库。

在 PHP 中比较同类标识,而不是混合不同编码
先用对象比较明确表达“按标识值排序”。下面的两个数组分别使用 ULID 和 UUIDv7,不把它们拼成一个混合列表,也不把显示格式当作业务时间。代码只展示最小写法,未在本文环境执行。
$a->compare($b)); usort($uuids, static fn (Uuid $a, Uuid $b): int => $a->compare($b)); // 统一输出表示,避免同一列混用 Base32、Base58 与带连字符格式。 $ulidValues = array_map(static fn (Ulid $id): string => $id->toBase32(), $ulids); $uuidValues = array_map(static fn (Uuid $id): string => $id->toRfc4122(), $uuids); // 这里返回给调用方,不伪造任何具体运行结果。 return ['ulid' => $ulidValues, 'uuid_v7' => $uuidValues];
compare() 给出值的大小关系,不会检查数据库提交顺序。同样,把 UUIDv4 数组交给它也能排序,只是排序结果没有时间含义。混合 UUIDv4 与 UUIDv7 时,可以形成值顺序,但不应把整个集合称作统一的时间序列。
改造存储时,把编码约定与业务排序一起写清
如果使用规范化文本存储 ULID,就固定大小写、长度和按字符值比较的排序规则;不要依赖可能改变字符权重的语言排序规则。UUID 文本也应统一为同一种规范表示。若采用二进制存储,则应用写入、数据库比较和读取转换必须采用一致的字节布局,不能一部分原样保存、一部分自行交换字节。
这是工程约束而非某一种数据库的通用性能保证。UID 的二进制体积仍是 128 位,换成时间有序标识不会把它缩成整数主键,也不会自动优化按客户、状态等字段过滤的查询。若现有 API 要求 UUID 格式,应优先评估 UUIDv7,而不是仅因 ULID 文本更短就改动全部外部接口。
列表的推荐约定是:以明确语义的 created_at 表示业务创建时间,以唯一 id 消除相同时间的并列。这两个字段共同形成稳定的排序键。若业务要求提交顺序,应另建满足该需求的序列或日志机制,不能靠解释 ID 来代替。
-- 示意查询:两个时间参数绑定同一游标时间;ID 参数按列的实际存储类型绑定。 SELECT id, created_at FROM orders WHERE created_at > :cursor_time_gt OR (created_at = :cursor_time_eq AND id > :cursor_id) ORDER BY created_at ASC, id ASC LIMIT 50;
示例采用向前升序分页;首页没有游标时省略整个 WHERE 条件。时间字段需统一精度、时区和空值策略,并尽量保持不可修改。该规则给相同时间的记录一个确定次序,但不是跨分页请求的数据库快照;并发插入、删除或修改仍需按业务需要决定一致性策略。

上线后看哪些信号,哪些收益不能提前承诺
从 UUIDv4 迁到 ULID 或 UUIDv7,可能改善某些有序索引的写入局部性,但本文没有实测数据,不能承诺延迟下降、吞吐翻倍或消除全部页分裂。比较方案时,应使用相同负载、存储类型、索引结构与数据规模,记录写入延迟、索引占用和目标查询开销。
更先要关心的是业务结果:分页是否因为并列时间而重复、是否漏掉补录记录、API 是否接受规范化后的 ID、旧记录是否仍可查询。迁移时保留旧 ID 的定位能力,不要把“格式可转换”理解成任意 UUID 都能无损变为同一时刻的 ULID。时间有序标识可能暴露生成时间信息,外部系统也不应把 ID 当作秘密或访问授权。
常见疑问
ULID 排序后就能删掉 created_at 吗?不能。生成时刻与业务时间的含义不同,业务查询应保留明确字段。
UUIDv7 和 ULID 混用能按时间一起排序吗?不应直接比较两种显示字符串。需要跨类型展示时,使用独立的业务时间和明确的唯一排序键。
选 ULID 还是 UUIDv7?需要短一些的 Base32 文本且生态可接受时考虑 ULID;已有 UUID 接口契约且需要时间有序生成时考虑 UUIDv7。两者都不能承担集群全局提交顺序。
选择时先写清“按什么时间、在什么范围、以什么表示比较”,再确定标识类型。把存储标识、业务时间与分页顺序分开建模,比单纯更换 ID 名字更重要。
参考资料:Symfony UID:https://symfony.com/doc/current/components/uid.html;ULID 规范:https://github.com/ulid/spec;UUID 标准:https://www.rfc-editor.org/rfc/rfc9562.html。
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
446 收藏
-
276 收藏
-
114 收藏
-
151 收藏
-
258 收藏
-
368 收藏
-
188 收藏
-
243 收藏
-
495 收藏
-
102 收藏
-
231 收藏
-
381 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习