INT和VARCHAR主键之间在真实性能上有区别吗?


173

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

我的论点,详见这里,是INT和VARCHAR之间的性能差异可以忽略不计,因为每个INT外键引用将需要JOIN的参考意义,一个VARCHAR键则直接呈现的信息。

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


3
我在帖子中回答“否”,并详细介绍了我已经运行过的测试……但这是SQL Server,而不是MySQL。所以我删除了答案。
蒂莫西·库里

17
@Timothy-您不应该删除它。我正在投票。大多数SQL数据库服务器具有类似的查询计划程序和类似的性能瓶颈。
Paul Tomblin

9
@Timothy,请重新发布您的结果。
杰克·麦格劳

2
如此多的注释和答案都假定键将用于连接。他们不是。密钥用于保证数据的一致性-避免重复行(多行代表同一实体)。联接中可以使用任何列(或一组列),并且要确保联接是一对零或多个,只需要唯一的列即可。任何唯一索引都可以保证这一点,并且它不一定有意义。
Charles Bretana

Answers:


78

您提出了一个很好的观点,即可以通过使用自然键而不是代理键来避免一些联接查询。只有您可以评估这样做的好处在您的应用程序中是否很重要。

也就是说,您可以测量应用程序中最重要的查询,因为它们要处理大量数据或执行得非常频繁,因此它们对于提高查询速度至关重要。如果这些查询从消除联接中受益,并且不因使用varchar主键而受苦,则可以这样做。

不要对数据库中的所有表都使用这两种策略。在某些情况下,自然键可能会更好,但在其他情况下,替代键会更好。

其他人指出,在实践中,很少有自然密钥永远不会更改或重复,因此代理密钥通常是值得的。


3
有时,(恕我直言,经常)两者都更好,这是替代其他表中的FK引用和Joins的代理,以及确保数据一致性的自然键
Charles Bretana

@CharlesBretana很有意思。在FK旁边使用自然键来确保数据一致性是一种常见做法吗?我首先想到的是,大表上所需的额外存储空间可能不值得。任何信息表示赞赏。仅供参考-我有一个体面的编程背景,但我的SQL经验大多局限于SELECT查询
罗布

2
@CharlesBretana当我阅读“将它们都存储”时,我认为“冗余”和“未规范化”,分别表示“这东西可能搞砸了”和“如果必须更改,则必须确保两者都被更改”。如果您有冗余,那么应该有一个很好的理由(例如完全无法接受的性能),因为冗余总是有可能使您的数据变得不一致。
jpmc26 2013年

3
@ jpmc26,绝对没有冗余或规范化问题。代理键与自然键中的值没有任何有意义的联系,因此永远不需要更改。关于规范化,您在谈论什么规范化问题?规范化适用于关系的有意义的属性;代理密钥的数字值(实际上是代理密钥本身的概念)完全在任何规范化上下文之外。
查尔斯·布雷塔纳

1
并回答另一个问题,特别是关于状态表的问题,如果此表上有一个替代键,其值从frpom 1到50,但是没有在状态邮政编码上放置另一个唯一索引或键, (在我看来,也包括州名),那么该如何阻止某人输入具有不同代理键值但具有相同邮政编码和/或州名的两行呢?如果有两行带有“ NJ”,“ New Jersey”的行,客户端应用程序将如何处理?自然键可确保数据一致性!
Charles Bretana

80

这与性能无关。这是什么使一个好的主键。随着时间的流逝,保持不变。您可能会认为诸如国家/地区代码之类的实体不会随时间变化,并且将是主键的理想选择。但是痛苦的经历很少如此。

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


24
真正。我最大的数据库之一是南斯拉夫和苏联的条目。我很高兴他们不是主键。
Paul Tomblin

8
@Steve,那么为什么ANSI SQL支持ON UPDATE CASCADE的语法?
比尔·卡文

5
不变性不是关键的要求。无论如何,代理键有时也会更改。如果需要,更改键没什么问题。
nvogel

9
保罗,所以您在数据库中将苏联更改为俄罗斯?并假装SU永远不存在?现在所有关于SU的参考都指向俄罗斯?
Dainius 2012年

6
@alga我出生于SU,所以我知道它是什么。
Dainius

51

网上缺乏基准测试使我有些恼火,所以我自己进行了测试。

请注意,尽管我不会定期进行此操作,所以请检查我的设置和步骤以了解可能无意影响结果的任何因素,并在评论中发表您的疑虑。

设置如下:

  • 英特尔®酷睿™i7-7500U CPU @ 2.70GHz×4
  • 15.6 GiB RAM,我确保其中有8 GB可用空间。
  • 148.6 GB SSD驱动器,具有足够的可用空间。
  • Ubuntu 16.04 64位
  • 适用于Linux(x86_64)的MySQL Ver 14.14 Distrib 5.7.20

表格:

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'用于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 | 
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

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


我知道现在已经晚了,但是如果您为where条件选择了一个不太理想的字符串,我很想知道结果。“ califo [rnia]”是理想的,因为它可以在比较第一个字符后丢弃不匹配项,只需要进一步检查实际匹配项即可;像“ newham”之类的东西会给出更有趣的结果,因为比较更多字符以消除所有不匹配现象将是新的。同样,以这种方式限制整数也会堆积针对它们的几率,我会给它们至少 26个值。
Uueerdo '18

14
令人惊讶的是,在一个已有10年历史的问题中,这仅是两个答案之一,不仅是猜测,而且还取决于实际基准。
阿德里安·贝克

1
但是您的表没有主键,实际上在InnoDB中它是一种排序的数据结构。整数排序和字符串排序之间的速度应有所不同。
梅尔科尔

1
@Melkor我INDEX代替的公平点PRIMARY KEY。我不记得我的推理-我可能认为PRIMARY KEY这只是INDEX具有唯一性约束的。但是,在federico-razzoli.com/primary-key-in-innodb中阅读有关如何在InnoDB中存储内容的部分,我认为我的结果仍然适用于主键,并回答了有关值查找性能差异的问题。另外,您的评论还建议您查看排序算法的性能,这不适用于我研究的用例,而是在一个集合中查找值。
JanŻankowski19年

查找操作还需要在主键字段上进行比较(例如二进制搜索),其中int应该比varchar快一点。但是正如您的实验所建议的那样,它并不是那么明显(或者也许是因为您没有主键,所以查询速度都比较慢)。我认为插入和查找是同一回事。
Melkor

38

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

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

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

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


当然?难道不是大多数基于行的数据格式?除了键以外,还有其他数据。因子5是否为utopic?
ManuelSchneid3r

1
@ manuelSchneid3r,什么?乌托邦?不,因子5不是“ utopic”。它只有20除以4。“基于数据格式行”是什么意思?索引不是“基于行”的,它们是平衡的树结构。
Charles Bretana

36

绝对不。

我已经做了几个...几个...在INT,VARCHAR和CHAR之间进行性能检查。

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

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


42
如果不知道varchar的长度,则毫无意义...如果它们的宽度为100字节,那么保证您不会获得与4字节整数相同的性能
Charles Bretana

6
这也有助于了解您正在使用哪个数据库以及该数据库的版本。性能调整几乎总是在进行,并且在不同版本之间进行了改进。
Dave Black

VARCHAR 对于索引大小绝对重要。索引确定可以容纳多少内存。内存中的索引远比不存在的索引快得多。可能是对于您的1000万行,该索引有250MB的可用内存,这很好。但是,如果您有1亿行,那么在该内存中的内存就不会那么好了。
保罗·德雷珀

9

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

对于键之间变化较大的较大表,这可能很危险。例如,考虑使用“用户”表中的电子邮件地址/用户名。当您拥有数百万个用户并且其中一些用户具有长名或电子邮件地址时,会发生什么。现在,任何时候您需要使用该键加入该表时,它的成本就会大大提高。


2
您确定这会很贵吗?还是您只是在猜测?
史蒂夫·麦克劳德

当然,这取决于rdbms的实现,但是据我了解,大多数服务器将保留实际值的哈希值以用于建立索引。即使这样,即使它是一个相对较短的哈希(例如10个字节),比较2个10个字节的哈希也要比2个4个字节的int进行更多的工作。
Joel Coehoorn

永远不要使用长(宽)键进行联接...但是,如果这是表中各行唯一性的最佳表示,则最好在表中有唯一键(或索引-相同)。使用这些自然值的表格。没有用于联接的键,您可以随心所欲地联接。那里有密钥来确保数据一致性。
查尔斯·布雷塔纳

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

这完全取决于您真正需要担心的结构以及最重要的含义。


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

为什么?

  • InnoDB辅助键需要额外的查找;通过将线对移入PK,可以避免一个方向。
  • 次要索引是“ covering”,因此它不需要额外的查找。
  • 由于删除了id一个索引,因此该表较小。

另一个案例(国家):

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

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


2

在HauteLook,我们更改了许多表以使用自然键。我们确实体验了真实的性能提升。如您所述,我们的许多查询现在使用较少的联接,这使查询的性能更高。如果合理的话,我们甚至将使用复合主键。话虽如此,如果某些表具有代理键,则更易于使用。

另外,如果您要让人们为您的数据库编写接口,则代理键可能会有所帮助。第三方可以依靠以下事实:代理密钥仅在非常罕见的情况下才会更改。


2

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

事实表关系:

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

RDMS:MySQL 5.6

本地的“事故索引”是一个带有15位数字的varchar(数字和字母)。一旦事故索引永远不会改变,我就尝试没有代理键。在i7(8核)计算机上,DW在记录了1200万条负载记录(取决于尺寸)后变得太慢而无法查询。经过大量的重新工作并添加了bigint代理键,我的速度性能平均提高了20%。尚未获得较低的性能增益,但可以尝试。我正在从事MySQL调整和群集工作。


1
听起来您需要查看分区。
jcoffland 2014年

2

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

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

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

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

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


0

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


0

像往常一样,没有一揽子答案。'这取决于!' 我也没戏 我对原始问题的理解是针对小型表上的键-例如Country(整数ID或char / varchar代码)是潜在大型表(如地址/联系表)的外键。

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

对于后者,FK的基础可能无关紧要,因为我们将针对单个记录或一些记录以及键读取的表放在一起。前一种(搜索或列表)方案可能会受到我们选择的影响。由于要求显示国家(至少是可识别的代码,甚至搜索本身都包括国家代码),因此不必通过代理键加入另一个表(我在这里要谨慎,因为我尚未实际测试过)这样,但似乎很有可能)提高性能;尽管它确实有助于搜索。

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

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


0

考虑到性能范围(开箱即用的定义),请允许我说肯定有区别:

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

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.