MySQL · 性能優化 · MySQL常見SQL錯誤用法

1. LIMIT 語句

分頁查詢是最經常使用的場景之一,但也一般也是最容易出問題的地方。好比對於下面簡單的語句,通常DBA想到的辦法是在type, name, create_time字段上加組合索引。這樣條件排序都能有效的利用到索引,性能迅速提高。php

SELECT * 
FROM   operation 
WHERE  type = 'SQLStats' AND name = 'SlowLog' ORDER BY create_time LIMIT 1000, 10; 

好吧,可能90%以上的DBA解決該問題就到此爲止。但當 LIMIT 子句變成 「LIMIT 1000000,10」 時,程序員仍然會抱怨:我只取10條記錄爲何仍是慢?前端

要知道數據庫也並不知道第1000000條記錄從什麼地方開始,即便有索引也須要從頭計算一次。出現這種性能問題,多數情形下是程序員偷懶了。在前端數據瀏覽翻頁,或者大數據分批導出等場景下,是能夠將上一頁的最大值當成參數做爲查詢條件的。SQL從新設計以下:mysql

SELECT   * 
FROM     operation 
WHERE    type = 'SQLStats' AND name = 'SlowLog' AND create_time > '2017-03-16 14:00:00' ORDER BY create_time limit 10; 

在新設計下查詢時間基本固定,不會隨着數據量的增加而發生變化。程序員

2. 隱式轉換

SQL語句中查詢變量和字段定義類型不匹配是另外一個常見的錯誤。好比下面的語句:算法

mysql> explain extended SELECT * 
     > FROM   my_balance b 
     > WHERE  b.bpn = 14000000123 
     >       AND b.isverified IS NULL ;
mysql> show warnings;
| Warning | 1739 | Cannot use ref access on index 'bpn' due to type or collation conversion on field 'bpn' 

其中字段bpn的定義爲varchar(20),MySQL的策略是將字符串轉換爲數字以後再比較。函數做用於表字段,索引失效。sql

上述狀況多是應用程序框架自動填入的參數,而不是程序員的原意。如今應用框架不少很繁雜,使用方便的同時也當心它可能給本身挖坑。數據庫

3. 關聯更新、刪除

雖然MySQL5.6引入了物化特性,但須要特別注意它目前僅僅針對查詢語句的優化。對於更新或刪除須要手工重寫成JOIN。ruby

好比下面UPDATE語句,MySQL實際執行的是循環/嵌套子查詢(DEPENDENT SUBQUERY),其執行時間可想而知。bash

UPDATE operation o 
SET    status = 'applying' WHERE o.id IN (SELECT id FROM (SELECT o.id, o.status FROM operation o WHERE o.group = 123 AND o.status NOT IN ( 'done' ) ORDER BY o.parent, o.id LIMIT 1) t); 

執行計劃:app

+----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+ | 1 | PRIMARY | o | index | | PRIMARY | 8 | | 24 | Using where; Using temporary | | 2 | DEPENDENT SUBQUERY | | | | | | | | Impossible WHERE noticed after reading const tables | | 3 | DERIVED | o | ref | idx_2,idx_5 | idx_5 | 8 | const | 1 | Using where; Using filesort | +----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+ 

肯定從語義上查詢條件能夠直接下推後,重寫以下:

SELECT target, 
       Count(*) 
FROM   operation 
WHERE  target = 'rm-xxxx' GROUP BY target 

執行計劃變爲:

+----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+ | 1 | SIMPLE | operation | ref | idx_4 | idx_4 | 514 | const | 1 | Using where; Using index | +----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+ 

7. 提早縮小範圍

先上初始SQL語句:

SELECT * 
FROM   my_order o 
       LEFT JOIN my_userinfo u 
              ON o.uid = u.uid
       LEFT JOIN my_productinfo p 
              ON o.pid = p.pid 
WHERE  ( o.display = 0 ) AND ( o.ostaus = 1 ) ORDER BY o.selltime DESC LIMIT 0, 15 

該SQL語句原意是:先作一系列的左鏈接,而後排序取前15條記錄。從執行計劃也能夠看出,最後一步估算排序記錄數爲90萬,時間消耗爲12秒。

+----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+ | 1 | SIMPLE | o | ALL | NULL | NULL | NULL | NULL | 909119 | Using where; Using temporary; Using filesort | | 1 | SIMPLE | u | eq_ref | PRIMARY | PRIMARY | 4 | o.uid | 1 | NULL | | 1 | SIMPLE | p | ALL | PRIMARY | NULL | NULL | NULL | 6 | Using where; Using join buffer (Block Nested Loop) | +----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+ 

因爲最後WHERE條件以及排序均針對最左主表,所以能夠先對my_order排序提早縮小數據量再作左鏈接。SQL重寫後以下,執行時間縮小爲1毫秒左右。

SELECT * 
FROM (
SELECT * 
FROM   my_order o 
WHERE  ( o.display = 0 ) 
       AND ( o.ostaus = 1 ) 
ORDER  BY o.selltime DESC 
LIMIT  0, 15
) o 
     LEFT JOIN my_userinfo u 
              ON o.uid = u.uid 
     LEFT JOIN my_productinfo p 
              ON o.pid = p.pid 
ORDER BY  o.selltime DESC
limit 0, 15 

再檢查執行計劃:子查詢物化後(select_type=DERIVED)參與JOIN。雖然估算行掃描仍然爲90萬,可是利用了索引以及LIMIT 子句後,實際執行時間變得很小。

+----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+ | 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 15 | Using temporary; Using filesort | | 1 | PRIMARY | u | eq_ref | PRIMARY | PRIMARY | 4 | o.uid | 1 | NULL | | 1 | PRIMARY | p | ALL | PRIMARY | NULL | NULL | NULL | 6 | Using where; Using join buffer (Block Nested Loop) | | 2 | DERIVED | o | index | NULL | idx_1 | 5 | NULL | 909112 | Using where | +----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+ 

8. 中間結果集下推

再來看下面這個已經初步優化過的例子(左鏈接中的主表優先做用查詢條件):

SELECT    a.*, 
          c.allocated 
FROM      ( 
              SELECT   resourceid 
              FROM     my_distribute d 
                   WHERE    isdelete = 0 
                   AND      cusmanagercode = '1234567' ORDER BY salecode limit 20) a LEFT JOIN ( SELECT resourcesid, sum(ifnull(allocation, 0) * 12345) allocated FROM my_resources GROUP BY resourcesid) c ON a.resourceid = c.resourcesid 

那麼該語句還存在其它問題嗎?不難看出子查詢 c 是全表聚合查詢,在表數量特別大的狀況下會致使整個語句的性能降低。

其實對於子查詢 c,左鏈接最後結果集只關心能和主表resourceid能匹配的數據。所以咱們能夠重寫語句以下,執行時間從原來的2秒降低到2毫秒。

SELECT    a.*, 
          c.allocated 
FROM      ( 
                   SELECT   resourceid 
                   FROM     my_distribute d 
                   WHERE    isdelete = 0 
                   AND      cusmanagercode = '1234567' ORDER BY salecode limit 20) a LEFT JOIN ( SELECT resourcesid, sum(ifnull(allocation, 0) * 12345) allocated FROM my_resources r, ( SELECT resourceid FROM my_distribute d WHERE isdelete = 0 AND cusmanagercode = '1234567' ORDER BY salecode limit 20) a WHERE r.resourcesid = a.resourcesid GROUP BY resourcesid) c ON a.resourceid = c.resourcesid 

可是子查詢 a 在咱們的SQL語句中出現了屢次。這種寫法不只存在額外的開銷,還使得整個語句顯的繁雜。使用WITH語句再次重寫:

WITH a AS ( SELECT resourceid FROM my_distribute d WHERE isdelete = 0 AND cusmanagercode = '1234567' ORDER BY salecode limit 20) SELECT a.*, c.allocated FROM a LEFT JOIN ( SELECT resourcesid, sum(ifnull(allocation, 0) * 12345) allocated FROM my_resources r, a WHERE r.resourcesid = a.resourcesid GROUP BY resourcesid) c ON a.resourceid = c.resourcesid 

九、總結

數據庫編譯器產生執行計劃,決定着SQL的實際執行方式。可是編譯器只是盡力服務,全部數據庫的編譯器都不是盡善盡美的。

上述提到的多數場景,在其它數據庫中也存在性能問題。瞭解數據庫編譯器的特性,才能避規其短處,寫出高性能的SQL語句。

程序員在設計數據模型以及編寫SQL語句時,要把算法的思想或意識帶進來。

編寫複雜SQL語句要養成使用 WITH 語句的習慣。簡潔且思路清晰的SQL語句也能減少數據庫的負擔 。

相關文章
相關標籤/搜索