ID와 UUID를 사용해야합니까


11

로깅에서 지연된 상관 관계에 이르기까지 다양한 이유로 시스템에서 UUID를 잠시 사용해 왔습니다. 내가 사용하지 않은 형식은 다음과 같이 순진하지 않게 변경되었습니다.

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

내가 마지막 BINARY(16)자동 증가 정수와 성능을 비교하기 시작한 마지막에 도달했을 때였습니다 . 테스트 결과는 다음과 같다,하지만 당신은 단지 요약을 원하는 경우, 그것은을 나타냅니다 INT AUTOINCREMENTBINARY(16) RANDOM(데이터베이스가 이전에 시험을 미리 입력 된) 20까지의 범위 데이터에서 동일한 성능을 가지고있다.

처음에는 기본 키로 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

답변:


10

UUID는 매우 큰 테이블의 성능 장애입니다. (200K 행은 "매우 크지"않습니다.)

# 3은 CHARCTER SETutf8 일 때 정말 나쁩니다 CHAR(36)-108 바이트를 차지합니다! 업데이트 : ROW_FORMATs36이 유지됩니다.

UUID (GUID)는 매우 "무작위"입니다. 테이블 에서 UNIQUE 또는 PRIMARY 키로 사용하는 것은 매우 비효율적입니다. INSERT새로운 UUID 또는 SELECTUUID에 의해 매번 테이블 / 인덱스를 이동해야하기 때문입니다 . 테이블 / 인덱스가 너무 커서 캐시에 맞지 innodb_buffer_pool_size않을 경우 (일반적으로 RAM보다 작아야 함 (보통 70 %) 참조) '다음'UUID가 캐시되지 않아 디스크 히트가 느려질 수 있습니다. 테이블 / 인덱스가 캐시보다 20 배 큰 경우 히트의 1/20 (5 %) 만 캐시됩니다. I / O 바운드입니다. 일반화 : 비 효율성은 모든 "무작위"액세스-UUID / MD5 / RAND () / etc에 적용됩니다

따라서 UUID를 사용하지 않으면

  • "작은"테이블이 있거나
  • 다른 장소에서 고유 한 ID를 생성하기 때문에 실제로 필요합니다 (다른 방법을 찾지 못했습니다).

UUID에 대한 추가 정보 : http://mysql.rjweb.org/doc.php/uuid (표준 36 문자 UUIDsBINARY(16). 사이를 변환하는 기능이 포함되어 있습니다 .) 업데이트 : MySQL 8.0에는 이러한 기능이 내장되어 있습니다.

동일한 테이블에 UNIQUE AUTO_INCREMENTUNIQUEUUID 를 모두 갖는 것은 낭비입니다.

  • 이 때 INSERT발생 하는 모든 고유 / 기본 키는 중복 확인해야합니다.
  • InnoDB의 요구 사항에는 고유 키가 충분합니다 PRIMARY KEY.
  • BINARY(16) (16 바이트)는 다소 부피가 크지 만 (PK를 만드는 것에 대한 논쟁), 그렇게 나쁘지는 않습니다.
  • 보조 키가 있으면 부피가 중요합니다. InnoDB는 PK를 각 보조 키의 끝에 자동으로 고정시킵니다. 여기서 중요한 교훈은 특히 매우 큰 테이블의 경우 보조 키 수를 최소화하는 것입니다. 정교화 : 하나의 2 차 키의 경우, 부피 토론은 일반적으로 무승부로 끝납니다. 2 개 이상의 보조 키의 경우, PK가 많을수록 일반적으로 인덱스를 포함하여 테이블의 디스크 공간이 더 커집니다.

비교의 경우 : INT UNSIGNED0.4 억 범위의 4 바이트입니다. BIGINT8 바이트입니다.

기울임 꼴 업데이트 등이 2017 년 9 월에 추가되었습니다. 중요한 변화는 없습니다.


귀하의 답변에 감사드립니다. 캐시 최적화의 손실에 대해 덜 알고있었습니다. 부피가 큰 외래 키에 대해 덜 걱정했지만 결국 어떻게 문제가 될지 알았습니다. 그러나 교차 시스템 상호 작용에 매우 유용하기 때문에 사용을 완전히 제거하는 것을 꺼려합니다. BINARY(16)우리 둘 다 UUID를 저장하는 가장 효율적인 방법이라고 생각하지만 UNIQUE인덱스와 관련하여 간단히 일반 인덱스를 사용해야합니까? 바이트는 암호화 된 보안 RNG를 사용하여 생성되므로 임의성에 전적으로 의존하고 검사를 포기해야합니까?
Flosculus

고유하지 않은 인덱스는 일부 성능을 향상시키는 데 도움이되지만 일반 인덱스조차도 결국 업데이트해야합니다. 예상 테이블 크기는 얼마입니까? 결국 캐시하기에는 너무 큽니까? 권장되는 값 innodb_buffer_pool_size은 사용 가능한 램의 70 %입니다.
Rick James

2 개월 후 데이터베이스 1.2GB, 가장 큰 테이블은 300MB이지만 데이터는 절대 사라지지 않을 것입니다. 테이블의 절반 미만이 UUID를 필요로하기 때문에 가장 피상적 인 사용 사례에서 제거 할 것입니다. 현재 필요한 행은 50,000 행, 250MB 또는 10 년 내에 30-100GB입니다.
Flosculus

2
10 년 후에는 100GB의 RAM 만있는 머신을 구입할 수 없습니다. 당신은 항상 RAM에 적합하므로 내 의견은 아마도 귀하의 경우에 적용되지 않을 것입니다.
Rick James

1
@a_horse_with_no_name-이전 버전에서는 항상 3 배였습니다. 최신 버전 만 똑똑합니다. 아마도 5.1.24 일 것입니다. 아마 내가 잊어 버릴만큼 나이가 들었을 것이다.
Rick James

2

'Rick James'는 "유일한 테이블에 UNIQUE AUTO_INCREMENT와 UNIQUE UUID를 모두 갖는 것은 낭비입니다."라고 대답했습니다. 그러나이 테스트 (내 컴퓨터에서 수행)는 다른 사실을 보여줍니다.

예를 들어 테스트 (T2)를 사용하여 (INT AUTOINCREMENT) PRIMARY 및 UNIQUE BINARY (16) 및 다른 필드를 제목으로 사용하여 테이블을 만든 다음 성능이 매우 좋은 1.6M 개 이상의 행을 삽입하지만 다른 테스트 (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
+++++*

따라서 자동 증가 int_id가있는 binary (16) UNIQUE가 자동 증가 int_id가없는 binary (16) UNIQUE보다 낫습니다.

최신 정보:

나는 같은 테스트를 다시하고 자세한 내용을 기록합니다. 이것은 위에서 설명한 것처럼 (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;

이것은 전체 테스트 코드이며 tbl2 또는 tbl3 (vb.net 코드)에 600,000 레코드를 삽입합니다.

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.

2
개인용 컴퓨터에서 벤치 마크를 실행하는 것 이상의 답변이 무엇인지 설명하십시오. 이상적으로는 벤치 마크 출력 대신 관련된 일부 트레이드 오프에 대해 설명합니다.
Erik

1
몇 가지 설명을 부탁드립니다. 무엇입니까 innodb_buffer_pool_size? "테이블 크기"는 어디에서 왔습니까?
Rick James

1
트랜잭션 크기로 1000을 사용하여 다시 실행하십시오. 이렇게하면 tbl2 및 tbl3의 이상한 딸꾹질이 제거 될 수 있습니다. 또한 COMMIT이전이 아닌 이후의 타이밍을 인쇄하십시오 . 이것은 다른 예외를 제거 할 수 있습니다.
Rick James

1
난 당신이 사용하는 언어에 익숙하지 해요,하지만 난 얼마나 다른 값을 볼 수 있습니까 @rec_id@src_id생성 된 각 행에 적용되고있다. 몇 INSERT문장을 인쇄하면 나를 만족시킬 수 있습니다.
Rick James

1
또한 계속 600K를 넘어서십시오. 어떤 시점 (부분적으로 rec_title의 크기에 따라) t2은 절벽에서 떨어질 것입니다. 그것은 도보다 느리게 이동 t3; 확실하지 않습니다. 벤치 마크는 "도넛 구멍"에 t3있으며 일시적으로 느려집니다.
Rick James
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.