MySQL:避免GROUPBY子句导致的临时/Filesort
我有一个相当简单的查询,它试图显示已订阅的电子邮件地址数和未订阅的电子邮件地址数,并按客户端分组 查询:MySQL:避免GROUPBY子句导致的临时/Filesort,mysql,sql,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 c
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是contacts(具有客户端id)和emailAddresses(在此查询中未实际使用)之间的连接表
下面是解释:
+----+-------------+-------------------------+--------+---------------+---------+---------+-------------------------------------------+----------+---------------------------------+
| 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)
这里的问题显然是GROUPBY子句,因为我可以删除连接(以及依赖它的项),而性能仍然很糟糕(40秒以上)。联系人电子邮件地址中有1000万条记录,联系人中有1200万条记录,分组中有10-15条客户记录
从:
可以在以下条件下创建临时表:
如果存在ORDER BY子句和其他GROUP BY子句,或者ORDER BY或GROUP BY包含联接队列中第一个表以外的表中的列,则会创建一个临时表
DISTINCT与ORDERBY组合可能需要一个临时表
如果使用SQL\u SMALL\u RESULT选项,MySQL将使用内存中的临时表,除非查询还包含需要磁盘存储的元素(稍后描述)
我显然没有将GROUP BY与ORDER BY组合在一起,我尝试了多种方法来确保GROUP BY位于应该正确放置在加入队列中的列上(包括重写查询以将联系人放入FROM中,而不是加入到contacts_emailAddresses中),但都没有用
如果您对性能调整有任何建议,我们将不胜感激 我认为摆脱“使用临时;使用文件排序”操作(给定当前模式、当前查询和指定的结果集)的唯一方法是在选择列表中使用相关子查询
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的额外列中为eu
和es
表看到“使用索引”。但要做到这一点,我们需要一个合适的索引,它的前导列是contact\u id
,包括subscribed
列。例如:
CREATE INDEX cemail_IX2 ON contacts_emailAddresses (contact_id, subscribed);
新索引可用时,EXPLAIN
输出显示MySQL将使用新索引:
注释
这就是引入少量冗余可以提高性能的问题。(就像我们在传统数据仓库中所做的那样。)
为了获得最佳性能,我们真正想要的是在contacts\u emailAddresses
表中提供client\u id
列,而无需加入contacts表
在当前模式中,与contacts
表的外键关系为我们获得客户机id
(相反,原始查询中的连接操作是我们获得它的原因)。如果我们可以完全避免该连接操作,我们可以完全从单个索引满足查询,使用索引进行聚合,并避免了“使用临时;使用文件排序”和联接操作的开销
使用可用的client\u 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
这将在查询计划中为我们提供一个“使用索引”,而这个结果集的查询计划没有比这更好的了
但是,这需要更改您的scheam,它并不能真正回答您的问题
如果没有client_id
列,那么我们可能会做的最好的查询就是像Gordon在其答案中发布的查询(尽管您仍然需要按c.client_id
添加组以获得指定的结果)。Gordon推荐的索引将是有益的
... ON contacts_emailAddresses(contact_id, subscribed)
定义了该索引后,联系人id上的独立索引是多余的。新索引将是支持现有外键约束的合适替代品。(仅联系人id
上的索引可能会被删除。)
另一种方法是在执行联接之前,首先在“大”表上进行聚合,因为它是外部联接的驱动表。实际上,由于外键列被定义为NOTNULL,并且有一个外键,所以它实际上根本不是一个“外部”连接
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表不会被索引。感谢您的建议!虽然方法1和3(您的“相关子查询”和“大”表上的聚合”建议)产生了比原始方法稍好的性能,但相差不大。正如预期的那样,更改模式以将客户机id(和关联的组合索引)添加到contacts\u emailAddresses表中,产生了迄今为止最佳的性能增益,将查询时间从40多秒缩短到3秒。我需要用我实现的另一种解决方法来权衡模式的变化,即创建一个“stats”表,该表使用来自orig的数据定期更新。query.@jcq:如果不需要截止到小时的结果,如果截止到昨天的结果足够好,那么summary stats表是一种可行的方法。通过添加client_id
列,该列必须与contacts
表上的client_id
保持“同步”。我
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
... ON contacts_emailAddresses(contact_id, subscribed)
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