UUIDとIDを使用する必要がありますか


11

ロギングから遅延相関まで、さまざまな理由で、システムでUUIDをしばらく使用しています。私が使用したフォーマットは、次のように単純になったときに変化しました。

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

BINARY(16)パフォーマンスを基本的な自動インクリメント整数と比較し始めたのは、最後に到達したときです。テストとその結果を以下に示すが、あなただけの要約をしたい場合は、それがあることを示しているINT AUTOINCREMENTBINARY(16) RANDOM(データベースが前の試験に事前に入力された)200000までの範囲のデータで同じ性能を有します。

私は当初、UUIDを主キーとして使用することに懐疑的でしたが、実際にはまだそうですが、両方を使用できる柔軟なデータベースを作成する可能性はここにあります。多くの人がどちらか一方の利点を強調しますが、両方のデータ型を使用することで相殺される欠点は何ですか?

  • PRIMARY INT
  • UNIQUE BINARY(16)

このタイプの設定の使用例は、システム間の関係に一意の識別子が使用される、テーブル間の関係の従来の主キーです。

私が本質的に発見しようとしているのは、2つのアプローチの効率の違いです。追加のデータが追加された後はほとんど無視できる使用される4倍のディスク容量に加えて、それらは同じように見えます。

スキーマ:

-- 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

回答:


10

UUIDは、非常に大きなテーブルのパフォーマンスが低下します。(20万行は「非常に大きい」わけではありません。)

CHARCTER SETisがutf8の場合、#3は本当に悪いCHAR(36)です-108バイトを占めます! 更新:これがROW_FORMATs36のままになるものがあります。

UUID(GUID)は非常に「ランダム」です。それらを大きなテーブルのUNIQUEまたはPRIMARYキーとして使用することは非常に非効率的です。これはINSERT、新しいUUIDまたはUUID を使用するたびにテーブル/インデックスをジャンプする必要があるためですSELECT。テーブル/インデックスが大きすぎてキャッシュに収まらない場合(を参照してください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との間で変換するための関数が含まれていますBINARY(16)。) 更新:MySQL 8.0には、そのための組み込み関数があります。

UNIQUE AUTO_INCREMENTUNIQUEUUIDの両方を同じテーブルに入れるのは無駄です。

  • INSERT発生した場合、すべての一意/主キーの重複をチェックする必要があります。
  • InnoDBのの要件には、いずれかの一意のキーで十分ですPRIMARY KEY
  • BINARY(16) (16バイト)は多少かさばります(PKにすることに対する反対意見)が、それほど悪くはありません。
  • 二次キーがある場合、かさ高さは重要です。InnoDBは各セカンダリキーの最後にPKを静かに追加します。ここでの主なレッスンは、特に非常に大きなテーブルの場合、セカンダリキーの数を最小限に抑えることです。エラボレーション:1つの副キーについて、かさばる議論は通常引き分けに終わります。2つ以上のセカンダリキーの場合、PKが大きいと、通常、インデックスを含むテーブルのディスクフットプリントが大きくなります。

比較の場合: INT UNSIGNED4バイトで、範囲は0〜4十億です。 BIGINT8バイトです。

イタリックの更新などが2017年9月に追加されました。重要な変更は何もありません。


回答ありがとうございます。キャッシュの最適化が失われることはあまり意識していませんでした。かさばる外部キーについてはそれほど心配していませんでしたが、それが最終的にどのように問題になるかを理解しています。ただし、システム間の相互作用に非常に役立つことが判明しているため、それらの使用を完全に削除することはしません。 BINARY(16)私はUUIDを保存する最も効率的な方法が両方に同意すると思いますが、UNIQUEインデックスに関しては、通常のインデックスを使用する必要がありますか?バイトは暗号的に安全なRNGを使用して生成されるので、ランダム性に完全に依存し、チェックを忘れますか?
Flosculus、2015年

一意でないインデックスはパフォーマンスに役立ちますが、通常のインデックスでさえ最終的に更新する必要があります。予測されるテーブルのサイズはどれくらいですか?最終的には大きすぎてキャッシュできませんか?の推奨値innodb_buffer_pool_sizeは、使用可能なRAMの70%です。
リックジェームズ

2か月後のデータベースは1.2 GBで、最大のテーブルは300 MBですが、データが消えることはありません。許可されたテーブルの半分未満はUUIDも必要とするため、最も表面的な使用例からそれらを削除します。これにより、現在50,000行と250MB、または10年で30〜100 GBが必要になります。
Flosculus、2015年

2
10年後には、RAMが100GBしかないマシンを購入できなくなります。常にRAMに収まるので、私のコメントはおそらくあなたのケースには当てはまりません。
リックジェームズ

1
@a_horse_with_no_name-古いバージョンでは、常に3倍でした。新しいバージョンだけがそれについて賢くなりました。おそらく5.1.24でした。それはおそらく私がそれを忘れるのに十分古いものです。
リックジェームス

2

「リック・ジェームズ」は受け入れられた答えで言った:「同じテーブルにUNIQUE AUTO_INCREMENTとUNIQUE UUIDの両方を置くことは無駄です」。しかし、このテスト(私は自分のマシンでそれを行いました)は異なる事実を示しています。

例:テスト(T2)で(INT AUTOINCREMENT)PRIMARYとUNIQUE BINARY(16)を使用してテーブルを作成し、別のフィールドをタイトルとして作成した後、非常に優れたパフォーマンスで160万行以上を挿入しましたが、別のテスト(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
あなたの答えが、あなたの個人的なマシンでベンチマークを実行する以上のものであることを説明してください。理想的には、答えは、ベンチマークの出力だけでなく、関連するトレードオフのいくつかについて議論することです。
エリック

1
いくつかの説明をお願いします。なにinnodb_buffer_pool_size?「テーブルサイズ」はどこから来たのですか?
リックジェームス

1
トランザクションサイズに1000を使用して再実行してください。これにより、tbl2とtbl3の両方で奇妙な問題が解消される場合があります。また、タイミングCOMMITはの前ではなく、後に印刷します。これにより、他のいくつかの異常が解消される場合があります。
リックジェームズ

1
私はあなたが使用している言語に慣れていないんだけど、私はどのように異なるの値を見ています@rec_idし、@src_id生成され、それぞれの行に適用されています。いくつかのINSERTステートメントを印刷すると、満足するかもしれません。
リックジェームス

1
また、600Kを超えてください。ある時点で(rec_titleの大きさに部分的に依存します)、t2崖から落ちます。それよりも遅くなるかもしれませんt3。私はわかりません。あなたのベンチマークは一時的に遅い「ドーナツの穴」にt3あります。
リックジェームス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.