Kimliğin yanı sıra UUID kullanmalı mıyım


11

Bir süredir sistemlerimde UUID'leri günlüğe kaydetmekten gecikmeli korelasyona kadar çeşitli nedenlerle kullanıyorum. Kullandığım formatlar, daha az saflaştıkça değişti:

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

Ben son bir tane ulaştığı zaman oldu BINARY(16)ben temel otomatik artış tamsayı ile performansını karşılaştırmak başladığını. Test ve sonuçlar aşağıda gösterilmiştir, ancak sadece özeti istiyorsanız, 200.000'e kadar veri aralıklarında aynı performansa sahip olduğunu INT AUTOINCREMENTve BINARY(16) RANDOMaynı performansa sahip olduğunu gösterir (veritabanı testlerden önce önceden doldurulmuştur).

Başlangıçta UUID'leri birincil anahtar olarak kullanma konusunda şüpheliydim ve gerçekten de hala öyleyim, ancak burada her ikisini de kullanabilen esnek bir veritabanı oluşturma potansiyelini görüyorum. Birçok kişi her ikisinin de avantajları üzerinde dururken, her iki veri türünü kullanarak iptal edilen dezavantajlar nelerdir?

  • PRIMARY INT
  • UNIQUE BINARY(16)

Bu tür bir kurulum için kullanım durumu, sistemler arası ilişkiler için kullanılan benzersiz tanımlayıcı ile tablolar arası ilişkiler için geleneksel birincil anahtar olacaktır.

Aslında keşfetmeye çalıştığım şey, iki yaklaşım arasındaki verimlilik farkıdır. Ek veriler eklendikten sonra büyük ölçüde ihmal edilebilecek olan kullanılan dörtlü disk alanının yanı sıra, bunlar bana aynı gibi geliyor.

Şema:

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

Karşılaştırma ölçütü ekle:

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

Kıyaslama seçin:

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

Testler:

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

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

Sonuçlar:

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

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

Yanıtlar:


10

UUID'ler çok büyük tablolar için bir performans felaketidir. (200K satır "çok büyük" değildir.)

CHARCTER SETUtf8 olduğunda # 3 gerçekten kötü - CHAR(36)108 bayt kaplar! Güncelleme: Bunun ROW_FORMATsiçin 36 kalacak.

UUID'ler (GUID'ler) çok "rastgele" dir. Büyük tablolarda UNIQUE veya PRIMARY anahtarı olarak kullanmak çok verimsizdir. Bunun nedeni, her INSERTyeni UUID veya SELECTUUID tarafından masa / dizin etrafında atlamak zorunda kalmanızdır . Tablo / dizin önbelleğe sığmayacak kadar büyük olduğunda (bkz. innodb_buffer_pool_sizeRAM'den daha küçük olması gerekir, genellikle% 70), 'sonraki' UUID önbelleğe alınmayabilir, dolayısıyla yavaş bir disk vuruşu olabilir. Tablo / dizin önbellekten 20 kat daha büyük olduğunda, isabetlerin yalnızca 1 / 20'si (% 5) önbelleğe alınır - G / Ç'ye bağlı olursunuz. Genelleme: Verimsizlik herhangi bir "rastgele" erişim için geçerlidir - UUID / MD5 / RAND () / vb.

Yani, UUID'leri de

  • "küçük" tablolarınız var veya
  • farklı yerlerden benzersiz kimlikler oluşturduğundan onlara gerçekten ihtiyacınız var (ve bunu yapmak için başka bir yol bulamadınız).

UUID'ler hakkında daha fazla bilgi: http://mysql.rjweb.org/doc.php/uuid (Standart 36 karakter UUIDsve arasında dönüştürme işlevlerini içerir BINARY(16).) Güncelleme: MySQL 8.0 bunun için yerleşik bir işleve sahiptir.

UNIQUE hem sahip AUTO_INCREMENTve bir UNIQUEaynı tabloda UUID'sini kaybıdır.

  • Bir INSERTdurum meydana geldiğinde, tüm benzersiz / birincil anahtarların kopya olup olmadığı kontrol edilmelidir.
  • Her iki anahtar da InnoDB'nin a PRIMARY KEY.
  • BINARY(16) (16 bayt) biraz hantal (PK yapmak için bir argüman), ama o kadar da kötü değil.
  • İkincil anahtarlarınız olduğunda büyüklük önemlidir. InnoDB sessizce her ikincil anahtarın sonuna PK'yi kilitler. Buradaki ana ders, özellikle çok büyük tablolar için ikincil anahtar sayısını en aza indirmektir. Detaylandırma: Bir ikincil anahtar için, büyüklük tartışması genellikle berabere biter. 2 veya daha fazla ikincil anahtar için, daha şişman bir PK genellikle dizinleri dahil olmak üzere tablo için daha büyük bir disk alanına yol açar.

Karşılaştırma için: INT UNSIGNED0,4 milyar aralıklı 4 bayttır. BIGINT8 bayttır.

İtalik Güncellemeler / vb. Eklendi Eylül 2017; kritik bir şey değişmedi.


Cevabınız için teşekkür ederim, önbellek optimizasyonunun kaybının daha az farkındaydım. Hacimli yabancı anahtarlar hakkında daha az endişeliydim ama sonunda nasıl bir sorun haline geleceğini görüyorum. Ancak, çapraz sistem etkileşimi için çok yararlı olduklarını kanıtladıkları için kullanımlarını tamamen kaldırmak konusunda isteksizim. BINARY(16)İkimizin de bir UUID depolamanın en etkili yolu olduğunu kabul ediyoruz, ancak UNIQUEdizinle ilgili olarak, normal bir dizin kullanmalı mıyım? Baytlar kriptografik olarak güvenli RNG'ler kullanılarak üretilir, bu yüzden rastgelelığa tamamen bağlı mıyım ve kontrollerden vazgeçeyim mi?
Flosculus

Benzersiz olmayan bir dizin bazılarının performansına yardımcı olur, ancak normal bir dizinin bile sonunda güncellenmesi gerekir. Öngörülen masa boyutunuz nedir? Sonunda önbelleklemek için çok büyük olacak mı? İçin önerilen değer innodb_buffer_pool_sizemevcut ramın% 70'idir.
Rick James

Veritabanı 2 ay sonra 1.2 GB, en büyük tablo 300MB, ancak veriler asla kaybolmayacak, bu yüzden uzun süre dayanacak, 10 yıl belki. Tabloların yarısından daha azının UUID'lere bile ihtiyacı olacak, bu yüzden onları en yüzeysel kullanım durumlarından kaldıracağım. Bu, şu anda onlara ihtiyaç duyacak olanı 50.000 satır ve 250MB veya 10 yıl içinde 30 - 100 GB'de bırakır.
Flosculus

2
10 yıl içinde, sadece 100GB RAM'e sahip bir makine satın alamazsınız. Her zaman RAM'e sığacaksınız, bu yüzden yorumlarım muhtemelen davanız için geçerli olmayacak.
Rick James

1
@a_horse_with_no_name - Eski sürümlerde her zaman 3x'ti. Sadece yeni sürümler bu konuda akıllıydı. Belki de 5.1.24 idi; bu muhtemelen unutmam için yeterince eski.
Rick James

2

'Rick James' kabul edilen cevapta şöyle dedi: "Aynı tabloda hem UNIQUE AUTO_INCREMENT hem de UNIQUE UUID olması israftır". Ama bu test (makinemde yaptım) farklı gerçekleri gösteriyor.

Örneğin: (T2) testi ile (INT AUTOINCREMENT) PRIMARY ve UNIQUE BINARY (16) ve başlık olarak başka bir alan ile tablo oluşturuyorum, sonra çok iyi bir performansla 1.6M'den fazla satır ekliyorum, ancak başka bir testle (T3) Aynı şeyi yaptım ama sonuç sadece 300.000 satır eklendikten sonra yavaş.

Bu benim test sonucum:

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

Otomatik artış int_id ile ikili (16) UNIQUE otomatik artış int_id olmadan ikili (16) UNIQUE daha iyidir.

Güncelleme:

Aynı testi tekrar yaparım ve daha fazla ayrıntı kaydederim. bu tam koddur ve yukarıda açıklandığı gibi (T2) ve (T3) arasındaki sonuç karşılaştırmasıdır.

(T2) tbl2 (mysql) oluştur:

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) oluştur:

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;

Bu tam test kodudur, tbl2 veya tbl3'e (vb.net kodu) 600.000 kayıt ekler:

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

Sonuç (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.

Sonuç (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
Lütfen yanıtınızın kişisel makinenizde kıyaslama yapmaktan daha fazlası olduğunu açıklayın. İdeal olarak bir cevap, sadece kıyaslama çıktıları yerine, bazı ticari ödünçleri tartışacaktır.
Erik

1
Bazı açıklamalar lütfen. Ne oldu innodb_buffer_pool_size? "Masa boyutu" nereden geldi?
Rick James

1
Lütfen işlem boyutu için 1000 kullanarak yeniden çalıştırın - bu hem tbl2 hem de tbl3'teki garip hıçkırıkları ortadan kaldırabilir. Ayrıca, COMMITönce değil , zamanlamayı yazdırın . Bu, diğer bazı anomalileri ortadan kaldırabilir.
Rick James

1
Ben kullandığınız dilin aşina değilim, ama ben nasıl farklı değerlere görüyorsunuz @rec_idve @src_idoluşturulur ve her bir satıra uygulanıyor. Birkaç INSERTifade yazdırmak beni tatmin edebilir.
Rick James

1
Ayrıca, 600K'yı geçmeye devam edin. Bir noktada (kısmen rec_title'ın büyüklüğüne bağlıdır), t2bir uçurumdan da düşecektir. Bu olabilir hatta daha yavaş gidin t3; Emin değilim. Kişisel kriter bir "çörek delik" olduğunu t3olduğunu geçici olarak daha yavaş.
Rick James
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.