Soll ich UUID sowie ID verwenden

11

Ich verwende UUIDs in meinen Systemen seit einiger Zeit aus verschiedenen Gründen, von der Protokollierung bis zur verzögerten Korrelation. Die Formate, die ich verwendet habe, haben sich geändert, als ich weniger naiv wurde von:

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

Als ich die letzte erreichte BINARY(16), begann ich, die Leistung mit der grundlegenden Auto-Inkrement-Ganzzahl zu vergleichen. Der Test und die Ergebnisse sind unten gezeigt, aber wenn Sie nur die Zusammenfassung wollen, bedeutet dies , dass INT AUTOINCREMENTund BINARY(16) RANDOMauf Daten identische Leistung haben reicht bis 200.000 bis (die Datenbank wurde Tests vorausgefüllt vor).

Ich war anfangs skeptisch gegenüber der Verwendung von UUIDs als Primärschlüssel und bin es auch heute noch. Ich sehe hier jedoch das Potenzial, eine flexible Datenbank zu erstellen, die beide verwenden kann. Während viele Menschen über die Vorteile von beidem streiten, welche Nachteile werden durch die Verwendung beider Datentypen ausgeglichen?

  • PRIMARY INT
  • UNIQUE BINARY(16)

Der Anwendungsfall für diese Art der Einrichtung wäre der traditionelle Primärschlüssel für Beziehungen zwischen Tabellen, wobei eine eindeutige Kennung für Beziehungen zwischen Systemen verwendet wird.

Was ich im Wesentlichen zu entdecken versuche, ist der Unterschied in der Effizienz zwischen den beiden Ansätzen. Abgesehen von dem vierfachen Speicherplatz, der nach dem Hinzufügen zusätzlicher Daten weitgehend vernachlässigbar sein kann, scheinen sie mir gleich zu sein.

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

Benchmark einfügen:

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

Benchmark auswählen:

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

Tests:

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

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

Ergebnisse:

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

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

Antworten:

10

UUIDs sind eine Leistungskatastrophe für sehr große Tabellen. (200K Zeilen sind nicht "sehr groß".)

Ihre # 3 ist wirklich schlecht, wenn die CHARCTER SETutf8 ist - CHAR(36)belegt 108 Bytes! Update: Es gibt ROW_FORMATsfür die dies 36 bleiben wird.

UUIDs (GUIDs) sind sehr "zufällig". Die Verwendung als EINZIGARTIGER oder PRIMÄRER Schlüssel für große Tabellen ist sehr ineffizient. Dies liegt daran, dass Sie jedes Mal, wenn Sie INSERTeine neue UUID oder eine UUID SELECTeingeben , in der Tabelle / im Index herumspringen müssen. Wenn die Tabelle / der Index zu groß ist, um in den Cache zu passen (siehe innodb_buffer_pool_size, dieser muss kleiner als RAM sein, normalerweise 70%), wird die 'nächste' UUID möglicherweise nicht zwischengespeichert, was zu einem langsamen Festplattentreffer führt. Wenn die Tabelle / der Index 20-mal so groß wie der Cache ist, werden nur 1/20 (5%) der Treffer zwischengespeichert - Sie sind E / A-gebunden. Verallgemeinerung: Die Ineffizienz gilt für jeden "wahllosen" Zugriff - UUID / MD5 / RAND () / etc.

Verwenden Sie daher nur UUIDs

  • Sie haben "kleine" Tische, oder
  • Sie brauchen sie wirklich, weil Sie eindeutige IDs von verschiedenen Orten aus generieren (und keinen anderen Weg gefunden haben, dies zu tun).

Weitere Informationen zu UUIDs: http://mysql.rjweb.org/doc.php/uuid (Enthält Funktionen zum Konvertieren zwischen Standard-36-Zeichen UUIDsund BINARY(16).) Update: MySQL 8.0 verfügt über eine integrierte Funktion für solche.

Es ist eine Verschwendung, sowohl eine EINZIGARTIGE AUTO_INCREMENTals auch eine UNIQUEUUID in derselben Tabelle zu haben.

  • Wenn ein INSERTauftritt, alle eindeutigen / Primärschlüssel müssen für Duplikate geprüft werden.
  • Jeder eindeutige Schlüssel ist ausreichend für die Anforderung von InnoDB, einen zu haben PRIMARY KEY.
  • BINARY(16) (16 Bytes) ist etwas sperrig (ein Argument gegen die PK), aber nicht so schlimm.
  • Die Sperrigkeit ist wichtig, wenn Sie Sekundärschlüssel haben. InnoDB steckt die PK stillschweigend an das Ende jedes Sekundärschlüssels. Die Hauptlektion besteht darin, die Anzahl der Sekundärschlüssel zu minimieren, insbesondere bei sehr großen Tabellen. Ausarbeitung: Bei einem Sekundärschlüssel endet die Bulk-Debatte normalerweise mit einem Unentschieden. Bei zwei oder mehr Sekundärschlüsseln führt eine dickere PK normalerweise zu einem größeren Speicherbedarf für die Tabelle einschließlich ihrer Indizes.

Zum Vergleich: INT UNSIGNED4 Bytes mit einem Bereich von 0,4 Milliarden. BIGINTbeträgt 8 Bytes.

Kursiv-Updates / etc wurden im September 2017 hinzugefügt. nichts kritisches hat sich geändert.

Rick James
quelle
Vielen Dank für Ihre Antwort. Ich war mir des Verlusts der Cache-Optimierung weniger bewusst. Ich war weniger besorgt über sperrige Fremdschlüssel, aber ich sehe, wie es irgendwann zu einem Problem werden würde. Ich zögere jedoch, ihre Verwendung vollständig zu entfernen, da sie sich für die systemübergreifende Interaktion als sehr nützlich erweisen. BINARY(16)Ich denke, wir sind uns beide einig, dass dies die effizienteste Methode zum Speichern einer UUID ist. Sollte UNIQUEich jedoch in Bezug auf den Index einfach einen regulären Index verwenden? Die Bytes werden mit kryptografisch sicheren RNGs generiert. Soll ich mich also vollständig auf die Zufälligkeit verlassen und auf die Überprüfungen verzichten?
Flosculus
Ein nicht eindeutiger Index würde die Leistung verbessern, aber selbst ein regulärer Index muss eventuell aktualisiert werden. Was ist Ihre projizierte Tischgröße? Wird es irgendwann zu groß sein, um es zwischenzuspeichern? Ein empfohlener Wert für innodb_buffer_pool_sizeist 70% des verfügbaren RAM.
Rick James
Die Datenbank 1,2 GB nach 2 Monaten, größte Tabelle ist 300 MB, aber die Daten werden nie verschwinden, so lange es dauern wird, vielleicht 10 Jahre. Zugegeben, weniger als die Hälfte der Tabellen benötigt sogar UUIDs, daher werde ich sie aus den oberflächlichsten Anwendungsfällen entfernen. Damit bleibt derjenige, der sie benötigt, derzeit bei 50.000 Zeilen und 250 MB oder 30 - 100 GB in 10 Jahren.
Flosculus
2
In 10 Jahren können Sie keine Maschine mit nur 100 GB RAM kaufen. Sie passen immer in den Arbeitsspeicher, daher gelten meine Kommentare wahrscheinlich nicht für Ihren Fall.
Rick James
1
@a_horse_with_no_name - In älteren Versionen war es immer 3x. Nur neuere Versionen wurden schlau. Vielleicht war das 5.1.24; Das ist wahrscheinlich alt genug, um es zu vergessen.
Rick James
2

'Rick James' sagte in akzeptierter Antwort: "Es ist eine Verschwendung, sowohl eine EINZIGARTIGE AUTO_INCREMENT als auch eine EINZIGARTIGE UUID in derselben Tabelle zu haben." Aber dieser Test (ich habe ihn auf meiner Maschine durchgeführt) zeigt verschiedene Fakten.

Zum Beispiel: Mit dem Test (T2) erstelle ich eine Tabelle mit (INT AUTOINCREMENT) PRIMARY und UNIQUE BINARY (16) und einem anderen Feld als Titel, dann füge ich mehr als 1,6 Millionen Zeilen mit sehr guter Leistung ein, aber mit einem anderen Test (T3). Ich habe das gleiche getan, aber das Ergebnis ist langsam, nachdem nur 300.000 Zeilen eingefügt wurden.

Dies ist mein Testergebnis:

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

Binär (16) UNIQUE mit automatischem Inkrement int_id ist also besser als binäres (16) UNIQUE ohne automatisches Inkrement int_id.

Aktualisieren:

Ich mache den gleichen Test noch einmal und nehme weitere Details auf. Dies ist ein vollständiger Code- und Ergebnisvergleich zwischen (T2) und (T3), wie oben erläutert.

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

Dies ist ein vollständiger Testcode, der 600.000 Datensätze in tbl2 oder tbl3 einfügt (vb.net-Code):

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

Das Ergebnis für (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.

Das Ergebnis für (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.
user2241289
quelle
2
Bitte erläutern Sie, wie Ihre Antwort mehr ist, als nur den Benchmark auf Ihrem PC auszuführen. Im Idealfall würde eine Antwort einige der damit verbundenen Kompromisse diskutieren und nicht nur die Benchmark-Ergebnisse.
Erik
1
Bitte einige Klarstellungen. Was war innodb_buffer_pool_size? Woher kommt "Tischgröße"?
Rick James
1
Bitte wiederholen Sie den Vorgang mit 1000 für die Transaktionsgröße. Dies kann die seltsamen Probleme sowohl in tbl2 als auch in tbl3 beseitigen. Drucken Sie auch das Timing nach COMMIT, nicht vor. Dies kann einige andere Anomalien beseitigen.
Rick James
1
Ich bin nicht mit der Sprache vertraut, die Sie verwenden, aber ich sehe, wie unterschiedliche Werte von @rec_idund @src_idgeneriert und auf jede Zeile angewendet werden. Das Drucken einiger INSERTAussagen könnte mich befriedigen.
Rick James
1
Fahren Sie auch weiter über 600 km hinaus. Irgendwann (teilweise abhängig davon, wie groß rec_title ist) fällt t2auch eine Klippe. Es kann sogar langsamer gehen als t3; Ich bin mir nicht sicher. Ihr Benchmark befindet sich in einem "Donut-Loch", in dem t3es vorübergehend langsamer ist.
Rick James