[英]MySQL : Avoid Temporary/Filesort Caused by GROUP BY Clause
我有一個相當簡單的查詢,試圖顯示訂閱的電子郵件地址的數量以及取消訂閱的數字,按客戶分組。
查詢:
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),一切都無濟於事。
任何性能調整的建議將非常感謝!
我認為你唯一能夠擺脫“使用臨時;使用文件排序”操作(給定當前模式,當前查詢和指定結果集)的實際鏡頭將是在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
為了獲得此查詢的最佳性能,我們非常希望在解釋的Extra列中看到“使用索引”,用於eu
和es
表。 但要實現這一點,我們需要一個合適的索引,一個包含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
筆記
這是一種引入少量冗余可以提高性能的問題。 (就像我們在傳統的數據倉庫中一樣。)
為了獲得最佳性能,我們真正想要的是在contacts_emailAddresses
表上提供client_id
列,而無需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
這將使我們在查詢計划中獲得“使用索引”,並且此結果集的查詢計划沒有比這更好。
但是,這需要更改你的scheam,它並沒有真正回答你的問題。
如果沒有client_id
列,那么我們可能做的最好的事情就是像Gordon在他的回答中發布的那樣查詢(盡管你仍然需要添加GROUP BY c.client_id
來獲得指定的結果。)Gordon推薦的指數將是有益...
... ON contacts_emailAddresses(contact_id, subscribed)
定義了該索引后,contact_id上的獨立索引是多余的。 新索引將是支持現有外鍵約束的合適替代。 (只有contact_id
的索引才能被刪除。)
另一種方法是在執行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表是不會被索引。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.