当前位置: 首页 > news >正文

如何在PostgreSQL正确的 使用UUID 作为主键

如何在PostgreSQL正确的 使用UUID 作为主键

UUID 经常用作数据库表主键。它们易于生成,易于在分布式系统之间共享并保证唯一性。

考虑到 UUID 的大小,这是否是一个正确的选择值得怀疑,但通常这不是由我们决定的。

本文的重点不是“UUID 是否是主键的正确格式”,而是如何有效地使用 UUID 作为 PostgreSQL 的主键。

Postgres 数据的UUID类型

UUID 可以被视为一个字符串,并且可能很容易将它们存储为字符串。 Postgres 具有用于存储字符串的灵活数据类型: text ,并且通常用作存储 UUID 值的主键。

它是正确的数据类型吗?当然不是。

Postgres 有一个专用于 UUID 的数据类型: uuid 。 UUID 是 128 位数据类型,因此存储单个值需要 16 个字节。 text 数据类型有 1 或 4 个字节的开销加上存储实际的字符串。

这些差异在小表中并不那么重要,但一旦开始存储数十万或数百万行,就会成为问题。

我进行了一个实验,看看实践中有何不同。有两个表只有一列 - id 作为主键。第一个表使用 text ,第二个表使用 uuid

create table bank_transfer(id text primary key
);create table bank_transfer_uuid(id uuid primary key
);

我没有指定主键索引的类型,因此 Postgres 使用默认的 B 树。

然后我使用 Spring 的 JdbcTemplate 中的 batchUpdate 向每个表插入 10 000 000 行:

jdbcTemplate.batchUpdate("insert into bank_transfer (id) values (?)",new BatchPreparedStatementSetter() {@Overridepublic void setValues(PreparedStatement ps, int i) throws SQLException {ps.setString(1, UUID.randomUUID().toString());}@Overridepublic int getBatchSize() {return 10_000_000;}
});
jdbcTemplate.batchUpdate("insert into bank_transfer_uuid (id) values (?)",new BatchPreparedStatementSetter() {@Overridepublic void setValues(PreparedStatement ps, int i) throws SQLException {ps.setObject(1, UUID.randomUUID());}@Overridepublic int getBatchSize() {return 10_000_000;}});

查询表大小和索引大小:

select relname as "table", indexrelname as "index",pg_size_pretty(pg_relation_size(relid)) "table size",pg_size_pretty(pg_relation_size(indexrelid)) "index size"
from pg_stat_all_indexes
where relname not like 'pg%';
+------------------+-----------------------+----------+----------+
|table             |index                  |table size|index size|
+------------------+-----------------------+----------+----------+
|bank_transfer_uuid|bank_transfer_uuid_pkey|422 MB    |394 MB    |
|bank_transfer     |bank_transfer_pkey     |651 MB    |730 MB    |
+------------------+-----------------------+----------+----------+

使用 text 的表增大了 54%,索引大小增大了 85%。这也反映在 Postgres 用于存储这些表和索引的页数上:

select relname, relpages from pg_class 
where relname like 'bank_transfer%';
+-----------------------+--------+
|relname                |relpages|
+-----------------------+--------+
|bank_transfer          |83334   |
|bank_transfer_pkey     |85498   |
|bank_transfer_uuid     |54055   |
|bank_transfer_uuid_pkey|50463   |
+-----------------------+--------+

更大的表、索引和更多的表意味着 Postgres 必须执行插入新行和获取行的工作 - 特别是当索引大小大于可用 RAM 内存时,Postgres 必须从磁盘加载索引。

UUID 和 B 树索引

随机 UUID 不太适合 B 树索引 - 并且 B 树索引是主键唯一可用的索引类型

B 树索引最适合处理有序值 - 例如自动递增列或时间排序列。

UUID - 尽管看起来总是相似 - 有多种变体。 Java 的 UUID.randomUUID() - 返回 UUID v4 - 这是一个伪随机值。对我们来说,更有趣的是 UUID v7 - 它生成按时间排序的值。这意味着每生成一个新的UUID v7,它就有一个更大的值。这使得它非常适合 B 树索引。

要在 Java 中使用 UUID v7,我们需要一个第三方库,例如 java-uuid-generator:

<dependency><groupId>com.fasterxml.uuid</groupId><artifactId>java-uuid-generator</artifactId><version>5.0.0</version>
</dependency>

然后我们可以使用以下命令生成 UUID v7:

UUID uuid = Generators.timeBasedEpochGenerator().generate();
// 多次运行结果, 字符串长度为36
0190a48c-2ee3-7c50-b45c-d51fecb36a9a
0190a48c-8fce-7fd3-b141-1fb312f9096e
0190a48c-f36b-707d-9826-644fe72b7e02
0190a48d-e236-76b5-a03c-54f09fa27e9c

理论上,这应该可以提高执行 INSERT 语句的性能。

UUID v7 如何影响 INSERT 性能

我创建了另一个表,与 bank_transfer_uuid 完全相同,但它将仅存储使用上述库生成的 UUID v7:

create table bank_transfer_uuid_v7(id uuid primary key
);

然后,我运行 10 轮,向每个表插入 10000 行,并测量需要多长时间:

for (int i = 1; i <= 10; i++) {measure(() -> IntStream.rangeClosed(0, 10000).forEach(it -> {jdbcClient.sql("insert into bank_transfer (id) values (:id)").param("id", UUID.randomUUID().toString()).update();}));measure(() -> IntStream.rangeClosed(0, 10000).forEach(it -> {jdbcClient.sql("insert into bank_transfer_uuid (id) values (:id)").param("id", UUID.randomUUID()).update();}));measure(() -> IntStream.rangeClosed(0, 10000).forEach(it -> {jdbcClient.sql("insert into bank_transfer_uuid_v7 (id) values (:id)").param("id", Generators.timeBasedEpochGenerator().generate()).update();}));
}

结果看起来有点随机,在与常规 text 列和 uuid v4 的表的时间比较时:

+-------+-------+---------+
| text  | uuid  | uuid v7 |
+-------+-------+---------+
| 7428  | 8584  | 3398    |
| 5611  | 4966  | 3654    |
| 13849 | 10398 | 3771    |
| 6585  | 7624  | 3679    |
| 6131  | 5142  | 3861    |
| 6199  | 10336 | 3722    |
| 6764  | 6039  | 3644    |
| 9053  | 5515  | 3621    |
| 6134  | 5367  | 3706    |
| 11058 | 5551  | 3850    |
+-------+-------+---------+

但我们可以清楚地看到,插入 UUID v7 比插入常规 UUID v4 快约 2 倍。

Further reading 进一步阅读

  • UUID v7 will likely be supported natively in Postgres 17
    Postgres 17 可能会原生支持 UUID v7
  • UUID Version 7 format
    UUID 版本 7 格式
  • UUIDs are Popular, but Bad for Performance
    UUID 很流行,但对性能不利
  • https://vladmihalcea.com/uuid-database-primary-key/

概括

正如一开始提到的 - 由于 UUID 长度 - 即使进行了所有这些优化,它也不是主键的最佳类型。如果您可以选择,请查看由 Vlad Mihalcea 维护的 TSID。

但如果您必须或出于某种原因想要使用 UUID,请考虑我提到的优化。另请记住,此类优化对于大型数据集会产生影响。如果您存储数百甚至数千行,并且访问量较低,您可能不会看到应用程序性能有任何差异。但是,如果您有可能拥有大型数据集或大访问量 - 最好从一开始就这样做,因为更改主键可能是一个相当大的挑战。

Maciej Walkowiak | PostgreSQL and UUID as primary key

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 虚幻引擎ue5游戏运行界面白茫茫一片,怎么处理
  • ES6 Module 的语法(十二)
  • ubuntu服务器安装labelimg报错记录
  • vue父组件样式穿透修改子组件样式
  • LeetCode热题100刷题13:64. 最小路径和、62. 不同路径、5. 最长回文子串、1143. 最长公共子序列
  • AI人工智能开源大模型生态体系分析
  • 基于lstm的股票Volume预测
  • Rust 测试的组织结构
  • 护网HW面试——redis利用方式即复现
  • 快速读出linux 内核中全局变量
  • log4j2.xml 使用 application.yml 配置的属性
  • centos部署jar包
  • 【qt】正则表达式来判断是否为邮箱登录
  • 代理模式(大话设计模式)C/C++版本
  • SQL注入安全漏洞与防御策略
  • Android路由框架AnnoRouter:使用Java接口来定义路由跳转
  • Apache的基本使用
  • input的行数自动增减
  • iOS筛选菜单、分段选择器、导航栏、悬浮窗、转场动画、启动视频等源码
  • js
  • Spring Cloud Feign的两种使用姿势
  • 高性能JavaScript阅读简记(三)
  • 个人博客开发系列:评论功能之GitHub账号OAuth授权
  • 聊一聊前端的监控
  • 前端临床手札——文件上传
  • 如何合理的规划jvm性能调优
  • 如何在 Tornado 中实现 Middleware
  • 消息队列系列二(IOT中消息队列的应用)
  • 与 ConTeXt MkIV 官方文档的接驳
  • 400多位云计算专家和开发者,加入了同一个组织 ...
  • 阿里云IoT边缘计算助力企业零改造实现远程运维 ...
  • ​Java基础复习笔记 第16章:网络编程
  • ​软考-高级-系统架构设计师教程(清华第2版)【第12章 信息系统架构设计理论与实践(P420~465)-思维导图】​
  • ​探讨元宇宙和VR虚拟现实之间的区别​
  • ######## golang各章节终篇索引 ########
  • #Linux(make工具和makefile文件以及makefile语法)
  • #我与Java虚拟机的故事#连载07:我放弃了对JVM的进一步学习
  • (12)目标检测_SSD基于pytorch搭建代码
  • (python)数据结构---字典
  • (Redis使用系列) Springboot 整合Redisson 实现分布式锁 七
  • (二)hibernate配置管理
  • (二)构建dubbo分布式平台-平台功能导图
  • (附源码)springboot码头作业管理系统 毕业设计 341654
  • (个人笔记质量不佳)SQL 左连接、右连接、内连接的区别
  • (六)Flink 窗口计算
  • (篇九)MySQL常用内置函数
  • (原创)攻击方式学习之(4) - 拒绝服务(DOS/DDOS/DRDOS)
  • (转)Android中使用ormlite实现持久化(一)--HelloOrmLite
  • (转)EXC_BREAKPOINT僵尸错误
  • (轉貼) VS2005 快捷键 (初級) (.NET) (Visual Studio)
  • (自用)gtest单元测试
  • *算法训练(leetcode)第四十天 | 647. 回文子串、516. 最长回文子序列
  • .net core 使用js,.net core 使用javascript,在.net core项目中怎么使用javascript
  • .net core使用ef 6
  • .NET4.0并行计算技术基础(1)