使用INT与VARCHAR作为MySQL中的主键之间是否存在可衡量的性能差异?我想使用VARCHAR作为参考列表的主键(想想美国,国家代码),并且同事不会将INT AUTO_INCREMENT作为所有表的主键。
我的论点,如详细here,是INT和VARCHAR之间的性能差异可以忽略不计,因为每个INT外键引用都需要JOIN来理解引用,VARCHAR键将直接呈现信息。
那么,是否有人有过这个特定用例的经验以及与之相关的性能问题?
答案 0 :(得分:75)
这不是关于表现。这是关于什么是一个好主键。随着时间的推移,独特而不变。您可能认为诸如国家/地区代码之类的实体从未随时间而变化,并且是主键的良好候选者。但痛苦的经历是很少见的。
INT AUTO_INCREMENT符合“独特且不变的时间”条件。因此偏好。
答案 1 :(得分:69)
通过使用所谓的natural key代替surrogate key,您可以避免一些已加入的查询。只有您可以评估其在您的应用中的好处是否重要。
也就是说,您可以在应用程序中测量最快速最重要的查询,因为它们可以处理大量数据,也可以非常频繁地执行。如果这些查询从消除连接中受益,并且不使用varchar主键,那么就这样做。
不要对数据库中的所有表使用任一策略。在某些情况下,自然键可能更好,但在其他情况下,代理键更好。
其他人提出一个很好的观点,即在实践中很少有自然键永远不会改变或有重复,所以代理键通常是值得的。
答案 2 :(得分:33)
取决于长度..如果varchar将是20个字符,并且int是4,那么如果使用int,则索引将在磁盘上每页索引空间的节点数为FIVE ...意味着遍历索引将需要五分之一的物理和/或逻辑读取..
因此,如果性能是一个问题,给定机会,总是为表使用一个完整的无意义键(称为代理),以及引用这些表中的行的外键...
同时,为了保证数据的一致性,每个重要的表格 都有一个有意义的非数字备用密钥, (或唯一索引)以确保无法插入重复行(基于有意义的表属性重复)。
对于您正在讨论的特定用途(如状态查找),它确实无关紧要,因为表的大小非常小。通常,对少于几个表的索引的性能没有影响千行...
答案 3 :(得分:31)
绝对不是。
我在INT,VARCHAR和CHAR之间进行了几次......几次......性能检查。
带有PRIMARY KEY(唯一和群集)的1000万记录表具有完全相同的速度和性能(以及子树成本),无论我使用哪三个。据说......使用最适合您应用的东西。不要担心性能。
答案 4 :(得分:23)
我对这个在线缺乏基准测试感到有些恼火,所以我自己进行了测试。
请注意,我不会在常规基础上执行此操作,因此请检查我的设置和步骤,了解可能会无意中影响结果的任何因素,并在评论中发布您的问题。 < / p>
设置如下:
表格:
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;
然后,我在每个表中填充了1000万行,其中PHP脚本的本质如下:
$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:
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'
表为char
,myindex = 'california'
表为varchar
。每个表上BENCHMARK
查询的时间:
关于表&amp;索引大小,这是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 |
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
我的结论是,这个特定用例没有性能差异。
答案 5 :(得分:9)
对于短代码,可能没什么区别。这一点尤其如此,因为持有这些代码的表可能非常小(最多几千行)并且不经常更改(我们最后一次添加新的美国州时)。
对于密钥中变化较大的较大表,这可能很危险。例如,考虑使用User表中的电子邮件地址/用户名。当您拥有数百万用户且其中一些用户拥有长名称或电子邮件地址时会发生什么。现在,只要您需要使用该密钥加入此表,它就会变得更加昂贵。
答案 6 :(得分:6)
对于主键,无论物理上使行唯一,都应确定为主键。
作为外键的引用,使用自动递增整数作为代理是一个好主意,主要有两个原因。
- 首先,通常在加入过程中产生的开销较少
- 其次,如果需要更新包含唯一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
这一切都取决于你真正需要担心的结构和最重要的意义。
答案 7 :(得分:2)
在HauteLook,我们改变了许多桌子以使用自然键。我们确实经历了实际的性能提升。正如您所提到的,我们的许多查询现在使用较少的连接,这使查询更具性能。如果有意义,我们甚至会使用复合主键。话虽这么说,如果有一个代表键,一些表格更容易使用。
此外,如果您让人们为您的数据库编写接口,代理键可能会有所帮助。第三方可以依赖于代理密钥仅在非常罕见的情况下才会改变的事实。
答案 8 :(得分:2)
代理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,更易读。
答案 9 :(得分:2)
问题是关于MySQL所以我说有一个显着的区别。如果它是关于Oracle(将数字存储为字符串 - 是的,我一开始并不相信)那么差别不大。
表中的存储不是问题,但更新和引用索引是。查询涉及根据主键查找记录的查询很频繁 - 您希望它们尽可能快地发生,因为它们经常发生。
事情是CPU在硅中自然处理4字节和8字节整数。它比较两个整数真的很快 - 它发生在一个或两个时钟周期。
现在看一个字符串 - 它由很多字符组成(这些天每个字符超过一个字节)。比较两个字符串的优先级不能在一个或两个周期内完成。相反,字符串&#39;必须迭代字符,直到找到差异。我确信有些技巧可以让它在某些数据库中变得更快,但这与此无关,因为int比较是由CPU自然而然地在硅片中快速完成的。
我的一般规则 - 每个主键应该是一个自动增量INT,特别是在使用ORM(Hibernate,Datanucleus,无论如何)的OO应用程序中,对象之间有很多关系 - 它们通常总是被实现作为一个简单的FK和DB快速解决这些问题的能力对您的应用程序非常重要。 的反应能力。
答案 10 :(得分:1)
我面临同样的困境。我做了一个DW(星座图),有3个事实表,道路交通事故,事故车辆和事故伤亡人员。数据包括1979年至2012年在英国记录的所有事故,以及60个维度表。总共约有2000万条记录。
+----------+ +---------+
| Accident |>--------<| Vehicle |
+-----v----+ 1 * +----v----+
1| |1
| +----------+ |
+---<| Casualty |>---+
* +----------+ *
RDMS:MySQL 5.6
本机事故索引是一个varchar(数字和字母),有15位数字。一旦事故索引永远不会改变,我尽量不要使用代理键。 在i7(8核)计算机中,根据尺寸,DW在1200万次负载记录后变得太慢而无法查询。 经过大量的重新工作并添加bigint代理键后,我的平均速度提升了20%。 然而,低性能增益,但有效的尝试。我正在从事MySQL调优和集群工作。
答案 11 :(得分:0)
不确定性能影响,但似乎可能的妥协,至少在开发过程中,可能包括自动递增的整数“代理”键,以及您想要的,独特的“自然”键。这将使您有机会评估性能以及其他可能的问题,包括自然键的可变性。
答案 12 :(得分:0)
像往常一样,没有一揽子答案。 &#39;这取决于!&#39;而且我不是很滑稽。我对原始问题的理解是针对小型表上的键 - 比如Country(整数id或char / varchar代码)是一个可能很大的表(如地址/联系表)的外键。
当您希望从数据库返回数据时,此处有两种情况。首先是列表/搜索类型的查询,您要列出所有具有州和国家/地区代码或名称的联系人(ID不会有帮助,因此需要查找)。另一个是主键上的get方案,它显示了一个联系人记录,其中需要显示州名,国家/地区。
对于后者来说,FK基于什么并不重要,因为我们将表格汇集到一个记录或几个记录和关键读取。前者(搜索或列表)方案可能会受到我们的选择的影响。由于需要显示国家/地区(至少是一个可识别的代码,甚至可能搜索本身包含国家代码),因此不必通过代理键加入另一个表可能(我在这里只是谨慎,因为我实际上没有测试过这,但似乎极有可能)提高性能;尽管它确实有助于搜索。
由于代码很小 - 通常对于国家和州不超过3个字符,在这种情况下可以使用自然键作为外键。
另一种情况,其中键依赖于较长的varchar值,也可能依赖于较大的表;代理键可能具有优势。
答案 13 :(得分:0)
请允许我说是的,确实存在差异,考虑到性能范围(开箱即用的定义):
1-使用surrogate int在应用程序中更快,因为您不需要在代码或查询中使用ToUpper(),ToLower(),ToUpperInvarient()或ToLowerInvarient(),这4个函数具有不同的性能基准。请参阅Microsoft性能规则。 (申请表现)
2-使用surrogate int保证不随时间改变密钥。即使国家/地区代码可能会发生变化,请参阅维基百科,ISO代码如这将花费大量时间来更改子树的主键。 (数据维护的表现)
3-似乎ORM解决方案存在问题,例如当PK / FK不是int时NHibernate。 (开发人员表现)