ChatGPT解决这个技术问题 Extra ChatGPT

INT 和 VARCHAR 主键之间是否存在真正的性能差异?

在 MySQL 中使用 INT 与 VARCHAR 作为主键之间是否存在可测量的性能差异?我想使用 VARCHAR 作为参考列表的主键(想想美国各州、国家/地区代码),并且同事不会将 INT AUTO_INCREMENT 作为所有表的主键。

我的论点,详细的 here,是 INT 和 VARCHAR 之间的性能差异可以忽略不计,因为每个 INT 外键引用都需要一个 JOIN 来理解引用,一个 VARCHAR 键将直接呈现信息。

那么,是否有人对这个特定的用例以及与之相关的性能问题有经验?

我发表了一篇回答“否”的帖子,其中包含一些我运行过的测试的详细信息……但那是 SQL Server,而不是 MySQL。所以我删除了我的答案。
@Timothy - 你不应该删除它。我正在投票。大多数 SQL 数据库服务器都有类似的查询计划器和类似的性能瓶颈。
@Timothy 请重新发布您的结果。
如此多的评论和答案都假设键可用于连接。他们不是。键用于数据一致性 - 避免重复行(多行表示同一实体)。任何列(或一组列)都可以在连接中使用,并且要保证连接是一对零或多个列,只需唯一。任何唯一索引都可以保证这一点,并且不需要有意义。

T
Tamlyn

您提出了一个很好的观点,您可以通过使用所谓的 natural key 而不是 surrogate key 来避免一些连接查询。只有您可以评估这样做的好处在您的应用程序中是否显着。

也就是说,您可以衡量应用程序中对速度最重要的查询,因为它们处理大量数据或执行非常频繁。如果这些查询从消除连接中受益,并且不会因使用 varchar 主键而受到影响,那么就这样做。

不要对数据库中的所有表使用任何一种策略。在某些情况下,自然键可能更好,但在其他情况下,代理键更好。

其他人提出了一个很好的观点,即在实践中自然键很少更改或重复,因此代理键通常是值得的。


有时,(恕我直言,经常),两者都更好,用于其他表中的 FK 引用和连接,以及确保数据一致性的自然键
@CharlesBretana 这很有趣。在 FK 旁边使用自然键来实现数据一致性是一种常见的做法吗?我的第一个想法是大型表所需的额外存储可能不值得。任何信息表示赞赏。仅供参考 - 我有不错的编程背景,但我的 SQL 经验主要限于 SELECT 查询
@CharlesBretana当我阅读“将它们都存储”时,我认为“冗余”和“未标准化”,这等于“这些东西可能会搞砸”和“如果有一个被改变,我必须确保两者都被改变”。如果您有冗余,则应该有一个很好的理由(例如完全不可接受的性能),因为冗余总是有可能使您的数据变得不一致。
@jpmc26,绝对不涉及冗余或规范化问题。代理键与自然键中的值没有有意义的联系,因此永远不需要更改它。至于规范化,你在说什么规范化问题?规范化适用于关系的有意义的属性;代理键的数值(实际上,代理键本身的概念)完全位于任何规范化的上下文之外。
并回答您的另一个问题,特别是关于州表的问题,如果您在该表上有一个代理键,其值例如 frpom 1 到 50,但您没有在州邮政编码上放置另一个唯一索引或键, (在我看来,州名也是如此),那么如何阻止某人输入具有不同代理键值但具有相同邮政编码和/或州名的两行?如果有两行带有“NJ”、“New Jersey”,客户端应用程序将如何处理它?自然键确保数据一致性!
J
Jan Żankowski

我对缺乏在线基准感到有点恼火,所以我自己进行了测试。

请注意,虽然我不会在常规基础上执行此操作,因此请检查我的设置和步骤以了解可能无意中影响结果的任何因素,并在评论中发表您的疑虑。

设置如下:

Intel® Core™ i7-7500U CPU @ 2.70GHz × 4

15.6 GiB RAM,我确保在测试期间有大约 8 GB 可用。

148.6 GB SSD 驱动器,有足够的可用空间。

Ubuntu 16.04 64 位

MySQL Ver 14.14 Distrib 5.7.20,适用于 Linux (x86_64)

表格:

create table jan_int (data1 varchar(255), data2 int(10), myindex tinyint(4)) ENGINE=InnoDB;
create table jan_int_index (data1 varchar(255), data2 int(10), myindex tinyint(4), INDEX (myindex)) ENGINE=InnoDB;
create table jan_char (data1 varchar(255), data2 int(10), myindex char(6)) ENGINE=InnoDB;
create table jan_char_index (data1 varchar(255), data2 int(10), myindex char(6), INDEX (myindex)) ENGINE=InnoDB;
create table jan_varchar (data1 varchar(255), data2 int(10), myindex varchar(63)) ENGINE=InnoDB;
create table jan_varchar_index (data1 varchar(255), data2 int(10), myindex varchar(63), INDEX (myindex)) ENGINE=InnoDB;

然后,我用一个 PHP 脚本在每个表中填充了 1000 万行,其本质是这样的:

$pdo = get_pdo();

$keys = [ 'alabam', 'massac', 'newyor', 'newham', 'delawa', 'califo', 'nevada', 'texas_', 'florid', 'ohio__' ];

for ($k = 0; $k < 10; $k++) {
    for ($j = 0; $j < 1000; $j++) {
        $val = '';
        for ($i = 0; $i < 1000; $i++) {
            $val .= '("' . generate_random_string() . '", ' . rand (0, 10000) . ', "' . ($keys[rand(0, 9)]) . '"),';
        }
        $val = rtrim($val, ',');
        $pdo->query('INSERT INTO jan_char VALUES ' . $val);
    }
    echo "\n" . ($k + 1) . ' millon(s) rows inserted.';
}

对于 int 表,位 ($keys[rand(0, 9)]) 仅替换为 rand(0, 9),对于 varchar 表,我使用了完整的美国州名,没有将它们剪切或扩展为 6 个字符。 generate_random_string() 生成一个 10 个字符的随机字符串。

然后我在 MySQL 中运行:

设置会话查询缓存类型=0;

对于 jan_int 表:SELECT count(*) FROM jan_int WHERE myindex = 5; SELECT BENCHMARK(1000000000, (SELECT count(*) FROM jan_int WHERE myindex = 5));

从 jan_int WHERE myindex = 5 中选择计数(*);

SELECT BENCHMARK(1000000000, (SELECT count(*) FROM jan_int WHERE myindex = 5));

对于其他表,与上面相同,myindex = 'califo' 用于 char 表,myindex = 'california' 用于 varchar 表。

每张表上 BENCHMARK 查询的次数:

jan_int:21.30 秒

jan_int_index:18.79 秒

jan_char:21.70 秒

jan_char_index:18.85 秒

jan_varchar:21.76 秒

jan_varchar_index:18.86 秒

关于桌子&索引大小,这是 show table status from janperformancetest; 的输出(有几列未显示):

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Name              | Engine | Version | Row_format | Rows    | Avg_row_length | Data_length | Max_data_length | Index_length | Data_free | Auto_increment | Collation              |
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| jan_int           | InnoDB |      10 | Dynamic    | 9739094 |             43 |   422510592 |               0 |            0 |   4194304 |           NULL | utf8mb4_unicode_520_ci |  
| jan_int_index     | InnoDB |      10 | Dynamic    | 9740329 |             43 |   420413440 |               0 |    132857856 |   7340032 |           NULL | utf8mb4_unicode_520_ci |   
| jan_char          | InnoDB |      10 | Dynamic    | 9726613 |             51 |   500170752 |               0 |            0 |   5242880 |           NULL | utf8mb4_unicode_520_ci |  
| jan_char_index    | InnoDB |      10 | Dynamic    | 9719059 |             52 |   513802240 |               0 |    202342400 |   5242880 |           NULL | utf8mb4_unicode_520_ci |  
| jan_varchar       | InnoDB |      10 | Dynamic    | 9722049 |             53 |   521142272 |               0 |            0 |   7340032 |           NULL | utf8mb4_unicode_520_ci |   
| jan_varchar_index | InnoDB |      10 | Dynamic    | 9738381 |             49 |   486539264 |               0 |    202375168 |   7340032 |           NULL | utf8mb4_unicode_520_ci | 
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

我的结论是这个特定用例没有性能差异。


令人惊讶的是,在一个已有 10 年历史的问题中,这只是两个答案之一,而不仅仅是猜测并且依赖于实际基准。
但是你的表没有主键,在 InnoDB 中它实际上是一个排序的数据结构。整数排序和字符串排序的速度应该不同。
@Melkor Fair 指出我使用 INDEX 而不是 PRIMARY KEY。我不记得我的推理了 - 我可能假设 PRIMARY KEY 只是一个具有唯一性约束的 INDEX。但是,阅读 federico-razzoli.com/primary-key-in-innodb 中关于事物如何存储在 InnoDB 中的部分,我认为我的结果仍然适用于主键,并回答了关于值查找性能差异的问题。此外,您的评论建议查看 sorting 算法的性能,这不适用于我调查的用例,即 查找 集合中的值。
查找操作还需要对主键字段进行比较(如二进制搜索),其中 int 应该比 varchar 快一点。但是正如您的实验所建议的那样,这并不是那么明显(或者可能是因为您没有主键所以查询都比较慢)。我认为插入和查找是一样的。
@Melkor 我相信没有区别,因为字符串被散列成整数。它使 B-Tree/Index 中的查找和存储/排序更快。我在 MySQL 的文档中找不到任何证据。
S
Steve McLeod

这与性能无关。这是关于什么是好的主键。随着时间的推移独特且不变。您可能认为像国家代码这样的实体永远不会随着时间而改变,并且会成为主键的良好候选者。但痛苦的经历是很少如此。

INT AUTO_INCREMENT 满足“随着时间的推移唯一且不变”的条件。因此偏好。


真的。我最大的数据库之一有南斯拉夫和苏联的条目。我很高兴它们不是主键。
@Steve,那么为什么 ANSI SQL 支持 ON UPDATE CASCADE 的语法?
不变性不是密钥的要求。无论如何,代理键有时也会改变。如果需要,更改密钥没有错。
保罗,所以您在数据库中将苏联更改为俄罗斯?并假装SU从不存在?现在所有对 SU 的引用都指向俄罗斯?
@alga 我出生在苏,所以我知道它是什么。
C
Charles Bretana

取决于长度。如果 varchar 为 20 个字符,而 int 为 4,那么如果使用 int,则索引在磁盘上每页索引空间的节点数将是 5 倍……这意味着遍历该索引将需要五分之一的物理和/或逻辑读取。

因此,如果性能是一个问题,如果有机会,请始终为您的表和引用这些表中行的外键使用一个完整的无意义键(称为代理项)......

同时,为了保证数据的一致性,每个重要的表也应该有一个有意义的非数字备用键,(或唯一索引),以确保不能插入重复的行(基于有意义的表属性重复)。

对于您正在谈论的特定用途(例如状态查找),这实际上并不重要,因为表的大小非常小。一般来说,少于几千行的表的索引对性能没有影响。 ..


当然?大多数数据格式不是基于行的吗?除了键之外还有其他数据。因子5不是乌托邦吗?
@manuelSchneid3r,什么?乌托邦?不,因素 5 不是“乌托邦”。它只是 20 除以 4。“基于行的数据格式”是什么意思?索引不是“基于行的”,它们是平衡的树结构。
@CharlesBretana 索引引用表中的单行。我不明白你关于额外唯一键的第二点。根据定义,主键是用于标识表中单行的唯一键。
@Sebi,我希望您不要认为“唯一”一词确实暗示它是唯一的关键。它只是意味着表中只有一行可以具有该值。您可以有多个唯一键...例如,交易的一个唯一键可能是日期、时间、产品、RegisterId 和美元金额,可能是 100 字节或更多。另一个可能只是登记收据或发票,比如 4-8 个字节。在查询、连接或过滤器中使用哪个会产生更好的性能?
T
Timothy Khouri

绝对不。

我已经在 INT、VARCHAR 和 CHAR 之间进行了数次……数次……性能检查。

无论我使用三个中的哪一个,具有 PRIMARY KEY(唯一和集群)的 1000 万个记录表都具有完全相同的速度和性能(以及子树成本)。

话虽这么说...使用最适合您的应用程序的任何东西。不用担心性能。


在不知道 varchars 有多长的情况下毫无意义...如果它们是 100 字节宽,那么保证您不会获得与 4 字节 int 相同的性能
了解您正在使用的数据库以及数据库的版本也将有所帮助。性能调整几乎总是在版本之间进行改进和改进。
VARCHAR 绝对对索引大小很重要。并且 index 决定了内存中可以容纳多少。内存中的索引比那些没有的索引要快得多。可能是对于您的 10m 行,您有 250MB 的内存可用于该索引,这很好。但是,如果您有 100m 行,那么您在该内存中的表现将不那么好。
J
Joel Coehoorn

对于短代码,可能没有区别。尤其如此,因为包含这些代码的表可能非常小(最多几千行)并且不会经常更改(我们最后一次添加新的美国州是什么时候)。

对于键之间变化较大的较大表,这可能很危险。例如,考虑使用用户表中的电子邮件地址/用户名。当您有几百万用户并且其中一些用户的姓名或电子邮件地址很长时会发生什么情况。现在,任何时候您需要使用该键加入此表,它变得更加昂贵。


你确定这会很贵吗?或者你只是在猜测?
当然,这取决于 rdbms 实现,但据我了解,大多数服务器都会保留实际值的哈希值以用于索引目的。即便如此,即使它是一个相对较短的散列(例如,10 字节),与 2 个 4 字节整数相比,比较 2 个 10 字节散列仍然需要更多的工作。
永远不要使用长(宽)键进行连接......但如果它是表中行唯一性的最佳表示,那么最好有一个唯一键(或索引 - 这是同一件事)使用这些自然值的表。没有加入的钥匙,你可以加入任何你想要的东西。密钥用于确保数据的一致性。
L
LeppyR64

至于主键,任何物理上使行唯一的东西都应该被确定为主键。

对于作为外键的引用,使用自动递增整数作为代理是一个好主意,主要有两个原因。 - 首先,通常在连接中产生的开销较少。 - 其次,如果您需要更新包含唯一 varchar 的表,则更新必须级联到所有子表并更新所有子表以及索引,而使用 int 代理,它只需要更新主表及其索引。

使用代理的缺点是您可能允许更改代理的含义:

ex.
id value
1 A
2 B
3 C

Update 3 to D
id value
1 A
2 B
3 D

Update 2 to C
id value
1 A
2 C
3 D

Update 3 to B
id value
1 A
2 C
3 B

这一切都取决于您在结构中真正需要担心什么以及最重要的是什么。


R
Rick James

代理 AUTO_INCREMENT 受伤的常见情况:

常见的模式模式是多对多映射:

CREATE TABLE map (
    id ... AUTO_INCREMENT,
    foo_id ...,
    bar_id ...,
    PRIMARY KEY(id),
    UNIQUE(foo_id, bar_id),
    INDEX(bar_id) );

这种模式的性能要好得多,尤其是在使用 InnoDB 时:

CREATE TABLE map (
    # No surrogate
    foo_id ...,
    bar_id ...,
    PRIMARY KEY(foo_id, bar_id),
    INDEX      (bar_id, foo_id) );

为什么?

InnoDB 二级键需要额外查找;通过将这对移动到 PK 中,可以避免一个方向。

二级索引是“覆盖”的,所以不需要额外的查找。

由于去掉了 id 和一个索引,因此该表更小。

另一个案例(国家):

country_id INT ...
-- versus
country_code CHAR(2) CHARACTER SET ascii

新手经常将 country_code 规范化为 4 字节 INT,而不是使用“自然”的 2 字节、几乎不变的 2 字节字符串。更快,更小,更少的 JOIN,更具可读性。


H
Herman J. Radtke III

在 HauteLook,我们更改了许多表格以使用自然键。我们确实体验到了现实世界的性能提升。正如您所提到的,我们的许多查询现在使用更少的连接,这使得查询的性能更高。如果有意义的话,我们甚至会使用复合主键。话虽如此,如果某些表具有代理键,则它们更易于使用。

此外,如果您让人们为您的数据库编写接口,代理键可能会有所帮助。第 3 方可以依赖代理键仅在极少数情况下才会更改的事实。


r
rodrigo-silveira

我面临同样的困境。我用 3 个事实表制作了一个 DW(星座模式),道路事故、事故中的车辆和事故中的伤亡。数据包括 1979 年至 2012 年在英国记录的所有事故,以及 60 个维度表。总共有大约 2000 万条记录。

事实表关系:

+----------+          +---------+
| Accident |>--------<| Vehicle |
+-----v----+ 1      * +----v----+
     1|                    |1
      |    +----------+    |
      +---<| Casualty |>---+
         * +----------+ *

RDMS:MySQL 5.6

事故索引本身是一个 varchar(数字和字母),有 15 位数字。我尽量不要有代理键,一旦事故索引永远不会改变。在 i7(8 核)计算机中,根据维度,在 1200 万条负载记录后,DW 变得太慢而无法查询。经过大量返工和添加 bigint 代理键后,我的速度性能平均提升了 20%。尚未获得低性能增益,但有效的尝试。我从事 MySQL 调优和集群工作。


听起来您需要研究分区。
V
Volksman

问题是关于 MySQL 的,所以我说有很大的不同。如果是关于 Oracle(将数字存储为字符串 - 是的,一开始我不敢相信),那么差别不大。

表中的存储不是问题,而是更新和引用索引。涉及基于其主键查找记录的查询很频繁 - 您希望它们尽可能快地发生,因为它们经常发生。

问题是 CPU 在硅中自然地处理 4 字节和 8 字节整数。比较两个整数真的很快 - 它发生在一两个时钟周期内。

现在看一个字符串——它由很多字符组成(现在每个字符超过一个字节)。比较两个字符串的优先级不能在一两个周期内完成。相反,必须迭代字符串的字符,直到找到差异。我确信在某些数据库中有一些技巧可以使其更快,但这在这里无关紧要,因为 int 比较是自然完成的,并且 CPU 在硅片中的速度快如闪电。

我的一般规则——每个主键都应该是一个自动递增的 INT,尤其是在使用 ORM(Hibernate、Datanucleus 等)的 OO 应用程序中,对象之间有很多关系——它们通常总是被实现为一个简单的 FK,并且能够快速解决这些问题的数据库对您的应用程序的响应能力很重要。


S
Shadi Alnamrouti

请允许我说是的,考虑到性能范围(开箱即用的定义),肯定存在差异:

1- 在应用程序中使用代理 int 更快,因为您不需要在代码或查询中使用 ToUpper()、ToLower()、ToUpperInvarient() 或 ToLowerInvarient(),这 4 个函数具有不同的性能基准。请参阅 Microsoft 性能规则。 (应用程序的性能)

2- 使用代理 int 保证不会随着时间的推移更改密钥。甚至国家代码也可能发生变化,请参阅 Wikipedia ISO 代码如何随时间变化。这将花费大量时间来更改子树的主键。 (数据维护性能)

3- ORM 解决方案似乎存在问题,例如当 PK/FK 不是 int 时的 NHibernate。 (开发者表现)


D
Dexygen

不确定性能影响,但至少在开发过程中,似乎一个可能的折衷方案是同时包含自动递增的整数“代理”键,以及您预期的、唯一的“自然”键。这将使您有机会评估性能以及其他可能的问题,包括自然键的可变性。


V
Vinod

像往常一样,没有笼统的答案。 '这取决于!'我不是在开玩笑。我对原始问题的理解是针对小表上的键 - 例如 Country(整数 id 或 char/varchar 代码)是地址/联系表等潜在巨大表的外键。

当您想要从数据库中返回数据时,这里有两种情况。首先是一种列表/搜索类型的查询,您想在其中列出所有带有州和国家代码或姓名的联系人(id 无济于事,因此需要查找)。另一个是获取主键的场景,它显示了需要显示州名和国家/地区名称的单个联系人记录。

对于后者,FK 基于什么可能并不重要,因为我们将针对单个记录或几条记录以及关键读取的表组合在一起。前一种(搜索或列表)场景可能会受到我们选择的影响。由于需要显示国家/地区(至少是一个可识别的代码,甚至搜索本身可能包含一个国家/地区代码),因此不必通过代理键加入另一个表(我在这里只是谨慎,因为我没有实际测试过这,但似乎很有可能)提高性能;尽管它确实有助于搜索。

由于代码很小——国家和州通常不超过 3 个字符,在这种情况下使用自然键作为外键可能是可以的。

另一种情况是键依赖于更长的 varchar 值,并且可能依赖于更大的表;代理键可能具有优势。