Haruskah saya menggunakan UUID serta ID

11

Saya telah menggunakan UUID di sistem saya untuk sementara waktu sekarang karena berbagai alasan mulai dari logging hingga korelasi yang tertunda. Format yang saya gunakan berubah karena saya menjadi kurang naif dari:

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

Ketika saya mencapai final BINARY(16), saya mulai membandingkan kinerja dengan integer kenaikan-otomatis dasar. Tes dan hasil ditunjukkan di bawah ini, tetapi jika Anda hanya ingin ringkasan, ini menunjukkan bahwa INT AUTOINCREMENTdan BINARY(16) RANDOMmemiliki kinerja yang identik pada rentang data hingga 200.000 (database telah diisi sebelumnya sebelum tes).

Awalnya saya ragu menggunakan UUID sebagai kunci utama, dan memang masih demikian, namun saya melihat potensi di sini untuk membuat database fleksibel yang bisa menggunakan keduanya. Sementara banyak orang menekankan kelebihan dari keduanya, kerugian apa yang dibatalkan dengan menggunakan kedua tipe data ini?

  • PRIMARY INT
  • UNIQUE BINARY(16)

Kasus penggunaan untuk jenis pengaturan ini akan menjadi kunci utama tradisional untuk hubungan antar-tabel, dengan pengidentifikasi unik yang digunakan untuk hubungan antar-sistem.

Apa yang saya coba temukan pada dasarnya adalah perbedaan efisiensi antara kedua pendekatan. Selain ruang disk quadruple yang digunakan, yang sebagian besar dapat diabaikan setelah data tambahan ditambahkan, mereka tampaknya sama.

Skema:

-- 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 */;

Masukkan patokan:

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

Pilih patokan:

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

Tes:

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

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

Hasil:

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

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

Jawaban:

10

UUID adalah bencana kinerja untuk tabel yang sangat besar. (Baris 200K tidak "sangat besar".)

# 3 Anda benar-benar buruk ketika CHARCTER SETutf8 - CHAR(36)menempati 108 byte! Pembaruan: Ada ROW_FORMATsuntuk mana ini akan tinggal 36.

UUID (GUID) sangat "acak". Menggunakannya sebagai kunci UNIQUE atau PRIMARY pada tabel besar sangat tidak efisien. Ini karena harus melompati tabel / indeks setiap kali Anda INSERTUUID baru atau SELECToleh UUID. Ketika tabel / indeks terlalu besar untuk muat dalam cache (lihat innodb_buffer_pool_size, yang harus lebih kecil dari RAM, biasanya 70%), UUID 'berikutnya' mungkin tidak di-cache, karenanya hit disk yang lambat. Ketika tabel / indeks 20 kali lebih besar dari cache, hanya 1/20 (5%) dari hit yang di-cache - Anda terikat I / O. Generalisasi: Inefisiensi berlaku untuk akses "acak" - UUID / MD5 / RAND () / dll

Jadi, jangan gunakan UUID kecuali

  • Anda memiliki tabel "kecil", atau
  • Anda benar-benar membutuhkannya karena membuat id unik dari tempat yang berbeda (dan belum menemukan cara lain untuk melakukannya).

Lebih lanjut tentang UUID: http://mysql.rjweb.org/doc.php/uuid (Termasuk fungsi untuk mengkonversi antara standar 36-char UUIDsdan BINARY(16).) Pembaruan: MySQL 8.0 memiliki fungsi bawaan untuk itu.

Memiliki UNIK AUTO_INCREMENTdan UNIQUEUUID dalam tabel yang sama adalah sia-sia.

  • Ketika suatu INSERTterjadi, semua kunci unik / primer harus diperiksa untuk duplikat.
  • Baik kunci unik sudah cukup untuk persyaratan InnoDB untuk memiliki PRIMARY KEY.
  • BINARY(16) (16 byte) agak besar (argumen menentang menjadikannya PK), tetapi tidak terlalu buruk.
  • Bulkiness penting ketika Anda memiliki kunci sekunder. InnoDB diam-diam menekan PK ke ujung setiap kunci sekunder. Pelajaran utama di sini adalah untuk meminimalkan jumlah kunci sekunder, terutama untuk tabel yang sangat besar. Elaborasi: Untuk satu kunci sekunder, perdebatan bulkiness biasanya berakhir imbang. Untuk 2 atau lebih kunci sekunder, PK yang lebih gemuk biasanya mengarah ke tapak disk yang lebih besar untuk tabel termasuk indeksnya.

Sebagai perbandingan: INT UNSIGNEDadalah 4 byte dengan kisaran 0,4 miliar. BIGINTadalah 8 byte.

Pembaruan Italics / etc ditambahkan September, 2017; tidak ada yang berubah kritis.

Rick James
sumber
Terima kasih atas jawaban Anda, saya kurang menyadari hilangnya optimasi cache. Saya kurang khawatir tentang kunci asing yang besar tetapi saya melihat bagaimana itu akhirnya akan menjadi masalah. Namun saya enggan menghapus penggunaannya sepenuhnya karena mereka terbukti sangat berguna untuk interaksi lintas sistem. BINARY(16)Saya pikir kita berdua sepakat adalah cara paling efisien untuk menyimpan UUID, tetapi mengenai UNIQUEindeks, haruskah saya menggunakan indeks biasa? Byte dibuat menggunakan RNGs yang aman secara kriptografis, jadi haruskah saya bergantung sepenuhnya pada keacakan, dan melupakan pemeriksaan?
Flosculus
Indeks non-unik akan membantu kinerja beberapa, tetapi bahkan indeks reguler perlu diperbarui pada akhirnya. Berapa ukuran meja Anda yang diproyeksikan? Apakah nantinya akan terlalu besar untuk di-cache? Nilai yang disarankan innodb_buffer_pool_sizeadalah 70% dari ram yang tersedia.
Rick James
Basis datanya 1,2 GB setelah 2 bulan, tabel terbesar adalah 300MB, tetapi datanya tidak akan pernah hilang, jadi berapa lama itu akan bertahan, mungkin 10 tahun. Memang kurang dari setengah dari tabel bahkan akan membutuhkan UUID, jadi saya akan menghapusnya dari kasus penggunaan yang paling dangkal. Yang meninggalkan satu yang akan membutuhkan mereka saat ini di 50.000 baris dan 250MB, atau 30 - 100 GB dalam 10 tahun.
Flosculus
2
Dalam 10 tahun, Anda tidak akan dapat membeli mesin dengan hanya 100GB RAM. Anda akan selalu masuk dalam RAM, jadi komentar saya mungkin tidak berlaku untuk kasus Anda.
Rick James
1
@a_horse_with_no_name - Dalam versi yang lebih lama, selalu 3x. Hanya versi yang lebih baru yang pandai tentang hal itu. Mungkin itu 5.1.24; itu mungkin sudah cukup tua bagi saya untuk melupakannya.
Rick James
2

'Rick James' mengatakan dalam jawaban yang diterima: "Memiliki AUTO_INCREMENT UNIK dan UUID UNIK di tabel yang sama adalah sia-sia". Tetapi tes ini (saya melakukannya di mesin saya) menunjukkan fakta yang berbeda.

Sebagai contoh: dengan tes (T2) saya membuat tabel dengan (INT AUTOINCREMENT) PRIMARY dan UNIQUE BINARY (16) dan bidang lain sebagai judul, kemudian saya memasukkan lebih dari 1,6M baris dengan kinerja sangat baik, tetapi dengan pengujian lain (T3) Saya melakukan hal yang sama tetapi hasilnya lambat setelah memasukkan 300.000 baris saja.

Ini adalah hasil pengujian saya:

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

Jadi biner (16) UNIQUE dengan penambahan otomatis int_id lebih baik daripada biner (16) UNIQUE tanpa penambahan otomatis int_id.

Memperbarui:

Saya melakukan tes yang sama lagi dan mencatat lebih banyak detail. ini adalah kode lengkap dan hasil perbandingan antara (T2) dan (T3) seperti yang dijelaskan di atas.

(T2) buat 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) buat 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;

Ini adalah kode pengujian lengkap, ini memasukkan 600.000 catatan ke tbl2 atau tbl3 (kode 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

Hasil untuk (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.

Hasil untuk (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.
pengguna2241289
sumber
2
Tolong jelaskan bagaimana jawaban Anda lebih dari sekadar menjalankan patokan mereka di mesin pribadi Anda. Idealnya jawaban akan membahas beberapa trade off yang terlibat, bukan hanya output benchmark.
Erik
1
Tolong, beberapa klarifikasi. Apa innodb_buffer_pool_size? Dari mana "ukuran tabel" berasal?
Rick James
1
Jalankan kembali, gunakan 1000 untuk ukuran transaksi - ini dapat menghilangkan cegukan aneh di tbl2 dan tbl3. Juga, cetak waktu setelah COMMIT, bukan sebelumnya. Ini dapat menghilangkan beberapa anomali lainnya.
Rick James
1
Saya tidak terbiasa dengan bahasa yang Anda gunakan, tetapi saya melihat betapa berbedanya nilai @rec_iddan @src_idsedang dihasilkan serta diterapkan pada setiap baris. Mencetak beberapa INSERTpernyataan mungkin memuaskan saya.
Rick James
1
Juga, terus melewati 600 ribu. Pada titik tertentu (sebagian tergantung pada seberapa besar rec_title), t2juga akan jatuh dari tebing. Ini mungkin bahkan pergi lebih lambat dari t3; Saya tidak yakin. Benchmark Anda berada di "lubang donat" t3yang sementara waktu lebih lambat.
Rick James