使用昂贵的 INNER JOIN 优化 MySQL 查询

Posted

技术标签:

【中文标题】使用昂贵的 INNER JOIN 优化 MySQL 查询【英文标题】:Optimizing MySQL query with expensive INNER JOIN 【发布时间】:2012-02-02 09:02:37 【问题描述】:

通过反复试验,我发现从以下查询中删除连接时,它的运行速度快了大约 30 倍。有人可以解释为什么会这样,以及是否可以优化查询以包含额外的连接而不会影响性能。

这是说明的屏幕截图,显示索引未用于 uesr_groups 表。

http://i.imgur.com/9VDuV.png

这是原始查询:

SELECT `comments`.`comment_id`, `comments`.`comment_html`, `comments`.`comment_time_added`, `comments`.`comment_has_attachments`, `users`.`user_name`, `users`.`user_id`, `users`.`user_comments_count`, `users`.`user_time_registered`, `users`.`user_time_last_active`, `user_profile`.`user_avatar`, `user_profile`.`user_signature_html`, `user_groups`.`user_group_icon`, `user_groups`.`user_group_name`
FROM (`comments`)
INNER JOIN `users` ON `comments`.`comment_user_id` = `users`.`user_id`
INNER JOIN `user_profile` ON `users`.`user_id` = `user_profile`.`user_id`
INNER JOIN `user_groups` ON `users`.`user_group_id` = `user_groups`.`user_group_id`
WHERE `comments`.`comment_enabled` =  1
AND `comments`.`comment_content_id` =  12
ORDER BY `comments`.`comment_time_added` ASC
LIMIT 20

如果我删除“user_groups”连接,那么查询的运行速度会比上面提到的快 30 倍。

SELECT `comments`.`comment_id`, `comments`.`comment_html`, `comments`.`comment_time_added`, `comments`.`comment_has_attachments`, `users`.`user_name`, `users`.`user_id`, `users`.`user_comments_count`, `users`.`user_time_registered`, `users`.`user_time_last_active`, `user_profile`.`user_avatar`, `user_profile`.`user_signature_html`
FROM (`comments`)
INNER JOIN `users` ON `comments`.`comment_user_id` = `users`.`user_id`
INNER JOIN `user_profile` ON `users`.`user_id` = `user_profile`.`user_id`
WHERE `comments`.`comment_enabled` =  1
AND `comments`.`comment_content_id` =  12
ORDER BY `comments`.`comment_time_added` ASC
LIMIT 20

我的表格如下,任何人都可以提供有关如何避免包含 user_groups 表格的性能损失的任何见解吗?

--
-- Table structure for table `comments`
--

CREATE TABLE IF NOT EXISTS `comments` (
  `comment_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `comment_content_id` int(10) unsigned NOT NULL,
  `comment_user_id` mediumint(6) unsigned NOT NULL,
  `comment_original` text NOT NULL,
  `comment_html` text NOT NULL,
  `comment_time_added` int(10) unsigned NOT NULL,
  `comment_time_updated` int(10) unsigned NOT NULL,
  `comment_enabled` tinyint(1) NOT NULL DEFAULT '0',
  `comment_is_spam` tinyint(1) NOT NULL DEFAULT '0',
  `comment_has_attachments` tinyint(1) unsigned NOT NULL,
  `comment_has_edits` tinyint(1) NOT NULL,
  PRIMARY KEY (`comment_id`),
  KEY `comment_user_id` (`comment_user_id`),
  KEY `comment_content_id` (`comment_content_id`),
  KEY `comment_is_spam` (`comment_is_spam`),
  KEY `comment_enabled` (`comment_enabled`),
  KEY `comment_time_updated` (`comment_time_updated`),
  KEY `comment_time_added` (`comment_time_added`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=352 ;

-- --------------------------------------------------------

--
-- Table structure for table `users`
--

CREATE TABLE IF NOT EXISTS `users` (
  `user_id` mediumint(6) unsigned NOT NULL AUTO_INCREMENT,
  `user_ipb_id` int(10) unsigned DEFAULT NULL,
  `user_activated` tinyint(1) NOT NULL DEFAULT '0',
  `user_name` varchar(64) CHARACTER SET latin1 NOT NULL,
  `user_email` varchar(255) NOT NULL,
  `user_password` varchar(40) NOT NULL,
  `user_content_count` int(10) unsigned NOT NULL DEFAULT '0',
  `user_comments_count` int(10) unsigned NOT NULL DEFAULT '0',
  `user_salt` varchar(8) NOT NULL,
  `user_api_key` varchar(32) NOT NULL,
  `user_auth_key` varchar(32) DEFAULT NULL,
  `user_paypal_key` varchar(32) DEFAULT NULL,
  `user_timezone_id` smallint(3) unsigned NOT NULL,
  `user_group_id` tinyint(3) unsigned NOT NULL,
  `user_custom_permission_mask_id` tinyint(3) unsigned DEFAULT NULL,
  `user_lang_id` tinyint(2) unsigned NOT NULL,
  `user_time_registered` int(10) unsigned NOT NULL,
  `user_time_last_active` int(10) unsigned NOT NULL
  PRIMARY KEY (`user_id`),
  UNIQUE KEY `user_email` (`user_email`),
  KEY `user_group_id` (`user_group_id`),
  KEY `user_auth_key` (`user_auth_key`),
  KEY `user_api_key` (`user_api_key`),
  KEY `user_custom_permission_mask_id` (`user_custom_permission_mask_id`),
  KEY `user_time_last_active` (`user_time_last_active`),
  KEY `user_paypal_key` (`user_paypal_key`),
  KEY `user_name` (`user_name`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=33 ;

-- --------------------------------------------------------

--
-- Table structure for table `user_groups`
--

CREATE TABLE IF NOT EXISTS `user_groups` (
  `user_group_id` tinyint(3) unsigned NOT NULL AUTO_INCREMENT,
  `user_group_name` varchar(32) NOT NULL,
  `user_group_permission_mask_id` tinyint(3) unsigned NOT NULL,
  `user_group_icon` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`user_group_id`),
  KEY `user_group_permission_mask_id` (`user_group_permission_mask_id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=8 ;

-- --------------------------------------------------------

--
-- Table structure for table `user_profile`
--

CREATE TABLE IF NOT EXISTS `user_profile` (
  `user_id` mediumint(8) unsigned NOT NULL,
  `user_signature_original` text,
  `user_signature_html` text,
  `user_avatar` varchar(64) DEFAULT NULL,
  `user_steam_id` varchar(64) DEFAULT NULL,
  `user_ps_id` varchar(16) DEFAULT NULL,
  `user_xbox_id` varchar(64) DEFAULT NULL,
  `user_wii_id` varchar(64) DEFAULT NULL,
  PRIMARY KEY (`user_id`),
  KEY `user_steam_id` (`user_steam_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

【问题讨论】:

值得运行“优化” (dev.mysql.com/doc/refman/5.0/en/optimize-table.html) - 特别是如果您的表快速增长。 这仍处于开发环境中,因此表并不大。但是我尝试了,它似乎产生了巨大的变化,现在正在使用索引并且运行速度更快。 您是否愿意提供更多关于优化器的使用和实用性的见解,并在实时环境中定期运行它。如果您添加完整答案而不仅仅是评论,我将能够接受您的答案:) MyISAM 的扩展性和性能与 InnoDB 相比非常糟糕。此外,INNER 和 LEFT 连接(或无连接)之间存在显着差异。您加入表的键也有很大差异,这也取决于底层引擎。例如,与 InnoDB 的主键连接将比 MyISAM 快得多。如果 InnoDB,您也可以利用此“功能”并相应地构建您的数据库。另一方面,您有许多无意义的索引(它们的基数很低),例如 comment_is_spamcomment_is_enabled - 这些索引实际上什么都不做。 【参考方案1】:

MySQL 有一个EXPLAIN 功能,可以帮助您理解查询:

$ mysql
> EXPLAIN SELECT `comments`.`comment_id`, `comments`.`comment_html`,`comments`.`comment_time_added`, `comments`.`comment_has_attachments`, `users`.`user_name`, `users`.`user_id`, `users`.`user_comments_count`, `users`.`user_time_registered`, `users`.`user_time_last_active`, `user_profile`.`user_avatar`, `user_profile`.`user_signature_html`
  FROM (`comments`)
  INNER JOIN `users` ON `comments`.`comment_user_id` = `users`.`user_id`
  INNER JOIN `user_profile` ON `users`.`user_id` = `user_profile`.`user_id`
  WHERE `comments`.`comment_enabled` =  1
  AND `comments`.`comment_content_id` =  12
  ORDER BY `comments`.`comment_time_added` ASC
  LIMIT 20

MySQL 可能只是丢失或跳过索引。

您可以在此处from the documentation (a little hard-core) 了解有关了解EXPLAIN 输出的更多信息,或者通过simpler explanation here, (ignore the fact that it's on a Java site.) 了解更多信息

数据量很大,或者索引过时或不完整意味着 MySQL 错误地进行了表扫描。当您看到表扫描顺序序列时,您通常可以很容易地看到哪个字段缺少索引,或者索引不可用。

【讨论】:

如您所见,它缺少用户组的索引,可能是因为数据太小(只有 7 行),但不可能比这更大。 这是有或没有JOIN 的解释吗?我会寻找避免临时表的方法,或者调整 MySQL 引擎以提高效率,a related post from stackexchange might help you 优秀!!!当我运行一个非常复杂的内部连接嵌套时,真的帮助我理解了索引世界中发生了什么【参考方案2】:

你能试试这个吗(你可以用user_group删除加入)。如果查询从comments 表中检索小数据集,它会更快:

SELECT 
   comments.comment_id, comments.comment_html, comments.comment_time_added, comments.comment_has_attachments, users.user_name, users.user_id, users.user_comments_count, users.user_time_registered, users.user_time_last_active, user_profile.user_avatar, user_profile.user_signature_html, user_groups.user_group_icon, user_groups.user_group_name
FROM 
   (select * from comments where comment_content_id = 12 and active = 1) comments
      INNER JOIN users u ON c.comment_user_id = users.user_id
      INNER JOIN user_profile ON users.user_id = user_profile.user_id
      INNER JOIN user_groups ON users.user_group_id = user_groups.user_group_id
ORDER BY comments.comment_time_added ASC
LIMIT 20

【讨论】:

【参考方案3】:

大多数数据库引擎根据有关表的统计信息计算其查询计划 - 例如,如果表的行数较少,则转到表比索引要快。这些统计数据在“正常”操作期间保持 - 例如插入、更新和删除 - 但在表定义更改或进行批量插入时可能会不同步。

如果您在查询计划中看到意外行为,您可以强制数据库更新其统计信息;在 MySQL 中,您可以使用 Optimize Table - 它可以完成所有操作,包括重新排序表本身,或者 Analyze Table 仅更新索引。

这在生产环境中很难做到,因为这两个操作都会锁定表;如果您可以协商维护窗口,那是迄今为止处理问题的最简单方法。

值得衡量“优化表”的性能 - 在指定良好的硬件上,“正常”大小的表应该只需要几秒钟(最多几百万条记录,只有几个索引)。这可能意味着您可以有一个“非正式的”维护窗口——您不会使应用程序脱机,您只需接受一些用户在运行脚本时性能会下降。

【讨论】:

【参考方案4】:

尝试在非空关系上使用左连接。

似乎由于内部连接始终是对称的,mysql 将重新排序连接以首先使用最好看的(通常是最小的)表。

由于左连接并不总是对称的,mysql 不会重新排序它们,因此您可以使用它们来强制表顺序。但是,对于非空字段 left 和 inner 是等效的,因此您的结果不会改变。

表格顺序将决定使用哪些指标,这些指标会极大地影响性能。

【讨论】:

以上是关于使用昂贵的 INNER JOIN 优化 MySQL 查询的主要内容,如果未能解决你的问题,请参考以下文章

mysql 优化例子:IN 换 INNER JOIN

MySQL Using temporary; Using filesort INNER JOIN优化

MySQL中inner join 和 cross join 的区别

MySql Join 语法 性能 优化

mysql中两表inner join连接 如何去重

Mysql 多表连接查询 inner join 和 outer join 的使用