MySQL 如何确定 INSERT 是否唯一?

How does MySQL determine if an INSERT is unique?

我想知道在对任何列定义为 UNIQUE 的 table 执行 INSERT 之前,是否存在隐含的 SELECT 是 运行。我在 INSERT 的文档中找不到任何相关信息。

我问了一些似乎没有人能够回答的其他问题 - 可能是因为我没有正确解释自己 - 与上述问题有关。

如果我理解正确,那么我假设以下内容是正确的:

案例 1: 您有一个包含 10 亿行的 table。每行都有一个唯一的 UUID 列。如果执行插入,服务器必须执行某种 implicit SELECT COUNT(*) FROM table WHERE UUID = [new uuid] 并确定计数是 0 还是 1。正确吗?

案例 2: 您有一个包含 10 亿行的 table。每行都有一个由 DATE 和 UUID 组成的复合唯一键。如果执行插入,服务器必须执行某种 implicit SELECT COUNT(*) FROM table WHERE DATE = [date] AND UUID = [new uuid] 并检查计数是 0 还是 1。是吗?

我使用隐式这个词是因为在某些时候,在进程的某个地方,服务器必须检查值。如果不是这样,它将要求物理定律规定不能存在两个相同的行——据我所知,当涉及到以二进制形式在某处写下的数字的唯一性时,物理学并没有发挥重要作用。计算机中的磁盘。

让我们假设您的 10 亿行在 2,000 个不同的日期中按顺序均匀分布。这是否意味着案例 2 会更快地执行插入,因为它可以查找分段为日期的 UUID?如果不是,那么使用案例 1 来提高插入速度会更好吗?在那种情况下,为什么?

这个问题是理论性的,所以在这种情况下不要考虑常规 SELECT 性能。主键不会是 UUID+DATE 索引。

作为对评论的回应:在我的案例中,UUID 的设计目的仅仅是为了避免由于连接不良而导致的重复条目。由于您不能为不同的日期创建相同的条目两次(如果逻辑上不是新条目),UUID 不需要是全局唯一的——它只需要对每个日期都是唯一的。这就是为什么我可以允许它成为复合键的一部分。

这就是UNIQUE constraint的目的:

A UNIQUE index creates a constraint such that all values in the index must be distinct. An error occurs if you try to add a new row [or update an existing row] with a key value that matches [another] existing row.

前面在同一手册页中指出

A column list of the form (col1,col2,...) creates a multiple-column index. Index key values are formed by concatenating the values of the given columns.

没有记录此约束的实现方式,但它必须以某种方式等同于值为 inserted/updated 的初步 SELECT。这种检查的成本通常可以忽略不计,因为根据定义,字段是索引的(这种开销变得相关when dealing with bulk inserts)。

索引覆盖的列数在性能方面没有意义(例如,与table中的行数相比)。它确实会影响索引占用的磁盘 space,但这在您的设计决策中应该无关紧要。

在 table 中插入大量数据时,请记住数据最终会物理存储在某个磁盘上。为了实际从磁盘读取和写入数据,MySQL(以及大多数其他 RDBMS)使用称为 clustered index 的东西。如果您在 table 上指定主键或唯一索引,则参与 key/index 的列将成为聚簇索引键。这意味着在磁盘上,数据的物理存储顺序与键列中的值相同。

利用聚簇索引,数据库引擎可以快速判断一个值是否已经存在,而无需扫描整个table。理论上,如果一个 table 包含 N = 1.000.000 条记录,引擎平均需要 log2(N) = 20 次操作来检查一个值是否存在,无论有多少列参与索引。对于二级索引,通常使用 B 树或散列 table(在网上搜索这些术语,了解它们如何工作的详细说明)。

this article的结论是错误的:

"... MySQL is unable to buffer enough data to guarantee a value is unique and is therefore caused to perform a tremendous amount of reading for each insert to guarantee uniqueness"

这是不正确的。检查唯一性实际上并不需要任何额外的工作,因为无论如何引擎都必须找到插入新记录的位置。导致性能下降的原因是 UUID 的使用。请记住,无论何时插入新记录,UUID 都是随机生成的。这意味着新记录需要插入磁盘上的随机物理位置,这会导致现有数据移动以容纳新记录。另一方面,如果索引列是单调递增的值(例如自动递增 INT),则新记录将始终插入到最后一条记录之后,这意味着不需要移动现有数据。

在您的情况下,情况 1 和情况 2 之间不会有任何性能差异。但是由于 UUID 的随机性,您仍然 运行 会遇到麻烦。如果您使用自动递增的值而不是 UUID 会好得多。此外,由于 UUID 在本质上始终是唯一的,因此使用 UNIQUE 约束对它们进行索引实际上没有多大意义。或者,如果您真的必须使用 UUID,请确保您的 table 上有一个基于自动递增 INT 的主键,以确保新记录永远不会随机插入磁盘。

前面的回答有一些瑕疵和误解;我不会指出它们,而是从头开始。

仅指 InnoDB...

一个INDEX(包括UNIQUE和PRIMARY KEY)就是一个BTree。 BTree 非常有效地根据 BTree 排序的键定位一行。 (它在按键顺序扫描时也很有效。)MySQL 中典型 BTree 的 "fan out" 大约为 100。因此,对于一百万行,BTree 大约有 3 层深(log100(百万));对于一万亿行,它只有两倍深(大约)。因此,即使没有缓存任何内容,也只需 3 次磁盘命中即可在百万行索引中找到一个特定行。

我在这里对 "index" 和 "table" 比较宽松,因为它们本质上是相同的(至少在 InnoDB 中)。两者都是 B 树。不同之处在于叶节点中的内容:table BTree 的叶节点具有所有列。 (我忽略了 InnoDB 中 TEXT/BLOB 的块外存储。)索引(主键除外)在叶节点中有主键的副本。这就是辅助键如何从 INDEX BTree 获取行的其余列,以及 InnoDB 如何不必存储 all 列的多个副本。

PRIMARY KEY 与数据 "clustered"。即一个 BTree既包含所有行的所有列,又按照PRIMARY KEY规范排序

通过PRIMARY KEY定位一条记录是一个 BTree搜索。通过 SECONDARY KEY 定位记录是 two BTree 搜索,其中一个位于辅助 INDEX 的 BTree 中,它为您提供 PRIMARY KEY;然后是第二个向下钻取 data/PK BTree。

PRIMARY KEY(UUID)... 由于 UUID 非常 随机,您插入的 "next" 行将位于 'random' 点.如果 table 比 buffer_pool 中缓存的大很多,那么新行需要进入的块很可能不会被缓存。这会导致磁盘命中以将块拉入缓存(缓冲池),并最终导致另一个磁盘命中将其写回磁盘。

由于 PRIMARY KEY 是 UNIQUE KEY,因此同时发生了其他事情(没有 SELECT COUNT(*) 等)。 UNIQUEness 在获取块之后和决定是否给出 "duplicate key" 错误或存储行之前进行检查。此外,如果块是 "full",则块需要 'split' 才能为新行腾出空间。

INDEX(UUID) 或 UNIQUE(UUID)... 该索引有一个 BTree。在 INSERT 上,一些 随机 定位的块将需要被提取、修改、可能拆分并写回磁盘,非常类似于上面的 PK 讨论。如果您有 UNIQUE(UUID),还会检查 UNIQUEness 并可能出现错误消息。无论哪种情况,现在 and/or 之后,磁盘 I/O.

AUTO_INCREMENT PK... 如果 PRIMARY KEY 是 auto_increment,则新记录将添加到数据 BTree 中的 'last' 块。当它变满时(每 100 条左右的记录)(逻辑上)有一个块拆分并将旧块刷新到磁盘。 (实际上,I/O 可能会延迟并在后台完成。)

PRIMARY KEY(id) + UNIQUE(UUID)... 两个 BTrees。在 INSERT 上,两者都有 activity。这可能 比简单的 PRIMARY KEY(UUID) 更糟糕。将上面的磁盘命中率相加,看看我的意思。

"Disk hits" 是巨大 table 的杀手,尤其是 UUID。 "Count the disk hits" 感受性能,尤其是在比较两种可能的技术时。

现在是你的秘诀... PRIMARY KEY(date, UUID)... 您允许相同的 UUID 在两个不同的日子出现。这可以帮助!回到 PK 如何工作并检查 UNIQUEness...插入记录时检查 "compound" 索引(日期,UUID)的 UNIQUEness。记录按日期+UUID 排序,所以今天的所有记录都聚集在一起。如果(这可能是一个很大的如果)一天的数据适合缓冲池(但整个 table 不适合),那么这就是每天早上发生的事情...... INSERTs 突然将新记录添加到"end" 的 table 因为新的 "date"。这些插入在新日期内随机发生。 buffer_pool 中的块被推出到磁盘以为新块腾出空间。但是,很好,您看到的是流畅、快速的 INSERT。这与您在 PRIMARY KEY(UUID) 中看到的情况不同,在检查 UNIQUEness 之前,许多行必须等待磁盘读取。今天的所有块都保留在缓存中,您不必等待 I/O.

但是,如果您变得如此之大以至于无法在缓冲池中容纳一天的数据,那么事情就会开始变慢,首先是在一天结束时,然后随着INSERT 增加。

顺便说一下,PARTITION BY RANGE(date) 和 PRIMARY KEY(uuid, date) 具有一些相似的特征。 (是的,我故意翻了PK栏。)