【问题标题】:Slow MySQL joins with inner select使用内部选择慢速 MySQL 连接
【发布时间】: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 建议的查询的解释:

【问题讨论】:

    标签: mysql performance join query-optimization


    【解决方案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_id 和subscriber_id 上有一个多部分索引,您的查询不能使用它,因为它将搜索subscriber_id,并且在索引中排在第二位,您需要在subscriber_id 上有一个单独的索引:

    INDEX( subscriber_id )
    

    【讨论】:

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