Должен ли я использовать UUID, а также ID

11

Я уже некоторое время использую UUID в своих системах по разным причинам - от регистрации до отложенной корреляции. Форматы, которые я использовал, изменились, поскольку я стал менее наивным из:

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

Когда я дошел до последнего BINARY(16), я начал сравнивать производительность с базовым целым числом с автоинкрементом. Тест и результаты приведены ниже, но если вы просто хотите , резюме, это указывает на то, что INT AUTOINCREMENTи BINARY(16) RANDOMимеют одинаковую производительность данных составляет до 200000 (база данных была предварительно заполненные до испытаний).

Сначала я скептически относился к использованию UUID в качестве первичных ключей, и на самом деле я все еще ими пользуюсь, однако я вижу здесь потенциал для создания гибкой базы данных, которая может использовать оба. В то время как многие люди подчеркивают преимущества обоих, какие недостатки устраняются при использовании обоих типов данных?

  • PRIMARY INT
  • UNIQUE BINARY(16)

Вариант использования для этого типа установки будет традиционным первичным ключом для межстоловых отношений с уникальным идентификатором, используемым для межсистемных отношений.

По сути, я пытаюсь обнаружить разницу в эффективности между этими двумя подходами. Помимо используемого четырехкратного дискового пространства, которое может быть незначительным после добавления дополнительных данных, они кажутся мне одинаковыми.

Схема:

-- 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
Flosculus
источник

Ответы:

10

UUID - это падение производительности для очень больших таблиц. (200К строк не "очень большие".)

Ваш # 3 действительно плох, когда CHARCTER SETutf8 - CHAR(36)занимает 108 байт! Обновление: есть ROW_FORMATsдля которых это останется 36.

UUID (GUID) очень «случайны». Использование их в качестве УНИКАЛЬНОГО или ПЕРВИЧНОГО ключа для больших таблиц очень неэффективно. Это связано с необходимостью переходить по таблице / индексу каждый раз, когда у вас INSERTновый UUID или SELECTUUID. Когда таблица / индекс слишком велики, чтобы поместиться в кэш (см. innodb_buffer_pool_size, Который должен быть меньше ОЗУ, обычно на 70%), «следующий» UUID может не кэшироваться, следовательно, медленное обращение к диску. Когда таблица / индекс в 20 раз больше кеша, кэшируется только 1/20 (5%) хитов - вы привязаны к вводу / выводу. Обобщение: неэффективность применяется к любому «произвольному» доступу - UUID / MD5 / RAND () / и т. Д.

Поэтому не используйте UUID, если только

  • у вас есть "маленькие" столы, или
  • Вы действительно нуждаетесь в них из-за создания уникальных идентификаторов из разных мест (и не нашли другого способа сделать это).

Подробнее о UUID: http://mysql.rjweb.org/doc.php/uuid (Включает функции для преобразования между стандартными 36-символами UUIDsи BINARY(16).) Обновление: MySQL 8.0 имеет встроенную функцию для таких целей.

Наличие как УНИКАЛЬНОГО, так AUTO_INCREMENTи UNIQUEUUID в одной таблице - пустая трата времени.

  • Когда INSERTпроисходит, все уникальные / первичные ключи должны быть проверены на наличие дубликатов.
  • Любой уникальный ключ достаточен для требования InnoDB иметь PRIMARY KEY.
  • BINARY(16) (16 байт) является несколько громоздким (аргумент против создания ПК), но не так уж плохо.
  • Объемность имеет значение, когда у вас есть дополнительные ключи. InnoDB молча прикрепляет PK к концу каждого вторичного ключа. Основным уроком здесь является минимизация количества вторичных ключей, особенно для очень больших таблиц. Разработка: Для одного вторичного ключа дебаты о громоздкости обычно заканчиваются ничьей. Для 2 или более вторичных ключей более толстый PK обычно приводит к большему пространству на диске для таблицы, включая ее индексы.

Для сравнения: INT UNSIGNED4 байта с диапазоном 0, 4 млрд. BIGINT8 байтов.

Обновления курсивом и т. Д. Были добавлены в сентябре 2017 г .; ничего критического не изменилось.

Рик Джеймс
источник
Спасибо за ваш ответ, я меньше осознавал потерю оптимизации кеша. Я меньше беспокоился о громоздких внешних ключах, но я вижу, как это в конечном итоге станет проблемой. Однако я не хочу полностью исключать их использование, поскольку они оказываются весьма полезными для межсистемного взаимодействия. BINARY(16)Я думаю, что мы оба согласны, что это наиболее эффективный способ хранения UUID, но что касается UNIQUEиндекса, я должен просто использовать обычный индекс? Байты генерируются с использованием криптографически защищенных ГСЧ, поэтому я буду полностью зависеть от случайности и отказаться от проверок?
Flosculus
Неуникальный индекс может повысить производительность, но в конечном итоге даже обычный индекс должен быть обновлен. Какой у вас прогнозируемый размер таблицы? Будет ли он слишком большим для кеширования? Рекомендуемое значение для innodb_buffer_pool_sizeсоставляет 70% от доступной оперативной памяти.
Рик Джеймс
Его база данных объемом 1,2 ГБ через 2 месяца, самая большая таблица - 300 МБ, но данные никогда не исчезнут, поэтому, сколько бы она ни длилась, возможно, 10 лет. Конечно, менее половины таблиц будут даже нуждаться в UUID, поэтому я уберу их из самых поверхностных случаев использования. Что оставляет тот, который будет нуждаться в них, в настоящее время в 50 000 строк и 250 МБ, или 30 - 100 ГБ через 10 лет.
Flosculus
2
Через 10 лет вы не сможете купить машину с 100 ГБ ОЗУ. Вы всегда будете вписываться в оперативную память, поэтому мои комментарии, вероятно, не будут применяться к вашему делу.
Рик Джеймс
1
@a_horse_with_no_name - в старых версиях это всегда было 3x. Только более новые версии стали умными об этом. Возможно, это было 5.1.24; это, вероятно, достаточно стар для меня, чтобы забыть об этом.
Рик Джеймс
2

«Рик Джеймс» сказал в принятом ответе: «Наличие в одной таблице и UNIQUE AUTO_INCREMENT, и UNIQUE UUID - пустая трата времени». Но этот тест (я сделал это на своей машине) показывает разные факты.

Например: с помощью теста (T2) я создаю таблицу с (INT AUTOINCREMENT) PRIMARY и UNIQUE BINARY (16) и другим полем в качестве заголовка, затем я вставляю более 1,6 млн строк с очень хорошей производительностью, но с другим тестированием (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
+++++*

Поэтому двоичный (16) UNIQUE с автоматическим приращением int_id лучше двоичного (16) UNIQUE без автоматического приращения int_id.

Обновить:

Я делаю тот же тест снова и записываю больше деталей. это полный код и сравнение результатов между (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.
user2241289
источник
2
Пожалуйста, объясните, что ваш ответ - это больше, чем просто запуск теста на вашем персональном компьютере. В идеале ответ будет обсуждать некоторые из компромиссов, а не только результаты тестов.
Эрик
1
Некоторые уточнения, пожалуйста. Что было innodb_buffer_pool_size? Откуда взялся «размер стола»?
Рик Джеймс
1
Пожалуйста, повторите, используя 1000 для размера транзакции - это может устранить странные икоты в tbl2 и tbl3. Кроме того, распечатайте время после COMMIT, а не до. Это может устранить некоторые другие аномалии.
Рик Джеймс
1
Я не знаком с языком, который вы используете, но я вижу, как различные значения @rec_idи @src_idгенерируются и применяются к каждой строке. Печать пары INSERTутверждений может удовлетворить меня.
Рик Джеймс
1
Кроме того, продолжайте идти мимо 600K. В какой-то момент (частично зависит от того, насколько большой rec_title), t2также будет падение скалы. Это может даже идти медленнее, чем t3; Я не уверен. Ваш эталонный тест находится в «дыре от бублика», где t3он временно медленнее.
Рик Джеймс