ロギングから遅延相関まで、さまざまな理由で、システムでUUIDをしばらく使用しています。私が使用したフォーマットは、次のように単純になったときに変化しました。
VARCHAR(255)
VARCHAR(36)
CHAR(36)
BINARY(16)
最後のBINARY(16)
に到達したとき、パフォーマンスを基本的な自動インクリメント整数と比較し始めました。テストと結果を以下に示しますが、概要が必要な場合は、_INT AUTOINCREMENT
_とBINARY(16) RANDOM
が200,000までのデータ範囲で同じパフォーマンスを持っていることを示します(データベースは事前に事前に入力されていました)テスト)。
私は当初、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
_
UUIDは、非常に大きなテーブルのパフォーマンスが低下します。 (20万行は「非常に大きく」ありません。)
CHARCTER SET
がutf8の場合、#3は本当に悪いです-CHAR(36)
は108バイトを占有します! 更新:これが36のままになるROW_FORMATs
があります。
UUID(GUID)は非常に「ランダム」です。それらをlargeテーブルのUNIQUEまたはPRIMARYキーとして使用することは非常に非効率的です。これは、新しいUUIDをINSERT
またはSELECT
UUIDするたびにテーブル/インデックスをジャンプする必要があるためです。テーブル/インデックスが大きすぎてキャッシュに収まらない場合(innodb_buffer_pool_size
を参照してください。これはRAMよりも小さくなければならず、通常は70%です)、「次の」UUIDがキャッシュされないため、ディスクヒットが遅くなります。テーブル/インデックスがキャッシュの20倍の大きさの場合、キャッシュされるヒットの1/20(5%)だけがI/Oバウンドになります。 一般化:非効率は「ランダム」アクセスに適用されます-UUID/MD5/Rand()/ etc
したがって、いずれかでない限り、UUIDを使用しないでください。
UUIDの詳細: http://mysql.rjweb.org/doc.php/uuid (標準の36文字UUIDs
とBINARY(16)
の間で変換するための関数が含まれています。)更新:MySQL 8.0にはそのための組み込み関数があります。
UNIQUE AUTO_INCREMENT
とUNIQUE
UUIDの両方を同じテーブルに含めるのは無駄です。
INSERT
が発生した場合、all一意/主キーの重複をチェックする必要があります。PRIMARY KEY
の要件には、どちらの一意のキーでも十分です。BINARY(16)
(16バイト)は多少かさばります(PKにすることに対する反対論)が、それほど悪くはありません。比較の場合:INT UNSIGNED
は4バイトで、範囲は0〜4億です。 BIGINT
は8バイトです。
イタリック体の更新などが2017年9月に追加されました。重要な変更はありません。
「リック・ジェームズ」は受け入れられた答えで言った:「同じテーブルに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.