使用内部选择慢速 MySQL 连接

Posted

技术标签:

【中文标题】使用内部选择慢速 MySQL 连接【英文标题】:Slow MySQL joins with inner select 【发布时间】:2011-08-12 08:58:22 【问题描述】:

以下查询有效,但随着 sendlog 表的大小随时间增加而变慢。目标是从 newsletter_subscribers 表中选择一个列表,其中没有在 newsletter_sendlog 表中具有给定时事通讯 ID 的电子邮件条目。目前,在我的 mysql 服务器上大约需要 2.2 秒,而 sendlog 中只有几千个条目。

SELECT `newsletter_subscribers`.* 
FROM `newsletter_subscribers`
    INNER JOIN `newsletter_to_subscriber` 
        ON newsletter_to_subscriber.subscriber_id = newsletter_subscribers.id
    LEFT JOIN (
        SELECT `newsletter_sendlog`.`subscriber_email` 
        FROM `newsletter_sendlog` 
        WHERE (newsletter_id='7')
      ) AS `sendlog` 
        ON newsletter_subscribers.email = sendlog.subscriber_email 
WHERE (sendlog.subscriber_email IS NULL) 
AND (newsletter_to_subscriber.newsletter_id = '7')

EXPLAIN(query) 输出以下内容:

我对 EXPLAIN 的输出不太熟悉,但如果我阅读正确,它会表明它没有使用我在 newsletter_sendlog.subscriber_email 上定义的索引。我试过在那个表上使用 USE INDEX(email) ,但它似乎没有生效。

关于如何优化它的任何建议?或者可能建议另一个相同的查询?


newsletter_sendlog 的创建表:

CREATE TABLE `newsletter_sendlog` (
  `id` int(11) unsigned NOT NULL auto_increment,
  `subscriber_email` varchar(100) NOT NULL default '',
  `newsletter_id` int(11) default NULL,
  `sendstatus` int(11) default NULL,
  `senddate` timestamp NOT NULL default CURRENT_TIMESTAMP,
  PRIMARY KEY  (`id`),
  KEY `newsletter_id` (`newsletter_id`),
  KEY `email` (`subscriber_email`)
) ENGINE=MyISAM AUTO_INCREMENT=2933 DEFAULT CHARSET=latin1;

为 newsletter_subscribers 创建表:

CREATE TABLE `newsletter_subscribers` (
  `id` int(11) unsigned NOT NULL auto_increment,
  `email` varchar(100) NOT NULL default '',
  `name` tinytext,
  PRIMARY KEY  (`id`),
  KEY `email` (`email`)
) ENGINE=MyISAM AUTO_INCREMENT=2964 DEFAULT CHARSET=utf8;

为 newsletter_to_subscriber 创建表:

CREATE TABLE `newsletter_to_subscriber` (
  `id` int(11) unsigned NOT NULL auto_increment,
  `newsletter_id` int(11) NOT NULL,
  `subscriber_id` int(11) NOT NULL,
  PRIMARY KEY  (`id`),
  KEY `newsletter_subscriber` (`newsletter_id`,`subscriber_id`)
) ENGINE=MyISAM AUTO_INCREMENT=2964 DEFAULT CHARSET=latin1;

更新:

在订阅者 ID 上添加索引后,为 newsletter_to_subscriber 创建表现在如下所示:

CREATE TABLE `newsletter_to_subscriber` (
  `id` int(11) unsigned NOT NULL auto_increment,
  `newsletter_id` int(11) NOT NULL,
  `subscriber_id` int(11) NOT NULL,
  PRIMARY KEY  (`id`),
  KEY `newsletter_subscriber` (`newsletter_id`,`subscriber_id`),
  KEY `subscriber` (`subscriber_id`)
) ENGINE=MyISAM AUTO_INCREMENT=2964 DEFAULT CHARSET=latin1;

@nobody 建议的查询的解释:

【问题讨论】:

【参考方案1】:

最好选择特定字段,而不是星号 (*),并避免使用反引号 (`)。试试看下面的(重写的)查询是否效果更好:

SELECT 
    newsletter_subscribers.id,
    newsletter_subscribers.email,
    newsletter_subscribers.name
FROM
    newsletter_subscribers
    LEFT JOIN
        newsletter_to_subscriber
        ON
            newsletter_to_subscriber.subscriber_id = newsletter_subscribers.id
    LEFT JOIN
        newsletter_sendlog
        ON
            newsletter_subscribers.email = newsletter_sendlog.subscriber_email
WHERE
    newsletter_to_subscriber.newsletter_id = 7
    AND
        newsletter_sendlog.newsletter_id = 7
    AND
        newsletter_sendlog.subscriber_email IS NULL

【讨论】:

虽然非常快(大约 5 毫秒),但这个查询给出了一个空的结果集。我猜那是因为 newsletter_sendlog.newsletter_id=7 在 newsletter_sendlog.email 为 NULL 的地方不匹配?【参考方案2】:
SELECT `newsletter_subscribers`.* FROM `newsletter_subscribers`
  INNER JOIN `newsletter_to_subscriber` 
    ON newsletter_to_subscriber.subscriber_id = newsletter_subscribers.id
  LEFT JOIN (
      SELECT `newsletter_sendlog`.`subscriber_email` FROM `newsletter_sendlog` 
        WHERE (newsletter_id='7')) AS `sendlog` 
    ON newsletter_subscribers.email=sendlog.subscriber_email 
  WHERE (sendlog.subscriber_email IS NULL) 
    AND (newsletter_to_subscriber.newsletter_id = '7')

你可以尝试在单列上实现索引键newsletter_to_subscriber.subscriber_id

看看有没有用?

尝试使用如下表结构:

CREATE TABLE `newsletter_to_subscriber` (
  `id` int(11) unsigned NOT NULL auto_increment,
  `newsletter_id` int(11) NOT NULL,
  `subscriber_id` int(11) NOT NULL,
  PRIMARY KEY  (`id`),
  KEY `newsletter_subscriber` (`newsletter_id`,`subscriber_id`)
  KEY `subscriber_id_key` (`subscriber_id`)
  KEY `newsletter_id_key` (`newsletter_id`)
) ENGINE=MyISAM AUTO_INCREMENT=2964 DEFAULT CHARSET=latin1;

【讨论】:

感谢您的回答,但这并没有帮助,尽管它也没有受到伤害。查询的持续时间是相同的【参考方案3】:

不完全确定,但我认为索引被忽略,因为您正在寻找 NULL 值。

虽然有一种不同的、希望更有效的方式来运行此查询:

select *
from newsletter_subscribers
where email not in 
(select subscriber_email
from newsletter_sendlog
where newsletter_id='7')

【讨论】:

这个查询,虽然它提供了正确的结果,但每次运行它都需要大约 20 秒才能完成。【参考方案4】:

首先你不需要那个子查询:

SELECT `newsletter_subscribers`.* 
FROM `newsletter_subscribers`
    INNER JOIN `newsletter_to_subscriber` 
        ON( newsletter_to_subscriber.subscriber_id = newsletter_subscribers.id )
    LEFT JOIN `newsletter_sendlog`
        ON( newsletter_subscribers.email = newsletter_sendlog.subscriber_email AND
            newsletter_sendlog.newsletter_id = '7' )
WHERE newsletter_sendlog.subscriber_email IS NULL

上面的查询将完成这项工作。

其次,在newsletter_to_subscriber 中,您在newsletter_idsubscriber_id 上有一个多部分索引,您的查询不能使用它,因为它将搜索subscriber_id,并且在索引中排在第二位,您需要在subscriber_id 上有一个单独的索引:

INDEX( subscriber_id )

【讨论】:

感谢您的回答。不幸的是,它没有帮助。该查询虽然提供了正确的结果,但与我的原始查询大约需要 2 秒相比,它需要大约 5 秒才能完成。添加索引并没有明显改变持续时间 @Jens 这很奇怪,因为第一个连接应该使用您在newsletter_to_subscriber.subscriber_id 上的索引,而第二个连接应该使用newsletter_sendlog.newsletter_idnewsletter_sendlog.subscriber_email 上的那个匹配行较少的那个,所以查询应该不时间,在添加我提到的索引后对这个查询做一个解释,这可能有助于我们找出问题。 我为您建议的更改添加了新的创建表,并将您的查询的解释输出添加到我的原始帖子中。

以上是关于使用内部选择慢速 MySQL 连接的主要内容,如果未能解决你的问题,请参考以下文章

SQL Server 2012 链接服务器到 MySQL 慢速选择查询

cmd 中连接mysql时报'mysql'不是内部或外部命令,也不是可运行的程序或批处理文件,该怎么办?

MYSQL - 更新/内部连接 ​​[重复]

嵌套选择的内部连接 ​​- 多次使用同一列时

内部连接与 MySQL 的区别

如何在使用两个内部连接并将表设置为连接时在 MySQL 中使用更新语句?