1. 程式人生 > >劍指架構師系列-MySQL調優

劍指架構師系列-MySQL調優

日期轉換 存在 重復 mysq 更新 unique mps 方便 like

介紹MySQL的調優手段,主要包括慢日誌查詢分析與Explain查詢分析SQL執行計劃

1、MySQL優化

1、慢日誌查詢分析

首先需要對慢日誌進行一些設置,如下:

SHOW VARIABLES LIKE ‘slow_query_log‘;        -- 查看是否開啟了慢查詢
SET GLOBAL slow_query_log_file=‘/var/lib/mysql/mysql-slow.log‘;  -- 設置慢查詢日誌的位置
SET GLOBAL log_queries_not_using_indexes=ON; -- 是否記錄未使用索引的查詢
SET GLOBAL long_query_time=1;                -- 設置記錄超過多長時間的SQL語句
SET GLOBAL slow_query_log=ON;                -- 設置慢查詢日誌是否開啟

然後我新建t_report_app、t_application與t_developer表,t_report_app中有2萬多條數據,執行如下查詢後,總共用時1.019sec,超過1秒後將記錄到慢日誌中。

SELECT * FROM t_report_app  r ,t_application app,t_developer dev 
WHERE r.application_id = app.id AND app.developer_id = dev.id   

查看mysql-slow.log輸出日誌信息,如果有許多歷史信息,可以先使得如下命令清空:

echo "" > mysql-slow.log

重新運行後,進行查詢,主要的信息如下:

# [email protected]: root[root] @  [192.168.0.190]  Id:    51             -- 執行的用戶root和主機Host
# Query_time: 22.834043  Lock_time: 0.000458 Rows_sent: 20724  Rows_examined: 277  
SET timestamp=1469083853; 
SELECT * FROM t_report_app  r ,t_application app,t_developer dev  -- 執行的相關內容
WHERE r.application_id = app.id AND app.developer_id = dev.id;

我們也可以借助MySQL自帶的慢查詢分析工具mysqldumpslow,可以通過mysqldumpslow -h來查看具體的使用方法。

mysqldumpslow -t 3 mysql-slow.log | more

執行如上的語句將輸出最耗時的3條SQL語句。輸出格式如下:

Count: 1 (執行次數)Time=0.00s (0s) (執行時間)Lock=0.00s (0s)(鎖定時間) Rows=2.0 (2), root[root]@[192.168.0.190](以root身份在ip客戶端上執行如下內容)
SELECT QUERY_ID, SUM(DURATION) AS SUM_DURATION, SEQ FROM INFORMATION_SCHEMA.PROFILING GROUP BY QUERY_ID LIMIT N, N (執行的SQL內容)

還可以通過pt-query-digest來進行分析,不過首先要安裝這個工具,如下:

yum install perl-DBD-MySQL
perl Makefile.PL
make
make test
make install

安裝完成後就可以使用如下命令進行查看分析SQL語句了。

cd  /var/lib/mysql         -- 切換到存有慢日誌的文件夾中
echo "" >mysql-slow.log    -- 清空之前的記錄
pt-query-digest mysql-slow.log | more

輸出如下:

# 250ms user time, 60ms system time, 25.30M rss, 219.89M vsz
# Current date: Thu Jul 21 17:12:52 2016
# Hostname: localhost.localdomain
# Files: mysql-slow.log
# Overall: 2 total, 2 unique, 0 QPS, 0x concurrency ______________________
# Time range: all events occurred at 2016-07-21 17:12:47
# Attribute          total     min     max     avg     95%  stddev  median
# ============     ======= ======= ======= ======= ======= ======= =======
# Exec time             1s     1ms      1s   611ms      1s   862ms   611ms
# Lock time           12ms   357us    12ms     6ms    12ms     8ms     6ms
# Rows sent           1016      16    1000     508    1000  695.79     508
# Rows examine         384     107     277     192     277  120.21     192
# Query size           317     142     175  158.50     175   23.33  158.50

# Profile
# Rank Query ID           Response time Calls R/Call V/M   Item
# ==== ================== ============= ===== ====== ===== ===============
#    1 0x67E16746140E091A  1.2207 99.9%     1 1.2207  0.00 SELECT t_report_app
# MISC 0xMISC              0.0012  0.1%     1 0.0012   0.0 <1 ITEMS>

# Query 1: 0 QPS, 0x concurrency, ID 0x67E16746140E091A at byte 0 ________
# This item is included in the report because it matches --limit.
# Scores: V/M = 0.00
# Time range: all events occurred at 2016-07-21 17:12:47
# Attribute    pct   total     min     max     avg     95%  stddev  median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count         50       1
# Exec time     99      1s      1s      1s      1s      1s       0      1s
# Lock time     97    12ms    12ms    12ms    12ms    12ms       0    12ms
# Rows sent     98    1000    1000    1000    1000    1000       0    1000
# Rows examine  72     277     277     277     277     277       0     277
# Query size    44     142     142     142     142     142       0     142
# String:
# Hosts        192.168.0.190
# Users        root
# Query_time distribution
#   1us
#  10us
# 100us
#   1ms
#  10ms
# 100ms
#    1s  ################################################################
#  10s+
# Tables
#    SHOW TABLE STATUS LIKE ‘t_report_app‘\G
#    SHOW CREATE TABLE `t_report_app`\G
# EXPLAIN /*!50100 PARTITIONS*/
SELECT * FROM t_report_app  r ,t_application app,t_developer dev 
WHERE r.application_id = app.id AND app.developer_id = dev.id
 LIMIT 0, 1000\G

為developer_id與application_id添加了Normal索引後,清空mysql-slow.log,重新運行SQL,結果如下:

# 210ms user time, 130ms system time, 25.30M rss, 219.89M vsz
# Current date: Thu Jul 21 17:43:08 2016
# Hostname: localhost.localdomain
# Files: mysql-slow.log
# Overall: 2 total, 2 unique, 0 QPS, 0x concurrency ______________________
# Time range: all events occurred at 2016-07-21 17:43:01
# Attribute          total     min     max     avg     95%  stddev  median
# ============     ======= ======= ======= ======= ======= ======= =======
# Exec time           36ms     1ms    35ms    18ms    35ms    24ms    18ms
# Lock time          305us   125us   180us   152us   180us    38us   152us
# Rows sent           1016      16    1000     508    1000  695.79     508
# Rows examine       1.17k      90   1.08k     599   1.08k  719.83     599
# Query size           318     142     176     159     176   24.04     159

# Profile
# Rank Query ID           Response time Calls R/Call V/M   Item
# ==== ================== ============= ===== ====== ===== ===============
#    1 0x67E16746140E091A  0.0350 97.2%     1 0.0350  0.00 SELECT t_report_app
# MISC 0xMISC              0.0010  2.8%     1 0.0010   0.0 <1 ITEMS>

# Query 1: 0 QPS, 0x concurrency, ID 0x67E16746140E091A at byte 0 ________
# This item is included in the report because it matches --limit.
# Scores: V/M = 0.00
# Time range: all events occurred at 2016-07-21 17:43:01
# Attribute    pct   total     min     max     avg     95%  stddev  median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count         50       1
# Exec time     97    35ms    35ms    35ms    35ms    35ms       0    35ms
# Lock time     59   180us   180us   180us   180us   180us       0   180us
# Rows sent     98    1000    1000    1000    1000    1000       0    1000
# Rows examine  92   1.08k   1.08k   1.08k   1.08k   1.08k       0   1.08k
# Query size    44     142     142     142     142     142       0     142
# String:
# Hosts        192.168.0.190
# Users        root
# Query_time distribution
#   1us
#  10us
# 100us
#   1ms
#  10ms  ################################################################
# 100ms
#    1s
#  10s+
# Tables
#    SHOW TABLE STATUS LIKE ‘t_report_app‘\G
#    SHOW CREATE TABLE `t_report_app`\G
# EXPLAIN /*!50100 PARTITIONS*/
SELECT * FROM t_report_app  r ,t_application app,t_developer dev 
WHERE r.application_id = app.id AND app.developer_id = dev.id
 LIMIT 0, 1000\G

2、Explain查詢和分析SQL的執行計劃

使用Explain查詢分析SQL的執行計劃,如下:

EXPLAIN SELECT * FROM t_report_app  r ,t_application app,t_developer dev 
WHERE r.application_id = app.id AND app.developer_id = dev.id

輸出如下結果:

技術分享

為developer_id與application_id添加了Normal索引,執行結果如下:

技術分享

解釋如下:

列名稱 描述
table 顯示查詢是關於哪個表的
type 很重要的列,顯示連接使用了何種類型。從最好到最差的連接類型為const、eq_reg、ref、range、index和ALL
possible_keys 顯示可能應用在這張表中的索引。如果為空,沒有可能應用的索引
key 實際使用的索引。如果為NULL,則沒有使用索引
key_len 使用的索引的長度。在不損失精確性的情況下,長度越短越好
ref 顯示索引的哪一列被使用了
rows MYSQL認為必須檢查的用來返回請求的行數
extra 當這一列的值是Using filesort(需要進行額外的排序)或Using temporary(需要臨時表進行處理)時,說明查詢需要優化了

Using filesort:MYSQL需要進行額外的步驟來發現如何對返回的行排序。它根據連接類型以及存儲排序鍵值和匹配條件的全部行的行指針來排序全部行

Using temporary:MYSQL需要創建一個臨時表來存儲結果,這通常發生在對不同的列集進行ORDER BY上,而不是GROUP BY上

關於type字段的含義:

新建一個表,表名為tb_c,其中id是聚集索引,b和c建立了聯合索引。

(1)ALL:全表掃描

explain select * from tb_c where c=2 

技術分享

用不到索引時只能是全表掃描

(2)const:讀常量,且最多只會有一條記錄匹配,由於是常量,所以實際上只需要讀一次;

select b,c from tb_c where b=2

技術分享

還有一個const的特例,表有且僅有一行滿足條件,如下:

select * from (select b,c from tb_c where b=2) a

技術分享

(3)eq_ref:最多只會有一條匹配結果,一般是通過主鍵或者唯一鍵索引來訪問;

(4)index:全索引掃描;

該聯接類型與ALL相同,除了只有索引樹被掃描。這通常比ALL快,因為索引文件通常比數據文件小。當查詢只使用作為單索引一部分的列時,MySQL可以使用該聯接類型

select c from tb_c where b>2 

技術分享


(5)rang:索引範圍掃描。這個連接類型使用索引返回一個範圍中的行,比如使用>或<查找東西時發生的情況

select * from tb_c where b>2

技術分享

  

(6)ref:Join 語句中被驅動表索引引用查詢;這個連接類型只有在查詢使用了不是唯一或主鍵的鍵或者是這些類型的部分(比如,利用最左邊前綴)時發生。對於之前的表的每一個行聯合,全部記錄都將從表中讀出。這個類型嚴重依賴於根據索引匹配的記錄多少—越少越好

各個屬性可以參考:

(1)http://blog.csdn.net/xifeijian/article/details/19773795

(2)http://www.cnitblog.com/aliyiyi08/archive/2016/04/21/48878.html

2、MySQL常用SQL優化

1、 只返回需要的數據

返回數據到客戶端至少需要數據庫提取數據、網絡傳輸數據、客戶端接收數據以及客戶端處理數據等環節。如果返回不需要的數據,就會增加服務器、網絡和客戶端的無效勞動,其害處是顯而易見的,避免這類事件需要註意:

A、 橫向來看,不要寫SELECT * 的語句,而是選擇你需要的字段

B、 縱向來看,合理寫WHERE子句,不要寫沒有WHERE的SQL語句

C、對於聚合查詢,可以用HAVING子句進一步限定返回的行

2、 盡量少做重復的工作

這一點的側重點在客戶端程序,需要註意的如下:

A、控制同一語句的多次執行,特別是一些基礎數據的多次執行是很多程序員很少註意的

B、減少多次的數據轉換,也許需要數據轉換是設計的問題,但是減少次數是程序員可以做到的

C、杜絕不必要的子查詢和連接表,子查詢在執行計劃一般解釋成外連接,多余的連接表帶來額外的開銷??

D、UPDATE操作不要拆成(DELETE+INSERT)操作的形式,雖然功能相同,但是性能差別是很大的。

3、 子查詢的用法

子查詢可以使我們的編程靈活多樣,可以用來實現一些特殊的功能。但是在性能上,往往一個不合適的子查詢用法會形成一個性能瓶頸。

如果子查詢的條件中使用了其外層的表的字段,這種子查詢就叫作相關子查詢。相關子查詢可以用IN、NOT IN、EXISTS、NOT EXISTS引入。

A、NOT IN、NOT EXISTS的相關子查詢可以改用LEFT JOIN代替寫法

SELECT pub_name FROM publishers WHERE pub_id NOT IN (SELECT pub_id FROM titles WHERE TYPE =‘bubiness‘)

SELECT a.pub_name FROM publishers a LEFT JOIN titles b ON b.type = ‘business‘ AND a.pub_id = b.pub_id WHERE b.pub_id IS NULL

SELECT titile FROM titles WHERE NOT EXISTS (SELECT title_id FROM sales WHERE title_id = titles.title_id)
SELECT title FROM titles LEFT JOIN sales ON sales.title_id = titles.title_id WHERE sales.title_id IS NULL

B、如果保證子查詢沒有重復 ,IN、EXISTS的相關子查詢可以用INNER JOIN 代替。如果有重復,則內連接會去重,所以就不等價了

SELECT pub_name FROM publishers WHERE pub_id  IN (SELECT pub_id FROM titles WHERE TYPE =‘bubiness‘)
SELECT DISTINCT a.pub_name FROM publishers a INNER JOIN titles b ON b.type = ‘business‘ AND a.pub_id = b.pub_id

C、IN的相關子查詢用EXISTS代替

SELECT pub_name FROM publishers WHERE pub_id IN (SELECT pub_id FROM titles WHERE TYPE = ‘business‘)
SELECT pub_name FROM publishers WHERE EXISTS (SELECT 1 FROM titles WHERE TYPE = ‘business‘ AND pub_id = publishers.pub_id)

如上的語句完全正確,註意紅色為1,而不是L(l)

D、不要用COUNT(*)的子查詢判斷是否存在記錄,最好用LEFT JOIN或者EXISTS

SELECT id FROM jobs  WHERE (SELECT COUNT(*) FROM employee WHERE employee.id = jobs.id)<>0
SELECT id FROM jobs  WHERE EXISTS(SELECT 1 FROM employee WHERE employee .id = jobs.id)

4、盡量使用索引

索引固然可以提高相應的 select 的效率,但同時也降低了insert及update的效率,因為insert或update時有可能會重建索引,所以怎樣建索引需要慎重考慮,視具體情況而定。一個表的索引數最好不要超過6個,若太多則應考慮一些不常使用到的列上建的索引是否有必要。

為了使得優化器能高效使用索引,寫語句的時候應該註意:

A、盡量使用字段索引的本體

SELECT ID FROM T WHERE NUM/2=NUM1 -- NUM有索引
SELECT ID FROM T WHERE NUM=NUM1*2 -- NUM1有索引

B、不要對索引字段進行格式轉換

應盡量避免在where子句中對字段進行表達式操作,應盡量避免在where子句中對字段進行函數操作

如果使用了索引,那麽查詢條件應使用被索引的字段本體,而不應該在這個字段上再使用其他函數,否則索引將失效。 例如,數據表中有個表示日期時間的字段A,其類型為datetime,為該字段添加了索引,當根據時間範圍進行查詢時,如果出現如下查詢條件:
SELECT * WHERE date(date_col) between '2009-2-1'and '2009-3-1'  # MySQL中也是可以這樣寫的

則索引失效,因為查詢條件並不是字段A的本體,而是使用了date函數。解決方法是把查詢條件改為:

select * where date_col between '2009-2-1 00:00:00'and '2009-3-1 23:59:59'

多個字段添加復合索引時,其中單個字段如果作為索引來使用則失去索引功能??有待討論

在customer_copy表中為id添加了索引,如下語句卻會導致全表掃描 select * from customer_copy where id = ‘1‘ and cname=‘bb‘ 所以應考慮在 where 及 order by 涉及的列上建立索引,並且要進行強制使用才可以

C、不要對索引字段使用函數

WHERE LEFT(NAME, 3)=’ABC’ 或者 WHERE SUBSTRING(NAME,1, 3)=’ABC’
WHERE NAME LIKE ‘ABC%’

MySQL的日期轉換為字符串:

SELECT * FROM time_test WHERE DATE_FORMAT( date_col,‘%Y-%m-%d‘)>‘2010-08-01‘ AND DATE_FORMAT( date_col, ‘%Y-%m-%d‘)<‘2020-08-03‘

5、 註意連接條件的寫法

多表連接的連接條件對索引的選擇有著重要的意義,所以我們在寫連接條件的時候需要特別的註意。

A、 多表連接的時候,連接條件必須寫全,寧可重復,不要缺漏。

B、 連接條件盡量使用聚集索引

C、 註意ON部分條件和WHERE部分條件的區別 ???兩個SQL語句執行時有什麽不同呢?

6、 其他需要註意的地方

經驗表明,問題發現的越早解決的成本越低,很多性能問題可以在編碼階段就發現,為了提早發現性能問題,需要註意:

A、 程序員註意、關心各表的數據量。

B、 編碼過程和單元測試過程盡量用數據量較大的數據庫測試,最好能用實際數據測試。

C、 每個SQL語句盡量簡單

D、 不要頻繁更新有觸發器的表的數據

E、 註意數據庫函數的限制以及其性能

7、 對於Where子句及含有如下一些關鍵字時,要具體的使用執行計劃查看:

在出現where 子句時,尤其是where子句中含有is null、!=、<>、or、in、not in時,應盡量避免在 where 子句中使用!= 或 <>操作符,

MySQL只有對以下操作符才使用索引:<、<=、=、>、>=、between、in,以及某些時候的like。

A、“應盡量避免在WHERE子句中對字段進行 NULL 值判斷,否則將導致引擎放棄使用索引而進行全表掃描,如:

SELECT ID FROM T WHERE NUM IS NULL
SELECT ID FROM T WHERE NUM=0”

可以在NUM上設置默認值0,確保表中NUM列沒有NULL值,然後這樣查詢:

個人意見:經過測試,IS NULL也是可以用INDEX SEEK查找的,0和NULL是不同概念的,以上說法的兩個查詢的意義和記錄數是不同的。

2、 “應盡量避免在 WHERE 子句中使用!=或<>(表示不等於)操作符,否則將引擎放棄使用索引而進行全表掃描。”

個人意見:經過測試,<>也是可以用INDEX SEEK查找的。

3、MySQL大數據量分頁優化

(1)高效的統計行數:

select count(*) from news;    // 一般這個性能會更好一些
select count(id) from news;   // 這個會執行全表掃描,看一下id為空的情況是不計入內的

count(*)要比count(id)要快,因為如果count()括號中寫列名,count就會統計該列有值的次數。

如果count()括號中寫*,通配符並不是匹配所有的列,而是直接統計行數,當執行沒有where子句的count(*)時,MySQL的查詢速度非常的快,因為MySQL事先已經知道表中的記錄數量,但是如果執行含有where子句的count(*)時,MySQL同樣會執行全表掃描,所以,我們應該盡量為where子句的列建立索引。

使用count的查詢很難優化,因為很多情況都需要執行全表掃描,唯一的優化方式就是盡量為where子句的表建立索引,更多的時候我們應該調整應用程序,避免使用count,比如,大數據分頁采用估算分頁法等等。

(2)查詢記錄

首先需要新建一張輔助分頁的表pagination,有id與page字段,類型都為整數。同步id值與t_report_app表中的id,如下:

INSERT INTO pagination(id) SELECT id FROM t_report_app

然後使用如下語句插入page值。

SET @p:= 0;
UPDATE pagination SET page=CEIL((@p:= @p + 1) / 10) ORDER BY id DESC;

我們按每頁10條記錄,id降序進行分頁。如果插入或刪除記錄則需要同步pagination表記錄。 

這樣如果我們查詢第10頁的記錄就可以直接知道t_report_app記錄中10條數據的id了,如下:

技術分享  

也可以用另外的辦法,如下:

SELECT id
FROM (
   SELECT id, ((@cnt:= @cnt + 1) + $perpage - 1) % $perpage temp -- 對查詢出來的記錄進行編號,以方便篩選每頁中第一條記錄的id
   FROM t_report_app 
   JOIN (SELECT @cnt:= 0)T      -- 重置@cnt參數為0
   -- WHERE id < $last_id
   ORDER BY id DESC
   LIMIT $perpage * $buttons   -- 頁面上有10個分頁按鈕,每頁10條記錄,則需要篩選出80條記錄
)C
WHERE temp = 0;                -- 篩選每頁中第一條記錄的id,總計10個  

為每一個分頁的按鈕計算出一個offset對應的id。可以將上面的結果存入表中,方面分頁時查詢。

INSERT INTO pagination2(id,page) (
 SELECT id, @cntb:= @cntb + 1 AS page
 FROM (
   SELECT id, ((@cnt:= @cnt + 1) + 10 - 1) % 10 cnt
   FROM t_report_app 
   JOIN (SELECT @cnt:= 0)T  
   ORDER BY id DESC
   
)C  JOIN (SELECT @cntb:= 0)D
WHERE cnt = 0 ORDER BY id DESC  )

查看pagination2中的數據,如下:  

技術分享

第10頁的Max(id)為593620,與之前的表中是一致的。

劍指架構師系列-MySQL調優