1. 程式人生 > >MySQL中的檢視及效能問題

MySQL中的檢視及效能問題

檢視是MySQL 5.0中增加的三大新功能之一(另外兩個是儲存過程與觸發器),也是一般稍微“高階”一點的資料庫所必需要有的功能。MySQL在定義檢視上沒什麼限制,基本上所有的查詢都可定義為檢視,並且也支援可更新檢視(當然只有在檢視和行列與基礎表的行列之間存在一一對應關係時才能更新),因此從功能上說MySQL的檢視功能已經很完善了。


然而若要在應用中使用檢視,還需要了解處理檢視時的效能,而MySQL在這方面問題是比較大的,需要特別注意。首先要知道MySQL在處理檢視時有兩種演算法,分別稱為MERGE和TEMPTABLE。在執行"CREATE VIEW"語句時可以指定使用哪種演算法。所謂MERGE是指在處理涉及到檢視的操作時,將對檢視的操作根據檢視的定義進行展開,有點類似於C語言中的巨集展開。比如設有以下的表(類似於部落格中的評論):
CREATE TABLE `comment` (
  `id` int(11) NOT NULL,
  `user_id` int(11) default NULL,
  `content` varchar(255) default NULL,
  PRIMARY KEY  (`id`),
  KEY `idx_comment_uid` (`user_id`)
) ENGINE=InnoDB;
假設user_id < 10000的使用者為VIP使用者,我們可以這樣建立一個檢視來表示VIP使用者的評論:
CREATE VIEW vip_comment AS SELECT * FROM comment WHERE user_id < 10000;
這時我們在操作vip_comment檢視時使用的就是MERGE演算法。如:
mysql > EXPLAIN EXTENDED SELECT count(*) FROM vip_comment WHERE user_id < 0;
+----+-------------+---------+-------+-----------------+-----------------+---------+------+------+--------------------------+
| id | select_type | table   | type  | possible_keys   | key             | key_len | ref  | rows | Extra                    |
+----+-------------+---------+-------+-----------------+-----------------+---------+------+------+--------------------------+
|  1 | SIMPLE      | comment | range | idx_comment_uid | idx_comment_uid | 5       | NULL |   10 | Using where; Using index | 
+----+-------------+---------+-------+-----------------+-----------------+---------+------+------+--------------------------+
mysql> show warnings;
+-------+------+---------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                               |
+-------+------+---------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1003 | select count(0) AS `count(*)` from `test`.`comment` where ((`test`.`comment`.`user_id` < 0) and (`test`.`comment`.`user_id` < 10000)) | 
+-------+------+---------------------------------------------------------------------------------------------------------------------------------------+
可以看到,對vip_comment的操作已經被擴充套件為對comment表的操作。


一般來說在能夠使用MERGE演算法的時候MySQL處理檢視上沒什麼效能問題,但並非在任何時候都能使用MERGE演算法。事實上,只要檢視的定義稍稍有點複雜,MySQL就沒辦法使用MERGE演算法了。準確的說,只要檢視定義中使用了以下SQL構造塊就無法使用MERGE演算法:
聚集函式
DISTINCT
GROUP BY
HAVING
集合操作(在MySQL中只有UNION, UNION ALL,沒有EXCEPT和INTERSECT)
子查詢
確實,在檢視定義比較複雜的情況下,要對檢視操作進行有效的優化是非常困難的。因此在這個時候,MySQL使用了一種以不變應萬變的方法,即先執行檢視定義,將其結果使用臨時表儲存起來,這樣後續對檢視的操作就轉化為對臨時表的操作。不能不說從單從軟體設計的角度看,這樣的方法非常的優雅,然而從效能角度,這一方法也是非常的差。


比如我們希望使用如下的檢視來表示每個使用者的評論數,即:
CREATE VIEW comment_count AS SELECT user_id, count(*) AS count FROM comment GROUP BY user_id;
使用這個檢視的時候,我們可能心裡有個小算盤。目前我們先用這個檢視頂著,如果效能確實有問題,那我們就再來搞一張comment_count的表,其中就記下來每個使用者的評論數。而我們現在先用這個檢視是為了將來要是改的話會方便點(這也是檢視--即教科書中所謂的外模式--這個東西存在的主要原因之一,另一主要原因是便於許可權控制)。但是遇到了MySQL這個蠢貨,我們的算盤鐵定會失敗。
我們來看一下指定user_id從comment_count選取記錄時的執行策略:
mysql> explain select count(*) from comment_count where user_id = 90;
+----+-------------+------------+-------+---------------+-----------------+---------+------+--------+-------------+
| id | select_type | table      | type  | possible_keys | key             | key_len | ref  | rows   | Extra       |
+----+-------------+------------+-------+---------------+-----------------+---------+------+--------+-------------+
|  1 | PRIMARY     | <derived2> | ALL   | NULL          | NULL            | NULL    | NULL |    101 | Using where | 
|  2 | DERIVED     | comment    | index | NULL          | idx_comment_uid | 5       | NULL | 524833 | Using index | 
+----+-------------+------------+-------+---------------+-----------------+---------+------+--------+-------------+
2 rows in set (4.18 sec)
可以看出,MySQL首先是先執行comment_count的檢視定義,將結果儲存在臨時表中(即DERIVED),然後再掃描這一臨時表,選擇出滿足"user_id = 90"的那一條記錄。這樣,雖然我們最終只需要統計90號使用者的評論數,並且comment表的user_id欄位上也有索引,MySQL也會掃描整個comment表,並按user_id分組計算出所有使用者的評論數。一般來說,這鐵定會使你的系統玩完。這裡面還要注意的是即使在進行EXPLAIN時,檢視的物化也是要先執行的,因此若評論很多的話EXPLAIN也是一樣的慢。
這個問題的根源是MySQL的查詢優化本來就存在很多問題。對於上述的查詢,要達到比較好的優化效果在資料庫中一般是如下處理的:
1、將對檢視的操作轉化為FROM子句中的子查詢:
select * from (select user_id, count(*) as count from comment group by user_id) as comment_count where user_id = 90;
2、子查詢提升。因為子查詢中使用了group by,因此先將外面的條件作為提升後的having條件
select user_id, count(*) as count from comment group by user_id having user_id = 90;
3、由於having條件中不涉及聚集函式,轉化為where條件
select user_id, count(*) as count from comment where user_id = 90 group by user_id;
4、由於指定where條件後,user_id已經是一個常數,根據常數group by沒意義,因此去掉group by
select user_id, count(*) as count from comment where user_id = 90;
一般從概念上要經過這四步轉化,才能得到最後的優化語句。除第4步無法根據EXPLAIN輸出和查詢效能判斷出MySQL是否進行這一優化外,前3類優化MySQL都不會進行。因此,MySQL要能夠有效的處理上述查詢還有很長的路要走。


PS: 相對來說PostgreSQL的查詢優化能力就強得多,上面的查詢在PostgreSQL中就能夠產生上述優化後的最終執行計劃。PostgreSQL比較關注查詢優化估計與PostgreSQL的學院派風格或PostgreSQL中的rule system有關。