【问题标题】:Avoiding duplicate entries in an mySQL table with non unique columns避免具有非唯一列的 mySQL 表中的重复条目
【发布时间】:2014-01-25 13:58:09
【问题描述】:

我正在为私人网站开发 CMS 系统(主要是作为学习练习)。 Atm 我有三个表:一个用于文章,一个用于标签和一个连接表,以便每篇文章可以有多个标签。

我遇到问题的表由三列组成 -

article_tags: id (auto_increment), article_id, tag_id

我的问题源于一篇文章可以出现任意次数,标签也可以出现任意次数,但是两者的给定组合应该只出现一次 - 也就是说,每篇文章应该只有对任何单个标签的引用。目前可以在 id 不同的地方插入“重复”行,但 article_id 和 tag_id 的组合是相同的:

id , article_id, tag_id
1       1           1
2       1           2    
3       2           1    
4       1           1    <- this is wrong

我可以在 PHP 代码中检查包含此组合的记录,但如果可能的话,我更愿意在 sql 中执行此操作(如果不是,或者不合需要,那么我将使用 PHP 执行此操作)。由于 id 不同并且无法设置唯一列,因此 INSERT IGNORE 和 ON DUPLICATE 等操作不起作用。

我对 mySQL 很陌生,所以如果我在做一些愚蠢的事情,请指出正确的方向。

谢谢

【问题讨论】:

    标签: php mysql sql database-schema


    【解决方案1】:

    您应该检查您的表定义。

    你可以(从最好到最坏):

    1. 在(article_id 和 tag_id)上添加复合主键并移除 auto_increment(之前的主键)
    2. 在(article_id 和 tag_id)上添加索引(唯一)并保留您的 auto_increment 主键
    3. 在 php 中选择 distinct:SELECT DISTINCT(article_id, tag_id) FROM ... 而不更改表格中的任何内容

    现在,您的表定义如下:

    CREATE TABLE IF NOT EXISTS `article_tags` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `article_id` int(11) NOT NULL,
      `tag_id` int(11) NOT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
    

    最好的解决方案(选项 1)是删除您当前的(auto_increment)主键并在 article_id 和 tag_id 列上添加一个主键(复合):

    CREATE TABLE IF NOT EXISTS `article_tags` (
      `article_id` int(11) NOT NULL,
      `tag_id` int(11) NOT NULL,
      PRIMARY KEY (`article_id`,`tag_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
    

    但是(选项 2)如果您绝对想保留您的 auto_increment 主键,请在您的列上添加一个索引(唯一):

    CREATE TABLE IF NOT EXISTS `article_tags` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `article_id` int(11) NOT NULL,
      `tag_id` int(11) NOT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `article_id` (`article_id`,`tag_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
    

    无论如何,如果你不想改变你的表定义,你总是可以在你的 php 查询中使用 DISTINCT:

    SELECT DISTINCT(article_id, tag_id) FROM article_tags
    

    【讨论】:

    • 非常简洁的答案。如果我理解正确,#3 是最糟糕的,因为它增加了 SELECT 查询的开销?并且 #2 更糟糕,因为它基本上与 #1 相同,但有一个额外的(不必要的?)列,形式为旧的 auto_increment?
    • 虽然更详细!很抱歉造成混乱,但我更感兴趣的是你为什么以你的方式订购它们,而不是如何实施它们
    • 嗯,当您在两个表之间存在多对多关系并且想要加入它们时,第一个选项(复合主键)是最佳解决方案。第二个选项几乎相同,只是您有一个主键 (auto_increment) 和 2 列的唯一约束。在你的情况下没用,你应该选择第一个选项。第三个到目前为止还不好,即使它有效,但你的表中会有不连贯的数据(重复的行)
    • 感谢您花时间解释您的回答。重复的行正是我想要避免的!
    【解决方案2】:

    这种多对多关系表,有时称为连接表,通常只有两列,并且有一个由两者组合而成的主键。

      article_id
      tag_id
      pk = (article_id, tag_id)
    

    如果您更改该表的定义,您将彻底解决该问题。

    您应该如何对复合键中的列进行排序?这取决于您的应用程序将如何在连接表中查找项目。如果您总是从 article_id 开始并查找 tag_id,那么您将 article_id 放在键中的第一个位置。 DBMS 可以随机访问键中第一列的值,但必须扫描索引以查找键中第二(或后续)列中的值。

    您可能希望在表上创建第二个索引(tag_id, article_id)。这将允许基于 tag_id 的快速查找。您可能会问,“为什么要费心将两列都放在索引中?”也就是将索引变成一个覆盖索引。在一个覆盖索引中,可以直接从索引中获取想要的值。例如,使用覆盖索引,

     SELECT article_id FROM article_tag WHERE tag_id = 12345
    

    (或使用类似查找逻辑的 JOIN)只需要访问磁盘驱动器上的索引即可获得结果。如果没有覆盖索引,查询需要从索引跳转到数据表,这是一个额外的步骤。

    联接表通常有非常短的行(几个整数),因此几个覆盖索引(主键和额外索引)的重复数据不会占用大量磁盘空间。

    【讨论】:

    • 这正是我一直在寻找的优雅解决方案。复合键不是我在我的 - 诚然有限的 - 经验中遇到的东西。综合指数的顺序有没有明显的区别,还是没关系?
    • 哇,这比我想象的要复杂得多。非常有意思。感谢您的澄清
    • @ProFishChris ... 果然,像 RDBMS 一样可扩展的数据系统通常在表面下发生了很多事情。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-06-17
    • 2018-08-29
    • 1970-01-01
    • 1970-01-01
    • 2013-01-14
    相关资源
    最近更新 更多