Tôi có nên sử dụng UUID cũng như ID


11

Bây giờ tôi đã sử dụng UUID trong các hệ thống của mình vì nhiều lý do khác nhau, từ đăng nhập đến tương quan bị trì hoãn. Các định dạng tôi sử dụng đã thay đổi khi tôi trở nên ít ngây thơ hơn từ:

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

Đó là khi tôi đạt đến cái cuối cùng BINARY(16), tôi bắt đầu so sánh hiệu suất với số nguyên tăng tự động cơ bản. Kiểm tra và kết quả được hiển thị bên dưới, nhưng nếu bạn chỉ muốn tóm tắt, nó chỉ ra rằng INT AUTOINCREMENTBINARY(16) RANDOMcó hiệu suất giống hệt nhau trên phạm vi dữ liệu lên tới 200.000 (cơ sở dữ liệu đã được điền trước khi kiểm tra).

Ban đầu tôi rất nghi ngờ về việc sử dụng UUID làm khóa chính, và thực sự tôi vẫn vậy, tuy nhiên tôi thấy tiềm năng ở đây để tạo ra một cơ sở dữ liệu linh hoạt có thể sử dụng cả hai. Trong khi nhiều người nhấn mạnh vào những lợi thế của một trong hai, những nhược điểm bị loại bỏ bằng cách sử dụng cả hai loại dữ liệu là gì?

  • PRIMARY INT
  • UNIQUE BINARY(16)

Trường hợp sử dụng cho loại thiết lập này sẽ là khóa chính truyền thống cho các mối quan hệ giữa các bảng, với mã định danh duy nhất được sử dụng cho các mối quan hệ giữa các hệ thống.

Điều tôi chủ yếu cố gắng khám phá là sự khác biệt về hiệu quả giữa hai cách tiếp cận. Bên cạnh không gian đĩa tăng gấp bốn lần được sử dụng, có thể phần lớn không đáng kể sau khi dữ liệu bổ sung được thêm vào, chúng dường như giống nhau.

Lược đồ:

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

Chèn điểm chuẩn:

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

Chọn điểm chuẩn:

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

Các xét nghiệm:

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

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

Các kết quả:

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

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

Câu trả lời:


10

UUID là một thảm họa hiệu suất cho các bảng rất lớn. (Hàng 200K không phải là "rất lớn".)

Số 3 của bạn thực sự tệ khi CHARCTER SETutf8 - CHAR(36)chiếm 108 byte! Cập nhật: Có những ROW_FORMATsthứ này sẽ ở lại 36.

UUID (GUID) rất "ngẫu nhiên". Sử dụng chúng làm khóa UNIITE hoặc PRIMARY trên các bảng lớn là rất không hiệu quả. Điều này là do phải nhảy quanh bảng / chỉ mục mỗi khi bạn INSERTUUID mới hoặc SELECTUUID. Khi bảng / chỉ mục quá lớn để phù hợp với bộ đệm (xem innodb_buffer_pool_size, phải nhỏ hơn RAM, thường là 70%), UUID 'tiếp theo' có thể không được lưu vào bộ đệm, do đó, việc nhấn đĩa chậm. Khi bảng / chỉ mục lớn gấp 20 lần bộ đệm, chỉ 1/20 (5%) số lần truy cập được lưu vào bộ đệm - bạn bị ràng buộc I / O. Khái quát hóa: Tính không hiệu quả áp dụng cho mọi truy cập "ngẫu nhiên" - UUID / MD5 / RAND () / etc

Vì vậy, không sử dụng UUID trừ khi

  • bạn có các bảng "nhỏ" hoặc
  • bạn thực sự cần chúng vì tạo ra các id duy nhất từ ​​những nơi khác nhau (và chưa tìm ra cách nào khác để làm điều đó).

Thông tin thêm về UUID: http://mysql.rjweb.org/doc.php/uuid (Nó bao gồm các chức năng để chuyển đổi giữa 36-char tiêu chuẩn UUIDsBINARY(16).) Cập nhật: MySQL 8.0 có chức năng dựng sẵn như vậy.

Có cả UNIITE AUTO_INCREMENTUNIQUEUUID trong cùng một bảng là một sự lãng phí.

  • Khi INSERTxảy ra, tất cả các khóa duy nhất / chính phải được kiểm tra trùng lặp.
  • Khóa duy nhất là đủ cho yêu cầu của InnoDB là có một PRIMARY KEY.
  • BINARY(16) (16 byte) hơi cồng kềnh (một đối số chống lại việc biến nó thành PK), nhưng không tệ lắm.
  • Sự cồng kềnh quan trọng khi bạn có khóa phụ. InnoDB âm thầm xử lý PK vào cuối mỗi khóa phụ. Bài học chính ở đây là giảm thiểu số lượng khóa phụ, đặc biệt là đối với các bảng rất lớn. Xây dựng: Đối với một khóa phụ, cuộc tranh luận về số lượng lớn thường kết thúc bằng một trận hòa. Đối với 2 hoặc nhiều khóa phụ, PK béo hơn thường dẫn đến dung lượng đĩa lớn hơn cho bảng bao gồm các chỉ mục của nó.

Để so sánh: INT UNSIGNEDlà 4 byte với phạm vi 0.,4 tỷ. BIGINTlà 8 byte.

Cập nhật in nghiêng / vv đã được thêm vào tháng 9 năm 2017; không có gì quan trọng thay đổi.


Cảm ơn câu trả lời của bạn, tôi đã ít biết về việc mất tối ưu hóa bộ đệm. Tôi đã bớt lo lắng về các khóa ngoại cồng kềnh nhưng tôi thấy nó cuối cùng sẽ trở thành một vấn đề như thế nào. Tuy nhiên, tôi miễn cưỡng loại bỏ hoàn toàn việc sử dụng chúng vì chúng thực sự hữu ích cho tương tác hệ thống chéo. BINARY(16)Tôi nghĩ cả hai chúng tôi đều đồng ý là cách hiệu quả nhất để lưu trữ UUID, nhưng liên quan đến UNIQUEchỉ mục, tôi có nên sử dụng chỉ mục thông thường không? Các byte được tạo bằng RNG bảo mật bằng mật mã, vì vậy tôi sẽ phụ thuộc hoàn toàn vào tính ngẫu nhiên và từ bỏ kiểm tra?
Flosculus

Một chỉ mục không duy nhất sẽ giúp thực hiện một số, nhưng ngay cả một chỉ mục thông thường cũng cần được cập nhật. Kích thước bảng dự kiến ​​của bạn là gì? Nó cuối cùng sẽ quá lớn để lưu trữ? Giá trị đề xuất innodb_buffer_pool_sizelà 70% ram có sẵn.
Rick James

Cơ sở dữ liệu của nó 1,2 GB sau 2 tháng, bảng lớn nhất là 300 MB, nhưng dữ liệu sẽ không bao giờ biến mất, do đó, nó sẽ tồn tại trong 10 năm. Cấp ít hơn một nửa số bảng thậm chí sẽ cần UUID, vì vậy tôi sẽ xóa chúng khỏi các trường hợp sử dụng hời hợt nhất. Rời khỏi cái mà sẽ cần chúng hiện tại 50.000 hàng và 250MB, hoặc 30 - 100 GB trong 10 năm.
Flosculus

2
Trong 10 năm, bạn sẽ không thể mua một chiếc máy chỉ có 100GB RAM. Bạn sẽ luôn phù hợp với RAM, vì vậy những nhận xét của tôi có thể sẽ không áp dụng cho trường hợp của bạn.
Rick James

1
@a_horse_with_no_name - Trong các phiên bản cũ hơn, nó luôn là 3x. Chỉ có phiên bản mới hơn thông minh về điều đó. Có lẽ đó là 5.1.24; điều đó có lẽ đủ tuổi để tôi quên nó đi.
Rick James

2

'Rick James' đã nói trong câu trả lời được chấp nhận: "Có cả TỰ ĐỘNG TỰ ĐỘNG HẤP DẪN và UUID ĐỘC ĐÁO trong cùng một bảng là một sự lãng phí". Nhưng thử nghiệm này (tôi đã làm nó trên máy của tôi) cho thấy các sự thật khác nhau.

Ví dụ: với bài kiểm tra (T2) tôi tạo bảng với (INT AUTOINCREMENT) PRIMary và UNIQUE BINary (16) và một trường khác làm tiêu đề, sau đó tôi chèn hơn 1.6M hàng với hiệu suất rất tốt, nhưng với một thử nghiệm khác (T3) Tôi đã làm như vậy nhưng kết quả rất chậm sau khi chỉ chèn 300.000 hàng.

Đây là kết quả thử nghiệm của tôi:

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

Vì vậy, nhị phân (16) UNIQUE với int_id tăng tự động tốt hơn nhị phân (16) UNIITE mà không tự động tăng int_id.

Cập nhật:

Tôi làm bài kiểm tra tương tự một lần nữa và ghi lại nhiều chi tiết hơn. đây là mã đầy đủ và so sánh kết quả giữa (T2) và (T3) như đã giải thích ở trên.

(T2) tạo 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) tạo 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;

Đây là mã kiểm tra đầy đủ, nó đang chèn 600.000 bản ghi vào tbl2 hoặc tbl3 (mã 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

Kết quả cho (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.

Kết quả cho (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
Vui lòng giải thích làm thế nào câu trả lời của bạn không chỉ là chạy điểm chuẩn của họ trên máy cá nhân của bạn. Lý tưởng nhất là một câu trả lời sẽ thảo luận về một số sự đánh đổi liên quan thay vì chỉ là kết quả đầu ra chuẩn.
Erik

1
Một số làm rõ, xin vui lòng. Là bao nhiêu innodb_buffer_pool_size? Trường hợp "kích thước bảng" đến từ đâu?
Rick James

1
Vui lòng chạy lại, sử dụng 1000 cho kích thước giao dịch - điều này có thể loại bỏ các trục trặc lạ trong cả tbl2 và tbl3. Ngoài ra, in ra thời gian sau COMMIT, không phải trước. Điều này có thể loại bỏ một số dị thường khác.
Rick James

1
Tôi không quen thuộc với ngôn ngữ bạn đang sử dụng, nhưng tôi thấy các giá trị khác nhau @rec_id@src_idđang được tạo và áp dụng cho mỗi hàng. In một vài INSERTcâu có thể làm tôi hài lòng.
Rick James

1
Ngoài ra, tiếp tục đi qua 600K. Tại một số điểm (phụ thuộc một phần vào mức độ rec_title lớn), t2cũng sẽ rơi xuống một vách đá. Nó thậm chí có thể đi chậm hơn t3; Tôi không chắc. Điểm chuẩn của bạn đang ở trong một "donut hole", nơi t3đang tạm thời chậm hơn.
Rick James
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.