|
| 1 | +# How to use UUID |
| 2 | + |
| 3 | +>我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! |
| 4 | +
|
| 5 | +截至目前 (PG16, 2023 年),Postgres 根据 [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122) 实现了从 1 到 5 的 UUID 版本。 |
| 6 | + |
| 7 | +- 文档:[UUID Data Type](https://postgresql.org/docs/current/datatype-uuid.html) |
| 8 | +- 附加模块 [uuid-ossp](https://postgresql.org/docs/current/uuid-ossp.html) |
| 9 | + |
| 10 | +使用 `gen_random_uuid()` 会生成一个UUID值,它生成的是 4 版本的 UUID ([source code for PG16](https://github.com/postgres/postgres/blob/03749325d16c4215ecd6d6a6fe117d93931d84aa/src/backend/utils/adt/uuid.c#L405-L423)): |
| 11 | + |
| 12 | +```sql |
| 13 | +nik=# select gen_random_uuid(); |
| 14 | + gen_random_uuid |
| 15 | +-------------------------------------- |
| 16 | + c027497b-c510-413b-9092-8e6c99cf9596 |
| 17 | +(1 row) |
| 18 | + |
| 19 | +nik=# select gen_random_uuid(); |
| 20 | + gen_random_uuid |
| 21 | +-------------------------------------- |
| 22 | + 08e63fed-f883-45d8-9896-8f087074bff5 |
| 23 | +(1 row) |
| 24 | +``` |
| 25 | + |
| 26 | +在标准 UUID 中,可以通过第二个连字符后的第一个字符来判断版本: |
| 27 | + |
| 28 | +~~~bash |
| 29 | +08e63fed-f883-4 ... 👈 this means v4 |
| 30 | +~~~ |
| 31 | + |
| 32 | +这些值以"伪随机"顺序出现。这对性能有一定的负面影响:在 B-tree 索引中,插入发生在不同位置,这通常会影响写入性能以及 Top-N 读取 (选择最新的 N 行) 的性能。 |
| 33 | + |
| 34 | +目前有一个建议,在 RFC 和 Postgres 中实现更新版本的 UUID — 7 版本中提供了一种基于时间的 UUID,其中包含毫秒级精度的时间戳,序列号,以及随机或固定位形式的额外熵。这种 UUID 不仅确保了全局唯一性,还保留了时间顺序,这对性能非常有益。 |
| 35 | + |
| 36 | +- [Commitfest: UUID v7](https://commitfest.postgresql.org/45/4388/) |
| 37 | +- [rfc4122bis proposal](https://datatracker.ietf.org/doc/draft-ietf-uuidrev-rfc4122bis/) |
| 38 | + |
| 39 | +UUID 值是 16 字节的 — 与 `timestamptz` 或 `timestamp` 值相同。 |
| 40 | + |
| 41 | +以下是一些解释性能方面的优秀资料: |
| 42 | + |
| 43 | +- [The effect of Random UUID on database performance](https://twitter.com/hnasr/status/1695270411481796868) by [@hnasr](https://twitter.com/hnasr) (视频,大约 19 分钟) |
| 44 | +- [Identity Crisis: Sequence v. UUID as Primary Key](https://brandur.org/nanoglyphs/026-ids#ulids) by [@brandur](https://twitter.com/brandur) |
| 45 | + |
| 46 | +由于 Postgres 尚不原生支持UUID v7,目前有两个选择: |
| 47 | + |
| 48 | +1. 在客户端生成 UUID。 |
| 49 | +2. 在 Postgres 中实现辅助函数。 |
| 50 | + |
| 51 | +对于第二种方法,这里提供了一个 [SQL function](https://gist.github.com/kjmph/5bd772b2c2df145aa645b837da7eca74) (感谢 [@DanielVerite](https://twitter.com/DanielVerite)):: |
| 52 | + |
| 53 | +```sql |
| 54 | +create or replace function uuid_generate_v7() returns uuid |
| 55 | +as $$ |
| 56 | + -- use random v4 uuid as starting point (which has the same variant we need) |
| 57 | + -- then overlay timestamp |
| 58 | + -- then set version 7 by flipping the 2 and 1 bit in the version 4 string |
| 59 | +select encode( |
| 60 | + set_bit( |
| 61 | + set_bit( |
| 62 | + overlay( |
| 63 | + uuid_send(gen_random_uuid()) |
| 64 | + placing substring(int8send(floor(extract(epoch from clock_timestamp()) * 1000)::bigint) from 3) |
| 65 | + from 1 for 6 |
| 66 | + ), |
| 67 | + 52, 1 |
| 68 | + ), |
| 69 | + 53, 1 |
| 70 | + ), |
| 71 | + 'hex')::uuid; |
| 72 | +$$ language SQL volatile; |
| 73 | +``` |
| 74 | + |
| 75 | +示例 |
| 76 | + |
| 77 | +```sql |
| 78 | +nik=# select uuid_generate_v7(); |
| 79 | + uuid_generate_v7 |
| 80 | +-------------------------------------- |
| 81 | + 018c1be3-e485-7252-b80f-76a71843466a |
| 82 | +(1 row) |
| 83 | + |
| 84 | +nik=# select uuid_generate_v7(); |
| 85 | + uuid_generate_v7 |
| 86 | +-------------------------------------- |
| 87 | + 018c1be3-e767-76b9-93dc-23c0c48be6c7 |
| 88 | +(1 row) |
| 89 | + |
| 90 | +nik=# select uuid_generate_v7(); |
| 91 | + uuid_generate_v7 |
| 92 | +-------------------------------------- |
| 93 | + 018c1be3-e973-7704-82ad-5967b79cf5c4 |
| 94 | +(1 row) |
| 95 | +``` |
| 96 | + |
| 97 | +几分钟之后: |
| 98 | + |
| 99 | +```sql |
| 100 | +nik=# select uuid_generate_v7(); |
| 101 | + uuid_generate_v7 |
| 102 | +-------------------------------------- |
| 103 | + 018c1be8-5002-70ab-96c0-c96ad5afa151 |
| 104 | +(1 row) |
| 105 | +``` |
| 106 | + |
| 107 | +一些注意事项: |
| 108 | + |
| 109 | +1. 如果在 `ORDER BY` 子句中使用这些值,时间顺序会保持不变。 |
| 110 | + |
| 111 | +2. 在最初生成的三个值中 (几秒钟内生成),存在一个共同前缀 `018c1be3-e`。在稍后生成的最后一个值中,有一个共同前缀 `018c1be`。 |
| 112 | + |
| 113 | +3. 注意所有值中第二个连字符后的 7: |
| 114 | + |
| 115 | + ~~~bash |
| 116 | + 018c1be3-e973-7... 👈 this means v7 |
| 117 | + ~~~ |
| 118 | + |
| 119 | +4. 该函数返回 `UUID` 类型的值,因此仍然是 16 字节 (而它的文本表示需要 36 个字符,包括连接符,这意味着加上 `VARLENA` header 的话,总共需要 40 个字节): |
| 120 | + |
| 121 | +```sql |
| 122 | +nik=# select pg_column_size(gen_random_uuid()); |
| 123 | + pg_column_size |
| 124 | +---------------- |
| 125 | + 16 |
| 126 | +(1 row) |
| 127 | + |
| 128 | +nik=# select pg_column_size(uuid_generate_v7()); |
| 129 | + pg_column_size |
| 130 | +---------------- |
| 131 | + 16 |
| 132 | +(1 row) |
| 133 | +``` |
| 134 | + |
| 135 | +# 我见 |
| 136 | + |
| 137 | +关于 UUID 导致性能问题的案例,我也写过几篇 🔗 |
| 138 | + |
| 139 | +- [从DBA的角度聊聊UUID的利与弊](https://mp.weixin.qq.com/s?__biz=MzUyOTAyMzMyNg==&mid=2247489659&idx=1&sn=f5cbf3851cd457f1c8093132e97b35e2&chksm=fa66304acd11b95c2a1aad0cde2048e867eaf977e86dcbdea06ab6ffb84c3d04f326bcee90c8&token=1789316483&lang=zh_CN#rd) |
| 140 | +- [从一个案例聊聊FPI的危害](https://mp.weixin.qq.com/s?__biz=MzUyOTAyMzMyNg==&mid=2247488814&idx=1&sn=edd2be1b87259a0316f8d4ba233bc2a2&chksm=fa663d1fcd11b40992776be7f78052b8e77aa595d6773796d6e8e916859ecc521737c70c3383&token=1789316483&lang=zh_CN#rd) |
| 141 | + |
| 142 | +为了规避前文所说的问题,你可以选择使用有序UUID:https://github.com/tvondra/sequential-uuids,以及UUID v7,v6 和 v7 ( https://github.com/fboulnois/pg_uuidv7 提供了v7的支持 ) 都有考虑可排序性,解决 UUID 应用时最常遇到的数据库性能问题。社区也在实现中。 |
0 commit comments