【问题标题】:Join on "2 columns" is extremely slow加入“2列”非常慢
【发布时间】:2021-03-29 04:49:56
【问题描述】:

我有 2 张桌子,我需要像这样INNER JOIN on 2 equal columns

SELECT
    `table1`.*
FROM
    `table1`
    INNER JOIN `table2`
        ON  `table2`.`type` = `table1`.`type`
        AND `table2`.`num`  = `table1`.`num`
WHERE
    `table2`.`another_int` = 1
ORDER BY
    `table1`.`id` DESC
LIMIT 10 OFFSET 0

当我尝试时,查询需要 1500ms

但删除 2 个 JOIN 条件中的任何一个,或删除 ORDER BY 都会导致查询在 1ms

中运行

更多信息:

  • 表 2 有 1500 行,表 1 有 ~400,000 行

  • type 和 num 列都在两个表和 id 上都建立了索引 表 1 上的(排序依据)也是主要的,因此已编入索引。

  • type:两个表上的 ENUM 具有完全相同的选项(6 个枚举选项)

  • num:两个表上的无符号大整数


使用EXPLAIN: 两个表都使用键,但 Extra 列显示表 2:“使用索引;使用临时;使用文件排序”

删除 WHERE 条件或 LIMIT OFFSET 没有效果,但我刚刚注意到,删除 ORDER BY 同时保持 LIMIT 会导致查询在不到 1 毫秒内运行

不知道这里出了什么问题或者我应该怎么做,所以任何帮助都非常感谢......

编辑

/* Table 1 Keys */

KEY `posts_user_id_foreign` (`user_id`),
KEY `posts_composite_ind` (`post_followable_type`,`post_followable_id`,`id`) USING BTREE,
CONSTRAINT `posts_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=456501 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;


/* Table 2 Keys */

PRIMARY KEY (`id`),
UNIQUE KEY `unique_user_follows` (`user_id`,`followable_type`,`followable_id`),
CONSTRAINT `follows_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=1525 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

/* Query */
SELECT
`posts`.*, `follows`.`user_id` AS `current_user`
FROM
`posts`
INNER JOIN `follows` 
    ON `follows`.`followable_type` = `post_followable_type`
    AND `follows`.`followable_id` = `post_followable_id`
WHERE
    `follows`.`user_id` = 1
ORDER BY
    `posts`.`id` DESC
LIMIT 11 OFFSET 0


/* Explain */

| id | select_type | table   | partitions | type | possible_keys       | key                 | key_len | ref                                                 | rows | filtered | Extra                                        |
|----|-------------|---------|------------|------|---------------------|---------------------|---------|-----------------------------------------------------|------|----------|----------------------------------------------|
|  1 | SIMPLE      | follows |            | ref  | unique_user_follows | unique_user_follows | 8       | const                                               |  511 |   100.00 | Using index; Using temporary; Using filesort |
|  1 | SIMPLE      | posts   |            | ref  | posts_composite_ind | posts_composite_ind | 9       | db.follows.followable_type,db.follows.followable_id |  453 |   100.00 |                                              |

/* End */

【问题讨论】:

  • 通过(type, num, id)在table1和(another_int, type, num)在table2创建索引。
  • Mmm... 尝试添加 STRAIGHT_JOIN 以修复表格扫描订单帖子->关注。
  • 如果你不能使用 STRAIGHT_JOIN 那么你可以尝试使用 LEFT JOIN 而不是 INNER (它也修复了表扫描顺序),你的 WHERE 会隐式地将你的连接类型转换为 INNER,所以输出不会改变。
  • 请勿使用 STRAIGHT_JOIN。它很好地唤醒了当前值。对于其他一些值,它可能会非常缓慢!
  • @Akina - LEFT JOIN 变成 INNER JOIN 当优化器可以看到它们是等价的。做EXPLAIN SELECT ... ; SHOW WARNINGS;看“证明”。同时,LEFT 本身并不强制表顺序。

标签: mysql


【解决方案1】:

table2 需要INDEX(another_int, num, type)another_int 必须是第一个。但我猜你有。

去掉follows.id,将UNIQUE键提升为PRIMARY KEY

table1 需要INDEX(num, type);列可以按任意顺序排列。好的,我看到你有这样的索引。无需更改。

【讨论】:

  • 我按照你的建议做了,去掉了 table2 上的 id 并将复合唯一键 (UNIQUE(another_int, num, type)) 设为主要,设置在 table1 INDEX(num, type) 但问题仍然存在,需要 ~ 1350ms。我会在这方面做更多的工作。
  • @J.Doe - 让我们深入挖掘。 EXPLAIN FORMAT=JSON SELECT ... 有时会提供更多见解。并且使用“处理程序计数”可以提供表扫描与索引查找的线索:mysql.rjweb.org/doc.php/index_cookbook_mysql#handler_counts
  • 是否有可能在我进行这些更改之后,MySQL 需要一些时间来收集和保存索引等?因为在我睡觉之前,它运行得非常糟糕,但现在我再次测试,我的原始查询在1ms 中运行所有another_int 值(没有STRAIGHT_JOIN)......基本上,我只是从@ 更改了我的table1 索引987654335@ 到 INDEX(type, num) 并删除了我的 table2 上的主键,将现有的 UNIQUE(another_int, num, type) 设置为主键。我希望这些结果保持不变。非常感谢,问题似乎已经完全解决了!
  • 也非常感谢你教我EXPLAIN FORMAT=JSON我刚刚测试过,它返回的可用数据比正常的EXPLAIN多得多!
  • @J.Doe - 在 InnoDB 中,二级索引隐式地将主键的列默默地添加到它的末尾。当id 是PK 时,它们是相同的:INDEX(type, num, id)INDEX(type, num)。现在INDEX(type, num)INDEX(type, num, another_int) 是相同的。所以,是的,您的更改是“正确的”。
【解决方案2】:

如果其他人有这个问题:

感谢上面@Akina的cmets,让我了解到STRAIGHT_JOIN

遗憾的是我的框架不支持STRAIGHT_JOIN,但我发现你也可以将它添加到选择查询中。

所以这是最终的解决方案,大约在1ms - 25ms

SELECT
    STRAIGHT_JOIN `table1`.*
FROM
    `table1`
    INNER JOIN `table2`
        ON  `table2.type` = `table1.type`
        AND `table2.num`  = `table1.num`
WHERE
    `table2.another_int` = 1
ORDER BY
    `table1`.`id` DESC
LIMIT 10 OFFSET 0

编辑

在尝试了上面选择的答案后,我最终改用它,因为它解决了没有 STRAIGHT_JOIN 语法的问题。

变化如下:

/* previous indexes */
Table1: INDEX(type, num, id)
Table2: PRIMARY(id), UNIQUE(another_int, type, num)

/* new indexes */
Table1: INDEX(type, num)
Table2: PRIMARY(another_int, type, num)

【讨论】:

  • 框架麻烦多于价值的另一个原因。
  • 在庆祝太多之前,请为another_int 尝试一些其他值。你可能会失望。
  • @RickJames 我认为你是对的,我不应该庆祝太多,我很高兴我学到了一些新东西。我还尝试了所有可能的another_int,最短时间是1ms,最大的数字是~25ms,恕我直言。我会在这方面做更多的研究,也许我会了解 MySQL 如何计划这些事情以及如何优化我的查询。非常感谢。
  • 我为你的实验喝彩;这是一个很好的学习方式。 25ms 很棒;实际上,我很惊讶您在最坏情况下的速度如此之快。 [在我看来] 您的查询是一个棘手的问题——它可能非常快或非常慢,而且即使在优化器的帮助下,也没有简单的方法可以让它始终“好”。如果有一个“用户”在等待答案,那么“针对最坏的情况进行优化”往往是好的。如果系统因此类查询而过载,则需要“针对一般情况进行优化”。
猜你喜欢
  • 2014-05-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-28
  • 1970-01-01
  • 1970-01-01
  • 2016-01-04
  • 1970-01-01
相关资源
最近更新 更多