【问题标题】:Is there a REAL performance difference between INT and VARCHAR primary keys?INT 和 VARCHAR 主键之间是否存在真正的性能差异?
【发布时间】:2010-09-24 20:12:18
【问题描述】:

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

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

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

【问题讨论】:

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

标签: mysql performance primary-key innodb myisam


【解决方案1】:

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

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

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

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

【讨论】:

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

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

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

【讨论】:

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

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

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

设置如下:

  • 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 中运行:

  • 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个值。
  • 令人惊讶的是,在一个已有 10 年历史的问题中,这只是两个答案之一,而不仅仅是猜测,而是依赖于实际基准。
  • 但是你的表没有主键,在 InnoDB 中它实际上是一个排序的数据结构。整数排序和字符串排序的速度应该是不一样的。
  • @Melkor 公平点我使用INDEX 而不是PRIMARY KEY。我不记得我的推理了——我可能认为PRIMARY KEY 只是一个具有唯一性约束的INDEX。但是,阅读federico-razzoli.com/primary-key-in-innodb 中关于事物如何存储在 InnoDB 中的部分,我认为我的结果仍然适用于主键,并回答了关于值查找性能差异的问题。此外,您的评论建议查看 排序 算法的性能,这不适用于我调查的用例,即 查找 集合中的值。
  • 查找操作还需要对主键字段进行比较(如二分查找),其中 int 应该比 varchar 快一点。但是正如您的实验所建议的那样,这并不是那么明显(或者可能是因为您没有主键,所以查询都比较慢)。我认为插入和查找是一回事。
【解决方案4】:

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

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

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

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

【讨论】:

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

绝对不是。

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

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

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

【讨论】:

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

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

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

【讨论】:

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

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

对于作为外键的引用,使用自动递增整数作为代理是一个好主意,主要有两个原因。
- 首先,通常在连接中产生的开销较少。
- 其次,如果您需要更新包含唯一 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

这完全取决于您在结构中真正需要担心的问题以及最重要的是什么。

【讨论】:

    【解决方案8】:

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

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

    【讨论】:

      【解决方案9】:

      代理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、更易读。

      【讨论】:

        【解决方案10】:

        我也面临同样的困境。我用 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 调优和集群工作。

        【讨论】:

        • 听起来你需要考虑分区。
        【解决方案11】:

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

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

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

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

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

        【讨论】:

          【解决方案12】:

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

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

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

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

          【讨论】:

            【解决方案13】:

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

            【讨论】:

              【解决方案14】:

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

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

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

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

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

              【讨论】:

                猜你喜欢
                • 2011-06-05
                • 1970-01-01
                • 2019-08-09
                • 1970-01-01
                • 2020-04-20
                • 2012-03-05
                • 2014-05-17
                • 2012-08-21
                相关资源
                最近更新 更多