非整数主键注意事项


16

语境

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

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

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

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

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

识别应用程序对象

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

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

问题

知道:

  • 我想使用类似格式的主键ACC-f8kJd9xKCd
  • 这些主键将成为几个表的一部分。
  • 所有这些键都将在6NF数据库上的多个联接/关系中使用。
  • 大多数表的大小将为中到大(平均约100万行;最大的约1亿行)。

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

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

考虑的解决方案

1.存储为字符串(VARCHAR

(Postgres CHAR(n)和和之间没有区别VARCHAR(n),因此我忽略了CHAR)。

经过一些研究,我发现,与的字符串比较VARCHAR(特别是在join操作上)比使用慢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}

对于某些带有存储过程或函数的黑客来说,这也许是可能的,但是我想到了一些问题:

  • 这(在每个查询中删除/添加域)是否会产生大量开销?
  • 这有可能吗?

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

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

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

  • 我可以在前几个八位位组中添加域和子域,并使用其余的作为随机字符串。
  • 碰撞几率都OK。(虽然我不会使用2 ^ 128,但仍然可以。)
  • 平等比较(希望)得到了优化,所以我可能会比简单地使用获得更好的性能bytea
  • 实际上,我可以执行一些有趣的比较,例如contains,具体取决于域及其层次结构的表示方式。

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

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


5
可能有多少个分布式节点?您提前知道他们的电话号码(和姓名)吗?您是否考虑过复合(多列)PK?一个域(取决于我的第一个问题),加上一个普通的串行列可能是最小,最简单和最快的...
Erwin Brandstetter

@菲尔,谢谢!@ErwinBrandstetter关于应用程序,它被设计为根据负载自动缩放,因此提前的信息很少。我曾考虑过将(domain,UUID)用作PK,但这会重复整个“ domain”,domain仍然是varchar许多其他问题。我不知道pg的领域,这对我很了解。我看到域用于验证给定查询是否使用了正确的对象,但是它仍然依赖于具有非整数索引。不知道这里是否有“安全”的使用方法serial(没有一个锁定步骤)。
Renato Siqueira Massaro

1
域不一定必须是varchar。考虑将其FK integer设为一种类型,并为其添加查找表。这样一来,您既可以拥有人类可读性,又可以保护合成组件PK免受插入/更新异常(放入不存在的域)的影响。
yemet '16


1
我希望主键的格式为ACC-f8kJd9xKCd←看来这是好的老式复合主键的工作
MDCCL

Answers:


5

使用 ltree

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

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

你会这样用

CREATE EXTENSION ltree;
SELECT replace('ACC-f8kJd9xKCd', '-', '.')::ltree;

我们创建样本数据。

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

还有中提琴

                                                          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)

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

如果您要创建产品ID,我会ltree。如果您需要创建它们的方法,则可以使用UUID。


1

仅考虑与bytea的性能比较。网络的比较分为3个步骤:首先在网络部分的公共位上,然后在网络部分的长度上,然后在整个未屏蔽地址上。参见:network_cmp_internal

因此,它应该比bytea慢一些,而后者会转向memcmp。我在一个具有一千万行的表上运行了一个简单的测试,寻找一个:

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

我不能说bytea和cidr之间有很大的差异(尽管差距保持一致),只是补充if声明-猜想对于1000万个元组来说还算不错。

希望对您有所帮助-很想听听您最终选择了什么。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.