我应该使用UUID还是ID


11

我已经在系统中使用UUID一段时间了,原因有很多,从日志记录到延迟的关联。随着我变得越来越幼稚,我使用的格式发生了变化:

  1. VARCHAR(255)
  2. VARCHAR(36)
  3. CHAR(36)
  4. BINARY(16)

当我到达最后一个时BINARY(16),我开始将性能与基本自动递增整数进行比较。测试和结果如下所示,但如果你只是想总结,表示INT AUTOINCREMENTBINARY(16) RANDOM对数据相同的性能范围高达20万(该数据库已预先填充之前测试)。

最初,我对将UUID用作主键持怀疑态度,确实确实如此,但是我发现这里有潜力创建一个可以同时使用两者的灵活数据库。尽管许多人强调这两种方法的优点,但同时使用这两种数据类型可以消除哪些缺点呢?

  • PRIMARY INT
  • UNIQUE BINARY(16)

这种类型的设置的用例将是表间关系的传统主键,并且具有用于系统间关系的唯一标识符。

我本质上试图发现的是两种方法之间的效率差异。除了所使用的四倍磁盘空间(在添加其他数据后可能几乎可以忽略不计)外,在我看来它们是相同的。

架构:

-- phpMyAdmin SQL Dump
-- version 4.0.10deb1
-- http://www.phpmyadmin.net
--
-- Host: localhost
-- Generation Time: Sep 22, 2015 at 10:54 AM
-- Server version: 5.5.44-0ubuntu0.14.04.1
-- PHP Version: 5.5.29-1+deb.sury.org~trusty+3

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;

--
-- Database: `test`
--

-- --------------------------------------------------------

--
-- Table structure for table `with_2id`
--

CREATE TABLE `with_2id` (
  `guidl` bigint(20) NOT NULL,
  `guidr` bigint(20) NOT NULL,
  `data` varchar(255) NOT NULL,
  PRIMARY KEY (`guidl`,`guidr`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

-- --------------------------------------------------------

--
-- Table structure for table `with_guid`
--

CREATE TABLE `with_guid` (
  `guid` binary(16) NOT NULL,
  `data` varchar(255) NOT NULL,
  PRIMARY KEY (`guid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

-- --------------------------------------------------------

--
-- Table structure for table `with_id`
--

CREATE TABLE `with_id` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `data` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=197687 ;

/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

插入基准:

function benchmark_insert(PDO $pdo, $runs)
{
    $data = 'Sample Data';

    $insert1 = $pdo->prepare("INSERT INTO with_id (data) VALUES (:data)");
    $insert1->bindParam(':data', $data);

    $insert2 = $pdo->prepare("INSERT INTO with_guid (guid, data) VALUES (:guid, :data)");
    $insert2->bindParam(':guid', $guid);
    $insert2->bindParam(':data', $data);

    $insert3 = $pdo->prepare("INSERT INTO with_2id (guidl, guidr, data) VALUES (:guidl, :guidr, :data)");
    $insert3->bindParam(':guidl', $guidl);
    $insert3->bindParam(':guidr', $guidr);
    $insert3->bindParam(':data',  $data);

    $benchmark = array();

    $time = time();
    for ($i = 0; $i < $runs; $i++) {
        $insert1->execute();
    }
    $benchmark[1] = 'INC ID:     ' . (time() - $time);

    $time = time();
    for ($i = 0; $i < $runs; $i++) {
        $guid  = openssl_random_pseudo_bytes(16);

        $insert2->execute();
    }
    $benchmark[2] = 'GUID:       ' . (time() - $time);

    $time = time();
    for ($i = 0; $i < $runs; $i++) {
        $guid  = openssl_random_pseudo_bytes(16);
        $guidl = unpack('q', substr($guid, 0, 8))[1];
        $guidr = unpack('q', substr($guid, 8, 8))[1];

        $insert3->execute();
    }
    $benchmark[3] = 'SPLIT GUID: ' . (time() - $time);

    echo 'INSERTION' . PHP_EOL;
    echo '=============================' . PHP_EOL;
    echo $benchmark[1] . PHP_EOL;
    echo $benchmark[2] . PHP_EOL;
    echo $benchmark[3] . PHP_EOL . PHP_EOL;
}

选择基准:

function benchmark_select(PDO $pdo, $runs) {
    $select1 = $pdo->prepare("SELECT * FROM with_id WHERE id = :id");
    $select1->bindParam(':id', $id);

    $select2 = $pdo->prepare("SELECT * FROM with_guid WHERE guid = :guid");
    $select2->bindParam(':guid', $guid);

    $select3 = $pdo->prepare("SELECT * FROM with_2id WHERE guidl = :guidl AND guidr = :guidr");
    $select3->bindParam(':guidl', $guidl);
    $select3->bindParam(':guidr', $guidr);

    $keys = array();

    for ($i = 0; $i < $runs; $i++) {
        $kguid  = openssl_random_pseudo_bytes(16);
        $kguidl = unpack('q', substr($kguid, 0, 8))[1];
        $kguidr = unpack('q', substr($kguid, 8, 8))[1];
        $kid = mt_rand(0, $runs);

        $keys[] = array(
            'guid'  => $kguid,
            'guidl' => $kguidl,
            'guidr' => $kguidr,
            'id'    => $kid
        );
    }

    $benchmark = array();

    $time = time();
    foreach ($keys as $key) {
        $id = $key['id'];
        $select1->execute();
        $row = $select1->fetch(PDO::FETCH_ASSOC);
    }
    $benchmark[1] = 'INC ID:     ' . (time() - $time);


    $time = time();
    foreach ($keys as $key) {
        $guid = $key['guid'];
        $select2->execute();
        $row = $select2->fetch(PDO::FETCH_ASSOC);
    }
    $benchmark[2] = 'GUID:       ' . (time() - $time);

    $time = time();
    foreach ($keys as $key) {
        $guidl = $key['guidl'];
        $guidr = $key['guidr'];
        $select3->execute();
        $row = $select3->fetch(PDO::FETCH_ASSOC);
    }
    $benchmark[3] = 'SPLIT GUID: ' . (time() - $time);

    echo 'SELECTION' . PHP_EOL;
    echo '=============================' . PHP_EOL;
    echo $benchmark[1] . PHP_EOL;
    echo $benchmark[2] . PHP_EOL;
    echo $benchmark[3] . PHP_EOL . PHP_EOL;
}

测试:

$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');

benchmark_insert($pdo, 1000);
benchmark_select($pdo, 100000);

结果:

INSERTION
=============================
INC ID:     3
GUID:       2
SPLIT GUID: 3

SELECTION
=============================
INC ID:     5
GUID:       5
SPLIT GUID: 6

Answers:


10

对于非常大的表,UUID会对性能造成损害。(200K行不是“很大”。)

CHARCTER SETutf8 时,您的#3确实很糟糕- CHAR(36)占用108个字节! 更新:ROW_FORMATs它将保留36。

UUID(GUID)非常“随机”。在大型表上将它们用作UNIQUE或PRIMARY键非常低效。这是因为每次您INSERT使用新的UUID或SELECT通过UUID 时,都必须在表/索引周围跳转。当表/索引太大而无法放入高速缓存(请参阅innodb_buffer_pool_size,该值必须小于RAM,通常为70%)时,“下一个” UUID可能不会被高速缓存,因此磁盘命中速度很慢。当表/索引的大小是缓存的20倍时,将仅缓存1/20(5%)的匹配数-您受到I / O约束。 概括:低效率适用于任何“随机”访问-UUID / MD5 / RAND()/等

因此,除非任何一个都不要使用UUID

  • 您有“小”表,或者
  • 您真的需要它们,因为它们是从不同的地方生成唯一的ID(并且还没有想出另一种方法)。

有关UUID的更多信息:http : //mysql.rjweb.org/doc.php/uuid (它包括在标准36字符UUIDs和.char之间转换的函数BINARY(16)。) 更新:MySQL 8.0对此具有内置函数。

在同一表中同时具有UNIQUE AUTO_INCREMENTUNIQUEUUID是浪费的。

  • INSERT发生这种情况时,必须检查所有唯一/主键是否重复。
  • 这两个唯一密钥都足以满足InnoDB拥有PRIMARY KEY
  • BINARY(16) (16个字节)有点笨重(反对将其设为PK),但还不错。
  • 当您具有辅助键时,体积很重要。InnoDB默默地将PK附加到每个辅助密钥的末尾。这里的主要课程是最大程度地减少辅助键的数量,尤其是对于非常大的表。详细说明:对于一个第二把钥匙,关于松散度的辩论通常以抽签结束。对于2个或多个辅助键,较胖的PK通常会导致包含索引的表的磁盘占用量更大。

为了进行比较: INT UNSIGNED是4个字节,范围为0.4亿。 BIGINT是8个字节。

斜体更新/等已于2017年9月添加; 没有什么关键的改变。


感谢您的回答,我不太了解高速缓存优化的损失。我不太担心笨重的外键,但是我知道它最终将如何成为问题。但是,我不愿意完全删除它们的使用,因为事实证明它们对于跨系统交互非常有用。 BINARY(16)我认为我们都同意这是存储UUID的最有效方法,但是对于UNIQUE索引,我是否应该仅使用常规索引?字节是使用加密安全的RNG生成的,所以我应该完全依赖随机性,放弃检查吗?
Flosculus

非唯一索引将有助于提高性能,但即使是常规索引也最终需要更新。您预计的桌子尺寸是多少?最终会太大而无法缓存吗?建议值为innodb_buffer_pool_size可用RAM的70%。
里克·詹姆斯

2个月后,它的数据库1.2 GB,最大表为300MB,但是数据永远不会消失,因此它将持续多长时间,也许是10年。授予少于一半的表甚至需要UUID,因此我将它们从最肤浅的用例中删除。这样一来,当前将需要它们的存储空间为50,000行和250MB,即10年内需要30-100 GB。
Flosculus

2
在10年内,您将无法购买只有100GB RAM的计算机。您将永远适合RAM,因此我的评论可能不适用于您的情况。
里克·詹姆斯

1
@a_horse_with_no_name-在旧版本中,始终为3倍。只有较新的版本对此有所了解。也许那是5.1.24;那可能已经足够我忘了。
里克·詹姆斯

2

里克·詹姆斯(Rick James)在接受的回答中说:“在同一张表中同时具有UNIQUE AUTO_INCREMENT和UNIQUE UUID都是浪费”。但是这个测试(我在机器上做过)显示了不同的事实。

例如:使用测试(T2),我使用(INT AUTOINCREMENT)PRIMARY和UNIQUE BINARY(16)以及另一个字段作为标题制作表,然后我插入了超过1.6M行,具有非常好的性能,但是使用了另一个测试(T3)我做了同样的事情,但是仅插入300,000行后结果却很慢。

这是我的测试结果:

T1:
char(32) UNIQUE with auto increment int_id
after: 1,600,000
10 sec for inserting 1000 rows
select + (4.0)
size:500mb

T2:
binary(16) UNIQUE with auto increment int_id
after: 1,600,000
1 sec for inserting 1000 rows
select +++ (0.4)
size:350mb

T3:
binary(16) UNIQUE without auto increment int_id
after: 350,000
5 sec for inserting 1000 rows
select ++ (0.3)
size:118mb (~ for 1,600,000 will be 530mb)

T4:
auto increment int_id without binary(16) UNIQUE
++++

T5:
uuid_short() int_id without binary(16) UNIQUE
+++++*

因此,具有自动增量int_id的binary(16)UNIQUE要优于没有自动增量int_id的binary(16)UNIQUE。

更新:

我再次进行相同的测试,并记录更多详细信息。如上所述,这是完整的代码以及(T2)和(T3)之间的结果比较。

(T2)创建tbl2(mysql):

CREATE TABLE test.tbl2 (
  int_id INT(11) NOT NULL AUTO_INCREMENT,
  rec_id BINARY(16) NOT NULL,
  src_id BINARY(16) DEFAULT NULL,
  rec_title VARCHAR(255) DEFAULT NULL,
  PRIMARY KEY (int_id),
  INDEX IDX_tbl1_src_id (src_id),
  UNIQUE INDEX rec_id (rec_id)
)
ENGINE = INNODB
CHARACTER SET utf8
COLLATE utf8_general_ci;

(T3)创建tbl3(mysql):

CREATE TABLE test.tbl3 (
  rec_id BINARY(16) NOT NULL,
  src_id BINARY(16) DEFAULT NULL,
  rec_title VARCHAR(255) DEFAULT NULL,
  PRIMARY KEY (rec_id),
  INDEX IDX_tbl1_src_id (src_id)
)
ENGINE = INNODB
CHARACTER SET utf8
COLLATE utf8_general_ci;

这是完整的测试代码,它将600,000条记录插入到tbl2或tbl3(vb.net代码)中:

Public Class Form1

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Dim res As String = ""
        Dim i As Integer = 0
        Dim ii As Integer = 0
        Dim iii As Integer = 0

        Using cn As New SqlClient.SqlConnection
            cn.ConnectionString = "Data Source=.\sql2008;Integrated Security=True;User Instance=False;MultipleActiveResultSets=True;Initial Catalog=sourcedb;"
            cn.Open()
            Using cmd As New SqlClient.SqlCommand
                cmd.Connection = cn
                cmd.CommandTimeout = 0
                cmd.CommandText = "select recID, srcID, rectitle from textstbl order by ID ASC"

                Using dr As SqlClient.SqlDataReader = cmd.ExecuteReader

                    Using mysqlcn As New MySql.Data.MySqlClient.MySqlConnection
                        mysqlcn.ConnectionString = "User Id=root;Host=localhost;Character Set=utf8;Pwd=1111;Database=test"
                        mysqlcn.Open()

                        Using MyCommand As New MySql.Data.MySqlClient.MySqlCommand
                            MyCommand.Connection = mysqlcn

                            MyCommand.CommandText = "insert into tbl3 (rec_id, src_id, rec_title) values (UNHEX(@rec_id), UNHEX(@src_id), @rec_title);"
                            Dim MParm1(2) As MySql.Data.MySqlClient.MySqlParameter
                            MParm1(0) = New MySql.Data.MySqlClient.MySqlParameter("@rec_id", MySql.Data.MySqlClient.MySqlDbType.String)
                            MParm1(1) = New MySql.Data.MySqlClient.MySqlParameter("@src_id", MySql.Data.MySqlClient.MySqlDbType.String)
                            MParm1(2) = New MySql.Data.MySqlClient.MySqlParameter("@rec_title", MySql.Data.MySqlClient.MySqlDbType.VarChar)

                            MyCommand.Parameters.AddRange(MParm1)
                            MyCommand.CommandTimeout = 0

                            Dim mytransaction As MySql.Data.MySqlClient.MySqlTransaction = mysqlcn.BeginTransaction()
                            MyCommand.Transaction = mytransaction

                            Dim sw As New Stopwatch
                            sw.Start()

                            While dr.Read
                                MParm1(0).Value = dr.GetValue(0).ToString.Replace("-", "")
                                MParm1(1).Value = EmptyStringToNullValue(dr.GetValue(1).ToString.Replace("-", ""))
                                MParm1(2).Value = gettitle(dr.GetValue(2).ToString)

                                MyCommand.ExecuteNonQuery()

                                i += 1
                                ii += 1
                                iii += 1

                                If i >= 1000 Then
                                    i = 0

                                    Dim ts As TimeSpan = sw.Elapsed
                                    Me.Text = ii.ToString & " / " & ts.TotalSeconds

                                    Select Case ii
                                        Case 10000, 50000, 100000, 200000, 300000, 400000, 500000, 600000, 700000, 800000, 900000, 1000000
                                            res &= "On " & FormatNumber(ii, 0) & ": last inserting 1000 records take: " & ts.TotalSeconds.ToString & " second." & vbCrLf
                                    End Select

                                    If ii >= 600000 Then GoTo 100
                                    sw.Restart()
                                End If
                                If iii >= 5000 Then
                                    iii = 0

                                    mytransaction.Commit()
                                    mytransaction = mysqlcn.BeginTransaction()

                                    sw.Restart()
                                End If
                            End While
100:
                            mytransaction.Commit()

                        End Using
                    End Using
                End Using
            End Using
        End Using

        TextBox1.Text = res
        MsgBox("Ok!")
    End Sub

    Public Function EmptyStringToNullValue(MyValue As Object) As Object
        'On Error Resume Next
        If MyValue Is Nothing Then Return DBNull.Value
        If String.IsNullOrEmpty(MyValue.ToString.Trim) Then
            Return DBNull.Value
        Else
            Return MyValue
        End If
    End Function

    Private Function gettitle(p1 As String) As String
        If p1.Length > 255 Then
            Return p1.Substring(0, 255)
        Else
            Return p1
        End If
    End Function

End Class

(T2)的结果:

On 10,000: last inserting 1000 records take: 0.13709 second.
On 50,000: last inserting 1000 records take: 0.1772109 second.
On 100,000: last inserting 1000 records take: 0.1291394 second.
On 200,000: last inserting 1000 records take: 0.5793488 second.
On 300,000: last inserting 1000 records take: 0.1296427 second.
On 400,000: last inserting 1000 records take: 0.6938583 second.
On 500,000: last inserting 1000 records take: 0.2317799 second.
On 600,000: last inserting 1000 records take: 0.1271072 second.

~3 Minutes ONLY! to insert 600,000 records.
table size: 128 mb.

(T3)的结果:

On 10,000: last inserting 1000 records take: 0.1669595 second.
On 50,000: last inserting 1000 records take: 0.4198369 second.
On 100,000: last inserting 1000 records take: 0.1318155 second.
On 200,000: last inserting 1000 records take: 0.1979358 second.
On 300,000: last inserting 1000 records take: 1.5127482 second.
On 400,000: last inserting 1000 records take: 7.2757161 second.
On 500,000: last inserting 1000 records take: 14.3960671 second.
On 600,000: last inserting 1000 records take: 14.9412401 second.

~40 Minutes! to insert 600,000 records.
table size: 164 mb.

2
请说明您的答案不仅限于在您的个人计算机上运行其基准测试。理想情况下,答案应是讨论所涉及的一些权衡,而不仅仅是基准输出。
Erik

1
请澄清一下。那是innodb_buffer_pool_size什么 “表大小”从何而来?
里克·詹姆斯

1
请重新运行,使用1000作为事务大小-这样可以消除tbl2和tbl3中的奇怪故障。另外,在以后而COMMIT不是之前打印出时间。这可以消除其他一些异常。
里克·詹姆斯

1
我不熟悉您使用的语言,但是我确实看到了@rec_id@src_id产生不同的值并将其应用于每行的情况。打印一些INSERT声明可能会让我满意。
里克·詹姆斯

1
另外,请继续超过60万。在某些时候(部分取决于rec_title的大小),t2也会掉下悬崖。它甚至可能比慢t3; 我不确定。您的基准是在一个“甜甜圈洞”里t3暂时慢。
瑞克·詹姆斯
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.