MySQL 性能:两个表在公共外键上的连接:仍然“使用 where”

Posted

技术标签:

【中文标题】MySQL 性能:两个表在公共外键上的连接:仍然“使用 where”【英文标题】:MySQL performance: join of two tables on common foreign key: still "using where" 【发布时间】:2015-11-24 11:37:34 【问题描述】:

我有以下表格:test_usertest_tagproductproductinfoproductinfo 有一个指向 product 的外键。

此外,我还有一些表格,这些表格模拟了之前表格之间的一些关系:test_usertagging(FK 到 test_tagtest_user)、productinfo_tags(FK 到 test_tagtest_productinfo)。

(下面我附上更完整的描述,但这是主要思想)。

现在我有一个产品 ID 和用户 ID 的列表,我想找到它们之间的共同标签,以及来自 test_usertagging 的附加 extra_info 列:

mysql> explain SELECT `test_usertagging`.`user_id`, `test_usertagging`.`tag_id`, `test_usertagging`.`extra_info`
       FROM `productinfo`
       INNER JOIN `productinfo_tags` ON ( `productinfo_tags`.`productinfo_id` = `productinfo`.`id` )
       INNER JOIN `test_tag` ON ( `test_tag`.`id` = `productinfo_tags`.`tag_id` )
       INNER JOIN `test_usertagging` ON ( `test_usertagging`.`tag_id` = `test_tag`.`id` )
    WHERE
       (`test_usertagging`.`user_id` IN (1,2,3,4 ... ) AND
        `productinfo`.`product_id` IN ('abc', 'def', '000', '111', ...));

格式化查询:

SELECT test_usertagging.user_id,
       test_usertagging.tag_id,
       test_usertagging.extra_info
  FROM productinfo
  JOIN productinfo_tags ON productinfo_tags.productinfo_id = productinfo.id
  JOIN test_tag ON test_tag.id = productinfo_tags.tag_id
  JOIN test_usertagging ON test_usertagging.tag_id = test_tag.id
 WHERE test_usertagging.user_id IN (1,2,3,4 ... )
   AND productinfo.product_id IN ('abc', 'def', '000', '111', ...)

我得到的是:

+----+-------------+------------------+--------+------------------------------------------------------------------+------------------------------+---------+--------------------------------------+------+---    -----------------------+
| id | select_type | table            | type   | possible_keys                                                    | key                          | key_len | ref                                  | rows |     Extra                    |
+----+-------------+------------------+--------+------------------------------------------------------------------+------------------------------+---------+--------------------------------------+------+---    -----------------------+
|  1 | SIMPLE      | productinfo      | range  | PRIMARY,productinfo_218f3960                                       | productinfo_218f3960      | 62      | NULL                                |   55 | Using     where; Using index |
|  1 | SIMPLE      | productinfo_tags | ref    | productinfo_id,productinfo_tags_4b5946a2,productinfo_tags_5659cca2 | productinfo_id            | 4       | tookyo_prod.productinfo.id          |    1 | Using     index              |
|  1 | SIMPLE      | test_tag         | eq_ref | PRIMARY                                                            | PRIMARY                   | 4       | tookyo_prod.productinfo_tags.tag_id |    1 | Using     index              |
|  1 | SIMPLE      | test_usertagging | ref    | test_usertagging_5659cca2                                          | test_usertagging_5659cca2 | 4       | tookyo_prod.productinfo_tags.tag_id |  217 | Using where              |
+----+-------------+---------------------+--------+------------------------------------------------------------------+------------------------------+---------+--------------------------------------+------+--------------------------+

这个查询执行得很糟糕。困扰我的是最后一行(test_usertagging)的“使用位置”(无索引) - 正确的索引列在key 下,但它仍然显示“使用位置”。

我尝试添加FORCE INDEX,但这并没有改善问题(因为无论如何都列出了正确的索引)。

使用STRAIGHT JOIN 只会更改最后两行之间的顺序。 (请注意,test_tag 表本身对于此查询是多余的;完全删除它不会改变任何东西)。

知道如何从test_usertagging 使用相关可用索引(user_idtag_id)中的一个或两个进行查询吗?

这是连接表的SHOW CREATE TABLE 的输出:

CREATE TABLE `test_usertagging` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `tag_id` int(11) NOT NULL,
  `user_id` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `usertagging_user_per_tag_is_unique` (`user_id`,`tag_id`),
  KEY `test_usertagging_5659cca2` (`tag_id`),
  CONSTRAINT `test_usertagging_ibfk_3` FOREIGN KEY (`tag_id`) REFERENCES `test_tag` (`id`),
  CONSTRAINT `test_usertagging_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `test_user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6456921 DEFAULT CHARSET=utf8

CREATE TABLE `productinfo` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `product_id` varchar(20) NOT NULL,
  `text` varchar(256) NOT NULL,
  `inftype` varchar(64) NOT NULL,
  `extra_info` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `productinfo_218f3960` (`product_id`),
  CONSTRAINT `product_id_refs_rpk_d9184c73` FOREIGN KEY (`product_id`) REFERENCES `product` (`rpk`)
) ENGINE=InnoDB AUTO_INCREMENT=44088 DEFAULT CHARSET=utf8

CREATE TABLE `productinfo_tags` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `productinfo_id` int(11) NOT NULL,
  `tag_id` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `productinfo_id` (`productinfo_id`,`tag_id`),
  KEY `productinfo_tags_4b5946a2` (`productinfo_id`),
  KEY `productinfo_tags_5659cca2` (`tag_id`),
  CONSTRAINT `productinfo_id_refs_id_1c393f74` FOREIGN KEY (`productinfo_id`) REFERENCES `productinfo` (`id`),
  CONSTRAINT `tag_id_refs_id_0afa07dc` FOREIGN KEY (`tag_id`) REFERENCES `test_tag` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=62637 DEFAULT CHARSET=utf8

【问题讨论】:

您在 SHOW CREATE TABLE 中又错过了一张表。由于test_usertagging.user_id IN (1,2,3,4 ... ),您正在“使用 where”。 MySQL加入表,然后检查这个条件是否满足 如果您将WHERE test_usertagging.user_id IN (1,2,3,4 ... ) 改写为WHERE test_usertagging.user_id BETWEEN 1 AND 19 或类似的东西,您将使该搜索词可搜索。此外,您的 EXPLAIN 输出中的行数并不多。每个表有多少行?你有什么表现?您需要什么性能? @SamIvichuk 但是它不能使用 user_id 上的索引来检查更少的行吗? @OllieJones 不幸的是,实际上这些是任意数字(用户 ID),而不是连续的。这些表并不小——它们每个都有 100,000 到数百万行。这里给出的行数是 MySQL 对匹配行的估计。查询需要几分钟(例如 3 分钟)。我希望正确的实现可以在几秒钟内完成,我想 20 秒是可以接受的。 以及显示丢失的表格@OllieJones 【参考方案1】:

在典型的多对多映射表中,例如test_usertaggingproductinfo_tags,通常不需要id AUTO_INCREMENT PRIMARY KEY。去掉该列,将复合UNIQUE 键提升为PRIMARY KEY

这种变化可能会加快一个方向,因为通过 PK 进行查找的速度大约是通过辅助键进行查找的两倍。

您的标题谈到了FOREIGN KEY,而实际上,重要的是索引。 (如果 FK 尚不存在,则 FK 将创建一个索引。)

【讨论】:

以上是关于MySQL 性能:两个表在公共外键上的连接:仍然“使用 where”的主要内容,如果未能解决你的问题,请参考以下文章

通过在Oracle子表外键上建立索引提高性能

同一主键上的EF多个外键关系

在 MySQL 中从其中一个表中选择 MAX(differ_key) 来连接特定键上的 2 个表 [重复]

外键上的教义模式更新失败

外键 VS 主键上的聚集索引

我需要在 Oracle 的外键上创建索引吗?