MySQL:避免由GROUP BY子句引起的临时/文件排序

时间:2014-03-20 21:34:16

标签: mysql sql

我有一个相当简单的查询,试图显示订阅的电子邮件地址的数量以及取消订阅的数字,按客户分组。

查询:

SELECT
    client_id,
    COUNT(CASE WHEN subscribed = 1 THEN subscribed END) AS subs,
    COUNT(CASE WHEN subscribed = 0 THEN subscribed END) AS unsubs
FROM
    contacts_emailAddresses
LEFT JOIN contacts ON contacts.id = contacts_emailAddresses.contact_id
GROUP BY
    client_id

以下是相关表格的模式。 contacts_emailAddresses是联系人(具有client_id)和emailAddresses(在此查询中实际未使用)之间的联结表。

CREATE TABLE `contacts` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `firstname` varchar(255) NOT NULL DEFAULT '',
  `middlename` varchar(255) NOT NULL DEFAULT '',
  `lastname` varchar(255) NOT NULL DEFAULT '',
  `gender` varchar(5) DEFAULT NULL,
  `client_id` mediumint(10) unsigned DEFAULT NULL,
  `datasource` varchar(10) DEFAULT NULL,
  `external_id` int(10) unsigned DEFAULT NULL,
  `created` timestamp NULL DEFAULT NULL,
  `trash` tinyint(1) NOT NULL DEFAULT '0',
  `updated` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `client_id` (`client_id`),
  KEY `external_id combo` (`client_id`,`datasource`,`external_id`),
  KEY `trash` (`trash`),
  KEY `lastname` (`lastname`),
  KEY `firstname` (`firstname`),
  CONSTRAINT `contacts_ibfk_1` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14742974 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT

CREATE TABLE `contacts_emailAddresses` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `contact_id` int(10) unsigned NOT NULL,
  `emailAddress_id` int(11) unsigned DEFAULT NULL,
  `primary` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `subscribed` tinyint(1) unsigned NOT NULL DEFAULT '1',
  `modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `contact_id` (`contact_id`),
  KEY `subscribed` (`subscribed`),
  KEY `combo` (`contact_id`,`emailAddress_id`) USING BTREE,
  KEY `emailAddress_id` (`emailAddress_id`) USING BTREE,
  CONSTRAINT `contacts_emailAddresses_ibfk_1` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`),
  CONSTRAINT `contacts_emailAddresses_ibfk_2` FOREIGN KEY (`emailAddress_id`) REFERENCES `emailAddresses` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=24700918 DEFAULT CHARSET=utf8

这是EXPLAIN:

+----+-------------+-------------------------+--------+---------------+---------+---------+-------------------------------------------+----------+---------------------------------+
| id | select_type | table                   | type   | possible_keys | key     | key_len | ref                                       | rows     | Extra                           |
+----+-------------+-------------------------+--------+---------------+---------+---------+-------------------------------------------+----------+---------------------------------+
| 1  | SIMPLE      | contacts_emailAddresses | ALL    | NULL          | NULL    | NULL    | NULL                                      | 10176639 | Using temporary; Using filesort |
| 1  | SIMPLE      | contacts                | eq_ref | PRIMARY       | PRIMARY | 4       | icarus.contacts_emailAddresses.contact_id | 1        |                                 |
+----+-------------+-------------------------+--------+---------------+---------+---------+-------------------------------------------+----------+---------------------------------+
2 rows in set (0.08 sec)

这里的问题显然是GROUP BY子句,因为我可以删除JOIN(以及依赖它的项目)并且性能仍然很糟糕(40+秒)。 contacts_emailAddresses中有10m记录,联系人中有12m记录,分组中有10-15个客户记录。

来自doc

  

可以在以下条件下创建临时表:

     

如果存在ORDER BY子句和不同的GROUP BY子句,或者ORDER BY或GROUP BY包含连接队列中第一个表以外的表中的列,则会创建一个临时表。

     

DISTINCT与ORDER BY相结合可能需要一个临时表。

     

如果使用SQL_SMALL_RESULT选项,MySQL使用内存临时表,除非查询还包含需要磁盘存储的元素(稍后描述)。

我显然没有将GROUP BY与ORDER BY结合使用,我尝试了多种方法来确保GROUP BY位于应该正确放置在连接队列中的列上(包括重写查询以放置联系人)在FROM中并改为加入contacts_emailAddresses),一切都无济于事。

非常感谢任何有关性能调整的建议!

1 个答案:

答案 0 :(得分:8)

我认为你唯一能够远离一个"使用临时的;使用filesort"操作(给定当前模式,当前查询和指定的结果集)将使用SELECT列表中的相关子查询。

SELECT c.client_id
     , (SELECT IFNULL(SUM(es.subscribed=1),0)
          FROM contacts_emailAddresses es
          JOIN contacts cs
            ON cs.id = es.contact_id
         WHERE cs.client_id = c.client_id
       ) AS subs
     , (SELECT IFNULL(SUM(eu.subscribed=0),0)
          FROM contacts_emailAddresses eu
          JOIN contacts cu
            ON cu.id = eu.contact_id
         WHERE cu.client_id = c.client_id
       ) AS unsubs
  FROM contacts c
 GROUP BY c.client_id

这可能比原始查询运行得更快,或者可能不会。这些相关的子查询将为外部查询返回的每个子查询运行。如果那个外部查询返回了一大堆行,那就是一大堆子查询执行。

这是EXPLAIN

的输出
id  select_type        table type  possible_keys                       key        key_len  ref   Extra
--  ------------------ ----- ----- ----------------------------------- ---------- ------- ------ ------------------------
 1  PRIMARY            c     index (NULL)                              client_id  5       (NULL) Using index
 3  DEPENDENT SUBQUERY cu    ref   PRIMARY,client_id,external_id combo client_id  5       func   Using where; Using index
 3  DEPENDENT SUBQUERY eu    ref   contact_id,combo                    contact_id 4       cu.id  Using where
 2  DEPENDENT SUBQUERY cs    ref   PRIMARY,client_id,external_id combo client_id  5       func   Using where; Using index
 2  DEPENDENT SUBQUERY es    ref   contact_id,combo                    contact_id 4       cs.id  Using where

为了获得此查询的最佳效果,我们非常希望看到"使用索引"在eues表的说明的Extra列中。但要实现这一点,我们需要一个合适的索引,其中一个列为contact_id的前导列并包含subscribed列。例如:

CREATE INDEX cemail_IX2 ON contacts_emailAddresses (contact_id, subscribed);

在新索引可用的情况下,EXPLAIN输出显示MySQL将使用新索引:


id  select_type        table type  possible_keys                       key        key_len ref    Extra                     
--  ------------------ ----- ----- ----------------------------------- ---------- ------- ------ ------------------------
 1  PRIMARY            c     index (NULL)                              client_id  5       (NULL) Using index
 3  DEPENDENT SUBQUERY cu    ref   PRIMARY,client_id,external_id combo client_id  5       func   Using where; Using index
 3  DEPENDENT SUBQUERY eu    ref   contact_id,combo,cemail_IX2         cemail_IX2 4       cu.id  Using where; Using index
 2  DEPENDENT SUBQUERY cs    ref   PRIMARY,client_id,external_id combo client_id  5       func   Using where; Using index
 2  DEPENDENT SUBQUERY es    ref   contact_id,combo,cemail_IX2         cemail_IX2 4       cs.id  Using where; Using index

备注

这是一种引入少量冗余可以提高性能的问题。 (就像我们在传统的数据仓库中一样。)

为了获得最佳性能,我们真正喜欢的是在client_id表上提供contacts_emailAddresses列,而无需JOINI到contacts表。

在当前模式中,与contacts表的外键关系为我们提供了client_id(相反,原始查询中的JOIN操作就是为我们提供的。)如果我们可以避免这种情况完全JOIN操作,我们可以完全从单个索引满足查询,使用索引进行聚合,并避免"使用临时的开销;使用filesort"和JOIN操作...

client_id列可用的情况下,我们会创建一个覆盖索引,如...

... ON contacts_emailAddresses (client_id, subscribed)

然后,我们的查询速度非常快......

SELECT e.client_id
     , SUM(e.subscribed=1) AS subs
     , SUM(e.subscribed=0) AS unsubs
  FROM contacts_emailAddresses e
GROUP BY e.client_id

这会让我们得到一个"使用索引"在查询计划中,此结果集的查询计划并没有比这更好。

但是,这需要更改您的架构,它并没有真正回答您的问题。



如果没有client_id列,那么我们可能做的最好的事情就是像Gordon在答案中发布的那样查询(尽管您仍然需要添加GROUP BY c.client_id才能获得指定的结果。)戈登建议的指数将是有益的......

... ON contacts_emailAddresses(contact_id, subscribed)

定义了该索引后,contact_id上的独立索引是多余的。新索引将是支持现有外键约束的合适替代。 (可以删除contact_id上的索引。)


另一种方法是对" big"进行聚合。在执行JOIN之前,首先是表,因为它是外连接的驱动表。实际上,由于该外键列被定义为NOT NULL,并且有一个外键,它实际上不是"外部"加入吧。

SELECT c.client_id
     , SUM(s.subs) AS subs
     , SUM(s.unsubs) AS unsubs 
  FROM ( SELECT e.contact_id
              , SUM(e.subscribed=1) AS subs
              , SUM(e.eubscribed=0) AS unsubs
           FROM contacts_emailAddresses e
          GROUP BY e.contact_id
       ) s
 JOIN contacts c
   ON c.id = s.contact_id
GROUP BY c.client_id

同样,我们需要一个以contact_id为首要列的索引,并包含subscribed列,以获得最佳效果。 (s的计划应该显示"使用索引"。)不幸的是,这仍然会实现一个相当大的结果集(派生表s)作为临时MyISAM表,并且MyISAM表不会被编入索引。