Dovrei usare UUID e ID


11

Sto usando UUID nei miei sistemi da un po 'di tempo per una serie di motivi che vanno dalla registrazione alla correlazione ritardata. I formati che ho usato sono cambiati quando sono diventato meno ingenuo da:

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

È stato quando ho raggiunto quello finale BINARY(16)che ho iniziato a confrontare le prestazioni con il numero intero di autoincremento di base. Il test ed i risultati sono riportati di seguito, ma se si desidera solo la sintesi, significa che INT AUTOINCREMENTe BINARY(16) RANDOMhanno prestazioni identiche su dati campi fino a 200.000 (il database è stato pre-popolato prima di test).

Inizialmente ero scettico sull'uso degli UUID come chiavi primarie, e in effetti lo sono ancora, tuttavia vedo il potenziale qui per creare un database flessibile che può usare entrambi. Mentre molte persone sottolineano i vantaggi di entrambi, quali sono gli svantaggi eliminati utilizzando entrambi i tipi di dati?

  • PRIMARY INT
  • UNIQUE BINARY(16)

Il caso d'uso per questo tipo di installazione sarebbe la chiave primaria tradizionale per le relazioni tra tabelle, con identificatore univoco utilizzato per le relazioni tra sistemi.

Ciò che sto essenzialmente cercando di scoprire è la differenza di efficienza tra i due approcci. Oltre allo spazio quadruplo del disco utilizzato, che può essere in gran parte trascurabile dopo l'aggiunta di ulteriori dati, mi sembrano gli stessi.

Schema:

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

Inserisci benchmark:

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

Seleziona benchmark:

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

test:

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

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

risultati:

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

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

Risposte:


10

Gli UUID sono un disastro delle prestazioni per tabelle molto grandi. (200K righe non sono "molto grandi".)

Il tuo # 3 è davvero male quando CHARCTER SETè utf8 - CHAR(36)occupa 108 byte! Aggiornamento: ci sono ROW_FORMATsper i quali rimarrà 36.

Gli UUID (GUID) sono molto "casuali". Il loro utilizzo come chiave UNICA o PRIMARIA su tabelle di grandi dimensioni è molto inefficiente. Ciò è dovuto al fatto di dover saltare la tabella / l'indice ogni volta che si INSERTesegue un nuovo UUID o SELECTUUID. Quando la tabella / indice è troppo grande per adattarsi alla cache (vedi innodb_buffer_pool_size, che deve essere più piccola della RAM, in genere il 70%), l'UUID "successivo" potrebbe non essere memorizzato nella cache, quindi un rallentamento del disco. Quando la tabella / indice è 20 volte più grande della cache, nella cache viene memorizzato solo 1/20 (5%) di accessi: si è associati a I / O. Generalizzazione: l'inefficienza si applica a qualsiasi accesso "casuale" - UUID / MD5 / RAND () / ecc

Quindi, non utilizzare gli UUID a meno che nessuno dei due

  • hai "piccoli" tavoli, o
  • ne hai davvero bisogno a causa della generazione di ID unici da luoghi diversi (e non hai trovato un altro modo per farlo).

Altre informazioni sugli UUID: http://mysql.rjweb.org/doc.php/uuid (Include funzioni per la conversione tra 36 caratteri standard UUIDse BINARY(16).) Aggiornamento: MySQL 8.0 ha una funzione integrata per questo.

Avere sia un UNIQUE AUTO_INCREMENTche un UNIQUEUUID nella stessa tabella è uno spreco.

  • Quando si INSERTverifica, tutte le chiavi univoche / primarie devono essere verificate per i duplicati.
  • Entrambe le chiavi uniche sono sufficienti per il requisito di InnoDB di avere un PRIMARY KEY.
  • BINARY(16) (16 byte) è piuttosto voluminoso (un argomento contro il renderlo PK), ma non è poi così male.
  • La voluminosità è importante quando si hanno chiavi secondarie. InnoDB inserisce silenziosamente il PK sull'estremità di ogni chiave secondaria. La lezione principale qui è minimizzare il numero di chiavi secondarie, specialmente per tabelle molto grandi. Elaborazione: per una chiave secondaria, il dibattito sulla volgarità di solito termina con un pareggio. Per 2 o più chiavi secondarie, un PK più grasso di solito porta a un footprint del disco più grande per la tabella inclusi i suoi indici.

Per confronto: INT UNSIGNEDè di 4 byte con un intervallo di 0..4 miliardi. BIGINTè di 8 byte.

Aggiornamenti corsivi / ecc. Aggiunti settembre 2017; nulla di critico è cambiato.


Grazie per la tua risposta, ero meno consapevole della perdita di ottimizzazione della cache. Ero meno preoccupato per le ingombranti chiavi esterne ma vedo come sarebbe diventato un problema. Tuttavia, sono riluttante a rimuoverne completamente l'uso, poiché si rivelano molto utili per l'interazione tra sistemi. BINARY(16)Penso che entrambi siamo d'accordo sul modo più efficiente per archiviare un UUID, ma per quanto riguarda l' UNIQUEindice, dovrei semplicemente usare un indice regolare? I byte vengono generati utilizzando RNG crittograficamente sicuri, quindi dovrò dipendere interamente dalla casualità e rinunciare ai controlli?
Flosculus,

Un indice non univoco aiuterebbe alcune prestazioni, ma alla fine anche un indice regolare deve essere aggiornato. Qual è la dimensione del tavolo proiettata? Sarà infine troppo grande per essere memorizzato nella cache? Un valore suggerito per innodb_buffer_pool_sizeè il 70% della RAM disponibile.
Rick James,

Il suo database 1,2 GB dopo 2 mesi, la tabella più grande è 300 MB, ma i dati non scompariranno mai, quindi per quanto possa durare, forse 10 anni. Concesso meno della metà dei tavoli avrà anche bisogno di UUID, quindi li rimuoverò dai casi d'uso più superficiali. Il che lascia quello che ne avrà bisogno al momento a 50.000 file e 250 MB, o 30-100 GB in 10 anni.
Flosculus,

2
In 10 anni, non sarai in grado di acquistare una macchina con solo 100 GB di RAM. Ti inserirai sempre nella RAM, quindi i miei commenti probabilmente non si applicheranno al tuo caso.
Rick James,

1
@a_horse_with_no_name - Nelle versioni precedenti, era sempre 3x. Solo le versioni più recenti sono diventate intelligenti. Forse era il 5.1.24; che probabilmente è abbastanza vecchio per me da dimenticare.
Rick James,

2

"Rick James" disse nella risposta accettata: "Avere un UNI_INCREMENT UNICO e un UUID UNICO nella stessa tabella è uno spreco". Ma questo test (l'ho fatto sulla mia macchina) mostra fatti diversi.

Ad esempio: con il test (T2) faccio una tabella con (INT AUTOINCREMENT) PRIMARY e UNIQUE BINARY (16) e un altro campo come titolo, quindi inserisco più di 1,6 M righe con prestazioni molto buone, ma con un altro test (T3) Ho fatto lo stesso, ma il risultato è lento dopo aver inserito solo 300.000 righe.

Questo è il mio risultato del test:

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

Quindi binario (16) UNIQUE con auto increment int_id è meglio di binary (16) UNIQUE senza auto increment int_id.

Aggiornare:

Faccio di nuovo lo stesso test e registro più dettagli. questo è il codice completo e il confronto dei risultati tra (T2) e (T3) come spiegato sopra.

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

Questo è il codice di test completo, sta inserendo 600.000 record in tbl2 o tbl3 (codice 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

Il risultato per (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.

Il risultato per (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
Spiega in che modo la tua risposta è molto più che eseguire il proprio benchmark sul tuo computer personale. Idealmente, una risposta discuterà di alcuni dei compromessi coinvolti anziché solo dei risultati di riferimento.
Erik,

1
Alcuni chiarimenti, per favore. Che cosa era innodb_buffer_pool_size? Da dove viene la "dimensione del tavolo"?
Rick James,

1
Rieseguire, usando 1000 per la dimensione della transazione - questo potrebbe eliminare gli strani singhiozzi sia in tbl2 che in tbl3. Inoltre, stampa i tempi dopo il COMMIT, non prima. Ciò può eliminare alcune altre anomalie.
Rick James,

1
Non ho familiarità con la lingua che stai usando, ma vedo come diversi valori di @rec_ide @src_idvengono generati e applicati a ciascuna riga. Stampare un paio di INSERTdichiarazioni potrebbe soddisfarmi.
Rick James,

1
Inoltre, continua oltre 600K. Ad un certo punto (parzialmente dipendente da quanto è grande il rec_title), t2cadrà anche una scogliera. Si può anche andare più lento rispetto t3; Non sono sicuro. Il tuo benchmark si trova in un "buco di ciambella" dove t3è temporaneamente più lento.
Rick James,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.