非整数主键注意事项

Ren*_*aro 18 postgresql database-design

语境

我正在设计一个数据库(在 PostgreSQL 9.6 上),它将存储来自分布式应用程序的数据。由于应用程序的分布式特性,SERIAL由于潜在的竞争条件,我不能使用自动递增整数 ( ) 作为我的主键。

自然的解决方案是使用 UUID,或全局唯一标识符。Postgres 带有一个内置的UUIDtype,非常适合。

我对 UUID 的问题与调试有关:它是一个非人类友好的字符串。标识符ff53e96d-5fd7-4450-bc99-111b91875ec5什么也没告诉我,而ACC-f8kJd9xKCd,虽然不能保证是唯一的,但告诉我我正在处理一个ACC对象。

从编程的角度来看,调试与几个不同对象相关的应用程序查询是很常见的。假设程序员错误地ACCORD(订单)表中搜索(帐户)对象。使用人类可读的标识符,程序员可以立即识别问题,而在使用 UUID 时,他会花一些时间找出问题所在。

我不需要 UUID 的“保证”唯一性;我确实需要一些空间来生成没有冲突的密钥,但 UUID 有点矫枉过正。此外,最坏的情况是,如果发生冲突(数据库拒绝它并且应用程序可以恢复),也不会是世界末日。因此,权衡考虑,一个更小但对人友好的标识符将是我的用例的理想解决方案。

识别应用对象

我想出的标识符具有以下格式:{domain}-{string},其中{domain}替换为对象域(帐户,订单,产品)并且{string}是随机生成的字符串。在某些情况下,{sub-domain}在随机字符串之前插入一个甚至可能是有意义的。让我们忽略的长度{domain},并{string}为保证唯一性的目的。

如果有助于索引/查询性能,格式可以具有固定大小。

问题

知道:

  • 我想要具有类似ACC-f8kJd9xKCd.
  • 这些主键将是几个表的一部分。
  • 所有这些键都将用于 6NF 数据库上的多个连接/关系。
  • 大多数表将具有中到大的大小(平均约 1M 行;最大的表具有约 100M 行)。

关于性能,存储此密钥的最佳方法是什么?

以下是四种可能的解决方案,但由于我对数据库的经验很少,我不确定哪个(如果有)是最好的。

考虑的解决方案

1. 存储为字符串 ( VARCHAR)

(Postgres 在CHAR(n)和之间没有区别VARCHAR(n),所以我忽略了CHAR)。

经过一些研究,我发现使用 进行字符串比较VARCHAR,特别是在连接操作上,比使用INTEGER. 这是有道理的,但在这种规模下我应该担心吗?

2. 存储为二进制 ( bytea)

与 Postgres 不同,MySQL 没有本机UUID类型。有几篇文章解释了如何使用 16 字节BINARY字段而不是 36字节字段来存储 UUID VARCHAR。这些帖子让我想到了将密钥存储为二进制文件(bytea在 Postgres 上)。

这节省了尺寸,但我更关心性能。我没有找到关于哪个比较更快的解释:二进制或字符串的解释。我相信二进制比较更快。如果是,那么bytea可能比 更好VARCHAR,即使程序员现在每次都必须对数据进行编码/解码。

我可能是错的,但我认为两者byteaVARCHAR会(通过文字或文字),由字节比较(平等)字节。有没有办法“跳过”这个逐步比较并简单地比较“整个事情”?(我不这么认为,但它不需要检查费用)。

我认为按原样存储bytea是最好的解决方案,但我想知道是否还有其他我忽略的替代方案。此外,我在解决方案 1 中表达的相同担忧也成立:比较的开销是否足以让我担心?

“创意”解决方案

我想出了两个可以工作的非常“有创意”的解决方案,我只是不确定在多大程度上(即,如果我将它们缩放到表中的几千行以上会遇到问题)。

3. 保存为UUID但附有“标签”

不使用 UUID 的主要原因是程序员可以更好地调试应用程序。但是,如果我们可以同时使用两者呢:数据库UUID仅将所有键存储为s,但它在进行查询之前/之后包装对象。

例如,程序员请求ACC-{UUID},数据库忽略该ACC-部分,获取结果,并将所有结果作为 返回{domain}-{UUID}

也许这可以通过一些带有存储过程或函数的hackery来实现,但我想到了一些问题:

  • 这(在每个查询中删除/添加域)是一个很大的开销吗?
  • 这甚至可能吗?

我以前从未使用过存储过程或函数,所以我不确定这是否可能。有人可以透露一些信息吗?如果我能在程序员和存储的数据之间添加一个透明层,这似乎是一个完美的解决方案。

4.(我最喜欢的)存储为 IPv6 cidr

是的,你没有看错。事实证明,IPv6 地址格式完美地解决了我的问题。

  • 我可以在前几个八位字节添加域和子域,并将剩余的用作随机字符串。
  • 碰撞几率都OK。(虽然我不会使用 2^128,但它仍然可以。)
  • 相等比较(希望)经过优化,因此与简单地使用bytea.
  • 我实际上可以执行一些有趣的比较,例如contains,取决于域及其层次结构的表示方式。

例如,假设我使用代码0000来表示域“产品”。Key0000:0db8:85a3:0000:0000:8a2e:0370:7334将代表产品0db8:85a3:0000:0000:8a2e:0370:7334

这里的主要问题是:与 相比bytea,使用cidr数据类型有什么主要优点或缺点吗?

Eva*_*oll 5

使用 ltree

如果 IPV6 有效,那就太好了。它不支持“ACC”。ltree做。

标签路径是由点分隔的零个或多个标签的序列,例如 L1.L2.L3,表示从分层树的根到特定节点的路径。标签路径的长度必须小于 65kB,但最好保持在 2kB 以下。在实践中,这不是主要限制;例如,DMOZ 目录(http://www.dmoz.org)中最长的标签路径约为 240 字节。

你会像这样使用它,

CREATE EXTENSION ltree;
SELECT replace('ACC-f8kJd9xKCd', '-', '.')::ltree;
Run Code Online (Sandbox Code Playgroud)

我们创建示例数据。

SELECT x, (
  CASE WHEN x%7=0 THEN 'ACC'
    WHEN x%3=0 THEN 'XYZ'
    ELSE 'COM'
  END ||'.'|| md5(x::text)
  )::ltree
FROM generate_series(1,10000) AS t(x);

CREATE INDEX ON foo USING GIST (ltree);
ANALYZE foo;


  x  |                ltree                 
-----+--------------------------------------
   1 | COM.c4ca4238a0b923820dcc509a6f75849b
   2 | COM.c81e728d9d4c2f636f067f89cc14862c
   3 | XYZ.eccbc87e4b5ce2fe28308fd9f2a7baf3
   4 | COM.a87ff679a2f3e71d9181a67b7542122c
   5 | COM.e4da3b7fbbce2345d7772b0674a318d5
   6 | XYZ.1679091c5a880faf6fb5e6087eb1b2dc
   7 | ACC.8f14e45fceea167a5a36dedd4bea2543
   8 | COM.c9f0f895fb98ab9159f51fd0297e236d
Run Code Online (Sandbox Code Playgroud)

还有中提琴..

                                                          QUERY PLAN                                                          
------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on foo  (cost=103.23..234.91 rows=1414 width=57) (actual time=0.422..0.908 rows=1428 loops=1)
   Recheck Cond: ('ACC'::ltree @> ltree)
   Heap Blocks: exact=114
   ->  Bitmap Index Scan on foo_ltree_idx  (cost=0.00..102.88 rows=1414 width=0) (actual time=0.389..0.389 rows=1428 loops=1)
         Index Cond: ('ACC'::ltree @> ltree)
 Planning time: 0.133 ms
 Execution time: 1.033 ms
(7 rows)
Run Code Online (Sandbox Code Playgroud)

有关更多信息和运算符,请参阅文档

如果您要创建产品 ID,我会使用 ltree。如果你需要一些东西来创建它们,我会使用 UUID。


小智 1

仅仅关于与bytea的性能比较。网络的比较分三步完成:首先比较网络部分的公共位,然后比较网络部分的长度,然后比较整个未屏蔽的地址。请参阅:network_cmp_internal

所以它应该比直接进入 memcmp 的 bytea 慢一点。我在一个包含 1000 万行的表上运行了一个简单的测试,以查找单个行:

  • 使用数字 ID(整数)花了我 1000 毫秒。
  • 使用 cidr 花了 1300 毫秒。
  • 使用bytea花了1250ms。

我不能说 bytea 和 cidr 之间有很大差异(尽管差距保持一致),只是附加声明if- 猜测这对于 10m 元组来说还不错。

希望它有帮助 - 很想听听你最终选择了什么。