在 MySQL 中使用 INT 与 VARCHAR 作为主键之间是否存在可测量的性能差异?我想使用 VARCHAR 作为参考列表的主键(想想美国各州、国家/地区代码),并且同事不会将 INT AUTO_INCREMENT 作为所有表的主键。
我的论点,正如这里详述的那样,是 INT 和 VARCHAR 之间的性能差异可以忽略不计,因为每个 INT 外键引用都需要一个 JOIN 来理解引用,一个 VARCHAR 键将直接呈现信息。
那么,是否有人对这个特定的用例以及与之相关的性能问题有经验?
在 MySQL 中使用 INT 与 VARCHAR 作为主键之间是否存在可测量的性能差异?我想使用 VARCHAR 作为参考列表的主键(想想美国各州、国家/地区代码),并且同事不会将 INT AUTO_INCREMENT 作为所有表的主键。
我的论点,正如这里详述的那样,是 INT 和 VARCHAR 之间的性能差异可以忽略不计,因为每个 INT 外键引用都需要一个 JOIN 来理解引用,一个 VARCHAR 键将直接呈现信息。
那么,是否有人对这个特定的用例以及与之相关的性能问题有经验?
您提出了一个很好的观点,您可以通过使用所谓的自然键而不是代理键来避免一些连接查询。只有您可以评估这样做的好处在您的应用程序中是否显着。
也就是说,您可以衡量应用程序中对速度最重要的查询,因为它们处理大量数据或执行非常频繁。如果这些查询从消除连接中受益,并且不会因使用 varchar 主键而受到影响,那么就这样做。
不要对数据库中的所有表使用任何一种策略。在某些情况下,自然键可能更好,但在其他情况下,代理键更好。
其他人提出了一个很好的观点,即在实践中自然键很少发生变化或重复,因此代理键通常是值得的。
这与性能无关。这是关于什么是好的主键。随着时间的推移独特且不变。您可能认为像国家代码这样的实体永远不会随着时间而改变,并且会成为主键的良好候选者。但痛苦的经历是很少如此。
INT AUTO_INCREMENT 满足“随着时间的推移唯一且不变”的条件。因此偏好。
我对缺乏在线基准感到有点恼火,所以我自己进行了测试。
请注意,虽然我不会在常规基础上执行此操作,因此请检查我的设置和步骤以了解可能无意中影响结果的任何因素,并在评论中发表您的疑虑。
设置如下:
表格:
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)])
被替换为 just rand(0, 9)
,对于varchar
表格,我使用了完整的美国州名,没有将它们剪切或扩展为 6 个字符。generate_random_string()
生成一个 10 个字符的随机字符串。
然后我在 MySQL 中运行:
SET SESSION query_cache_type=0;
jan_int
表:
SELECT count(*) FROM jan_int WHERE myindex = 5;
SELECT BENCHMARK(1000000000, (SELECT count(*) FROM jan_int WHERE myindex = 5));
myindex = 'califo'
forchar
表和myindex = 'california'
forvarchar
表。BENCHMARK
每张表的查询次数:
关于表和索引大小,这是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 |
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
我的结论是这个特定用例没有性能差异。
取决于长度.. 如果 varchar 将是 20 个字符,而 int 是 4,那么如果使用 int,则索引将在磁盘上每页索引空间的节点数的五倍......这意味着遍历该索引将需要五分之一的物理和/或逻辑读取。
因此,如果性能是一个问题,如果有机会,请始终为您的表和引用这些表中行的外键使用一个完整的无意义键(称为代理项)......
同时,为了保证数据的一致性,每个重要的表也应该有一个有意义的非数字备用键,(或唯一索引),以确保不能插入重复的行(根据有意义的表属性重复)。
对于您正在谈论的特定用途(例如状态查找),这并不重要,因为表的大小非常小。一般来说,少于几千行的表的索引对性能没有影响。 ..
绝对不。
我已经在 INT、VARCHAR 和 CHAR 之间进行了数次……数次……性能检查。
无论我使用三个中的哪一个,具有 PRIMARY KEY(唯一和集群)的 1000 万个记录表都具有完全相同的速度和性能(以及子树成本)。
话虽这么说...使用最适合您的应用程序的任何东西。不用担心性能。
对于短代码,可能没有区别。尤其如此,因为包含这些代码的表可能非常小(最多几千行)并且不会经常更改(我们最后一次添加新的美国州是什么时候)。
对于键之间变化较大的较大表,这可能很危险。例如,考虑使用用户表中的电子邮件地址/用户名。当您有几百万用户并且其中一些用户的姓名或电子邮件地址很长时会发生什么情况。现在,任何时候您需要使用该键加入此表,它变得更加昂贵。
至于主键,任何物理上使行唯一的东西都应该被确定为主键。
对于作为外键的引用,使用自动递增整数作为代理是一个好主意,主要有两个原因。
- 首先,通常在连接中产生的开销较少。
- 其次,如果您需要更新包含唯一 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
这一切都取决于您在结构中真正需要担心什么以及最重要的是什么。
在 HauteLook,我们更改了许多表格以使用自然键。我们确实体验到了现实世界的性能提升。正如您所提到的,我们的许多查询现在使用更少的连接,这使得查询的性能更高。如果有意义的话,我们甚至会使用复合主键。话虽如此,如果某些表具有代理键,则它们更易于使用。
此外,如果您让人们为您的数据库编写接口,代理键可能会有所帮助。第 3 方可以依赖代理键仅在极少数情况下才会更改的事实。
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) );
为什么?
id
一个索引,该表更小。另一个案例(国家):
country_id INT ...
-- versus
country_code CHAR(2) CHARACTER SET ascii
新手经常将 country_code 规范化为 4 字节INT
,而不是使用“自然”的 2 字节、几乎不变的 2 字节字符串。更快,更小,更少的 JOIN,更具可读性。
我面临同样的困境。我用 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 调优和集群工作。
问题是关于 MySQL 的,所以我说有很大的不同。如果是关于 Oracle(将数字存储为字符串 - 是的,一开始我不敢相信),那么差别不大。
表中的存储不是问题,而是更新和引用索引。涉及基于其主键查找记录的查询很频繁 - 您希望它们尽可能快地发生,因为它们经常发生。
问题是 CPU 在硅中自然地处理 4 字节和 8 字节整数。比较两个整数真的很快 - 它发生在一两个时钟周期内。
现在看一个字符串——它由很多字符组成(现在每个字符超过一个字节)。比较两个字符串的优先级不能在一两个周期内完成。相反,必须迭代字符串的字符,直到找到差异。我确信在某些数据库中有一些技巧可以使其更快,但这在这里无关紧要,因为 int 比较是自然完成的,并且 CPU 在硅片中的速度快如闪电。
我的一般规则——每个主键都应该是一个自动递增的 INT,尤其是在使用 ORM(Hibernate、Datanucleus 等)的 OO 应用程序中,对象之间有很多关系——它们通常总是被实现为一个简单的 FK,并且能够快速解决这些问题的数据库对您的应用程序的响应能力很重要。
请允许我说是的,考虑到性能范围(开箱即用的定义),肯定存在差异:
1- 在应用程序中使用代理 int 更快,因为您不需要在代码或查询中使用 ToUpper()、ToLower()、ToUpperInvarient() 或 ToLowerInvarient(),这 4 个函数具有不同的性能基准。请参阅 Microsoft 性能规则。(应用程序的性能)
2- 使用代理 int 保证不会随着时间的推移更改密钥。甚至国家代码也可能发生变化,请参阅 Wikipedia ISO 代码如何随时间变化。这将花费大量时间来更改子树的主键。(数据维护性能)
3- ORM 解决方案似乎存在问题,例如当 PK/FK 不是 int 时的 NHibernate。(开发者表现)
不确定性能影响,但至少在开发过程中,这似乎是一种可能的折衷方案,即包括自动递增的整数“代理”键以及您预期的唯一“自然”键。这将使您有机会评估性能以及其他可能的问题,包括自然键的可变性。
像往常一样,没有笼统的答案。'这取决于!' 我不是在开玩笑。我对原始问题的理解是针对小表上的键 - 例如 Country(整数 id 或 char/varchar 代码)是地址/联系表等潜在巨大表的外键。
当您想要从数据库中返回数据时,这里有两种情况。首先是一种列表/搜索类型的查询,您想在其中列出所有带有州和国家代码或姓名的联系人(id 无济于事,因此需要查找)。另一个是获取主键的场景,它显示了需要显示州名和国家/地区名称的单个联系人记录。
对于后者,FK 基于什么可能并不重要,因为我们将针对单个记录或几条记录以及关键读取的表组合在一起。前一种(搜索或列表)场景可能会受到我们选择的影响。由于需要显示国家/地区(至少是一个可识别的代码,甚至搜索本身可能包含一个国家/地区代码),因此不必通过代理键加入另一个表(我在这里只是谨慎,因为我没有实际测试过这,但似乎很有可能)提高性能;尽管它确实有助于搜索。
由于代码很小——国家和州通常不超过 3 个字符,在这种情况下使用自然键作为外键可能是可以的。
另一种情况是键依赖于更长的 varchar 值,并且可能依赖于更大的表;代理键可能具有优势。