避免使用 UUID 第 4 版主键(适用于 Postgres)
鉴于其性能缺陷、普遍误解及可替代方案的存在,我最终形成了一个简单立场:全面避免使用UUID,
- 简介
- 本文涉及的UUID背景
- Web应用使用场景及其规模
- 随机性才是关键问题
- 为何选择UUID?从单个或多个客户端应用程序生成值
- 常见误区:UUID是安全的
- 使用整数生成混淆值
- 反对使用UUID的普遍理由:它们占用大量空间
- 反对理由:UUID v4因索引页拆分和碎片化增加插入延迟
- 即使使用可排序UUID,查找操作仍会产生过多I/O
- 整数、UUID v4 与 UUID v7 的使用实践
- 使用 pageinspect 扩展检查密度
- UUID的弊端:更低的缓存命中率
- 缓解方案:使用UUID值重建索引
- 缓解方案:共享缓冲区与工作内存大小配置
- Rails中的缓解方案:UUID与隐式排序列的Active Record
- 通过排序字段聚类缓解性能问题
- 建议:坚持使用序列、整数和大整数
- UUID v4替代方案:使用时间排序的UUID(如Version 7)
- 总结
- 深入了解
- 更新
引言§
过去十年间,当使用UUID版本4[1]作为主键数据类型的数据库时,这些数据库通常存在性能低下和IO消耗过大的问题。
UUID是Postgres的原生数据类型,以二进制数据形式存储。RFC中定义了多种UUID版本。版本4主要由随机位组成,掩盖了生成时间和生成位置等信息。
自Postgres 13版(2020年发布)起,可通过gen_random_uuid()[2]函数轻松生成版本4 UUID。
我发现人们对UUID版本4存在诸多误解,这些误解往往成为用户选择该数据类型的原因。
鉴于其性能缺陷、普遍误解及可替代方案的存在,我最终形成了一个简单立场:避免将UUID版本4用作主键。
我更具争议的观点是全面避免使用UUID,但我理解在缺乏实际替代方案时仍存在合理使用场景。
作为数据库爱好者,我希望在这个经典的“整型 vs UUID”辩论中阐明立场。
在数据库圈内,这类讨论或许已成陈词滥调。但基于咨询工作经验,我可以肯定:2024年乃至2025年仍会遇到使用UUID v4的数据库,本文探讨的问题依然存在。
让我们深入探讨。
本文UUID背景§
- UUID(或微软术语中的GUID)[3]是由36个字符组成的长字符串,包含32个数字和4个连字符,以128位(16字节)值存储,在Postgres中使用二进制
uuid数据类型存储 - RFC文档规定了128位的设置规则
- UUID 版本 4 的位元值多为随机数
- UUID 版本 7 在前 48 位包含时间戳,相较于随机值,该设计能显著提升数据库索引性能
尽管截至本文撰写时尚未发布,且此前已从 Postgres 17 中移除,但 UUID V7 作为 Postgres 18[4] 的组成部分,计划于 2025 年秋季发布。
本文讨论的应用数据库范围是哪些?
Web应用使用场景及其规模§
本文讨论的Web应用类型为采用Postgres作为主要OLTP数据库的单体应用,涵盖社交媒体、电子商务、点击追踪及业务流程自动化等领域。
文中探讨的性能问题均与存储检索效率低下相关,因此适用于上述所有应用类型。
UUID v4的核心问题是什么?
随机性是症结所在§
UUID Version 4的核心问题在于其122位组成值属于“随机或伪随机生成值”[1:1],这导致索引维护效率低下。由于主键默认由索引支持,相较于顺序值插入,每次插入操作的效率都会降低。
在查询场景中,由于Postgres中非顺序索引页面的遍历增加,对单个项或项范围的每次更新和删除操作效率均会下降。
由于随机生成的值并非按顺序(或在顺序/相邻页面中)插入,后续查找以进行更新或删除时效率较低。上述各类工作负载均使用主键索引。
UUID v4不具备与存储方式相匹配的有效自然排序,因此存储和检索效率均较低。
后续我们将具体分析:存储等效数据时Postgres需额外访问多少页,以及这对性能的影响。
尽管存在效率问题,但根据我在Postgres咨询领域的经验,UUID v4乃至UUID整体在过去十年仍(至少曾经)广受欢迎。
既然如此流行,UUID究竟有哪些应用场景?
为何选择UUID?从单个或多个客户端应用程序生成值§
UUID的一个典型场景是:当需要在客户端或多个服务端生成标识符,并将其传递给Postgres进行持久化存储时。
对于Web应用,通常会在内存中实例化对象,且不期望在实例作为行数据持久化存储前(此时数据库会生成标识符)使用该标识符进行查询。
在微服务架构中,当应用各自拥有数据库时,UUID可用于在不同数据库中生成无冲突的标识符。与整数相比,UUID还能在后续操作中标识值的来源数据库。
为避免冲突(参见HN讨论[5]),基于序列的整数无法提供同等保障。现有变通方案包括:两个实例间生成奇偶整数,或在int8范围内使用不同数值区间。
复合主键(CPK)等替代标识符虽可替代,但相同组合值无法唯一标识特定表。
维基百科[6]如此描述碰撞概率:
生成随机版本4 UUID时,达到50%碰撞概率所需数量:2.71京
该数字相当于:
以每秒10亿次生成速度持续约86年。
UUID是否安全?
误区:认为UUID是安全的§
关于UUID的一个常见误解是认为其具有安全性。然而RFC明确指出,不应将其视为安全的“能力”。
摘自RFC 4122[1:2]第6节安全考量:
切勿认为UUID难以被猜出;它们不应作为安全能力使用
如何用整数生成混淆代码?
使用整数生成混淆值§
虽然UUID V4能混淆生成时间,但其值无法排序以显示相对生成时间。通过稍作调整,我们也能用整数实现这些特性。
一种方案是:从整数生成伪随机码,在外部使用该值,同时内部仍保留整数处理。
如需查看该方案的完整细节,请参阅:Postgres中的短字母数字伪随机标识符[7]
本文将进行概要说明:
- 将十进制整数(如“2”)转换为二进制位。例如4字节32位整数:00000000 00000000 00000000 00000010
- 对所有位执行异或(XOR)运算,使用密钥
- 采用Base62字符集对每一位进行编码
混淆后的ID存储于生成列中。观察生成的值可发现,它们看似相似但并非按创建顺序排列。
按插入顺序排列的值为01Y9I、01Y9L、01Y9K。
按字母顺序排序时,后两项会颠倒:01Y9I排首,01Y9K居次,01Y9L居末(第五字符决定排序)。
若需将此方案应用于所有表,可尝试建立多态性集中表:为每个使用代码的表存储记录(并设置外键约束)。
这样就能追踪代码的使用位置。
还有哪些理由可能让我们放弃UUID?
反对使用UUID的普遍原因:它们占用大量空间§
每个UUID占用16字节(128位),是bigint(8字节)的两倍,更是4字节整数的四倍。当表中存有数百万行数据时,这种额外空间会累积成显著负担,尤其在数据库备份还原过程中频繁移动副本时。
但更严重的影响在于随机数据写入索引时的低效特性。
反对理由:UUID v4会因索引页拆分和碎片化增加插入延迟§
对于随机UUID v4,Postgres每次插入操作都会产生额外延迟。
整型主键行值存储于索引页中,通过“叶节点”的“主要追加”操作维护——因其值可排序,且B树索引按排序顺序存储条目。
而UUID v4在B树索引中的主键值存在问题。
插入操作不会追加到最右侧叶页,而是随机分配到可能处于页面中部或已满的页面,导致本可避免的页面分裂(若使用整数则不会发生)。
Planet Scale 提供了关于索引页面分裂与重新平衡的精彩图解。[8]
不必要的分裂与重新平衡增加了写入操作的空间消耗和处理延迟。这种额外I/O同样体现在写入前日志(WAL)的生成中。
Buildkite报告称采用时间排序UUID后,WAL的写入I/O减少了50%。
在固定页面的情况下,我们需要实现页面内的高密度存储。后续将使用pageinspect工具对比整数与UUID的叶节点平均密度,以辅助评估两种方案。
即使采用可排序UUID仍存在过量查找IO§
B-树页面布局意味着每个8KB页面能容纳的UUID数量较少。由于页面大小固定,我们至少需要实现尽可能高的数据压缩率。
由于UUID索引在叶页中的占用空间比bigint(int8)索引大约40%(在逻辑行数相同的情况下),其值的存储密度必然降低。正如Lukas所言:“总而言之,要实现Postgres的最佳I/O性能,物理数据结构与服务器配置同样重要。”[9]
这意味着在单行查询、范围扫描或更新操作中,UUID索引将因扫描更多页面而增加约40%的I/O开销。需注意:在Postgres中,即使访问单行数据,系统也会读取整页数据并复制到共享内存缓冲区。
让我们通过插入和查询数据,观察不同数据类型的具体数值表现。
整型、UUID v4 与 UUID v7 的操作§
创建整型、UUID v4和UUID v7字段,对其建立索引,并使用pg_prewarm加载至缓冲区缓存。
我将采用Ants Aasma在Cybertec文章PostgreSQL中UUID键的意外弊端中提供的模式示例。
查看andyatkinson/pg_scripts PR #20。
在我的Mac上,我编译了pg_uuidv7扩展。编译并启用后,即可使用扩展函数生成UUID V7值。
另一个扩展pg_prewarm被启用。该模块随Postgres自带,只需在使用数据库中启用即可。
测试中成功复现了延迟差异及缓冲区数量的巨大差异(如原文所述)。
“天哪,缓冲区数量简直庞然大物”- Ants Aasma
Cybertec测试结果:
- 27,332次缓冲区命中,
bigint列的索引仅扫描 - 8,562,960次缓冲区命中,UUID V4索引扫描
由于这些是缓冲区命中,数据直接从内存访问,速度快于磁盘读取。因此我们可以专注于不同数据类型造成的延迟差异。
UUID索引访问了多少额外页面?多访问了8,535,628个(850万!)8KB页面,增幅达31229.4%。换算为MB和MB/s:
- 访问数据量增加68,285,024 MB(约68.3 GB)
计算内存访问速度的低/高估算值:
- 低估算值:20 GB/s
- 高估算值:80 GB/s
从内存(PostgreSQL中的shared_buffers)访问68.3 GB数据将增加:
- ~3.4秒延迟(低速)
- ~0.86秒延迟 (高速场景)
仅基于数据类型差异,额外延迟就在1秒至3.4秒之间。此处测试采用1000万行数据执行100万次更新,但随着数据量和查询量增加,延迟将进一步恶化。
使用pageinspect扩展检查密度§
可通过pageinspect扩展检查叶页的平均填充率(密度)。
仓库中的 uuid_experiments/page_density.sql (andyatkinson/pg_scripts PR #20) 查询会获取整型、v4 和 v7 uuid 列的索引信息,包括总页数、页面统计数据及叶页数量。
该查询通过叶页数据计算平均填充率。
在对示例中提到的1000万行数据执行100万次更新后,该查询返回以下结果:
idxname | avg_leaf_fill_percent
---------------------+-----------------------
records_id_idx | 97.64
records_uuid_v4_idx | 79.06
records_uuid_v7_idx | 90.09
(3 rows)
可见integer索引平均填充率接近98%,而UUID v4索引约为79%。
UUID的缺点:更低的缓存命中率§
Postgres缓冲区缓存是性能优化的关键环节。
为确保性能,我们需要查询尽可能产生缓存“命中”。
缓冲区缓存空间有限。通常仅分配系统内存的25-40%,而包含表和索引数据的数据库总量往往远超该内存容量。这意味着我们需要权衡取舍——所有数据无法完全容纳于系统内存中,挑战由此产生!
访问页面时,它们会被复制到缓冲区缓存中作为缓冲区。当发生写操作时,缓冲区会被标记为脏缓冲区后再被刷新。[10]
由于UUID的存储位置是随机的,相比有序整数,需要复制更多缓冲区到缓存中。为腾出所需空间,缓冲区可能被驱逐,从而降低命中率。
缓解方案:重建UUID索引§
鉴于表和索引更易碎片化,定期重建是明智之举。
若能离线执行操作,可通过 pg_repack、pg_squeeze 或 VACUUM FULL 重建表。
索引可使用 REINDEX CONCURRENTLY 在线重建。
虽然页面中的数据布局已更新,但仍缺乏关联性,因此体积不会缩小。不过被删除数据原占用的空间将被回收再利用。
缓解方案:共享缓冲区与工作内存配置§
若条件允许,请将主实例内存配置为数据库大小的4倍。例如数据库为25GB时,应运行128GB内存实例。
这将为缓冲区缓存(shared_buffers)提供约32GB至50GB空间,理论上足以存储所有访问过的页面和索引条目。
使用 pg_buffercache[11] 检查缓存内容,通过 pg_prewarm[12] 将表数据预加载至缓存。
在处理需排序的UUID v4随机值时,我采用的策略是为排序操作分配更多内存资源。
要在 Postgres 中实现这一点,我们可以修改 work_mem 设置。该设置可针对整个数据库、会话甚至单个查询进行调整。
请参阅 PgMustard 上的 Postgres 中配置 work_mem,了解在会话中设置此参数的示例。
Rails 中的缓解方案:UUID 与隐式排序列 Active Record§
自 Rails 6 起,我们可控制隐式排序列[13] database_consistency gem甚至提供检查工具供使用UUID主键的开发者使用。
当查询中隐式生成ORDER BY时,建议改用其他高基数且已索引的字段排序,例如created_at时间戳字段。
通过可排序字段聚簇缓解性能问题§
对高基数且已索引的列进行聚簇可作为缓解方案。
例如,假设你的UUID主表包含一个带索引的created_at时间戳列(索引名为idx_on_tbl_created_at),此时可在此列上执行聚簇操作。
CLUSTER table_with_uuid_ok USING idx_on_tbl_created_at;
但实际中很少见到使用CLUSTER操作,因为它会获取访问独占锁。该操作仅执行一次,且需定期重复以维持其优势。
建议:坚持使用序列、整数及大整数§
对于规模可能较小且增长趋势未知的全新数据库,建议采用传统整数型主键配合序列支持的自增列[14]。此类主键采用32位有符号整数(4字节),每张表可提供约20亿个正向唯一值。
对于多数商业应用而言,单表数据量永远不会达到20亿,因此这种方案足以满足其整个生命周期需求。在其他场景中,我也始终建议使用bigint/int8类型。
归根结底取决于你对数据规模的认知以及增长趋势的预测能力。现实中存在大量增长缓慢的商业应用,它们往往受限于特定行业和有限的业务用户群体。
对于预期高增长的面向互联网的消费者应用(如社交媒体、点击追踪、传感器数据、遥测采集类应用),或迁移现有中大型数据库(含数亿至数十亿行数据)时,采用bigint(int8)64位8字节整型主键才是合理选择。
UUID v4替代方案:采用时间序列UUID(如Version 7§)
由于 Postgres 18 尚未发布,当前可通过 pg_uuidv7 扩展在 Postgres 中生成 UUID V7。
若现有数据库已填满 UUID v4 值且无法承担迁移至其他主键数据类型的成本,则开始使用 UUID v7 填充新值可起到一定缓解作用。
值得庆幸的是,Postgres的二进制uuid数据类型既可存储V4值也可存储V7值。
另一种依赖扩展的替代方案是sequential_uuids。[15]
总结§
- UUID v4会增加查询延迟,因其无法利用B树索引的快速有序查找特性
- 新建数据库时,请勿将
gen_random_uuid()设为主键类型,该函数生成随机UUID v4值 - UUID占用空间是
bigint的两倍 - 根据 UUID RFC 规范,UUID v4 值并非为安全性设计
- UUID v4 具有随机性。为保证性能,索引扫描时整个索引必须驻留缓冲区缓存,而数据量越大,这种可能性越低
- UUID v4会导致更多页面分裂,增加写入操作的I/O开销,加剧碎片化问题,并扩大WAL日志体积
- 若需生成不可猜测的混淆伪随机码,可采用整数生成方案替代UUID
- 若必须使用UUID,请选用可排序的UUID类型(如UUID v7)
深入学习§
- AWS英雄Franck Pachot对PostgreSQL中的UUID有独特见解
- Brandur发表精彩博文:身份危机:序列号与UUID作为主键的较量
- 5分钟了解Postgres:UUID与序列号作为主键——如何选择?
- andyatkinson/pg_scripts PR #20
更新§
- https://datatracker.ietf.org/doc/html/rfc4122#section-4.4 ↩︎ ↩︎ ↩︎
- https://www.postgresql.org/docs/current/functions-uuid.html ↩︎
- https://stackoverflow.com/a/6953207/126688 ↩︎
- https://www.thenile.dev/blog/uuidv7 ↩︎
- https://news.ycombinator.com/item?id=36429986 ↩︎
- https://en.wikipedia.org/wiki/Universally_unique_identifier ↩︎
- https://www.techug.com/generating-short-alphanumeric-public-id-postgres ↩︎
- https://planetscale.com/blog/the-problem-with-using-a-uuid-primary-key-in-mysql ↩︎
- https://pganalyze.com/blog/5mins-postgres-io-basics ↩︎
- https://stringintech.github.io/blog/p/postgresql-buffer-cache-a-practical-guide/ ↩︎
- https://www.postgresql.org/docs/current/pgbuffercache.html ↩︎
- https://www.postgresql.org/docs/current/pgprewarm.html ↩︎
- https://www.bigbinary.com/blog/rails-6-adds-implicit_order_column ↩︎
- https://www.cybertec-postgresql.com/en/uuid-serial-or-identity-columns-for-postgresql-auto-generated-primary-keys/ ↩︎
- https://pgxn.org/dist/sequential_uuids ↩︎
本文文字及图片出自 Avoid UUID Version 4 Primary Keys (for Postgres)
这是过早优化的典型案例。
永久标识符不应承载数据。这堪称数据管理的头号禁忌。你总会遇到这样的情况:自以为“这数据肯定不会变,塞进ID里省去查找步骤很安全”。结果突然有人发现自己有了新的性别认同,需要在身份证号末位添加数字。
即便数据不变也可能出问题。挪威公民编号的前六位是出生日期(DDMMYY格式)。这数据肯定不会变吧?错!虽然日期本身不变,但人们对日期的认知可能改变。不清楚确切出生日的移民默认被分配为1月1日…而真正的1月1日出生者却被告知:“抱歉,您不能用这个日期,该序列的数字已用完!”
模拟时代的图书馆员为节省检索时间而将数据塞进标识符的行为尚可原谅。当检索依赖实体卡片目录时,这种做法尚可理解(尽管他们同样可能因此陷入困境)。但当你手握强大的数据库时,请善加利用!别为了节省几毫秒就做出日后后悔的决定!
> 挪威人名编号的前六位是出生日期(DDMMYY格式)。这应该不会变吧?错!虽然日期本身不变,但你对日期的认知可能变。不清楚确切生日的移民默认被分配1月1日… 结果那些真实生日是1月1日的人被告知:“抱歉,您不能用这个日期,该序列的数字已用完!”
在我看来,你的例子真正揭示的是错误默认值的问题,而非将数据编码为键值本身的问题。若系统为未知值采用非日期格式——比如将日期/月份组件设为00或99——你描述的问题便不复存在。
但需注意,将时间戳编码为UUID的初衷并非赋予隐含意义,而是确保唯一性,同时附带ID大致单调递增的特性。这种特性是否真正必要取决于具体应用场景。但若作为数据库索引键使用,相较于完全随机的ID,它通常能提升性能——避免频繁重写B树的叶节点。当大量插入此类键值时,它们会在树的某侧形成聚簇,后续重新平衡时仅需重写顶层节点即可。
>在我看来,你的示例本质上揭示的是默认值设置不当的问题,而非数据编码为键值本身的问题。若他们为未知值选择非日期格式——比如用00或99表示日或月分量——那么你描述的问题就会消失。
但实际出生日期仍会引发相同问题,且需修改ID才能修正出生日期。
补充一点,生日数据同样可能出现聚集现象,就像任何看似“随机”的数据一样。
影响不大。实际出生数据中,某些节假日出生率极低,但从未出现过集中在特定日期的现象。
若完全随机,百万个散布在图表上的点仍可能精确落在同一坐标。
多数人直觉认定的随机性,实则是某种噪声函数——这种噪声通常呈分散分布,不会触发大脑的模式识别机制
但这种情况绝不会发生。即使尝试一万亿次,其概率也为0.00000000%。
> 大多数人直觉认定的随机性,本质上是一种分散的噪声函数,无法触发大脑的模式匹配机制
没错,当多数桶位空置时,人们对随机性的直觉判断确实存在偏差。但当事件数量比桶位数量多出几个数量级时,这种偏差就不成立了。此时结果会相当均匀,符合人们的预期。
> 但这种情况不会发生。即使尝试一万亿次,其发生概率也为0.00000000%。
它与随机分配的任意特定点阵配置概率相同。唯有人类过度活跃的模式匹配行为,才会将其视为特殊现象。
>它与随机分布的其他特定点阵组合概率相同
这在实践中毫无意义——所谓“概率相同”忽略了更分散的点阵组合在数量上远超该模式(甚至远超视觉上更规整的组合)的现实。
>人类过度活跃的模式识别行为才是其被特殊对待的唯一原因
不,关键还在于这是唯一一种配置,而其余所有配置的数量都大得多。仅凭这一点就足以让这种特定配置显得极其罕见(因为我们不是在它们之间比较,而是在所有其他配置的总和中进行比较)。
熵值揭示了其特殊性。假设你有百万个点和万个坐标,所有点落在同一坐标上的组合仅有万种可能,而每个坐标附近分布约百个点的组合则多达无穷无尽。
> 对我而言,你的例子真正揭示的是默认值设置不当的问题,而非数据编码为键值本身的问题。若他们为未知值选用非日期格式——比如用00或99表示日或月分量——你描述的问题便不复存在。
不过当移民群体中存在无法确定精确出生日期的人群时,数字资源终将耗尽
那岂不是要在所有系统中处理非日期值?如何判断无日期记录者是否已成年?是否符合养老金领取资格?
或许解决方案是将默认值均匀分布在365天内。
既然连生日都不知道,这类问题本就无法解答。
若仅知出生年份,且将未知月份设为99,那么算法会在次年年初判定其年龄符合要求——这或许正是合规所需的结果。
若连出生年份真伪都无法确认,正确流程就取决于政策了。或许选择任意年份,或许取可能年龄的最大值/最小值,也可能直接编码为0000/9999。
重申:若不知晓出生年份,便无法推算年龄。我不认为这意味着将生日嵌入身份证号的通用政策存在缺陷。
许多政府会为同一人重新签发不同编号的国民身份证,这远比那些向多人发放相同身份证号(美国社会安全号就是典型)的做法更合理。对于原本因出生日期不明而获发身份证的人,在确认生日后重新签发新证,似乎并不构成重大负担。或许可赋予当事人选择权:保留旧证明知会引发麻烦,或领取新证并承担告知他人证件号变更的责任。
推测将日期嵌入身份证号的政府,是因这种做法比随机分配号码更能满足其管理需求。
> 或选择新证件并承担告知他人号码变更的责任
又或者借此机会欺骗他人冒充他人身份。(例如:借100万美元贷款,宣告破产,待生日到来后再次贷款。)
比利时国民登记号采用类似结构:
YY.MM.DD-AAA.BB
AAA或BB组件中包含性别信息。
这意味着特定性别每日出生人数存在上限。
但对特定年份而言,使用化名只能延缓必然结果。虽然可用编号更多,但仍受限于某些必须反映现实的组成部分。年份、性别(若该字段仍保留?)等信息。
BB字段采用模97校验。AAA首字母A通过奇偶编码表示性别(具体是首字母还是末字母编码已记不清)。MM或DD若未知可设为00。此外MM在某些情况下会增加20或40。
若知晓某人生日与性别,INSZ号码出现概率约为500分之1,且高度偏向AAA数值较低的区间。所幸这类号码造成的危害有限(不像美国社保号),但仍建议保密处理。
> 我仍建议保密
爱沙尼亚的isikukood采用GYYMMDDNNNN格式,属于相对公开信息。若知晓查询渠道(此处不透露具体方式),可轻易查到我的编号。其危害性较低。
哈萨克斯坦的IIN格式为YYMMDDNNNNNN(其中N可能有特定结构),同样属于相对公开信息:例如个体经营者通常需将许可证张贴在墙上,上面会显示该号码。
这有点严重:我在邮局取件时,只需向工作人员出示IIN条形码。他们通常会扫描身份证上的条码,但我没有身份证,所以研究出格式后自制了.pkpass文件。全程零询问——“包裹在此,不需要护照,祝您愉快!”
(题外话:哈萨克斯坦邮局布局奇特得令人惊叹——简直和超市一模一样!你进去后按追踪号(我记得是这样)找到包裹,然后去结账。这种模式我别处从未见过。)
> 若他们为未知值选择非日期格式(例如日期或月份用00或99表示),那么你描述的问题就会消失
> 挪威个人编号的前六位数字即为出生日期(DDMMYY格式)
灾难已然在酝酿——某些程序始终要求后者格式
这并非解决之道,只是降低了出错概率
绝妙的现实案例。意大利个人编号还包含性别信息——这可是可以通过手术改变的,当处理大规模数据时终将撞上这堵墙。
不过我不同意绝对化的表述。永久标识符通常不应承载数据。某些场景下需要数据核对机制,或受限于存储空间/速度,此时可接受折中方案:对数据进行MD5哈希处理,以UUID形式存储在主索引中。索引虽会碎片化导致需执行真空操作,但整体运行仍可维持良好状态。
不知是否有意为之,但“大规模运行”这个说法真让我笑出声来 😀
我也忍不住笑了出来。
如何通过手术改变性别?
首选方法是性别确认手术。
“确认”意味着改变已经发生
没错,因为确实如此。性别认同的转变(或选择更公开地展现该认同)早已发生,手术只是对此的确认。
你无法做到,但既然性别并非由生理特征定义,便没有必要。
这仅适用于你采用极端特立独行的性别定义。对95%的英语使用者而言,性别由身体特征决定。
对近乎100%的布吉语使用者而言,五性别体系始终存在,他们会告诉你本族语言中对应的词汇。
* https://en.wikipedia.org/wiki/Buginese_language
这显然是文化建构。
物质的真实状态唯有固态、液态和气态。其余皆是觉醒主义的荒谬之谈。
我对此确信无疑,因数十年前小学便习得此理,人类不可能发现足以更新世界模型的全新知识。所有英语使用者都清楚“等离子体”和“玻色-爱因斯坦凝聚态”纯属杜撰。
我们都在等着你因发现第三类配子而获得诺贝尔奖。
这难道意味着数百年来英语使用者将帆船称为“她”,都是为了掩盖船只拥有晃动部位的阴谋?:p
Uuid v7 生成机制存在偏倚,并非承载信息。你不可能试图从 uuid 中提取时间戳。
选择随机生成而非时间偏倚的 uuid 绝非为了节省毫秒而做出的后悔决定。
这很可能是个能节省秒级的决策(没错,真的——尤其考虑局部效应时),你绝不会后悔。
我曾参与开发一个系统,其中使用ULID(非UUIDv7但原理相似)配合游标按时间顺序获取数据。然而某天突发状况——记录需回溯日期!这意味着要么伪造这些记录的ID(可能破坏其他地方的不变量),要么改进数据获取逻辑。
你可以选择永远不使用这个特性。但它确实诱人。
我曾用类似64位宽ULID的服务,但从未假设数据会早于最新记录插入或更新。
若领域模型涉及外部事件(如我的场景),且主键封装了外部时间戳,同时支持接收非时间顺序的事件,那么插入早于最新记录的数据便成为必然。
若将插入时间与事件实际发生时间混为一谈,就会出现“倒填日期”的问题。比如把两者当作相同概念处理,而它们本质上并不相同。
> 你不会试图从UUID中提取时间戳。
我在小型项目中确实用UUIDv7作为“插入时间”字段,并编写了查询方法:通过将时间戳转换为UUIDv7值,实现“WHERE id BETWEEN a AND b”的条件筛选。
> 你不会试图从UUID中提取时间戳。
海勒姆定律表明:总有人会这么做。
> 你不会试图从UUID中提取时间戳吧。
所以,随机库:https://pkg.go.dev/github.com/google/uuid#UUID.Time
> Time 返回自 1582 年 10 月 15 日起以纳秒为单位的时间戳(精确到 100 纳秒),该时间戳以 UUID 格式编码。此时间戳仅适用于版本 1、2、6 和 7 的 UUID。
> 你不会试图从UUID中提取时间戳吧。
什么?UUID7的前48位正是UNIX时间戳。
这究竟是UUID应用中的实际问题还是潜在优势,需要具体分析:某些场景下不容忽视,某些场景则无关紧要。
我明白你的意思——忽略时间戳特性能让UUID“更完美”,但这忽略了安全隐患和按高位(时间戳)分区的诱惑。
没人强制你使用真实的Unix时间戳。顺便说一句,原始Unix时间戳是32位(2038年失效),现在大家都转向64位time_t了。什么48位?
你需要的只是一个保证不递减的48位数。时钟是一种生成方式,但我不明白为何时钟偏移、快走或慢走会导致UUIDv7失效。我不会指望前48位是“真实”的时间戳。
> 没人强制要求使用真实的Unix时间戳。
除了UUIDv7规范之外?否则你生成的就只是某种任意UUID。
> 我不会指望前48位是“真实”的时间戳。
我同意;这正是我们讨论的本质风险——将可能并非数据的内容编码为不透明标识符所导致的隐患。
我个人并不像祖帖那样教条地坚持“主键标识符绝不应包含冗余数据”,但也不同意“直接使用UUIDv7并将UUID视为不透明数据”是完全可行的解决方案。
这就像HTML规范——没人会发布不符合规范的网页。;p
添加时间前缀的初衷是提升B树效率,但许多人采用客户端生成方式,这种方式不可靠。更关键的是,它本就只是ID而非时间记录机制,因此不应成为问题。
我的意思是,任何32位无符号整数在2038年1月19日前都是有效的Unix时间戳,由此推论,任何u64类型在更长时期内同样有效。
Unix时间戳的唯一保证是其值永不倒退,始终递增。这是UUID序列的整体特性,而非单个实例的属性。顶多可以说“完全有效的”UUIDv7不应包含遥远未来的时戳。但我认为只要时戳部分不倒退,包含过去任意时间点并无不可。
时间戳属性可能属于附加接口协议的一部分:例如“我们保证该值为UTC时间戳的UUIDv7,误差不超过一秒”。但我认为大多数理智的工程师不会提供此类保证。真正有用的保证是前缀的非递减特性,这使得排序成为可能。
这篇文章的有趣之处在于:对于小型数据库而言,这显然是过早优化;但当数据库规模达到需要这些优化时,你反而不想采用文中建议的做法。
具体而言:若数据库规模较小,性能影响可能微乎其微;若数据库庞大(例如主键超出32位整型容量),则必须考虑分片和系统分布式化——此时UUID比自动递增整型更具优势。
我认同存在一个临界点:低于该规模时(任何)优化都无关紧要,而超过该规模时则需要主键具备区域性(即明确记录由哪个分片/表块/…负责)。但…
* 我认为中间存在一个宽泛区间:若设计得当,数据库仍可部署于单台机器,但值得通过优化方案使用更廉价的机器和/或延迟迁移至分布式数据库的时间节点。你可能很快就会触及这个中间区间(且/或迁移过程可能足够痛苦),因此提前规划很有必要。
* 切换至分布式数据库时,并非总需重新分配所有键值:
** 可通过哈希查找或位反转将现有键值分散至不同分片。某些数据库(如DynamoDB)会强制执行此操作。
** 按旧方式分配新ID可能引发重大问题,但存在解决方案。若外部键值足够不透明,可完全切换分配方案而不被客户端察觉。若采用UUIDv7(该方案解决部分但非全部文章提及的问题),则可直接沿用。若坚持使用高密度(或近似高密度)、(基本)顺序的大整数,可通过批量预留区块来摊销延迟。
这其实是一个非常深刻且有趣的话题。从标识符中剥离信息会使数据与现实世界脱节,这意味着我们再也无法将它们匹配起来。但这种关联性正是最初保存数据的唯一目的。因此接下来发生的情况是:现实世界试图进行调整,而“无数据”的标识符便成了现实世界的产物。这种状况变得相同却更糟(例如:若你记不起社保号,便等同于不存在)。极端情况下,人们甚至会将编号纹在身上。
解决之道并非创造更多人造标识符,而是设计更优的识别机制——必须考虑到事物会随时间变化。
> 从标识符中剥离信息,意味着将数据与现实世界割裂,导致我们无法再进行匹配。但这种关联性正是最初保存数据的唯一目的。
标识符仍与用户数据相关联,只不过是通过表中其他相应字段实现,而非嵌入标识符本身。
> 于是现实世界开始调整,“无数据”的标识符逐渐演变为现实世界的实体。这种状况本质未变却更糟(例如忘记社保号就等于不存在)。极端情况下人们甚至会将编号纹在身上。
使用随机UUID作为主键并不意味着用户必须记住该UUID。事实上,大多数情况下我认为根本无需向用户展示该值。
例如,用户仍可通过当前邮箱或电话号码查询数据。索引功能并不局限于主键。
> 解决之道并非创造又一个人为标识符,而是设计更优的识别机制,同时考虑事物变化的特性。
完全随机的主键恰恰考虑了变化性——因其不嵌入任何现实世界信息。不过我认为,如文章所建议,出于性能考虑在UUID中嵌入创建时间也无大碍。
> 使用随机UUID作为主键并不意味着用户必须记住该UUID。事实上,大多数情况下我认为根本无需向用户暴露该值。
那么此类标识符有何用途?仅用于技术目的(如复制等)吗?
既然如此,为何还要为内部标识符费心使用UUID?序列号就足够了。
但“内部”的界限其实很模糊——你选择整数序列号,几年后API被附加到纯内部数据库上,系统就面临枚举攻击风险。当你通过供应商系统引用内部数据时,这还算“内部”吗?UID 1是否就是最初用于系统部署的系统用户?那最好专门针对这个ID发起攻击…类似问题不胜枚举。
UUID或其他随机化ID之所以有用,在于它们不包含任何排序信息或暗示重要性,尽管存在性能开销,但作为安全默认值仍非常可靠。
当然存在避免使用它们的理由,我们评论的文章也列举了大规模应用场景下的合理考量。但我认为,若遇到这些问题,通常意味着你有资源和经验来缓解风险。对于大多数新系统而言,除非存在非常具体的避免理由,真正随机生成的ID仍是更安全的默认选择。
> 不过“内部”的界限确实模糊
对我来说不模糊 🙂
“内部”即“不暴露于数据库之外”(涵盖应用程序及其他外部系统)
内部意指“不暴露于特定边界之外”。对多数人而言,该边界涵盖范围远超单一数据库,且边界本身可能变化。
在分布式系统协调困难的场景下,UUID适用于并发创建条目。
还可能涉及避免泄露业务信息的需求——例如当使用序列化ID时,通过
/fetch_order?id=123这类API可推测当前订单数量。尽管如此,序列化主键仍被广泛采用——这属于场景相关的权衡取舍。
若将标识符暴露在数据库外部,它便不再属于“内部”范畴。
假设讨论链如下:
> > 使用随机UUID作为主键并不意味着用户必须记住该UUID。[…]
> 那么此类标识符有何作用?[…] 既然如此,为何还要为内部标识符费心使用UUID?
你质疑“若非供用户使用则有何用”的语境表明,“内部”意指其补充属性。即指公司及软件使用的ID,甚至网站调用的API接口,但绝非用户需要知晓的内容。
否则,如果“内部”一词意指更严格的含义(仅由单个非分布式数据库使用,且当前及未来均不会被任何应用程序访问),那么我的回应是:许多ID既不符合此定义中的“内部”属性,也并非为用户记忆/保存而设计。
> 解决方案并非再创造一个人为标识符,而是设计更优的识别机制,同时考虑到事物会变化的事实。
我认为人为且无数据关联的标识符才是更优的识别方案,它们能适应事物变化。这些标识符不必作为对外展示的身份,但拥有它们非常实用。
例如:电话号码虽是半通用标识符,但号码变更往往不受用户控制。若将其作为内部标识符,账户间切换时将引发混乱——原号码持有者的身份标识将不复存在。
更优雅的方案是为每个人分配独立于外部环境的内部标识符,并通过电话号码实现外部ID/电话号码到内部ID的转换。旧账户仍保留标识符,只是不再有对应的外部标识符。同样地,若需变更标识符方案,可设置多个指向同一内部标识符的外部ID(即无论旧ID或新ID都能解析至相同内部标识符,而不会导致模式混乱)。
> 我认为人工生成且无数据关联的标识符才是更优的识别方案,它能适应事物变化的特性。它们不必作为对外展示的标识符,但存在这类标识符非常实用。
若使用代理键的唯一目的是在内部数据库设计中引入间接引用,序列号已足够,无需使用UUID。
整个讨论聚焦于外部可见标识符(即对外软件可见的标识符,可能作为数据的持久长期引用)。
> 例如:电话号码如今已成为半通用标识符,但号码变更往往源于不可控因素。若将其用作内部标识符,账户间变更号码将引发混乱——因为原号码持有者的身份标识将不复存在。
引入代理键(无论UUID或其他类型)在现实中根本无法解决问题。当我向你声明“我是X,这是我的电话号码和电子邮箱,要求删除我的GDPR记录”时,你仍需能定位所有与我相关的数据。代理键对此毫无助益。要么在数据库层面解决此问题,要么需要依赖预言机(即人工裁决者)根据我提供的信息临时判定对应数据条目。
核心症结在于:你试图在数据模型中建模可识别的“实体”,而更优方案是建模“采集信息”。
因此在你的示例中,不存在由“电话号码”标识的“人”,而是“在时间戳X时,我们捕获了当时名为Y、使用电话号码Z的人的信息”。一旦将数据库视为可用于推断结论的事实结构化存储,替代键的需求就会大幅降低。
> 因此在你的示例中,并不存在由“电话号码”标识的“人”,而是“在时间戳X时,我们捕获了当时名为Y、使用电话号码Z的人的相关信息”。一旦将数据库视为可用于推导结论的事实结构化存储,替代键的需求就会大幅降低。
这种表述过于复杂且自相矛盾。你声称不存在被识别的“人”,却又立即提到“关于某人的信息”。既然能断言信息属于某人,就意味着你已识别出该人。
明确将数据与个人关联能极大简化操作。我认为尝试你提出的方案,无异于在GDPR数据删除操作中埋下隐患。
> “某匿名用户要求删除我们记录的所有数据。他们通过邮寄地址和当前电话号码进行身份验证。于是我们删除了该电话号码关联的所有数据。”
> “那之前电话号码关联的数据呢?”
> “呃?什么?”
顽固拒绝创建持久标识符只会增加工作难度,而非简化流程。
> 若您仅为在内部数据库设计中引入间接引用而需要代理键,序列号已足够。无需使用UUID。
UUID可作为外部键的示例(例如防止爬虫轻易获取密钥)。本文提及了若干可能促使您后续选择更优外部键的原因。
> 当我向您提出“我叫X,这是我的电话号码和电子邮箱,要求删除我的GDPR记录”时,您仍需能检索到所有与我相关的数据。
若请求者自注册后更改了姓名、电话和邮箱,且你没有代理键,如何追踪所有记录?这三项信息变更都相当常见。我本人就多次更换过邮箱和电话,若结婚姓名也可能变更。
> 当你开始将数据库视为可用于推导结论的事实结构化存储时,代理键的需求就会大幅降低。
我认为这会导致比你想象中更复杂的局面。当你记录“获取名为Y、电话Z的用户信息”这类带时间戳的条目后,若用户Y更换电话号码,系统将开始接收“名为Y、电话A”的记录——但这实为同一账户。你可以记录“名为Y的人将电话号码从Z改为A”,但此时查询必须具备时间维度(即需知晓该人在不同时期的号码)。若将所有记录的Z批量更新为A,则会引发问题(例如短信日志会显示向错误号码发送了消息)。
更糟的是,姓名和电话号码都无法唯一标识个人。若某电话号码从约翰·多伊转移至另一位同名者,则可能出现“姓名Y、电话Z”的记录指向不同人的情况。
我承认技术上可行,但实在难以想象这种方案的价值。我无法想象不破坏记录的实现方式——要么a)通过回溯虚假历史数据破坏记录完整性,要么b)采用反复递归查询摧毁数据库(例如当某人拥有5个号码时,如何获取所有历史号码?必须先获取最新号码定位最后变更,再追溯前一个号码,如此循环)。使用代理键时这类查询极其简单:“SELECT * FROM phone_number_changes WHERE user_id = blah”。
> UUID属于外部键的典型示例(例如防止键值被轻易爬取)。本文提及了若干可能促使你后期选择更优外部键的理由。
因此我们讨论的是“外部”键(即数据库外部可见的键)。这又回到了原点:外部可见的代理键存在问题,因为它们与本应标识的现实世界信息脱节,实际上无法真正标识任何内容(参见我关于GDPR的例子)。
它们是否随机并不重要。
> 若缺少代理键,当用户注册后更改姓名、电话和邮箱时,你如何追溯所有相关记录?
代理键又能提供什么帮助?我根本不知道你数据库里标识我记录的代理键是什么。即便你内部使用代理键,那也只是实现细节。
若保留数据采集时间戳,至少能询问我:“上次交互时您的电话号码是什么?何时发生的?”
> 我认为这会导致远超预期的复杂性。
这种复杂性无论你是否愿意都客观存在,代理键无法消除它。必须通过显式处理来解决。
数据库管理系统提供了应对这种本质复杂性的手段:双时态扩展、视图、物化视图等。
事件溯源同样是解决此问题的复杂方案。
> 使用代理键时这类查询极其简单:“SELECT * FROM phone_number_changes WHERE user_id = blah”。
没错,但若你根本不知道user_id,这些查询就毫无用处。
> 外部可见的代理键存在问题,因为它们与本应标识的现实世界信息脱节,因此实际上无法真正标识任何事物(参见我关于GDPR的例子)。
所有ID都与现实世界脱节。这是ID的核心前提。它是一段专属于某人或某物的信息,但并非该人或该物本身。
你的电话号码是电信公司随机分配给手机的标识。房屋的街道名称和门牌号是人为指定的标记。电子邮箱是用于邮件路由的任意标签。社会保障号码是政府分配的随机标识。甚至你的姓名,也是父母为你取的任意标签。
从根本上说,你认为存在某种“真实世界”标识符的观点并不成立。没有任何标识符是真实的,它们都是抽象概念。问题不在于“真实”标识符是否优于“虚假”标识符,而在于现有标识符是否优于你为系统创建的标识符。
我认为在多数情况下,创建自有ID能让你长远省去不少麻烦。若将社会安全号、邮箱或电话号码硬编码到系统各处,当用户需要变更身份信息时,你将被迫进行全系统级别的级联更新,这无疑会给自己制造大量麻烦。
我国公民拥有“ID”(即UUID,多数人不知其价值!)和熟知的社会保障号码——后者存在上述所有问题。虽然社会保障号码可能变更(重复分配、性别变更等),但ID无需更改,因其对应的是同一个实体人。
公共部门IT系统可使用身份证号并依赖其稳定性。
私营部门IT系统无法通过身份证号查询个人,仅能使用社会保障号进行比对和检索,例如在GDPR“被遗忘权”情境下清除记录。社会保障号对此类用途尚具实用性,因其印于护照、驾照等证件上。然而该编号存在身份盗用风险,绝不应作为身份验证凭证(我们有更优方案)。个人ID本身不易被盗用,因其仅限于授权场景使用(排除公共部门存在恶意行为者的拜占庭式情境!)。除非遭遇电影情节般的特殊情况,否则无法通过社会工程手段利用该ID获取个人数据。
那么此处的“内部”指什么?个人ID确实属于公共部门IT系统的内部标识,用于跨机构信息追踪。对普通民众鲍勃或爱丽丝毫无价值(但对伊芙等恶意内部人员却极具价值——这属于另一类问题,现实中需全社会达到更高数字化成熟度才能应对)。
> 从标识符中剥离信息会使数据与现实世界脱节,导致无法匹配对应关系。但这种关联性恰恰是原始数据存在的根本意义。
代理键的目的并非直接存储自然键信息,而是提供指向自然键的索引。
> 解决方案并非创造又一个人为标识符,而应在考虑事物变化的前提下,设计更优的识别机制。
根本不存在“另一个”——只有一种方案:代理键。你描述的其他信息并非数据索引手段,而是你希望检索的数据本身。
任何能通过索引检索数据的信息,都必须存在于数据库“外部”——即执行“根据X标识符获取信息”的查询时,必须先知晓X。若X仅存在于索引中,则必须另建索引基于外部可获取的信息Y来检索X。此时X作为标识符便失去价值——它只是增加了额外的间接层,并未解决任何信息检索问题。
这正是我的核心观点:X要么成为“现实世界的实体”,要么就无法作为标识符发挥作用。
标识符本质上是“系统用于操作同一实体的通用标记”。
它不可或缺,因为可能是唯一不变的存在。以人为例:* 出生日期可能因文件错误修正而变更* 几乎所有生理特征都可能随时间改变——无论是认知转变(决定变性)、衰老,还是意外事故(皮肤严重烧伤后指纹失效)* DNA或许足够可靠,但这该死的长标识既不便共享又难现场验证。
因此,将唯一ID与少量其他特征绑定以识别个体当前状态,才是我们现有及未来最优解。
当未知变化可能发生时,你无法预判变量影响。最终可能被迫重建数据库、进行痛苦的迁移,或支持双重代码路径以兼容两种键值类型。
网络协议设计师深谙此道,默认会在消息格式规范中嵌入协议版本号。
我认为同样可以分配3-4位作为标识符版本号。
没错——对于长期存续的数据,兼容性问题不可避免,因此必须从一开始就将其纳入考量。
你无法通过预判所有未来变化来设计事物。事物终将改变并崩坏。
就我个人的设计理念而言,我发现避免泛化反而能让代码更持久(基于更具体的想法),并在变化来临时更易于修改。
根据我的经验,几乎每次将具体数据硬编码到标识符中,最终都会后悔。这并非试图预测所有可能的未来变化,而是为了避免重蹈覆辙。
我并非反对这个观点,而是反对“我们不能做出可能变更的协议或数据决策”这种说法。
那是我误解了。我原以为你主张在设计中摒弃通用标识符(如UUID),而偏好使用具体数据(如姓名、邮箱地址)作为ID。
你的评论过于笼统,无法判断你究竟认同、反对或补充了文章的哪个具体观点。
我不同意“性能”应成为优先选择序列号而非GUID的理由——除非确实别无选择。
我认为ID不应承载信息。是的,这也意味着我认为UUIDv7在ID中嵌入创建日期是错误的设计。
这样表述还不够明确吗?
但那确实是该GUID的创建日期。它与实体本身毫无关联。例如你可能出生于1987年,却因故直到2007年才获得社保号。
因此UUIDv7中的日期并不会为数据库外的记录增添任何意义。强行推断这种不存在的关联才是谬误。
你可以这么辩论,但问题在于它的存在意义何在?为何要关注这个设计上完全随机的字段的生成日期?
我敢打赌人们必然会提取该日期并加以利用,而这种用途几乎必然构成滥用。以PN/SSN和常规性别字段为例:你真希望别人能由此推断出你当时办理了新身份证件吗?若一个1987年出生的人在2022年左右获取新PN/SSN,你可能会产生什么联想?
此类泄露行为能绕过数据库所有访问控制机制,这本身就是采用真正随机ID的充分理由。
> 若1987年出生者在2022年左右取得新PN/SSN,你可能怀疑什么?
感谢你替我阐明。对读者而言,这泄露的信息暗示当事人很可能并非美国本土出生公民。这种推测未必百分百准确,但存在合理依据,且可能被用作不利证据。
记录创建日期可能存在无数种被用来针对你的方式。若对方未书面记录,你如何证明对方歧视了你?
思考中…对此我没有好答案。只要数据存在,人们就会从中解读意义——无论是否合理。
借用斯帕罗先生的名言:
> 真正重要的规则只有两条:人能做什么,人不能做什么。
评估安全事务时,最好彻底剥离道德价值判断(“正当性”),仅基于现有数据考量可能性。
除公民身份外另一潜在影响:证人保护计划中身份变更的情况。
> 你可以这么辩论,但这有何意义?谁会关心一个设计上完全任意的字段的创建日期?
我敢肯定,数据库按日期/时间范围排序筛选正是其存在目的。
若需按日期排序筛选,直接在表中添加时间戳即可,别滥用ID字段。
通常确实如此。其优势在于仅需通过UUID检索时——前缀是其磁盘块位置的索引。
> 前缀是其磁盘块位置的索引
什么?这绝对不可能成立,因为B树节点会变化而UUID不会。
我并非字面意思,但现在无法编辑了。本应加上“类似”等措辞。
但UUIDv7完全改变不了这个本质。无论选择哪种UUID格式,ID始终“类似”于块索引——你需要遍历树结构来定位节点。UUIDv7的作用是提升新建条目时的性能表现,并在缓存场景下可能带来优化。
> 仅此而已
对那些无需承担后果的事物,人们总容易抱有强烈观点。
完全正确,要明确设计——别把多重用途硬塞进本应是无意义唯一标识符的单一列中。
这绝非本意。uuidv7的具体设计目的是优化B树特性,而非让你基于ID的连续性来构造查询。
这种认为可跨ID查询的假设正是需要警惕的。一旦这样做,你就依赖于实现细节了。契约要求的是获取UUID,而非获取48位时间戳。UUID共有8种类型,即使v7也有多种变体。
我认为这属于极少数特殊情况——当你已持有ID时,泄露ID生成时间戳才可能构成潜在风险。
处理超大规模数据集时,完全随机的ID(这正是原帖讨论的核心)存在显著弊端。
时间字段要么具有实际意义(应独立成列),要么毫无价值(则完全不应存在)。
我并非规范化狂热者,但这里讨论的不过是第一范式而已。
这两个观点毫无关联,原帖中两者之间的联系并不明确。
所谓“过早优化”,是指在缺乏充分依据的情况下为追求性能而做出的权衡。这可能表现为:为编写更优化的代码牺牲可读性,导致代码难以理解;或是耗费大量时间研究数据库的写入模式,而这些时间本可用于开发其他功能。
我不认为应该为了规避过早优化,就刻意忽略既有知识而故意让初稿代码退化。
UUID v7 并不包含创建日期。若在应用中将其视为随机序列以外的任何东西,那纯属错误。
“它的功能”与“我认为你该如何使用它”不应被视为等价陈述。
恕我直言,我完全不明白你对原文的回应逻辑。该文根本未涉及自然键的讨论。
或许你能帮我澄清个问题,因为我可能漏掉了关键点。
> 挪威个人编号的前六位是出生日期(DDMMYY格式)
那么格式应该是DDMMYYXXXXX(X代表任意数量的数字),其中XXX代表某种自动递增的编号?
这意味着:若格式为DDMMYYXXX,则仅容纳1000人使用该出生日期;若为DDMMYYXXXXX,则可容纳100,000人使用该出生日期。
因此要形成如此大规模的重复条目导致人们无法使用真实生日,必然满足以下任一条件:
1. XXX计数器必须极其微小,以致每年因人们“用尽”1月1日日期而耗尽
2. 出生于1月1日或移民挪威时不知晓生日的人数必须极其庞大
若仅采用DDMMXXXXX(无年份)格式,我能理解该系统会迅速崩溃;但当涉及“2014年1月1日出生者,或移民挪威时不知生日且出生于2014年前后故被选定该年份者”时,我不确定这类人群如何能形成足够大的基数引发问题。或许这仅发生在特定年份——当大量身份记录不全的难民被接收时?
(欢迎指正,我可能遗漏了关键信息)
我认为带时间戳的UUID并非“承载数据”,只是提升查找效率的启发式方案。若时间戳错误,其运行速度将与无时间戳的UUID无异。
以性别为例:99%的人群性别固定为男/女,可用于负载均衡。但若后期发现桶内性别与预期不符,也无伤大雅——这只会导致分支预测错误,而随机值会引发50%概率的错误,此时错误率降至1%,在不损失功能性的前提下显著提升速度。
一旦将不完美数据编码为不可变键值,每次检索时都必须进行验证。若数据无法100%保证完美,最终仍需查询负载均衡数据库的两半部分。
>这可用于负载均衡
只要你不在特定年份的中国或印度…
GP的观点很有道理。
根本原因其实是“位数不足”。UUID采用128位设计,即便分配部分位数给时间戳,随机生成部分仍足够大。
这确实是个合理质疑…但与当前讨论无关。
此外,我们终究生活在现实中。虽然从数据理论角度看完全随机的标识符或许完美,但在实际应用中添加日期前缀能带来诸多性能优势。
永久标识符不应承载数据。这堪称数据管理的根本禁忌。
只要不使用这些数据,且为UUID中编码的内容设置实际字段,这种做法完全无可厚非——前提是随机部分足够长,能规避现实数据中的伪影问题。
您的评论虽有道理,但与本文无关。
更广泛地说,这是关于代理键与自然键的古老争论,但评论确实完全偏离了文章核心。我只能推测他们根本没读完!
本文明确反对将GUID用作主键,而我主张使用它。
序列号同样承载数据。不知不觉间,就会有人依赖排序顺序,或指望编号不存在间隙——甚至通过计算间隙来推断不该知道的信息。
> 递增编号本身也承载着数据。不知不觉间,就会有人依赖排序顺序,或指望编号不存在间隙——甚至通过计算间隙来推断不该知道的信息。
例如,若https://github.com/pytorch/pytorch/issues/111111可见而https://github.com/pytorch/pytorch/issues/111110不可见时,有人可能推断存在与关键安全问题相关的隐藏漏洞。
而若URL改为https://github.com/pytorch/pytorch/issues/761500e0-0070-4c0d… 则可规避该风险。
> 该文章明确反对使用GUID作为主键,而我主张使用。
让我们厘清问题。
作者基于性能考量,反对在大型数据库中将UUIDv4作为主键(相较于整型或大整型)。
你列举的例子涉及常见误区:将实体可变的非唯一属性用作主键。
>转眼间就会有人依赖
切勿暴露内部ID。道理就是这么简单。
我在最近两篇关于uuidv7的讨论中都看到这个观点。
这种说法毫无意义。任何对外暴露的ID必然同时是内部ID,而未暴露的ID则仅限内部使用。
若以可重复方式暴露数据,仍需选择暴露何种ID——无论是主键还是次键。(某些特殊场景可完全避免暴露键值,但这类情况极为罕见。)
主键是用于构建数据库关系的唯一标识。
第二个ID与数据内部结构无关,仅是普通字段。
你可以随意更改结构(或“内部”ID类型),无需顾虑外部消费者——他们仍会获得人工生成的ID。
所以你的意思是不要暴露主键?
这个说法更合理,但我仍不认同。这类似于人们不假思索套用的“最佳实践”,反而制造了无谓的复杂性。
只有当存在将主键与对外暴露键分离的合理依据时才需隐藏主键。若主键本身就是你希望对外暴露的形式,那么直接暴露主键即可。例如:若主键是UUID,却另行创建UUID用于公开暴露,这极可能给系统增添了无谓的复杂性。
> 制造无谓的复杂性
正是我的想法。
若此问题成为隐患,说明系统其他环节(从访问控制到API设计)早已存在缺陷。安全靠隐藏绝非解决之道。
若攻击者与数据库之间唯一的阻隔是“无法猜出ID”,那你已陷入严重困境。
正如其他评论者所言,问题根源在于默认值未将生日编码到个人编号中。
我认为记住特定数字的用途也很重要。例如,我认为没有生日信息的个人编号会更糟糕。在现行系统下(我只了解瑞典的系统,但假设其他系统相同),我只需记住4位数字(因为编号由出生日期加上4位唯一数字组成)。若改用完全随机生成的号码,我至少要记住8位数字(为确保未来适用性,可能需要9位)。对我个人而言这尚可接受(尽管我怀疑有些人已对此感到吃力),但若还要记住两个孩子和伴侣的号码,事情就会变得令人烦躁。尤其关键的是,这些数字使用频率既不足以形成记忆惯性,又频繁到每次查询都令人烦躁——特别是当人们并非时刻携带手机时。
其实没那么糟。巴西公民身份证号(CPF)有11位,大家都记得住。习惯就好=)
> 永久标识符不应承载数据。
你对序列号标识符也有同样的批评吗?哈希值呢?UUID中的版本字段又如何?
听起来你只是在为另一种过早优化辩护(具体来说,就是为那些可能永远不会发生的边缘情况过早改变整个架构)。
既有架构确实难以变更,或许可以推迟到那些极端情况真正发生时再调整——尽管这些情况可能永远不会发生。但对于新架构,与其追求微小的性能提升,不如珍惜自己积累的经验智慧。
> 即便一切不变,仍可能遭遇麻烦。挪威PN号码的前六位是出生日期(DDMMYY格式)。这应该不会变吧?
我猜挪威和瑞典采用相同或类似的解决方案?即通过PNR识别个人,而需要跨多个PNR追踪个人(如政府机构)的系统则使用PRI。而PRI本质上就是个人首个PNR编号中间插入数字1形成的标识。若该PRI已被占用,则使用2作为起始号,依此类推。
当然PRI也可以采用UUID替代方案。
> 永久标识符不应承载数据。
你读过那篇文章吗?他并非推荐自然键,而是建议采用基于整数的代理键。
> 这是典型的过早优化。
不同意。数据具有粘性,主键尤甚。况且若要在早期投入时间优化,数据模型才是重点。
> 别为节省几毫秒而做出后悔的决定!
在某些数据库(如InnoDB引擎、集群模式的SQL Server)中,糟糕的主键设计极易导致查询时间从亚毫秒级飙升至数十毫秒,尤其在存储非节点本地化的云解决方案中。我指的不仅是UUID;在1:M关系中使用BIGINT主键会彻底摧毁延迟表现——仅因每条记录都需要单独加载数据页。若改用复合主键(如(linked_id, id)组合键,其中id为单调递增整数),数据局部性将大幅提升。
Postgres因可见性映射查找存在类似但不同的问题。
我读过(并懊悔浪费了时间)。他们的论点是:
* 整数键更快;
* uuidv7键更快;
* 若需混淆键值,建议使用整数并自行实现混淆(!!!)。
我能接受uuidv7(当然需权衡更强的可猜测性)。整数键的论点则令人费解。这种情况下,你需要构建定制系统来避免分布式环境中的ID冲突,却只追求2倍性能提升(最低标准应是64位键值)。这建议令人费解且在我看来完全错误。
需注意整篇文章的建议并非针对自然键(如邮箱地址、用户标识组合等),因此我跳过相关讨论。
可通过中央协调器分配连续ID块来避免冲突,这是成熟的模式。
关于自然键(或类似方案),我仅将其作为示例说明主键选择如何在规模化场景中严重影响性能。
> (当然,代价是更强的可猜测性)。
你猜的不是2^72位的随机数。如果你的应用中猜UUID能实现某些功能,那你已经搞砸了
> 你读过那篇文章吗?他推荐的是基于整数的代理键,而非自然键。
我并非密码学专家,但希望他的建议能由密码学专家审核。之后我将负责具体实现。UUID已接受过密码学专家的广泛审查,我手头有多种优秀的实现方案可供选用,且确信它们能有效解决问题。我清楚它们可能引发性能问题;但作为易于实现的安全特性,若真出现性能瓶颈我随时可处理。(根据经验,这种情况实属罕见。即便在大公司,我接触的大多数数据库数据量都不足。在问题显现前我会优先保障安全性——这反而是值得拥有的“好问题”。)
为何称其为安全特性?实则不然,原文也明确指出。即便UUID4具有随机性,也无人能保证其由加密安全的随机数生成器生成——事实上多数实现根本不具备此特性!
在多数场景中使用UUID的真正原因,是分布式系统中需要客户端自主生成ID,该ID随后存储于互不通信的多系统中。这无疑是随机UUID的合理应用场景。
我认为核心原则是:将UUID作为面向用户的标识符(如用户ID、订单ID等),通过API公开暴露;同时使用整数ID作为实体间关联的内部标识符,且内部ID始终保密。这样既能让更高效的数字ID留在数据库中用于数据关联,又能仅在API访问对象时使用UUID(例如),而在内部进行关联操作(需处理大量行数据时)时则可使用更高效的数字ID。
顺带一提,我认为“使用UUID”的做法源自NoSQL数据库——该场景确实需要UUID,但无需处理数据连接。人们将这种特定场景的最佳实践直接移植到SQL数据库中,却未能意识到这并非真正的最佳实践…
若向客户端暴露序列ID,客户端便能轻易推算出记录总数及任意记录的相对年龄。UUID能解决此问题,且无需依赖加密级安全数值生成器。作者方案或许同样有效,但我更信赖UUID的可靠性。显然除UUID外还有多种隐藏信息的方法,但UUID简单易用且无需额外思考,我能直接获得安全收益。不必担心向客户端暴露ID,可自由使用。
我从未见过有人实际举例说明德国坦克问题给他们造成困扰,仅是理论上可能存在风险。
> 无需费心考虑
这正是我日常处理数据库问题时遇到的核心症结——有人不愿深究操作的潜在影响,结果问题爆发时却将紧急处理责任推给我,因为他们根本不知如何应对。
若能预测用户ID,这在构思漏洞利用方案时极具价值——既可创建特权用户,亦可生成由未来用户拥有的可访问对象。
当我说“无需考虑”时,意指攻击者无法预测我的用户ID信息从而入侵账户——因为我确信他们无法预测用户ID信息。
你正在忽视使用安全性低于UUID方案的潜在风险,而你并未说服我认为是我未能充分考量后果。我知道存在性能问题,也明白可能需要创新解决方案。我担忧的不是不可预测的性能问题,而是不可预测的安全隐患。
或许这暴露了我的偏见。我日复一日处理数据库,主要问题来自设计拙劣的模式和查询导致的性能瓶颈;其次则是引用完整性违规引发的未定义行为。我遇到的安全问题全都是人们做些荒谬的基础操作,比如暴露会泄露密码的接口。
在我看来,若将主键匹配作为安全保障,系统本身已存在缺陷。认证授权本可通过其他方式实现。诚然需要“深度防御”策略,但若基础层仅依赖“不可猜测的用户ID”,根据经验人们会因此掉以轻心,最终在栈的其他环节造成漏洞。
反驳观点:现实中,PostgreSQL这类系统的数据值会补齐到字边界,所以要么浪费位资源,要么就是“携带数据”。
我觉得你在攻击稻草人。文章并未主张“用生日这类暴露语义含义的键替代UUIDv4主键”。恰恰相反,文中专门讨论了如何在内部使用序列号,同时对外暴露混淆后的密钥。(尽管我认同dfox和formerly_proven的评论[1, 2]——他们提出的异或混淆方法极其糟糕。重复使用一次性密码本堪称糟糕密码学最基础的教科书案例。作者将处理后的值称为“混淆”,说明他们可能心知肚明。本应采用更优方案才对。)
[1] https://news.ycombinator.com/item?id=46272985
[2] https://news.ycombinator.com/item?id=46273325
插入顺序或时间本身就是信息。若依赖此类信息,当需要插入过期记录时,你将面临巨大失望。
没错,为确保客户端不依赖该信息,应通过dfox和formerly_proven建议的方法,使键值在数据库外部保持不可见——正如我所言。
我认为反对的理由并非暴露语义含义,而是任何有意义的信息都存在于键值中——例如包含生成时间戳信息的UUID在某种意义上也是“不妥”的,因为它泄露了信息。唯一标识符应当是不可见的且本质上无意义的。
你的理解与vintermann评论中的示例存在矛盾。将序列号作为仅限内部使用的代理键(在数据库外部传输时刻意进行不透明化处理),与将性别身份、出生日期或书籍的任何自然属性嵌入广泛共享的标识符完全不同。
并非如此,后续评论中他们明确指出唯一标识符不应嵌入任何有意义的内容。参见:
https://news.ycombinator.com/item?id=46276995
https://news.ycombinator.com/item?id=46273798
好吧,但他们忽略了我提到的内容,这正印证了我所说的这是稻草人攻击。
> 序号本身也承载着数据。不知不觉间,有人就会依赖排序顺序,或指望序号间不存在空缺——甚至通过计算空缺来推断不该知道的信息。
混淆机制能防止这种情况。
他们还称此为“过早优化”。这只对了一半:它确实是优化。拥有支持优化的数据,并专注于优化后期难以迁移的部分,绝非过早。
> 挪威个人编号包含出生日期
奥地利社会保障号亦然,但某些情况下既不包含出生日期,甚至完全不包含日期信息。
然而许多网站强制要求有效日期,并据此推算个人出生日期…
> 这种做法有误,因为日期本身并不会改变。
真该告诉凯撒大帝和格列高利十三世这个道理 :-p
这完全取决于数据库特性。在Postgres中随机主键是糟糕的设计,但在Cockroach、Google Cloud Datastore和Spanner等分布式数据库中则相反——单调主键反而有害。你需要让负载在键空间中分散,这样才能避免热点分片。
在Google Cloud Bigtable中,我们遇到过这样的问题:域的主键是由另一个应用程序自动生成的顺序整数。于是我们直接将其反转,结果负载就自动分布得相当均匀。
这种操作(反转序列ID的位序)是谷歌分布式数据存储(如Spanner和Bigtable)内部的通用建议。
即便在分布式数据库中,也需要递增(即使非单调递增)的键值,因为底层B树等结构处理完全随机数据时很可能表现不佳。
UUIDv7在此类场景中尤为实用,因为:
A: 由于低位随机或伪随机特性(即在节点间分布良好),键值的哈希值或模运算结果将近乎随机
B: 高位可排序…因此各节点底层存储不会失控。
我不会说这完全取决于数据库,而是取决于数据库类型。对于大多数通用型非分片数据库而言,随机键值会导致B树等结构过度碎片化,确实存在问题。
确实如此,不过分片式PostgreSQL除外——在这种情况下,我同意你的观点:需要随机主键来实现分布。
这也取决于具体的工作负载。如果你想按主键列出数据范围,随机分配显然行不通。但此时会面临矛盾:列出范围需要数据集中在同一分片,而将工作负载集中到单一分片又会削弱水平扩展能力。因此你必须权衡优先级(或采取更复杂的方案)。
这还取决于具体应用场景。若工作负载以写入为主、存在时间偏移且高度并发,但很少创建新记录,那么即使在PostgreSQL中,随机主键可能是更优选择。
只要键值具备足够熵值(即非单调递增的整数序列),就能确保键空间均匀分布,对吧?因此UUID≥v4、ULID、KSUID乃至雪花键(Snowflake)都可满足哈希分布均匀的要求。
UUIDv7其实并非单调递增
> 关键在于将负载分散到整个键空间以避免热点分片。
这本质上是键值携带信息的又一例证,并非明智之举。
显而易见的解决方案是设置驱动分布的字段,从而实现自动平衡等机制。
这需要与Cockroach Labs、Google Cloud等厂商的开发者深入探讨。
作为数据库消费者,我们只能遵循现有设计,这意味着必须关注键值分布问题。
完全同意。可采用会合哈希算法确定分片。序列的哈希值应随机分布,因为改变最低有效位会导致输出位发生50%的变化。
我记得文章中提到过,这个建议仅适用于单体应用程序,不过我可能记错了(我只是粗略浏览了一下)。
你的意思是单体架构不能使用分布式数据库?
我并未断言任何结论,只是补充了讨论中缺失的上下文——根据我对文章的记忆。
编辑:原文表述如下:> 本文讨论的Web应用类型是采用Postgres作为主要OLTP数据库的单体Web应用。
因此你说的没错,这并不排除分布式数据库的可能性。
文章总结了反对将UUIDv4用作主键的合理论据,但作者提出的整数混淆方案我可能不会在生产环境中采用。对于中小型数据库,UUIDv7仍是个合理的折中方案。
我倾向于避免使用UUIDv7而采用UUIDv4,因为不想泄露所有对象的创建时间。
当然,当数据量大到UUIDv4密钥的随机性成为实际数据库性能问题时,这种做法就不适用。但我认为在认定v7是解决方案前,必须对应用程序中每个标识符的使用场景进行深入考量。或许v7适用于某些场景(例如资源标识符——其创建时间对所有访问者可见),却不适用于其他场景(如用户或组织标识符——这些信息公开可见但创建时间不公开)。
我同样不赞成泄露服务器端信息;我怀疑UUIDv7仍可能被用于密钥空间的统计分析(类似于整数ID的德国坦克问题)。此外,你另一条评论中提到的泄露用户活动时间数据,确实是个我未曾考虑过的关键问题。
我看到有人建议采用 UUIDv7 作为主键,同时使用 UUIDv4 作为用户可见键来解决此问题。
初读此建议时,我心想:“但你仍需在 v4 ID 上建立索引,这究竟能带来什么实际收益?” 但答案在于它能降低连接操作的开销:仅需在根据用户数据构建查询时使用索引一次,其余操作均采用性能更优的v7 ID。
需要明确的是,从实际角度看这属于微优化范畴;据我理解,它主要通过提升时间相关项的数据局部性来发挥作用。例如,若你有一个“订单商品”表,其中存储着订单中所有商品的行数据,这种设计能加快检索速度——因为访问特定订单的所有商品时,你无需进行多次索引遍历。但在用户表等场景(你不太可能同时查询两个恰好在同一时间创建的不同用户),这种优化就收效甚微。当然,在类似场景下使用整数ID同样会面临相同质疑。
不过仔细想想,采用v4用户可见ID搭配v7主键还有个优势:可以为v4 ID配置不同类型的索引。具体而言,为用户可见的v4 ID创建哈希索引应该是个不错的折中方案。
我至今仍不确定是否喜欢这个想法,但它绝对不是我听过最离谱的点子。
> 我倾向于避免使用UUIDv7而采用UUIDv4,因为不想泄露所有对象的生成时间。
可参考“UUIDv47 — UUIDv7输入/UUIDv4输出(SipHash掩码时间戳)”:
* https://github.com/stateless-me/uuidv47
* 2025年9月:https://news.ycombinator.com/item?id=45275973
若需处理此类数据,也可采用常规的64位整数密钥并进行加密(例如[1])。这本质上是文章作者方案的优化版本。
若需在多台后端服务器上生成密钥且无需同步,UUIDv47可能包含空格。但在我看来这属于非常小众的需求。
1: https://wiki.postgresql.org/wiki/XTEA_(crypt_64_bits)
出于好奇,泄露生成时间为何构成问题?
这取决于具体场景。换言之,要(合理)回答这个问题,必须针对每个应用单独评估。
举例来说,假设你在开发投票系统软件。你绝对不希望每张选票附带(隐藏的)时间戳(更别说递增ID),因为这会破坏选民的保密性。
更普遍而言,这涉及数据管理的底层原则。相比“我们泄露了记录创建的日期时间,但想不出这有什么问题”的说法,“不泄露附属数据”更容易获得合理性论证。
个人认为最大隐患在于那些“聪明”的程序员,他们把UUID当作数据直接展示日期时间。这必然引发复杂问题(‘用户看到的内容总想修改’)。有人宣称日期“错误”必须“修正”只是时间问题,更别提时区转换和夏令时调整了。
这等于泄露用户数据。试想“被告于某日在此网站注册”这类场景——用户注册时根本不知道创建日期是公开的,因为除URL中的UUID外,公开资料里根本找不到这个信息。
Discord做得很好。
Hacker News也做得很好,尽管我只需点击你的个人资料就能看到你加入于2024年10月。并非所有使用场景都要求保密。
但某些场景确实至关重要。使用UUIDv7作为标识符意味着每次创建UUID标识的新表时,都需审慎评估安全与隐私影响,最终可能出现部分表使用v4而部分表使用v7的混杂局面。最糟糕的情况是,当安全审查将时间戳标识符认定为安全隐患时,你将被迫进行痛苦的v7到v4迁移。
几乎所有社交媒体应用都会在公开资料页显示“注册于X年”。我认为这不成问题。
谁说我在讨论社交媒体?
那用户公开资料还能出现在哪里?
关键在于ID本身会泄露信息,即使个人资料未公开。许多场景中,你通过外键引用对象时,即便无法查看该外键对应的完整记录。
我想不出任何例子。
发送好友请求就是明显例子。
若系统(伪)随机数生成器(RNG)存在漏洞,导致其熵值部分源自可通过运行时间推知的因素,那么同一时期生成的密钥破解搜索空间将大幅缩小。
这甚至不需要依赖系统内置RNG质量低下。即使经过审计且已知可规避此类问题,但若编译器或操作系统遭到入侵并注入篡改过的RNG,同样会造成风险。
例如:若服务用户将时间戳纳入密钥且该数据对其他用户可见,攻击者便能知晓账户创建时间,这可能构成安全隐患。
曾有HN评论提及竞争对手通过追踪新注册量来动态调整折扣力度和营销力度,原理与此类似。
我曾任职的公司中,某位在线订购系统的用户贡献了超过50%的业务收入——这种情况你未必希望对方知晓。
然而由于该系统采用顺序分配订单号,该公司完全可以轻易推算出该用户业务的重要性。
例如,在一个月的时间里,他们可以在月初下单一次,月末再下单一次。这样就能得到该时段内的总订单数。他们已经知道当月下了多少订单,所以公司订单数 / 总订单数 = 业务占比
甚至不必精确计算,近似值即可。我不确定他们是否意识到这种操作方式,但若真如此也不足为奇。
这同样高度依赖法规限制。在我家乡,法律规定发票编号必须连续递增,不过每年可以重置编号序列。
这种情况普遍存在。你从财富500强企业订购工业零件时,若对方疏于管理,也能在零件编号中发现类似现象。
这取决于数据性质。若在不应包含年龄的个人数据中使用主键(例如为避免年龄歧视),则等同泄露了年龄的不完美替代指标。
所以UUID能作为记录创建时间的不完美指示器?
UUIDv7可以,但UUIDv4不行。
我想时序攻击也会成为问题。
UUIDv7仍包含大量随机位。多数通过批量生成ID的攻击都能被其抵御。
管理员、早期用户、创始人、CEO等群体的创建时间必然最低…
除上述所有回答外:外部实体若知晓两个账户的相对创建时间,或仅知晓两账户创建时间相近,都可能构成有意义的信息泄露。
通常不应将主键用作公开标识符,更不该使用UUID这类用户体验极差的方案。
我实在看不出URL中包含UUID有什么问题。
在Postgres中我常倾向于使用单一序列处理所有情况。确实会泄露些信息,但在繁忙系统中这种泄露往往“足够隐蔽”。
问题不在于泄露本身,而在于对象名称不易枚举性是系统强有力的安全增强特性。
当然,开发者应通过检查和单元测试确保每个对象都归属加载它的用户或账户,但要求攻击者比直接访问“/my_things/23”后再访问“/my_things/24”更复杂的操作,这本身就是重大安全收益。
在单序列和高负载系统中,多数高阶表/集合的ID分布极其稀疏。这并非意味着无法枚举,但当系统突然因“未找到”返回大量404或410错误时,你必然会察觉异常。
此外,若多数接口需要身份验证,通常不会构成问题。
这确实取决于具体应用场景。但确实需要注意:若需确保某些ID不可猜测,请务必避免其具有可预测性 🙂
> 此外,若多数接口需要身份验证,通常不会构成问题。
许多系统并非稀疏分布,且该说法本身存在谬误。不可猜测的名称并非首要安全措施,而是针对漏洞或劣质代码的被动补救手段。访问控制缺陷始终位列OWASP十大漏洞榜单,而ID注入正是其中一环。企业至今仍因这类漏洞遭受攻击。
例如谷歌2019年的漏洞事件,正是因不可猜测的名称而大幅降低了影响范围https://infosecwriteups.com/google-did-an-oopsie-a-simple-id…
若仅需掩盖社交网站仅有200名用户和80条帖子的事实,只需对自动递增的主键进行排列组合即可。例如IDEA或CAST-128,再进行base64编码。若因代码库中使用了禁用的旧式密码被盯上,直接换用AES-128即可。(这算是格式保留加密的退化/自洽基础案例)
(你以为YouTube视频ID是什么?)
此方案的问题在于,你需要长期管理密钥/密文(可能长达数年)。
几周前我分享过这篇文章,探讨此类方案的缺陷:https://notnotp.com/notes/do-not-encrypt-ids/
我认为某些场景下这种做法有其合理性,但你真的愿意引入如此复杂的加密机制吗?
该文章存在自相矛盾之处:一方面将密钥描绘得至关重要("运维工作将陷入噩梦。你现在需要管理加密密钥。密钥存放何处?是否由存储在KMS或HSM中的包装密钥保护?生产、预发布和开发环境是否使用相同密钥?若开发环境需使用生产数据测试,是否需要访问生产加密密钥?持续集成管道又该如何处理?本地开发机呢?)却又承认我们讨论的只是混淆层级的内容,本身并不敏感(“用于隐藏非敏感的时间戳”)。别误会,这确实是该方案扩展性上的明显缺陷,但多数应用程序都需要管理各类密钥,其中绝大多数才是真正重要的。例如会话签名密钥、API密钥等。应用程序使用带签名的会话配合RCE数据格式仍很常见。该文章的表述虽无误,但更适用于此类密钥场景。
话虽如此,该方案虽适用于混淆,但不应用于此类安全场景,例如隐藏/未列出的链接、确认链接等。此类场景应采用真实的、较长的随机密钥进行访问,因为无法枚举密钥本身就是一项安全特性。
我一直认为它们被原样使用和存储,是因为你提到的转换方式在YouTube的规模下代价高昂,且在此处添加任何混淆措施都看不到明显益处。
> 你觉得YouTube视频ID是什么?
我其实完全不清楚。它们是什么?
(另外它们的`si=…`格式代表什么?)
记不清从哪听来的,但我相当确定si=…是关联分享者与链接的追踪信息。
当然,我只是好奇它具体包含什么内容。
为什么不默认使用AES-128?你的CPU有加速AES-128的指令集。
你不能直接修改序列的起始值吗?
> 切勿认为UUID难以破解;它们不应作为安全机制使用
关键不仅在于真空环境下难以猜出有效唯一标识符。在此情境下,无论是UUID还是未修饰整数,随机(或至少近似随机)值的核心价值在于:难以从单个值或多个值中推测出特定值。
关于“不是x而是y”的表述形式:我不是LLM,真的!
反驳观点…我从事技术尽职调查,常接触处于转折点的公司,也接触过许多陷入困境的企业。
能够快速对所有数据进行分片可能具有极高价值。“我们能即刻分片”与“分片需要大量精细工作”之间的差异可能代价高昂。若公司利润率低迷,这可能决定着“能否轻松扩展”与“无法获得投资”的分水岭。
我认为,若团队具备在避免使用代理唯一键的前提下实现分片的实力,那固然很好。但若不具备…在每张表添加UUID可能成为一张巨大的“免死金牌”,对许多公司而言,这远比数据库上些许空间和时间优化重要得多。
值得深思。
我理解大整型外键会让分片困难,但若我理解正确,UUIDv7仍能完美适配。本文核心观点在于:在PostgreSQL中,单调递增的外键相较随机UUIDv4外键具有性能优势。
虽有些关联,但我们因业务增长不得不进行分片,当时未使用UUID,过程相当棘手。不过最棘手的并非此处——当数据模型高度关联且需在迁移期间保持在线时,整个过程本身就极其复杂,无论是否使用UUID。
没错,但若从一开始就采用UUID并预留分片需求,就能在数据模型设计时充分考虑。正如你所言,事后改造往往困难得多。
我参与的应用程序会加密整型主键,再用字节序列生成类似UUID的标识符。
我们的核心诉求是避免API和URL中暴露数据库ID。序列化ID会引发字典攻击,并泄露用户数量估算值。
加密数据库ID能清晰暴露扫描行为——UUID无法解密,甚至无需数据库往返即可识别。
若加密密钥丢失、泄露或因其他原因轮换,这听起来相当棘手。
密钥管理似乎和备份同样重要,但我理解这么小的东西(加密密钥)可能显得不重要,毕竟数据库备份体积庞大哈哈。但它们确实共享着关键属性(切勿丢失密钥,切勿丢失数据;切勿泄露密钥,切勿泄露数据,等等等等)。
密钥永不轮换(没有必要)。
我们并不担心密钥泄露风险。
若密钥丢失,那我们面临的将是更严重的问题。
几个问题:
* 身份标识加密密钥如何管理?通过环境变量注入应用环境?还是直接嵌入源代码?我问这个是因为想了解若采用此方案,管理密钥时需要投入多少“精力”。
* ID加密是否采用AEAD方案(如AES-GCM)?还是普通AES就足够?我推测ID长度不会超过AES分组大小,但作为非密码学专家,不确定这样是否安全。
> ID加密密钥如何管理?
与应用程序中管理其他密钥的方式相同(详见下文总结)
> 标识符是否采用AEAD方案(如AES-GCM)加密?还是普通AES加密即可?我认为标识符长度不会超过AES的分组大小,但再次强调,我并非密码学专家,无法确定此做法是否安全。
目前手头没有具体源代码。这是.Net中易用性较高的对称加密算法之一。我们这里谈论的并非军用级安全。通常处理32位整数加密后为64位,因此我们会用几个Unicode字符填充,使64位加密结果达到128位。
—
关于应用程序中的密钥管理:我们采用自研的配置文件生成器,该工具可根据需求灵活适配。它既能生成配置文件,又能生成读取文件的强类型类。所有配置值均在启动时加载完毕,因此无需担心运行时因配置缺失引发的错误。
密钥(连接字符串、加密密钥等)在配置文件中以base64字符串形式加密存储。读写密钥的证书存储于Azure密钥库中。
所有应用程序的启动逻辑大致如下:
1: 确定运行环境(生产、测试、开发)
2: 获取对应环境证书
3: 读取配置文件,包括解密其中的密钥(如主键加密密钥)
4: 填充存储配置值的强类型对象
5: 通过依赖注入将这些对象注入运行时对象
> 多数业务应用单表唯一值绝不会突破20亿,因此该方案足以满足其全生命周期需求。我亦建议在其他场景始终采用bigint/int8类型。
相信每位数据库管理员都曾因类似决策而陷入过经典战役
引自精辟文章:
作者是否意指随机值不具连续性,因此排序效率低下?当然随机值可以排序——而他所称的“字节顺序”排序方式,恰恰是所有整数排序的实现原理。这同样适用于我们Unicode时代之前采用的简单字符串排序。
将UUIDv4用作主键是权衡取舍:当需要分布式生成唯一键时才采用它。没错,它们不具备时间顺序特性,确实占用128位空间。若无法接受这些特性,自然需要考虑替代方案。不过我怀疑“避免使用UUIDv4主键”是否真的能成为经验法则。
若需时间戳排序,UUIDv7是不错的替代方案。
但作者并未提及时间戳排序,他只说排序。我认为他实际指的是UUIDv4排序存在某些问题。
没错。具体方案取决于场景:在非分布式环境下,可直接使用足够大的整型字段(例如存储人类数据时可采用较小整型)。若时间戳至关重要,可另设独立时间戳列。
但若需基于UUID的检索功能,将其设为主键反而更优,这样能省去实际主键的额外索引开销。若同时需要日期字段,且UUIDv7的剩余位数足以满足随机性要求,这也是个不错的方案(本质上相当于创建由日期时间和随机数组成的复合字段)。
> 当需要以分布式方式生成唯一键时使用它
补充一点,目前主流数据库管理系统中并不存在真正意义上的分布式系统——即需要使用UUID生成内部键值的系统。
互联网上确实存在此类系统,某些机构的内部系统也采用这种机制。但近乎普遍的规则是:人们所知的“数据库”并非此意义上的分布式系统,若列创建在数据库内部完成,则无需使用UUID。
我不理解为何128位被视为过大——显然不能更小,因为在64位系统上,除最小数据库外,实际工作负载下的碰撞概率过高。
自动递增键虽可行,但整数用尽时如何处理?此外分布式数据库可能难以实现,且无法在客户端生成键值。
Postgres内部必然存在按主键排序存储记录的机制,虽然默认设置尚可,但我确信可禁用此行为——对写入密集型负载而言这并非理想方案。
问题更根本在于:若采用纯随机键值,索引数据基本不存在空间局部性。这意味着要保证性能,整个索引必须驻留内存而非仅缓存近期数据。同时写入放大效应会大幅增加,因为同一索引页在足够短时间内被多次修改以避免二次写入的情况极为罕见。
现实场景中很少会耗尽64位ID的递增空间——随机生成确实可能发生冲突,但64位整数的最大值为9,223,372,036,854,775,807。即使每行仅占用1位空间,其容量也略超1艾字节。
64位整数绝不会耗尽。我认为64位整数(对于预期增长有限的表甚至更小位数)是数据库内部ID的最佳方案。若需暴露ID,可为特定表引入第二UUID;若需隐藏内部ID,则无需此操作。
您好,感谢反馈。我已更新该部分内容以更清晰地传达核心意图。本主题关注的排序类型,实质是B树索引在插入新条目及查找现有条目时的遍历效率(包括单值/多值查询如IN子句、更新、删除等操作)。我复现了Cybertec的一个有力案例,通过对比存储主键为大整数与UUID v4时所需访问的页面数量,展示了v4 UUID导致的额外页面消耗——两者产生相同用户可见结果所需的页面数量差异显著。该案例有力佐证了我作为顾问在各类“中等规模”Postgres数据库(记录量从单条到数千万条不等)中的实战经验:当客户遭遇查询延迟过高问题时,早期采用UUID v4作为主键/外键的设计往往是主因之一。索引无法完全加载至内存导致大量顺序扫描操作。我通过展示替代模式设计及查询集验证了这一结论:除使用整型主键/外键外,所有条件均相同。结果显示:索引更小(可内存加载)、索引扫描更可靠、延迟更低、执行速度更快。
恕我直言,本文的技术基础并不扎实。
具体为何?
难道不是因为当键值呈递增而非随机分布时,插入B树索引的性能更优?随机ID会引发更多重新平衡操作,而递增ID更利于缓存利用。
没错,对Postgres而言还会因高概率的整页写入导致WAL日志膨胀。
关键在于频繁访问的数据在物理存储中是否邻近。若数据大致按创建时间排序,则时间相近的数据在磁盘上也会邻近存储。通常数据访问与创建时间存在关联性——虽非所有表都如此,但多数情况确实如此。
访问完全随机位置的数据可能引发性能问题。
当然具体情况因变量而异,但这正是人们讨论UUID主键存在隐患时的核心顾虑。
同意,我对此也感到意外。
若类型定义了排序规则,同类型值即可排序。
将“随机值”与“整数”对比也颇为奇怪。随机整数同样存在“排序”特性(具体取决于排序定义)。
为何需要按UUID排序?我可能漏掉了关键点。通常使用UUID主键是为了无需协调即可创建新键,而绝大多数情况下我们并不需要按主键排序。
大多数常见数据库索引都是有序的,因此若使用UUIDv4,不仅会导致索引膨胀,还会造成局部性差的问题。若试图通过复合键来改善局部性,最终只会使索引更加臃肿。
我见过许多人通过(生成的)整数值排序来返回“创建顺序”的行,他们错误地认为整数排序比正确的时间戳排序更快(而时间戳能提供比生成整数更可靠的“创建顺序”排序)。
假设该整数值是主键,由于InnoDB的聚簇索引特性,对MySQL/MariaDB而言实际可能更快。若能对主键执行范围扫描,且扫描方向与ORDER BY一致,恭喜你——数据行已按顺序排列,无需额外排序。但若需通过二级索引查找行,则无法保证排序效果。
任何固定长度的位字符串都具有明显的自然排序特性,但由于它们是随机分配的,因此缺乏顺序分配的密度和局部性。
本文讨论的是一种为解决虚构问题而设计的方案,属于典型的过早优化问题。UUIDv4在多数场景(包括小型数据库)中完全适用。性能考量仅应在性能瓶颈显现时启动。其他因素往往更具优先级。
我认为事后将UUIDv4数据库重新键值化为int64并不现实。虽然新表可以采用整数键,但绝大部分存储空间仍将长期承载UUID(若初始采用UUIDv4则持续使用该格式)。
若UUID数量过多,可通过增加数据库实例解决。但需确保架构设计未强制限制单一数据库使用场景。
没错,我的意思是这可能永远不需要。
我同意对多数公司而言这无关紧要。但曾在某公司亲历过因UUIDv4键导致的数据库性能持续问题,那体验糟透了。
根据我的经验,当性能问题显现时,开发团队往往正处于业务扩张期,既无意愿也无时间重新审视主键选择。
在UUID兴起之前,整型主键被认为完全可行——这种状态甚至持续了几十年。
文章核心要点:在PostgreSQL中,建议优先使用UUIDv7而非UUIDv4,因其性能略优。
若使用最新版PG,已有对应插件支持。
仅此而已。
你可能忽略了文中关于H2数据库的重要段落:
“建议:坚持使用序列、整型及大整型”
在此前提下,确实应优先选择UUIDv7而非UUIDv4。
本文稍显陈旧。PostgreSQL 此前缺乏原生支持,因此确实需要扩展。如今 PostgreSQL 18 已发布并支持 UUIDv7… 扩展虽非必需,但该扩展仍声明:
“[!NOTE] 自 Postgres 18 起,系统内置 uuidv7() 函数,但其功能尚不完整。”
具体缺失哪些功能,以及该扩展在PostgreSQL 18中是否带来更多冗余而非价值,我无法判断。但预计绝大多数用户已不再需要它。
若后续需要分片处理,坚持使用序列或其他整数类型会引发问题。
处理此类问题的方法很多。可通过其他标识符进行分片(不过我质疑这种表设计),也可为每个分片分配范围等。
我对分片并非专家,但既然使用递增整数,为何不能直接按(id % n)之类条件分片?
因为当’n’值变更时会引发问题。更关键的是,增量操作在何处执行?这需要单点容错计数器(顺便说,确实有人这么做)。
一旦将分片号编码到ID中,你将获得:
– 即时* 确定查询分片
– 每个分片拥有独立计数器
* 可通过程序化方式实现,具体实现还可支持可视化呈现
我设计的ID编码包含:实体类型(记得是4位?)、时间戳、分片号、分片内序列号。甚至设计了管理页面,可粘贴ID进行反编码。
id % n 适用于缓存场景——因为缓存可整体清空重构,或当’n’永不变化时。但实际中’n’通常会变化。
此处提及,且多数应用场景下可安全断言永远无需分片。
尤其在大型系统中,如何解决数据库整型变量达到最大值的问题?无符号大整型虽难触及上限,但普通整型呢?应用规模增长会迅速突破限制。
好吧…但这种顾虑似乎有些人为。如果bigint适用:就用它。如果表不会达到bigint的大小:就别用。我甚至为某些已知规模非常有限的表使用过smallint。但对于需要更大容量来存储更多记录的表,我不会担心smallint值域的局限性: 对于其他表,我会根据实际需求灵活选用int或bigint。现实情况是,除非处理特定场景需要精确控制字节数…否则我基本直接用bigint。没错,这可能有些浪费,但在那些每条记录多出的几个字节会累积成显著开销的场景下… 我可能本就需要bigint,而在bigint无关紧要的场景中,这些额外字节的总和也微不足道。统一使用单一数据类型本身就具有价值。
至于那些用int作为键值的情况…你可能想不到,现实中多少数据库根本用不完那么多ID,或者其工作负载规模连这种量级都遥不可及。
公平地说,我通常支持UUID方案,当前设计中采用UUIDv7。虽然原文观点有理,但我更关注另一类权衡场景——即UUID的开销值得承担的情况。具体效果和适用场景因人而异。
我只用最能扩展的方案,而近乎无限扩展的键值才是最佳选择。当你不得不为适应产品当前规模而更换数据库时,性能折损基本可以忽略不计——这仅适用于必须扩展的软件。显然,无需增长的系统则是另一回事。我同样支持UUID,但具体是v4还是v7并不在意。
这并非面临数十种选项需要频繁切换。你只需预估最大增长时表中将包含3.2万、20亿还是9000万亿条记录。即便所有场景都按9000万亿计算,其占用空间仍仅为UUID的一半。
UUIDv4在添加分片时表现出色,UUID能有效避免跨表ID混淆问题。但若数据规模达到20亿级,UUID可能也非最佳选择
最新Postgres版本(≥18)无需插件即可支持
周末开发ulid替代方案时[0]我还发现:Postgres内部Datum类型最多支持64位,这意味着每个uuid都需要堆分配[1](至少在128位机器普及前如此)。
0: https://bsky.app/profile/hugotunius.se/post/3m7wvfokrus2g
1: https://github.com/postgres/postgres/blob/master/src/backend…
您可能也会对这个Postgres扩展感兴趣
https://github.com/blitss/typeid-postgres
这确实很有意思。
我版本的目标略有不同。我要求所有内容都容纳在128位内,因此牺牲了部分随机位,同时确保Postgres内部表示也精确为128位。初始版本采用CBOR编码后实际占用160位。
我的方案为前缀保留16位,允许最多3个字符(仅限a-z字母)。
这些改进同样惠及MySQL。我们讨论的核心是随机主键插入(UUIDv4)与递增主键插入(UUIDv6或v7)的性能差异。
PlanetScale曾发表过一篇精彩文章,阐述了在B树性能层面,递增主键相较随机插入主键为何更具优势。https://planetscale.com/blog/btrees-and-database-indexes
这篇文章实在平庸。虽然提供了大量合理理由说明为何应避免在数据库中使用UUID,却未说明若需要难以预测的主键该采用何种替代方案。异或运算方案过于原始,虽然我理解为何要规避UUID,但替代方案究竟该用什么?
Postgresql 18已于九月发布并支持uuidv7
https://www.postgresql.org/docs/current/functions-uuid.html
作者应提供基准测试数据,否则仅称UUID“增加延迟”毫无意义。例如:插入UUID与整数所需时间差多少?索引扫描耗时增加多少?
作者未提及自身测试数据,但引用了cybertec文章[0]中的基准测试结果。
[0] https://www.cybertec-postgresql.com/en/unexpected-downsides-…
这篇文章写得混乱,真希望他能拆成两篇——一篇讲UUID4,另一篇讲UUID7。
我之前使用64位雪花主键(时间戳+序列号+随机数+数据中心+节点),后来为可排序的用户端主键切换到了UUID7。我完全接受让数据库处理128位整数而非64位整数,只要不必确保雪花函数最新版本部署到所有数据库,也不必担心雪花服务器永远不会出故障。
反正大部分用UUID7作为键的数据,最终都是直接从Redis中取用的。
试试雪花ID吧:https://en.wikipedia.org/wiki/Snowflake_ID
这和uuid v7一样泄露时间和顺序信息。
这不正是重点吗?有序主键能提供更优索引。
我见过好几次这类建议。虽然我绝非数据库专家,但经手的系统中从未见过UUID作为主键。
使用UUID作为主键(假设正确使用)是否有合理依据?我知道系统可能错误地将主键暴露给公众,但假设这不是问题所在。为什么选择UUID而非big-int?
UUID还能实现ID生成与数据库插入分离,这对分布式系统很有价值。
这才是核心优势!客户端可预先生成完整关系树结构,再将所有关联数据完整传输至数据库。由于设计上每个主键都具有全局唯一性,完全无需担心约束冲突。这简直太棒了。
大约十年前,我记得看到许多帖子警告“不要用整型作为ID!”。常见理由包括“ID暴露了数据库中对象的数量”以及“若安全措施不严,用户可通过增减ID获取更多数据!”。随后我目睹大批开发者争相为所有字段使用UUID。
UUIDv7看起来很有前景,但我不太可能重构所有表来使用它。
注意:若当前使用UUID v4,切换至v7无需模式迁移。处理新记录时即可享受其优势,例如降低插入延迟。uuid数据类型同时支持两种版本。
可采用相同技术方案,但需考虑较小的 int64 空间限制——详见雪花ID方案:https://en.wikipedia.org/wiki/Snowflake_ID
我们公司仅将UUID用作主键。
主要原因在于“德国坦克问题”:https://en.wikipedia.org/wiki/German_tank_problem
(简而言之:防止他人统计该表的记录数量)
我刚接触安全领域;理解泄露后端信息不可取,但为何表大小会构成问题?
老东家给新员工分配递增ID作为技术标识,GitHub个人主页链接也反映了这点。
有人可能每月运行脚本统计离职率,也可能没这么做))
这篇文章太棒了,感谢分享!
非常感谢!
为什么不另设一个uuid字段作为公开可见的标识符呢(这仅对少数表构成风险)?
这样既能规避本文提及的大部分问题,又不会泄露敏感数据。
至少对Spanner数据库而言,随机分布的主键能优化数据分片,避免高插入量时的“热点分片”问题。UUIDv4是典型解决方案,但反向位递增整数同样可行
https://cloud.google.com/blog/products/databases/announcing-…
这取决于诸多因素,因此我不赞成此类泛泛而谈的表述——它们往往更侧重特定数据库模式。但必须承认,每个人都应意识到潜在的权衡取舍。
当然我们可以设计多种自定义ID生成方案来确保唯一性,但我们面临以下要求:
查找性能对我们而言并非关键问题。在需要时,可通过投影转换为更简洁的格式。
更简洁的方案是保留现有表结构(使用整数主键),同时添加非顺序的公共标识符。
id => 123, public_id => 202cb962ac59075b964b07152d234b70
生成public_id的方法多种多样。采用带盐值的简单MD5算法即可实现极低成本的高效处理。
为该列添加唯一约束(同时建立索引),即使面对数亿级数据也能确保安全高效!
开发者为何总爱把事情搞复杂呢?;)
根据https://news.ycombinator.com/item?id=46273325,若采用分组密码而非哈希算法,甚至无需存储该值。
这偏离了重点。不使用UUIDv4的原因在于:对随机值建立索引会导致性能下降,因为底层B树的顺序插入比随机插入更快。你的
public_id列同样存在此问题,即使它不是主键也无法改变这个事实。对于非Aurora的InnoDB数据库,且次级索引非UNIQUE时,该方案确实有效——因为非唯一次级索引的变更会被缓存并批量写入,从而摊销随机访问成本。若哈希的是保证唯一的实体,我认为可以省略该索引的唯一约束。
但对于Aurora MySQL,无论哪种方式都会加剧问题,因为它没有变更缓冲区。
不过整型主键在连接操作等方面会更快。
> 使用整数生成混淆值
虽然这通常是简洁的解决方案,但切勿直接通过与常量异或来实现。应采用ECB模式的分组密码(若需短ID,NSA的Speck算法尤为适用,其支持32或48位分组)。
切勿考虑使用RC4(此类案例屡见不鲜),其安全性完全等同于常量异或运算。
关于为何不应将UUIDv4用作主键的长文,但…究竟谁这么做了?动机何在?如何满足其需求?仅抛出“可改用UUIDv7”的建议并不能解决实际问题,例如其占用空间过大的缺陷。
难道没有开发者采用(大)整型作为主键,同时使用UUID作为导入导出的逻辑键,从而解决跨机器的可移植性问题吗?
本文基于咨询顾问身份在多家“中等规模”企业处理Postgres数据库的经验撰写。这些数据库存在IO过载和延迟问题,且普遍采用UUID v4作为主键/外键。此类案例确实存在。我们可以将关键表的模式转换为大整数等效方案进行演示,展示IO延迟的降低效果。但现实中主键数据类型迁移成本高昂,最终取决于企业是否愿意为此投入。
UUID通常是解决枚举问题的首选方案。其空间足够大,攻击者无法推测你拥有多少个X(发票、用户、账户、组织等)。当人们用UUIDv4替换整数时,往往仍将其作为主键使用。
我补充一点:当数据在多处生成时也适用此方案。
以气象监测设备为例:5个站点数据汇入中央数据库时,所有站点都在创建行并上传数据。若使用顺序整数处理,不仅复杂度过高(甚至可能不可行)。
考虑到手机和平板产生的海量数据,此场景比初始设想更普遍。
在导出/编辑/更新场景中同样极具价值。若将数据子集导出(例如至Excel),用户可编辑其他列,我仍能安全导入结果。若使用整数,用户可能修改ID字段(这将导致严重问题);而使用UUID时,用户虽可修改,但我可忽略该行(或整个文件),因为其修改后的值将自动失效。
没错,数据库若采用列式存储或分布式键值架构,还能规避索引问题。
新手问题:为何不用整型作为主键,而用UUID作为public_id字段?
如果你在UUID字段上创建索引(因为你的API支持通过UUID检索对象),那么问题本质上相同——至少在Postgres中,主键索引与次级索引几乎没有区别(甚至完全合理地不为表定义主键,因为磁盘存储通过内部ID实现,而索引无论是否为主键 仅指向内存中的行ID)。此外,同一张表存在两个索引还会造成空间浪费。
当然这种情况并非总是弊大于利,例如当存在大量关联关系时,可仅保留一张包含UUID字段(因此索引成本较高)的表, 关联关系则使用更高效的整型键(例如用户实体同时拥有整型和UUID键,用户属性通过整型键引用用户——当然这需要在获取用户时额外执行连接操作,若仅需获取用户属性则无需此步骤)。
Postgres支持创建哈希索引,因此UUID的次级索引方案可行:
https://www.postgresql.org/docs/current/hash-index.html
*编辑:抱歉,之前误读了。我的回答与您的问题无关。
原回答:因为若未随机生成这些整数,它们会呈连续序列,可能导致用户猜出有效ID并由此推断数据内容。详见https://en.wikipedia.org/wiki/German_tank_problem
因此GP评论中public_id字段的隐含逻辑是:凡是暴露标识符的地方都使用public_id字段,这样既能防止ID被猜中,又能保留内部查询时有序ID的优势。
编辑:刚看到你的编辑,看来我们观点一致!
所以我们要在后端搞复杂化,只因抽象层泄露?我认为这毫无道理。
数十年来因顺序/可猜测主键引发的安全漏洞与系统崩溃,(仅是!)我们面临现状的部分原因。应用程序中任何授权检查的疏漏,都等于将整张数据库表奉上给任何有心索取的人。
我认为可采用持久ID(PID,我一直以为是公钥)与自动递增整数ID的组合方案。唯一键在跨系统迁移数据或跨系统引用数据时尤为重要。此外,在URL和API中使用序列ID可能泄露敏感信息,例如数据库中项的数量。
UUID的优势之一在于能轻松合并多数据库数据,而自动递增ID容易引发冲突。
文中提及的微服务会增加顺序递增键的冲突概率。
这又为尽可能避免采用微服务提供了理由。
务必避免两个服务共用同一数据库。我唯一会考虑共享数据库的情况是:仅有一个服务进行修改操作,其余服务仅读取数据。
祝你好运能执行这条规则 🙂
所谓“冲突”是指两个服务类同时尝试使用同一数据库。
若将它们分离(即采用微服务架构),就不会再出现共享数据库的情况。
没有任何机制能阻止多个微服务使用同一数据库,因此实践中这种情况必然发生。
有时甚至出于合理考量。
我个人的做法是先使用大整型,必要时再添加GUID代码字段。若需在租户间导入/导出具有复杂对象关系的数据,可提供基于代码匹配对象的导入功能。
但这也会增加复杂度。
我不喜欢大整数索引的两点:
– 当使用UUID作为外键关联其他表时,错误指定索引导致连接条件出错会立即显现。而整数索引可能返回看似合理的数据,因为连接操作仍会返回大量结果
– 调试时需检索日志,简单的UUID字符串更便于搜索
雪花ID或索尼雪花ID方案可行:
https://en.wikipedia.org/wiki/Snowflake_ID
https://github.com/sony/sonyflake?tab=readme-ov-file
“若使用PostgreSQL”
(在科学报告领域,这相当于经典的“在小鼠中”表述)
问题在于,我们都不是小鼠,但许多人都在使用Postgres。
这相当于说“如果你是中年男性”或“你是美国人”。
附注:我认为其中部分考量适用于任何使用B树索引的系统,但某些内容是Postgres特有的。
这不仅限于Postgres甚至OLTP场景。例如,若您拥有包含SCD2记录的冰山表,就需要定期定位并更新现有记录。记录越新,被更新的可能性就越大。
若采用UUIDv7,可通过键前缀对表进行分区。这样在批量更新时就能高效跳过大部分数据。
补充得很好!
无论使用何种关系型数据库,空间需求和索引碎片问题几乎相同。数学就是数学。
前些天我刚为客户实现显著性能提升——将约1.5亿个UUIDv4主键转换为经典的BIGINT类型。他们使用的MariaDB版本相当新。
我认为作者指的是单服务器部署的数据库。因为分布式数据库通常需要将负载均衡分配到多台服务器。
具体来说:通过避免热点区域来提升性能。
若他们能接受仅在单一位置创建键值,这种方案可行。但若需要跨机器保持极高唯一性且无需同步,则使用大整型不可行。
若能接受MariaDB则无妨,但如今我不会首选它。多数场景下Postgres性能更优。
确实,当时需求相对简单,BIGINT是快速优化方案。MariaDB能在多服务器集群中保证自动递增整数的唯一性,但仅此而已。
若需求不同,UUIDv7同样适用,因为碎片化才是此处的核心问题。
> UUID是否安全?
> 误区:认为UUID具有安全性
> *关于UUID的常见误解是其安全性。然而RFC明确指出,它们不应被视为安全的’能力’。"
> 摘自RFC 4122第6节安全考量:
> 切勿认为UUID难以猜测;它们不应作为安全能力使用
这种说法完全错误,且引用依据并不支持。你猜的并非122位长的随机标识符。更荒谬的是,该文章在紧接此前的段落中,甚至引用了证明其不可猜性的数学原理。
…所链接的引用(指向§4.4节,与正文引用的内容不同)仅涉及v4生成方法,与该论点毫无关联。正文引用的第6节讨论的是UUID的普遍性:声明“切勿假设[所有]UUID都难以猜测”,在逻辑上并不与正确生成的UUIDv4难以猜测相矛盾。只有当生成和使用系统的实现符合特定规范时,UUID的子集才具备安全属性,但我们不应假设所有UUID都具备该属性。
此外,用(实质上随机的)32位整数替换不可猜测的UUID确实会使其可被猜测,而该方案若用于UUIDv4作为不可猜测标识符的场景,则完全不具备安全性。
关于额外空间占用的论点同样站不住脚:在“数百万行数据”的情况下,UUID字段仅额外消耗约24MiB空间。
为何不直接将UUID作为bigint主键旁的唯一列?
UUID的核心价值在于分布式环境中生成易于创建且无冲突的引用。既然TFA明确限定为“单体Web应用”场景,完全可以在内部采用bigint主键,仅在需要对外提供行/对象引用时添加UUID。
没错,若您属于热衷数据库性能优化却排除了多数据库分布方案的开发者群体,继续使用顺序ID完全可行。
我已在生产环境使用ULIDs [0]多年,深感其优越性。目前仅采用字符串编码,但若需极致压缩存储空间,可通过转换将26字符占用的存储空间压缩至16字节。实际应用中从未因此受限,且全局统一使用字符串ID的简洁性令人称道。
有时需要对接遗留系统时,所有API都采用字符串ID,我会将整型ID编码为十进制格式,用前导零补足至26字符。严格来说这不符合ULID规范,但实际操作中,只要看到前导
00就知道这不是真正的ULID——因为真正的ULID诞生于2017年,而2004年11月之前根本不存在这种规范。ORM会自动去除前导零,查询依然正常运行。我实在厌倦了在非业余级别的项目中使用序列整数ID。当无需担心ID是否重复时,测试/固定数据/质量保证流程会轻松许多。
[0] https://github.com/ulid/spec
参见 https://news.ycombinator.com/item?id=46211578
我使用的Python实现不存在这个怪癖。它只是时间戳加上随机数,用Crockford Base32编码。这完全满足我的需求。虽然确实不完全“符合规范”,但坦白说序列号亚毫秒级偏移根本是个设计失误。
我对UUID的核心观点是:别把所有东西都UUID化。大多数场景用普通整数作为主键就足够了。
能创建对象并立即获取ID(无需等待HTTP往返)极大简化了代码,因此我认为UUID值得采用。不过之前确实没考虑过可排序ID的潜在性能优化——今后会考虑UUID v7方案。
太棒了!
你可能既不需要整数主键,也不需要UUID主键。根据具体场景,你需要的可能是介于两者之间的方案。UUID是这个光谱上的极端选择,它试图解决所有问题——包括那些你可能根本不存在的问题。
中间方案是什么?我发这篇文章正是因为正面临这个抉择,希望能引发讨论/争议。
目前大家讨论UUID的篇幅实在太长,我真心好奇中间方案是什么。
PostgreSQL对uuidv7的新支持方案呢?有人做过测试吗?这正是我们当前的决策方向,因此希望最终能撤回这个决定
我1962年出生于温哥华社会保障局538区域。
猜猜我得抵御多少次攻击者尝试538-62-xxxx的攻击?
我曾玩过个小把戏:生成类似UUID的ID。我们通常都能一眼识别UUIDv4:“啊,是UUID”。十多年前我在大型云平台工作时,放弃了上文作者建议的字符串密钥生成方式(整数→二进制→base62字符串),转而采用更“聪明”的方案。
UUID由128位组成:前64位是Java long类型,后64位也是Java long类型。我们直接将租户ID的long值与资源ID的long值组合,在平台上生成唯一标识符(直到某天失效为止)。
Atlassian为此采用了更长的“ARI”方案(例如https://developer.atlassian.com/cloud/guard-detect/developer…),通过GUID组合实现类似Amazon ARN的传递机制。
没错,资源ID才是我们的痛点。它究竟代表什么?是帖子?上传文件?还是工作区?描述性远不及我们的需求。
主键的均匀分布有时很有优势。作为读者,即使对写入者造成困扰。例如,这样能轻松实现查询和工作负载的分片。
> 这对索引中单项或值域的插入与检索有何影响?
经典的OLTP与OLAP之争。
我本期待作者探讨并行写入的分布式数据库替代方案。顺序键在此场景下将造成灾难性后果——热点不可避免地出现,彻底抵消分布式数据库的优势。
希望使用Google Spanner等系统的同行分享经验:你们是否遇到过UUID相关问题?目前我尚未遇到,多数优化在控制器层实现,数据转换因验证机制可能较慢。建议尽可能简化服务逻辑。
各位好,有个疑问:若我不愿像v7那样在UUID中嵌入时间戳,在特定场景下是否会暴露时间戳攻击风险?
另外,API客户端是否必须看到UUID?或者将查询复杂性完全隐藏在命名标识符后是否可行?即使这可能在连接和索引方面带来些许代价。
背景是经典的B2B SaaS场景,但欢迎分享其他场景的经验!
若要使用乐观锁机制,难道不需要暴露UUID吗?
我认为这恰恰是API中持续暴露UUID的合理依据之一。
若数据库表必须存储UUIDv4,这个论点是否依然成立?
而且我可能还想为其设置唯一约束?
有没有靠谱的资源提供Postgres中UUID与整型主键在插入、索引、检索等方面的基准测试数据?
这让我想起这个生成Firebase式“推送ID”的旧代码片段[1]。这类ID具有更优特性。
[1] https://gist.github.com/mikelehen/3596a30bd69384624c11
非常有用的文章,感谢分享!许多人推荐CUID2,但它效率较低,更适合用于前端/URL编码。对于后端/数据库,仅应使用UUID v7。
另一篇2024年2月的精彩文章[0]指出,插入uuid7()与bigint的成本基本相当。虽然我对缓冲区缓存的问题尚不完全理解,但作者的阐述比原帖清晰得多:
> 当数据块不在PostgreSQL缓冲区缓存中时,我们需要从磁盘读取数据块。值得庆幸的是,PostgreSQL提供了便捷的缓冲区缓存内容检查机制。这正是uuidv4与uuidv7差异的关键所在:由于uuidv4数据缺乏数据局部性,主键索引为支持新数据插入而消耗了大量缓冲区缓存空间——这些空间不再可供其他索引和表使用,从而显著拖慢了整体工作负载。
0 – https://ardentperf.com/2024/02/03/uuid-benchmark-war
我照做就是了,因为每次我过早追求这种优化时,硬件总会更快进化。
我们都遇到过这样的问题:无法确定数据最终存放何处才能最优匹配访问模式。
或是设备与服务执行异步操作时需要同步。
我搞的又不是关键任务型系统——那种“一旦失败就会引发灾难性事件”的破玩意儿。这不过是租值寻租的SaaS垃圾。
哎呀,多赚100美元竟要多花0.35美元成本。明年收益增长肯定能抵消成本上升。
这确实是Postgres的重要缺陷。
哈希索引本是UUID的理想选择,但不知为何Postgres的哈希索引无法保证唯一性。
除非从我冰冷僵硬的手中夺走,否则别想剥夺我在应用任何位置生成唯一编号并无冲突存储的能力。
预先确定主键值的能力(无需先持久化再返回)彻底改变了我的应用架构设计。许多原本笨拙的操作变得自然流畅。
听起来会引发大量参照完整性违规。
提前生成主键为何会导致参照完整性违规?非常好奇具体机制。
其含义在于:你需要提前知道主键值,才能将其插入引用该主键的外键表中,而无需等待主键返回。这进一步暗示你没有设置外键约束,因为数据库会禁止这种操作。
不过在Postgres中,你可以声明外键为延迟约束,这样其存在性检查会在事务提交时进行,而非插入时。
如果数据库不强制执行参照完整性,应用程序逻辑就必须极其谨慎;根据我的经验,这种做法终将失败。总会有人写出错误代码,导致数据异常。
> 诚然在Postgres中,可将外键声明为延迟约束,使其在事务提交时而非插入时进行存在性检查。但这进一步意味着你没有外键约束,因为数据库会禁止这种操作。
我使用EF Core来建立这些关系,并通过MSSQL服务器在单个事务中持久化它们。
> 若数据库未强制执行参照完整性
我正在构建电子医疗系统,深知参照完整性的重要性。
在DynamoDB等非关系型数据库中使用UUID作为主键是可行的,且不会引发文中提及的问题。
值得指出的是,该帖子应明确说明仅基于我对Postgres的实践经验。
若我们遵循Roy Fielding构想的REST理念,便不会产生此类讨论。REST不暴露标识符,仅暴露关系。标识符属于实现细节。
UUID主键试图解决错误的问题。只要永不对外暴露或使用,整型/序列号主键本身并非问题。几乎所有RESTful框架的关键缺陷在于暴露内部数据库标识符,而非使用加密标识符——后者既能保持相对性能,又能保留创建顺序,并消除无主键探测问题。
> 切勿认为UUID难以破解;它们不应作为安全能力使用
问题在于,这几乎适用于所有能力URL。我不会在此推荐使用UUID本身,随机数可能是更优选择。不过实践中确实见过为此目的使用的UUID,这些系统并未因此遭受安全威胁。
我讨厌密码重置流程中那种让验证链接仅有效5分钟的设计。当然这些链接需要有限时效性,但邮件并非实时通信媒介。将有效期从30分钟缩短到5分钟几乎毫无安全效益——这种做法并不能提升安全性。
反驳观点在于:使用整数ID本身存在诸多问题。它们不能公开,因为会泄露信息;跨环境不具备唯一性,意味着你得创建大量测试环境才能运行。不过抱怨测试环境不就是Retros的本职工作吗?
“UUID4只有224位”是无稽之谈,纯属人为制造的问题。
但合理建议是:应使用带时间戳的顺序UUID避免碎片化。
以下是UUID常能解决的典型场景:
– 客户原在本地部署应用,现需迁移至云端
– 支持工程师需将客户账户克隆至开发环境调试问题,避免破坏客户数据
– 客户需要将账户迁移至不同区域(如从美国迁至欧洲)。
基于UUID的数据合并极为简便,因ID冲突几乎不可能发生。若使用整数ID,则需编写复杂且易出错的ID重写脚本。与文章所述相反,即使表规模较小,UUID仍具有极高实用价值。
若跨环境数据合并或迁移属常态操作,我认同采用无冲突主键最为理想。我曾使用整数序列在不同AWS区域的新数据库中迁移约100张表,虽可行但成本极高。该公司还存在演示/客户预览环境概念,需在保留数据前提下实现迁移。
UUID能有效抵御枚举攻击,同时避免因观察到高有效ID值而推测私营企业的盈利状况——若企业按ID关联对象收费的话。若能采集足够多的对象ID值并追溯创建时间,便可逆向推导其ARR图表,从而判断企业是否处于增长期,这正是许多公司竭力避免的。
最让我抓狂的是无法双击选中对象。
这取决于应用程序特性。iTerm2不会按分隔符拆分——为何Firefox会?
呃,UUID真是嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡�
我的建议是:避免对任何技术做笼统评价。
我受够了这种半吊子论调:“技术X执行操作Z的速度比技术Y快N%。既然你的系统(有时)会执行操作Z,就意味着技术X是所有场景下的唯一合理选择!”
这种论调愚蠢得令人发指——操作Z可能仅占系统总CPU使用量的10%(平均值)…所谓50%的性能提升,放在整体架构中考量时,实际可能只有5%… 微不足道。若众人理性看待这种性能“优势”,绝不会认为值得为此牺牲关键的安全性或运行特性。
不知我们行业怎么了——理应是高智商群体,却屡屡目睹开发者陷入如此明显的逻辑谬误。
记得当年有位资深工程师讨论升级Python系统时,曾坦率承认新引擎比旧版慢了40%左右,但他根本无需解释为何升级仍是明智之举——公司所有人都明白他仅指代码执行速度,而这在整体性能中占比微乎其微。
并非说UUIDv7对Postgres是糟糕选择。我相信它适用于许多场景,但没必要像传教士般宣扬“唯一真UUID”的福音来为项目决策辩护。
不过我确实觉得社区选择沿用UUIDv7而非制定新标准的做法颇为狡黠。
UUID的初衷正是利用随机性特性生成无需协调的唯一标识符。而UUIDv7似乎在哲学层面另辟蹊径——人们选择UUID看重的是可扩展性与简洁性(这两点恰恰源于消除了协调开销),而非原始性能表现…
还有件事让我抓狂:那些分不清性能和可扩展性区别的人。他们愚蠢地将可扩展性等同于并行或并发,而这只是其中一个方面——可扩展性是更广阔的议题。它本质上是理论系统与实际系统的区别:前者在人为设定的微小输入规模下表现迅捷,后者则随输入规模增长而持续提升效能。
最后,关于生成UUIDv7标识符背后复杂的逻辑机制竟无人提及…人们理所当然地认为所有计算机都配备着能产生精确时间戳的时钟,且全球计算机竟能神奇地保持同步…UUIDv7绝非简单,它极其复杂。相较于UUIDv4,它增加了大量额外复杂性和依赖关系。这种复杂性虽被巧妙隐藏在多数开发者视线之外,但它真实存在且构成关键依赖…当我们迈向机器人与嵌入式系统时代时,这种缺陷将尤为凸显——廉价微芯片可能缺乏足够的闪存空间来容纳生成此类复杂ID所需的程序代码。
没错。我们有使用UUIDv4的表,包含6000万+行数据,性能毫无问题。换用其他方案是否能提升某些查询速度?或许可以,但对我们而言这根本不是瓶颈。若在6亿或60亿行数据时出现问题,届时再解决。我们未来可能会迁移到UUIDv7,但这并非当务之急,且会先在自有数据上进行测试。我的经验是否意味着你该用UUIDv4?不。关键是要理解自身系统,评估取舍对你的实际影响。
我也有数十亿行数据的表使用UUIDv4主键,同样未遇任何问题。写入密集型表确实采用UUIDv7,但即便如此,批量插入带来的性能提升远超从UUIDv4切换至UUIDv7的效果。这个问题被过度夸大了。
感谢反馈。好奇问一句,您是否对psql进行过精细调优从而显著提升性能?
选择UUID作为ID本身不就是中了谬论的圈套吗?
倒也未必。它们在特定场景下非常便利,总体表现也相当出色。我从未遇到过因使用UUID而引发的性能问题。
你未曾遇见问题并不代表它不存在;我过去确实处理过因GUID主键导致页面碎片化严重引发的重大性能问题。
回到你的核心观点,这些都是经验法则。开发者未必有时间深入推敲每个架构细节,因此掌握一套实用准则能以最低认知成本预防问题。“尽可能避免使用GUID作为主键”就是我的经验法则之一。
请问具体指哪些特定场景?
对我而言最关键的是防止重复记录。
当客户端POST新对象插入数据库时,若连接失败且未收到服务器成功响应,客户端无法确认记录是否插入——除非执行耗时且繁琐的额外读取操作进行验证… 客户端不能仅因未收到成功响应就断定插入失败。完全可能插入成功后立即发生连接中断导致响应丢失。若服务器采用自动递增ID机制,而客户端再次提交相同对象时未携带ID,服务器将在数据库表中创建重复记录(相同对象但ID不同)。
另一方面,若客户端在前端为待创建对象生成UUID,则可安全地无限次重发相同对象,完全避免重复插入风险;第二次提交时系统会直接拒绝该对象,此时可向用户展示“记录已存在”的明确错误提示,而非创建两个相同资源导致潜在错误和混乱。
呃…你的意思是INSERT … RETURNING id在客户端视角下并非原子操作?因为当客户端通过SQL驱动接收响应时,可能发生严重问题?
我其实更关注前端客户端,比如单页应用。网络不稳定可能导致插入成功后响应无法到达前端。这种情况虽不常见,但当用户量超过一定阈值时,对数据库管理员而言绝对是个问题。我在生产环境中见过此类故障,由于发生概率极低,重复记录的根本原因往往令人费解。往往会引发难以调试的问题。
明白了。我之前想的是SQL客户端,而非REST服务的客户端。明确这个区别后,你的逻辑就合理了,谢谢。
理想情况下,应设计具有幂等性的API和服务(例如使用PUT而非POST等)。
在我看来,使用幂等标识符是最后的手段。
不过UUID可能是生成此类幂等标识符最简单可靠的方式。
URI难道不是PUT操作的幂等标识符吗?
我始终不理解反对使用全局唯一标识符的论点。例如所谓破坏索引结构的说法——我虽非计算机专业出身,但索引不都是B树结构吗?若主键生成机制完全随机且每个数字概率均等,那么B树结构必然保持平衡。
虽然生成方式各有优劣,但比起数据库自动递增的垃圾,UUID终究优雅得多。这只是语义问题,未来随时可更换UUID算法。说真的,既然你应该把UUID当作不透明实体处理,何不直接选随机值?
有人会质疑:“但如果需要对UUID排序呢?”比如用于排序故事列表?再次强调——既然将UUID视为不透明数据,何必排序?你应该基于日期、标题等其他字段排序。UUID就是不透明数据,天杀的!不透明数据本就不该排序。有人会说:“但它们会形成奇怪的聚类。” 你凭什么用随机不透明键值聚类?若需特定数据聚类,就该用正确键值(比如用户ID字段,若数据需按用户聚类)。
让客户端生成主键实在太解放了!不必担心主键冲突,也避免了通过自动递增数字泄露信息,简直太棒!
在我看来UUID根本没被充分利用!
> 若主键生成机制完全随机且每个数值概率均等,那么B树结构将始终保持平衡。
平衡且均匀分布。随机索引意味着每次访问都需随机读取页面。若访问模式真正随机则无妨,但这种情况很少见。
> 为何要基于随机不透明键进行聚簇?
InnoDB会按主键聚簇(若存在主键则无法更改;若无主键则有其他选项,但此处假设存在主键)。MSSQL行为类似但可覆盖。若主键随机,聚簇结果也将随机。在Postgres中,你只会得到碎片化的索引,这虽不至于太糟糕,但仍会拖慢vacuum进程。这是否真正构成问题,同样取决于访问模式。
面对随机主键时不必立即惊慌,但至少应当意识到其可能导致的性能退化。
坦白说,虽然你在大多数情况下确实正确,但使用某种形式的uuid完全没问题。我认为在多数场景下其收益远大于成本。
这正是ULID存在的意义,也是我将其用于ext_id列的原因。对于数据库内部实际的关系ID,我则采用更小巧/更高效的数据类型。