Mysql查詢性能優化

      Mysql查詢性能優化要從三個方面考慮,庫表結構優化、索引優化和查詢優化。通常在實際應用中,我們要面對這三種攪和一起的情況。

一、 庫表結構優化

      良好的邏輯設計和物理設計是高性能的基石。庫表結構的設計既要關注全局,又要專注細節。要設計優秀的庫表結構,可從以下幾個方面着手:

1.  選擇優化的數據類型

         選擇優化的數據類型可以遵循以下幾個原則:

         更小的通常更好,應該儘量選擇正確數據類型的最小數據類型,更小數據類型通常更快,因爲它們佔用更少的磁盤、內存和cpu緩存。

         簡單就好,簡單的數據類型需要更少的cpu週期,比如整形比字符串操作代價更低。應該使用mysql內部類型存儲時間類型,使用整形存儲ip地址。

         儘量避免null,尤其是添加索引的列。可爲null的列,對mysql來說更難優化,可爲null的列使用索引、索引統計和值比較都更復雜。可爲null的列需要更多的存儲空間,可以null的列被索引時,每個索引記錄需要一個額外的字節,在myisam中甚至還可能導致固定大小的索引(列如只有一個整數列的索引)變成可變大小的索引。但是若現有的schema含有可爲null列,通常不要首先考慮改掉這種情況,因爲,通常修改這種情況對性能的提升不明顯。

針對mysql中各個具體的數據類型做優化處理:

1) 整數類型

         Mysql中的整數類型包括tinyint、smallint、mediumint、int、bigint,分別使用8、16、24、32和64位空間存儲,它們可以存儲的範圍爲-2n到2n-1,其中n表示位數。整數類型有可選的unsigned屬性,表示不允許爲負值,大致可以使用正數的範圍提高一倍。Mysql可以爲整數指定寬度,列如int(11),這個寬度不會限制值的合法範圍,只是規定了mysql的一些交互工具(命令行客戶端)用來顯示字符的個數,對於存儲和計算來說沒有任何作用。

2) 實數類型

         實數就是帶有小數點的數據類型,mysql中的實數類型包括:float、double和decimal。其中,float和double屬於浮點類型,是常見的數據類型,存儲空間分別佔用4和8個字節。Decimal可以指定精確的整數和小數位數,用於存儲精確的數值,在財務中的應用廣泛。Cpu無法直接執行decimal的計算,但是mysql內部實現了decimal的計算。可以使用decimal存儲比bigint更大的整數。

3) 字符串類型

        Mysql中字符串類型包括varchar和char,分別用於存儲可變長字符和固定長度字符。

4) Blob和text類型

         Blob與text是用來存儲大數據的數據類型,分別採用二進制和字符方式存儲,與oracle中blob與clob(clob也是二進制存儲,不是字符方式)類似。Mysql對blob和text列進行排序的方式與其它類型不同,它只對每個列的最前max_sort_length字節而不是整個字符做排序。Blob和text列不能添加索引。

5) 日期和時間類型

       Mysql中包含datetime和timestamp兩種存儲日期和時間的類型。Datetime可以存儲大範圍的值,從1001到9999年,精確到秒,與時區無關,使用8個字節存儲空間,顯示格式爲“2018-02-27 10:44:22”。Timestamp保存範圍從1970到2038,顯示時間依賴時區。通常儘量使用timestamp類型,因爲它比達特time的空間效率更高。另外對於需要精確到毫秒的時間存儲,可以先將它們轉換成整數後保存。

6) 位數類型

      位數類型包括bit和set,使用不多,不建議使用,若要存儲boolean型數據,建議轉換爲tinyint存儲。

7) 選擇標識符

      標識列一般用來作爲表的主鍵或在關聯操作中使用,在mysql中標識列最好採用整數,可以利用它的auto_increament屬性。對於字符串的標識列,如果能夠避免儘量避免,因此字符串需要更大的存儲空間,關聯操作慢,myisam對字符串使用壓縮索引,會導致查詢很慢。如果存儲uuid值時,儘量去除“-”符號,或者用unhex()函數轉換uuid爲16字節的數字,並且存儲在一個binary(16)列中。檢索時可以通過hex()函數來格式化爲十六進制格式。具體理解下這個兩個函數的使用??

2.  Mysql schema設計中的陷阱

      表的設計不能有太多的列,數千的列會影響性能,mysql的存儲引擎api工作時需要在服務器層和存儲引擎之間通過行緩存格式拷貝數據,然後在服務器層將緩存內容解碼成各個列。從行緩存中將編碼過的列轉換成行數據結構的操作代價是非常高的,轉換的代價依賴於列的數量。關聯操作設計的表不要太多,否則執行會很慢,mysql限制了每個關聯操作最多只能有61張表,在實際應用中,經驗法則得出關聯操作的表數量最好控制在12個以內。

3.  範式和反範式

       在初學數據庫設計時,我們往往要遵循數據庫的範式要求,尤其是前三個範式。嚴格遵循範式設計的表通常更小、數據冗餘少,做更新操作簡單快捷,但是,唯一的缺點就是在做查詢時需要表關聯,關聯查詢會不僅會帶來高的代價,而且還可能造成索引策略失效,導致更低效率的查詢。絕對的範式化是實驗室中的產物,在實際的應用中要混用範式化和反範式化,根據具有情況,往往會帶來較高的查詢效率。

4.  緩存表和彙總表

       有時提升性能最好的方法是將衍生的冗餘數據保存到緩存表或彙總表,然後執行查詢這些表即可得出所需要的數據。這裏緩存表表示存儲那些從其它表獲取(但是每次獲取的速度很慢)數據的表,而彙總表保存的是使用group by 語句聚合數據的表(數據不是邏輯上冗餘),也可以把這些表稱爲累積表。比如我們要統計某網站24小時之內發送的信息數量,那麼從相關表進行統計就會很慢,如果我們每一個小時就把進行統計一次,並把統計結果存放到一張表中,當我們需要統計24小時之內發送的信息數量時,只需要執行簡單地查詢就能得到需要的結果,這樣就極大地提高的查詢效率,但是缺點是數據不是100%精確。

       對於衍生數據保存的另外一種方式是使用物化視圖,物化視圖可以增量地重新計算其內容,不需要通過查詢原始表來更新其數據。

       計數器表在web應用中很常見,例如記錄每個用戶的發送信息的數量、下載文件的數量,利用計數器表可以很簡單的獲取個人的記錄數量,比直接從相關數據表中統計的效率高很多,比如一張計數器表,記錄網站的點擊數量,若只有一行記錄,那麼在條記錄上只能存在一個排它鎖,更新查詢操作等只能串行進行,影響併發,若將記錄保存在多行上,每次隨機選擇一行進行更新,那麼併發性能就會大幅提高。多行記錄與單行記錄的表設計,要參照具體的應用。

二、 索引優化

     索引應該是對查詢性能優化最有效的手段了,它能夠輕易地將查詢效率提高几個數量級。

1.  索引類型

     索引類型包括:b-tree索引、哈希索引、空間數據索引和全文索引等。

     B-tree索引採用b-tree方法實現索引,存儲字段值,而且索引列是順序組織存儲的,適合order by、範圍查找等操作。哈希索引存儲的是字段值的哈希值,並不是真正的字段值,另外哈希索引無序,不適合做order by或範圍查找等操作。空間索引是用來做地理數據存儲,但是mysql對地理信息的支持並不是太好,最好是使用postgis。全文索引是一種特殊的索引類型,它查找的是文本中的關鍵詞,而不是全值對比,在同一列上可以創建全文索引和基於值b-tree索引,不會相沖突,全文索引使用於matchagainst,而不是普通的where操作。

2.  索引策略

     瞭解索引類型後,正確地創建和使用索引是提高查詢性能的基礎。

1)  獨立的列

     獨立的列就是索引列應該是單獨使用不能作爲表達式的一部分或函數的參數,例如下面的例子:

Select actor_idfrom actor where actor_id+1=5;

Select actor_idfrom actor where to_days(current_date)-to_days(date_col)<5;

這兩種方式均無法使用索引。

2)  前綴索引

      有時候需要在很長的字符列上添加索引,這樣索引就會很大,會造成很慢。最好的方式就是使用字符串的部分前綴創建索引,前綴索引可以大大減少索引空間,提高索引效率。對於blob、text或很長的varchar類型列必須使用前綴索引,因爲mysql不允許索引這些列的完整內容。前綴索引的缺點是mysql無法使用前綴索引做order by和group by,無法覆蓋掃描。在創建前綴索引時,要保證選擇足夠多的前綴,以保證較高的選擇性,同時又不能太長以節約空間。

3)  多列索引

      多列索引是相對單列索引來說的,單列索引就是在單個列上創建索引,多列索引就是在多個列上創建索引。Where後面有多個條件時,多列索引往往比單列索引更有效。例如,

Select * fromactor where actor_id=1 or film_id=0;

雖然mysql在執行sql時,會執行一個“索引合併”的優化,但是這只是優化的結果,說明了表上的索引創建的很糟糕。

三、 查詢優化

1. 重構查詢方式

1) 切分查詢

         主要用於刪除信息時,一次性刪除大量數據時,則可能會需要鎖住很多數據、佔滿整個事務日誌,耗盡系統資源,阻塞很多查詢。因此對於影響很多數據的delete語句,切分成多個較小的delete語句,能夠快速執行完成,大大減少刪除持有鎖的時間,降低對mysql性能的影響。

2) 分解關聯查詢

         有時候一個大的關聯查詢,可能會很慢,如何把這個關聯查詢分解成多個單表查詢,然後在應用中對數據執行關聯操作。Mysql可以實現單表查詢緩存,因此多個單表查詢,可以有效利用查詢緩存。單表查詢可以減少鎖的影響。將查詢的數據在應用中關聯,可以更容易對數據庫拆分,實現數據庫的高性能和可擴展。可以減少冗餘記錄的查詢,而且在應用中做關聯,能夠數據之間的哈希關聯,避免在mysql中的嵌套循環查詢,效率就會大幅提高。

2. Mysql查詢優化器的限制

         服務器收到sql後,對其進行解析、預處理,並由查詢優化器處理生成對應的執行計劃。查詢優化器就是進一步對sql進行優化處理,使其執行速率更高。但是對一些特定的sql語句的寫法,查詢優化器是有侷限的,並不能實現優化處理。因此,在書寫sql語句時,要避開這些寫法,下面是對查詢優化器有侷限的sql寫法分類:

1)  關聯子查詢

         使用in()方法加子查詢,比如下面sql語句:

Select * from filmwhere film_id in(select film_id from film_actor where actor_id=1);

優化器會把該sql語句改成如下:

Select * from filmwhere exists(select * from film_actor where actor_id=1 and film.film_id=

film_actor.film_id);

由於子查詢關聯到外部表,需要用到外部表的film_id,因此這個sql無法先執行子查詢,只能先執行外部表的查詢,找到film_id,根據film_id執行子查詢語句。如果外部表數據非常大,那麼這個sql執行時間就會很長。因此,使用in()加子查詢,性能經常會很低,通常使用exists代替in()來獲取更高的效率

另外對於關聯子查詢的效率不如使用左外連接(left out join)效率高,這種觀點是不正確的,要具體分析。建議通過具體測試驗證。

2) Union的限制

         Union的限制外部的條件無法滲入到內層的查詢中,如下sql語句:

(Selectfirst_name,last_name from actor order by last_name) union all (selectfirst_name,

last_name fromcustomer order by last_name) limit 20;

假如actor表有200條記錄,customer表有900條記錄,那麼這條sql是掃描出actor的200條記錄和customer的900條記錄,然後將這些記錄放入臨時表時,再從臨時表中取出前20條記錄。可以通過在兩個子查詢上分別加上limt來減少臨時表的數據,見下:

(Selectfirst_name,last_name from actor order by last_name limit 20) union all (selectfirst_name,

last_name fromcustomer order by last_name limit 20) limit 20;

另外需要注意的是,臨時表的順序不一定是正確的,最好在外部加上一個全局的order by操作。

3) 最大值和最小值計算

      Max()與min()方法,mysql優化的不是太好,比如要查找最小的actor_id,如下sql語句:

Selectmin(actor_id) from film_actor;

Actor_id是主鍵,但是也會做全表掃描。曲線救國的方式,來改變查詢提交效率,就是移除min方法,改後的sql語句如下:

Select actor_idorder by desc limit 1;

但是,這條sql語句已經無法表達它的本意了,但是有時候爲了獲得更高的查詢性能,不得不放棄一些原則。

4) 不能同時在同一個表上執行查詢和更新

         如下sql語句:

Update tb1 asouter_tb1 set cnt=(select count(*) from tb1 as inner_tb1 where inner_tb1.type=

Outer_tb1.type);

這條sql語句是無法執行的,不過可以通過生成表的形式來繞過上面的限制,因爲mysql會把這個表當成臨時表來處理。這實際上執行了兩個查詢:一個是子查詢中的select語句,另一個是多表關聯update,只是關聯的表是一個臨時表,子查詢會在update語句打開表之前就完成,所以下面的sql能正常執行:

Update tb1 innerjoin(select type,count(*) as cnt from tb1 group by type) as der using(type) set

Tb1.cnt=der.cnt;

3. 優化特定類型的查詢

      對於特定類型的查詢,mysql具有相應的優化機制,當要使用這些特定類型的查詢時,遵循優化機制,才能達到性能最佳。下面介紹這些特定類型的查詢:

1)  優化count()查詢

         Count()函數是用來統計記錄行數或某列值的數量,只統計不爲null的值,例如count

(name),只統計name列不爲null的個數,若name列都不是null,那麼count(name)等價於

Count(*)。當爲count(*)時,是統計所有的行數。

         在沒有任何where條件的count()函數,查詢速率非常快,因爲mysql此時無需計算實際的行數,而是利用存儲引擎的特性直接獲取這個值。因此帶有where條件的查詢的行數越多,count()方法效率就會越低,可以通過間接的方式優化此類情況的查詢,例如下面的sql語句:

Select count(*)from city where id>5;

當id>5時,該查詢需要掃描上萬條數據時,而id<=5需要掃描的條數很少時,就可以通過如下的方式提高查詢效率:

Select (selectcount(*) from city) –count(*) from city where id<=5;

這樣就會大大降低掃描的行數,mysql查詢優化階段會把內部查詢結果當成一個常量來處理。

         另外,我們也可以使用count來統計同一列不同內容的數量,如下sql語句:

Selectcount(color=’blue’ or null) as bluecount,count(color=’red’ or null ) asredcount from city;

         對於不需要精確統計行數時,比如統計某網站的在線的活躍人數,那麼就可以利用explain命令來獲取近似值。如若要獲取精確的行數,記錄由非常多時,帶有where的統計,效率肯定會低,並且使用索引也達不到要求時,這時就需要修改應用架構了,比如使用匯總表或外部緩存等。

2)  優化關聯查詢

        當使用關聯查詢時,確保on或using子句中的列上有索引。確保group by和order by表達式只涉及到一個表中的列,這樣才能使用索引來優化這個過程。

3) 優化子查詢

         優化子查詢最重要的建議是儘可能的使用關聯查詢。但是在mysql5.6版本以上不用考慮這個問題。

4) 優化group by和distinct

         Mysql內部優化器會相互轉化這兩種查詢,它們都可以使用索引來來優化,而且索引優化是最優效的方式。使用標識列分組效率會比其它列更高,例如下面sql:

Selectactor.first_name,actor.last_name,count(*) from film_actor inner join actorusing(actor_id)

Group byactor.fist_name,actor.last_name;

改成使用標識列分組,效率會更高,見下面sql:

Selectactor.first_name,actor.last_name,count(*) from film_actor inner join actorusing(actor_id)

Group byactor.actor_id;

需要注意的是這種查詢是利用了actor_id與first_name、last_name的直接關係,改寫後結果不受影響。另外,這種sql寫法在oracle是行不通的,會報ORA-00979錯誤,因此oracle要求分組列必須包括所有的非組合函數涉及到的列,對於mysql這種寫法也不是都是可以是,可以通過將mysql的sql_mode設置爲ONLY_FULL_GROUP_BY來禁止這種模式。對於以上的寫法有時會造成查詢結果不準確,比如有如下數據:

如果想找到每個class裏面的最大的age,則需要使用group by和max。如下的sql語句,則輸出結果有錯誤:

得到的結果,姓名和年齡並不對應,若sql_mode設置爲ONLY_FULL_GROUP_BY,上述寫法會直接報錯的。可以採用下面的寫法:

Select id,namefrom test inner join( select max(age),class from test group by class) tusing(class);

或者

Select * from(select * from test order by age desc) as t group by class;

這兩種方式,均用到子查詢,成本就會有點高,因爲子查詢會創建臨時表。

5) 優化limit分頁

      Mysql中使用limit m,n函數分頁,其中m代表位置偏移量,n代表獲取的行數。在使用limit分頁時,通常加上order by子句。如果有對應的索引的,效率一般會不錯。但是若位置偏移量非常大時,比如limit 10000,20,那麼mysql需要查詢100020條記錄,只返回最後20條記錄,前面10000條記錄都被拋棄掉,這樣的代價很大,效率會很低。

      優化此類分頁查詢最簡單的方法就是儘可能的執行索引覆蓋掃描,而不是查詢所有的列,然後根據需要做一次關聯操作再返回所需要的列。例如下面的例子:

Selectfilm_id,description from film order by title limit 10000,20;

那麼查詢改寫成下面的形式:

Selectfilm_id,description from film inner join(select film_id from film order bytitle limit 10000,20) as A using(film_id).

這樣的方式就會大大提高查詢效率,讓mysql掃描儘可能少的頁面,獲取需要訪問的記錄後再根據關聯列回原表查詢所有的列。

      Limit的分頁效率問題,主要就是位置偏移量的問題,當位置偏移量很大時,mysql需要掃描大量的行並拋棄掉。如果可以使用書籤記錄上次取數據的位置,那麼下次就可以直接從該書籤記錄的位置開始掃描,而不需要位置偏移量。例如,若需要按租借記錄做翻頁,那麼可以根據最新一條租借記錄向後追溯,這種方法可行是因爲租借記錄的主鍵是單調增加的。首先使用下面的sql獲取第一組結果:

Select * fromrental order by rental_id desc limit 20;

假設上面的返回的的主鍵16049到16030的記錄,那麼下一頁的查詢sql爲:

Select * fromrental where rental_id<16030 order by rental_id desc limit 20;

該技術的好處是無論翻到多少頁,性能都是好的。

      以上是通過學習《高性能mysql》這本書,根據個人理解,對一些關鍵的知識做了總結,以便以後翻看學習。俗話說“好腦袋不如爛筆頭”,就是這個道理。

      本文只是總結了基礎的mysql性能優化的知識,更高級的性能優化,比如分區、分表等,請參考《高性能mysql》、mycat組件學習資料等。

 

 

 

 

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章