1. 程式人生 > >010 --MySQL查詢優化器的侷限性

010 --MySQL查詢優化器的侷限性

MySQL的萬能"巢狀迴圈"並不是對每種查詢都是最優的。不過還好,mysql查詢優化器只對
少部分查詢不適用,而且我們往往可以通過改寫查詢讓mysql高效的完成工作。
在這我們先來看看mysql優化器有哪些侷限性:


1.關聯子查詢

mysql的子查詢實現得非常糟糕。最糟糕得一類查詢是where條件中包含in()的子查詢語句。
例如,我們希望找到sakila資料庫中,演員Penlope Guiness參演的所有影片資訊。
很自然的,我們會按照下面的方式用子查詢實現:

   select * from sakila.film
  where film_id in (
    select film_id from sakila.film_actor where actor_id = 1
  )

 

你很容易認為mysql應該由內而外的去執行這個查詢,通過子查詢中的條件先找出所匹配的
film_id。所以你看你會認為這個查詢可能會是這樣:

-- SELECT GROUP_CONCAT(film_id) FROM sakila.film_actor WHERE actor_id = 1;
-- Result: 1,23,25,106,140,166,277,361,438,499,506,509,605,635,749,832,939,970,980
SELECT * FROM sakila.film
WHERE film_id
IN(1,23,25,106,140,166,277,361,438,499,506,509,605,635,749,832,939,970,980);

 

不幸的是,事實恰恰相反。MYSQL想通過外部的關聯條件用來快速的篩選子查詢,它可能認為
這會讓子查詢更效率。mysql會這樣重寫查詢:

SELECT * FROM sakila.film
WHERE EXISTS (
SELECT * FROM sakila.film_actor WHERE actor_id = 1
AND film_actor.film_id = film.film_id);

 

這樣的話,子查詢將會依賴外部表的資料,而不會被優先執行。
mysql將會全表掃描film表,然後迴圈執行子查詢。在外表很小的情況下,
不會有什麼問題,但在外表很大的情況下,效能將會非常差。幸運的是,
很容易用關聯查詢來重寫。

mysql> SELECT film.* FROM sakila.film
  -> INNER JOIN sakila.film_actor USING(film_id)
  -> WHERE actor_id = 1;

 

其他的好的優化方法是用group_concat手工生成in()的列表。有時甚至會比JOIN查詢
更快。總之,雖然in()子查詢在很多情況下工作不佳,但exist()或者其他等價的子查詢
有時也工作的不錯。

 

關聯子查詢效能並不是一直都很差的。

子查詢 VS 關聯查詢

 

--關聯子查詢
mysql> explain select film_id, language_id from sakila.film
    where not exsits (
      select * from sakila.film_actor
      where film_actor.film_id = film.film_id
    )

********************* 1. row ***********************************
id : 1
select_type: PRIMARY
table: film
type: all
possible_keys: null
key: null
key_len: null
ref: null
rows: 951
Extra: Using where

********************* 2. row ***********************************
id : 2
select_type: Dependent subquery
table: film_actor
type: ref
possible_keys: idx_fx_film_id
key: idx_fx_film_id
key_len: 2
ref: film.film_id
rows: 2
Extra: Using where;Using index

--關聯查詢
mysql> explain select film.film_id, film.language_id from sakila.film
    left outer join sakila.film_actor using(film_id)
    where film_actor.film_id is null


********************* 1. row ***********************************
id : 1
select_type: simple
table: film
type: all
possible_keys: null
key: null
key_len: null
ref: null
rows: 951
Extra:

********************* 2. row ***********************************
id : 1
select_type: simple
table: film_actor
type: ref
possible_keys: idx_fx_film_id
key: idx_fx_film_id
key_len: 2
ref: sakila.film.film_id
rows: 2
Extra: Using where;Using index;not exists;


可以看到,這裡的執行計劃幾乎一樣,下面是一些細微的差別:
1. 表 film_actor的訪問型別一個是Dependent subquery 另一是simple,這對底層儲存引擎介面來說,沒有任何不同;

2. 對 film表 第二個查詢沒有using where,但這不重要。using子句和where子句實際上是完全一樣的。

3. 第二個表film_actor的執行計劃的Extra 有 "Not exists" 這是我們先前提到的提前終止演算法,mysql通過not exits優化
來避免在表film_actor的索引中讀取任何額外的行。這完全等效於直接使用 not exist ,這個在執行計劃中也一樣,一旦匹配到一行
資料,就立刻停止掃描


測試結果為:
查詢 每秒查詢數結果(QRS)
NOT EXISTS 子查詢 360
LEFT OUTER JOIN 425
這裡顯示使用子查詢會略慢些。

另一個例子:
不過每個具體地案例會各有不同,有時候子查詢寫法也會快些。例如,當返回結果只有一個表的某些列的時候。
聽起來,這種情況對於關聯查詢效率也會很好。具體情況具體分析,例如下面的關聯,我們希望返回所有包含同一個演員參演的電影
因為電影會有很多演員參演,所以可能返回一些重複的記錄。

mysql-> select film.film_id from sakila.film
     inner join sakila.film_actor using (film_id)

我們需要用distinct 和 group by 來移除重複的記錄

mysql-> select distinct film.film_id from sakila.film
    inner join sakila.film_actor using (film_id)

但是,回頭看看這個查詢,到底這個查詢返回的結果意義是什麼?至少這樣的寫法會讓sql的意義不明顯。
如果是有exists 則很容易表達"包含同一個參演演員"的邏輯。而且不需要使用 distinct 和 Group by,也不會有重複的結果集。
我們知道一旦使用了 distinct 和 group by 那麼在查詢的執行過程中,通常需要產生臨時中間表。

mysql -> select film_id from sakila.film_actor 
    where exists(select * from sakila.film_actor 
    where film.film_id = film_actor.film_id)

 

測試結果為:
查詢 每秒查詢數結果(QRS)
INNER JOIN 185
EXISTS 子查詢 325
這裡顯示使用子查詢會略快些。


通過上面這個詳細的案例,主要想說明兩點: 
一是不需要聽取哪些關於子查詢的 "絕對真理",(即別用使用子查詢)
二是應該用測試來驗證子查詢的執行疾患和響應時間的假設。

 

2.union的限制
有時,mysql無法將限制條件從外層"下推"到內層,這使得一些可以限制結果集和附加的優化都無法執行。

如果你想任何單獨的查詢都可以從一個limit獲益,
或者你想order by也是基於所有子查詢一次結合,
則你需要在每個子查詢加上相應的子語句。

例如:

(SELECT first_name, last_name
  FROM sakila.actor
  ORDER BY last_name)
UNION ALL
(SELECT first_name, last_name
  FROM sakila.customer
  ORDER BY last_name)
LIMIT 20;

這個查詢將會儲存200行從actor查出來的資料和customer表的599行資料,

然後放入一個臨時表,然後選取靠前的20條資料。

你可以通過在每個查詢都加上limit 20 來預防這個情況。

如下:

 
(SELECT first_name, last_name
  FROM sakila.actor
  ORDER BY last_name
  LIMIT 20)
UNION ALL
(SELECT first_name, last_name
  FROM sakila.customer
  ORDER BY last_name
  LIMIT 20)
LIMIT 20;
 

這樣只會查出40條資料了,大大提升了查詢效率。



3.索引合併優化

  

4.等值傳遞

  有時候等值傳遞也會造成很大的效能消耗。

 

5.並行執行

  mysql不能並行執行一個單獨的查詢在不同的cpu.可能其他資料庫會提供這個特性,但mysql沒有提供。

  我們提及這個就是希望你們不要花時間去弄怎麼在mysql配置並行查詢。

 

6.索引關聯

  mysql並不是完全支援雜湊關聯,大部分關聯都是巢狀迴圈關聯。

 

7.鬆散索引掃描

  由於歷史原因,mysql不支援鬆散索引掃描

 

8.最大值和最小值優化

  對於min()和max(),mysql優化做的並不好

 

9.在同一個表查詢和更新

  下面是個無法執行的sql,雖然這是一個符合標準的sql語句。

  這個sql語句嘗試將兩個表中相似行的數量記錄到欄位cnt中:

  

mysql> UPDATE tbl AS outer_tbl
  -> SET cnt = (
  ->   SELECT count(*) FROM tbl AS inner_tbl
  ->   WHERE inner_tbl.type = outer_tbl.type
  -> );

  ERROR 1093 (HY000): You can’t specify target table 'outer_tbl' for update in FROM
  clause

 

 

可以通過生成表的形式來繞過上面的限制,因為mysql只會把這個表當做一個臨時表處理。實際上,

這執行了兩個查詢:一個是子查詢的select語句,另一個是多表關聯update,只是關聯的表是一個臨時表。

子查詢會在update語句開啟表之前就完成,所以下面的查詢將正常執行。

 
mysql> UPDATE tbl
  -> INNER JOIN(
  ->   SELECT type, count(*) AS cnt
  ->   FROM tbl
  ->   GROUP BY type
  -> ) AS der USING(type)
  -> SET tbl.cnt = der.cnt;