Sequential Guid相比标准Guid有哪些性能提升?

72
有人曾经比较过在数据库中作为主键时,顺序Guid和标准Guid的性能表现吗?
我认为唯一键是否可猜测并不需要,从Web UI或其他部分传递它们本身就是一个不好的做法。如果您有安全顾虑,我也看不出使用Guid可以改善什么(在这种情况下,请使用适当框架的加密函数来使用真正的随机数生成器)。我的方法涵盖了其他问题,顺序Guid可以从代码中生成而无需访问数据库(即使只用于Windows),它在时间和空间上是唯一的。
此外,问题的目的是回答它,为那些选择Guid作为PK的人提供一种改进数据库使用的方法(在我看来,这使得客户可以在不更换服务器的情况下承受更高的工作负载)。
似乎存在许多安全方面的担忧,在这种情况下,请勿使用Sequential Guid,或者更好的方法是,对于从UI传递来回的PK,请使用标准Guid,对于其他所有内容,请使用顺序Guid。像往常一样,没有绝对的真理,我也编辑了主要答案以反映这一点。
8个回答

118

GUID vs. Sequential GUID



通常在表中使用Guid作为主键,但正如其他讨论所指出的(请参见GUID / UUID数据库键的优缺点),这会带来一些性能问题。

这是典型的Guid序列

f3818d69-2552-40b7-a403-01a6db4552f7
7ce31615-fafb-42c4-b317-40d21a6a3c60
94732fc7-768e-4cf2-9107-f0953f6795a5


此类数据的问题包括:<
-

  • 值分布较广
  • 几乎是随机的
  • 索引使用非常、非常、非常差
  • 叶移动很多
  • 几乎每个主键都需要至少 在一个非聚集索引上
  • 这个问题在Oracle和SQL Server上都存在



可能的解决方案是使用连续的Guid,它们的生成方式如下:

cc6466f7-1066-11dd-acb6-005056c00008
cc6466f8-1066-11dd-acb6-005056c00008
cc6466f9-1066-11dd-acb6-005056c00008


如何从C#代码生成它们:

[DllImport("rpcrt4.dll", SetLastError = true)]
static extern int UuidCreateSequential(out Guid guid);

public static Guid SequentialGuid()
{
    const int RPC_S_OK = 0;
    Guid g;
    if (UuidCreateSequential(out g) != RPC_S_OK)
        return Guid.NewGuid();
    else
        return g;
}


好处:

  • 更好地利用索引
  • 允许使用聚簇键(在 NLB 方案中需要验证)
  • 减少磁盘使用
  • 最小成本下可提高20-25%的性能



实际测试结果: 场景:

  • 在SQL Server上存储为UniqueIdentifier类型的Guid
  • 在Oracle上存储为CHAR(36)的Guid
  • 大量 insert 操作,批处理合并到单个事务中
  • 根据表格数量从1到100次不等的insert操作
  • 一些表格有超过1000万行数据



实验室测试 - SQL Server

VS2008测试,10个并发用户,无思考时间,基准过程使用600个批量插入到叶子表
标准Guid
平均处理持续时间:10.5
平均请求每秒: 54.6
平均响应时间:0.26

顺序Guid
平均处理持续时间:4.6
平均请求每秒: 87.1
平均响应时间:0.12

Oracle上的测试结果 (抱歉,使用了不同的工具进行测试) 在具有Guid PK的表上插入1,327,613条记录

标准Guid,每个插入操作消耗0.02秒的时间,CPU时间2.861秒,总计31.049

顺序Guid,每个插入操作消耗0.00秒的时间,CPU时间1.142秒,总计3.667

DB文件按顺序读取等待时间从6.4百万等待事件降至1.2百万等待事件。

需要注意的是所有顺序Guid都可以猜到,因此如果安全性是问题的话,不建议使用它们,仍然使用标准Guid。
简而言之...如果您使用Guid作为主键,则每次使用顺序Guid(除非它们从UI返回和转发),它们将加快操作并且无需实施成本。


使用存储引擎“InnoDB”,MySQL以聚集方式按PK存储记录,因此您应该从Sequential GUIDs中受益。 - hgoebl
1
重要的是要看到所有的连续 GUID 都可以被猜测,因此如果安全是一个问题,使用它们不是一个好主意。在这种情况下,可以使用组合 GUID,它具有顺序和随机性的优点。 - Peter
1
请查看此博客文章:http://blogs.msdn.com/b/dbrowne/archive/2012/07/03/how-to-generate-sequential-guids-for-sql-server-in-net.aspx "... UuidCreateSequential 的结果与 SQL Server 的排序顺序不一致... 为了使它们成为顺序,SQL Server 的内部 NEWSEQUENTIALID 函数对 GUID 执行一些字节重排... 您需要执行相同的字节重排。" - Giorgi Chakhidze
为什么更好是我不理解的。 - johnny
1
拥有连续的 GUID 而不是连续的整数,其目的是什么? - entonio
@entonio,你仍然可以以分布式的方式生成它们(取决于你如何使它们成为顺序的^^)。 - Frank Hopkins

66

我可能有所疏漏(如果我错了,请随意纠正),但我认为使用顺序GUID / UUID作为主键几乎没有什么好处。

使用GUID或UUID而不是自增整数的优点

  • 它们可以在不需要联系数据库的情况下在任何地方创建
  • 它们是完全唯一的标识符,适用于您的应用程序(在UUID的情况下,是普遍唯一的)
  • 给定一个标识符,除了通过暴力攻击巨大的键空间来猜测下一个或上一个标识符之外,没有其他有效标识符可以猜到。

不幸的是,使用您的建议,您会失去所有这些好处。

所以,是的。你让GUID变得更好了。但是在此过程中,你几乎扔掉了使用它们的所有原因。

如果您真的想提高性能,请使用标准的自增整数主键。这提供了您描述的所有好处(甚至更多),同时在几乎所有方面都比“顺序guid”更好。

由于它并没有具体回答您的问题(显然,这是精心制作的问题,以便您可以立即回答它),因此它可能会被下调到灭亡,但我认为这是一个非常重要的观点。


1
除了“非猜测”(我认为这不重要,我们不需要随机函数),顺序 GUID 恰好具有您要查找的特征,我从 C# 代码中生成它们,并且它们在时间和空间上是唯一的。 - massimogentilini
18
顺序 UUID 并不能保证全局排序,但它们仍然是唯一的,同时也具有本地顺序性。这意味着在不同的主机/进程/线程(取决于顺序方案)上生成的 ID 会随机交错,但在同一环境下生成的 ID 将是有序的。 - nothingmuch
2
COMB GUIDs是有序的,对于插入/读取非常快,并且提供与标识列相当的速度。所有标识列的优点,但您不需要使用任何疯狂的GUID复制策略。标识列则需要。GUID的优势。 - bbqchickenrobot
如果它在云端,标准的自增整数主键不适合长期使用。 - GoYun.Info
它在表格之间不是唯一的。云计算是为了网络规模而设计的。除非您的数据库非常小,否则这并不重要。 - GoYun.Info

23

正如massimogentilini所说,使用UuidCreateSequential(在代码中生成GUID)可以提高性能。但是似乎缺少一个事实:SQL Server(至少Microsoft SQL 2005/2008)使用相同的功能,但是GUID的比较/排序在.NET和SQL Server上不同,这仍会导致更多的IO,因为GUID将不能正确排序。 为了为SQL Server生成正确排序的GUID,请执行以下操作(请参见比较详细信息):

[System.Runtime.InteropServices.DllImport("rpcrt4.dll", SetLastError = true)]
static extern int UuidCreateSequential(byte[] buffer);

static Guid NewSequentialGuid() {

    byte[] raw = new byte[16];
    if (UuidCreateSequential(raw) != 0)
        throw new System.ComponentModel.Win32Exception(System.Runtime.InteropServices.Marshal.GetLastWin32Error());

    byte[] fix = new byte[16];

    // reverse 0..3
    fix[0x0] = raw[0x3];
    fix[0x1] = raw[0x2];
    fix[0x2] = raw[0x1];
    fix[0x3] = raw[0x0];

    // reverse 4 & 5
    fix[0x4] = raw[0x5];
    fix[0x5] = raw[0x4];

    // reverse 6 & 7
    fix[0x6] = raw[0x7];
    fix[0x7] = raw[0x6];

    // all other are unchanged
    fix[0x8] = raw[0x8];
    fix[0x9] = raw[0x9];
    fix[0xA] = raw[0xA];
    fix[0xB] = raw[0xB];
    fix[0xC] = raw[0xC];
    fix[0xD] = raw[0xD];
    fix[0xE] = raw[0xE];
    fix[0xF] = raw[0xF];

    return new Guid(fix);
}

或者这个链接,或者这个链接


1
很好的观点。根据您的代码,性能可能还可以进一步提高,迟早我会进行一些测试。 - massimogentilini
4
关于此主题的更多信息,请参阅https://dev59.com/ZnI-5IYBdhLWcg3wpaB1和http://developmenttips.blogspot.com/2008/03/generate-sequential-guids-for-sql.html。 - bernhof

5
请参考此文章:(http://www.shirmanov.com/2010/05/generating-newsequentialid-compatible.html
尽管MSSql使用同样的函数来生成NewSequencialIds(UuidCreateSequential(out Guid guid)),但是MSSQL颠倒了第三个和第四个字节模式,这不会给你在代码中使用此函数时得到相同的结果。Shirmanov展示了如何获得与MSSQL创建的完全相同的结果。

5

我使用Entity Framework测量了Guid(聚集和非聚集)、Sequential Guid和int(Identity/autoincrement)之间的差异。结果让人惊讶的是,与具有identity的int相比,Sequential Guid速度更快。这里是Sequential Guid的结果和代码


未找到结果。我很想知道您是如何测量差异的。标准GUID存在问题,因为它们经常被使用,会在插入时导致页面分裂,这会逐渐降低查询性能。您是否以某种方式进行插入以导致页面分裂? - trees_are_great
1
URL已更新,您可以查看结果。 - Alex Siepman
谢谢。非常有趣的分析。做类似的事情会很棒,但是需要查询每个表有多少碎片。然后比较高度碎片化的Guid表与非唯一int表上的查询。我目前正在将Guid转换为COMB Guid,希望这样可以提高查询性能。 - trees_are_great

4

查看由Jimmy Nilsson创建的COMBs:一种GUID类型,其中一些位被替换为类似于时间戳的值。这意味着可以对COMBs进行排序,并且在用作主键时插入新值时会导致较少的索引页面分裂。

另请参见:使用唯一标识符(GUID)作为主键是否可行?

是的,唯一标识符(GUID)列可以作为主键,但它不是聚集索引的特别好选择。在许多情况下,最好在可能用于范围搜索的列上创建聚集索引,并在GUID列上创建非聚集索引。


2
我对COMBs和类似技术持有一些怀疑态度,因为“GUID是全球唯一的,但是GUID子字符串不是”:http://blogs.msdn.com/oldnewthing/archive/2008/06/27/8659071.aspx - Constantin
7
GUIDs是具有统计上唯一性的,也就是说,发生冲突的概率非常小。COMB会牺牲GUID中128位中的一些比特位。因此,是的,冲突的可能性更高了,但仍然非常低。 - Mitch Wheat
1
GUID的整个意义在于它们具有比整数更高的全局唯一性概率。这种概率不必达到100%。虽然使用COMB GUID会增加冲突的概率,但仍然比使用标识列低几个数量级。 - Thomas

4
如果您需要使用连续的GUID,SQL Server 2005可以通过NEWSEQUENTIALID()函数为您生成它们。
然而,由于GUID的基本用途是生成无法猜测的键(或备用键),例如避免人们在GET请求中传递猜测的键,我不认为它们如此适用,因为它们很容易被猜测。
来自MSDN的信息:
重要提示: 如果隐私是一个问题,请不要使用此函数。可以猜测下一个生成的GUID的值,并因此访问与该GUID相关联的数据。

5
我重申一遍,我认为Guid不是用于生成无法被猜测的密钥,而是用于产生在时间和空间上都是独特的密钥,并且可以很容易地用于复制。如果隐私很重要,请使用其他方法(如真随机数)。 - massimogentilini

4

好的,最终我自己完成了设计和制作。

我生成了一个COMB_GUID,其中高32位基于毫秒级Unix时间的33到1位。因此,在每2毫秒中有93位随机性,而上位位的翻转发生在每106年一次。 COMB_GUID(或类型4 UUID)的实际物理表示是128位的base64编码版本,是一个22个字符的字符串。

在将记录插入到Postgres时,完全随机UUID和COMB_GUID之间的速度比对于COMB_GUID而言是有益的。 在我的硬件上进行了多次测试,100万条记录测试时COMB_GUID比全随机UUID快 2X 。记录包含id(22个字符),字符串字段(110个字符),双精度和INT。

在ElasticSearch中,两者在索引方面没有明显区别。我仍将使用COMB_GUIDS,以防内容在任何链中都进入BTREE索引,因为内容是根据时间提供的,或者可以将其按id字段预排序,使其时间相关并部分顺序,这将加速。

非常有趣。 下面是生成COMB_GUID的Java代码。

import java.util.Arrays;
import java.util.UUID;
import java.util.Base64; //Only avail in Java 8+
import java.util.Date;

import java.nio.ByteBuffer; 

    private ByteBuffer babuffer = ByteBuffer.allocate( (Long.SIZE/8)*2 );
private Base64.Encoder encoder = Base64.getUrlEncoder();
public  String createId() {
    UUID uuid = java.util.UUID.randomUUID();
        return uuid2base64( uuid );
}

    public String uuid2base64(UUID uuid){ 

        Date date= new Date();
        int intFor32bits;
        synchronized(this){
        babuffer.putLong(0,uuid.getLeastSignificantBits() );
        babuffer.putLong(8,uuid.getMostSignificantBits() );

                long time=date.getTime();
        time=time >> 1; // makes it every 2 milliseconds
                intFor32bits = (int) time; // rolls over every 106 yers + 1 month from epoch
                babuffer.putInt( 0, intFor32bits);

    }
        //does this cause a memory leak?
        return encoder.encodeToString( babuffer.array() );
    }

}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接