SQL優化與診斷

Explain診斷
Explain各參數的含義如下:

列名    說明
id    執行編號,標識select所屬的行。如果在語句中沒有子查詢或關聯查詢,只有唯一的select,每行都將顯示1.否則,內層的select語句一般會順序編號,對應於其在原始語句中的位置
select_type    顯示本行是簡單或複雜select,如果查詢有任何複雜的子查詢,則最外層標記爲PRIMARY(DERIVED、UNION、UNION RESUIT)
table    訪問引用哪個表(引用某個查詢,如“derived3”)
type    數據訪問/讀取操作類型(All、index、range、ref、eq_ref、const/system、NULL)
possible_key    揭示哪一些索引可能有利於高效的查找
key    顯示mysql實際決定採用哪個索引來優化查詢
key_len    顯示mysql在索引裏使用的字節數
ref    顯示了之前的表在key列記錄的索引中查找值所用的列或常量
rows    爲了找到所需要的行而需要讀取的行數,估算值
Extra    額外信息,如using index、filesort等


select_type 常見類型及其含義:


SIMPLE:不包含子查詢或者 UNION 操作的查詢
PRIMARY:查詢中如果包含任何子查詢,那麼最外層的查詢則被標記爲 PRIMARY
SUBQUERY:子查詢中第一個 SELECT
DEPENDENT SUBQUERY:子查詢中的第一個 SELECT,取決於外部查詢
UNION:UNION 操作的第二個或者之後的查詢
DEPENDENT UNION:UNION 操作的第二個或者之後的查詢,取決於外部查詢
UNION RESULT:UNION 產生的結果集
DERIVED:出現在 FROM 字句中的子查詢
type常見類型及其含義
system:這是 const 類型的一個特例,只會出現在待查詢的表只有一行數據的情況下
consts:常出現在主鍵或唯一索引與常量值進行比較的場景下,此時查詢性能是最優的
eq_ref:當連接使用的是完整的索引並且是 PRIMARY KEY 或 UNIQUE NOT NULL INDEX 時使用它
ref:當連接使用的是前綴索引或連接條件不是 PRIMARY KEY 或 UNIQUE INDEX 時則使用它
ref_or_null:類似於 ref 類型的查詢,但是附加了對 NULL 值列的查詢
index_merge:該聯接類型表示使用了索引進行合併優化
range:使用索引進行範圍掃描,常見於 between、> 、< 這樣的查詢條件
index:索引連接類型與 ALL 相同,只是掃描的是索引樹,通常出現在索引是該查詢的覆蓋索引的情況
ALL:全表掃描,效率最差的查找方式


阿里編碼規範要求:至少要達到 range 級別,要求是 ref 級別,如果可以是 consts 最好

key列
實際在查詢中是否使用到索引的標誌字段

SQL優化

1、limit分頁優化

當偏移量特別大時,limit效率會非常低。

SELECT id FROM A LIMIT 1000,10 很快

SELECT id FROM A LIMIT 90000,10 很慢

方案一

select id from A order by id limit 90000,10;
複製代碼

如果我們結合order by使用。很快,0.04秒就OK。 因爲使用了id主鍵做索引!當然,是否能夠使用索引還需要根據業務邏輯來定,這裏只是爲了提醒大家,在分頁的時候還需謹慎使用!


2、批量插入


# 反例
INSERT into person(name,age) values('A',24)
INSERT into person(name,age) values('B',24)
INSERT into person(name,age) values('C',24)
 
# 正例
INSERT into person(name,age) values('A',24),('B',24),('C',24);
 
# 說明
比較常規,就不多做說明了


3、like語句的優化


like語句一般業務要求都是 '%關鍵字%'這種形式,但是依然要思考能否考慮使用右模糊的方式去替代產品的要求,其中阿里的編碼規範提到:

頁面搜索嚴禁左模糊或者全模糊,如果需要請走搜索引擎來解決

# 反例(耗時78.843s)
EXPLAIN select * from task_result where taskid LIKE '%tt600e6b601677b5cbfe516a013b8e46%' LIMIT 1;
 
# 正例(耗時0.986s)
select * from task_result where taskid LIKE 'tt600e6b601677b5cbfe516a013b8e46%' LIMIT 1
 
##########################################################################
# 對正例的Explain
1    SIMPLE    task_result        range    adapt_id    adapt_id    98        99    100.00    Using index condition
 
# 對反例的Explain
1    SIMPLE    task_result        ALL                                        33628554    11.11    Using where
 
# 說明
task_result表爲生產環境的一個表,總數據量爲3400萬,taskid是一個普通索引列,可見%%這種匹配方式完全無法使用索引,從而進行全表掃描導致效率極低,而正例通過索引查找數據只需要掃描99條數據即可


4、避免SQL中對where字段進行函數轉換或表達式計算:


# 反例
select * from task_result where id + 1 = 15551;
 
# 正例
select * from task_result where id = 15550;
 
##########################################################################
# 對正例的Explain
1    SIMPLE    task_result        const    PRIMARY    PRIMARY    8    const    1    100.00    
 
# 對反例的Explain
1    SIMPLE    task_result        ALL                                    33631512  100.00    Using where
 
# 說明
其實在知道了有SQL優化器之後,我個人感覺這種普通的表達式轉換應該可以提前進行處理再進行查詢,這樣一來就可以用到索引了,但是問題又來了,如果mysql優化器可以提前計算出結果,那麼寫sql語句的人也一定可以提前計算出結果,所以矛盾點在這個地方,導致5.7版本以前的此種情況都無法使用索引吧,未來可能會對其進行優化


5、使用 ISNULL()來判斷是否爲 NULL 值


說明:NULL 與任何值的直接比較都爲 NULL

# 1) NULL<>NULL 的返回結果是 NULL,而不是 false。 
# 2) NULL=NULL 的返回結果是 NULL,而不是 true。 
# 3) NULL<>1 的返回結果是 NULL,而不是 true。

反例

SELECT id FROM A WHERE num IS NULL
複製代碼

在where子句中使用 IS NULL 或 IS NOT NULL 判斷,索引將被放棄使用,會進行全表查詢

正例

優化成num上設置默認值0,確保表中num沒有null值, IS NULL 的用法在實際業務場景下SQL使用率極高,我們應注意避免全表掃描

SELECT id FROM A WHERE num=0


6、多表查詢

 


我所在的公司基本禁止了多表查詢,那如果必須使用到的話,我們可以一起參考一下阿里的編碼規範

Eg:超過三個表禁止 join。需要 join 的字段,數據類型必須絕對一致;多表關聯查詢時,保證被關聯的字段需要有索引

明明有索引爲什麼還走全表掃描
之前回答一些面試問題的時候,對某一個點的理解出現了偏差,即我認爲只要查詢的列有索引則一定會使用索引去Push數據

然而實際上不僅僅是這樣,真正應該是:針對查詢的數據行佔總數據量過多時會轉化成全表查詢

那麼這個過多指代的是多少呢?

我的測試結果是50%,但個人認爲MySQL優化器不會完全糾結於行數區分是否全表,而是有很多其他因素綜合考慮發現全表掃描的效率更高等等,所以充分認識到該問題即可

 

7、count(*) 還是 count(id):

 


阿里的Java編碼規範中有以下內容:

【強制】不要使用 count(列名) 或 count(常量) 來替代 count(*)

count(*) 是 SQL92 定義的標準統計行數的語法,跟數據庫無關,跟 NULL 和非 NULL 無關。

說明:count(*)會統計值爲 NULL 的行,而 count(列名)不會統計此列爲 NULL 值的行

 

8、字段類型不同導致索引失效:


阿里的Java編碼規範中有以下內容:

【推薦】防止因字段類型不同造成的隱式轉換,導致索引失效

實際上數據庫在查詢的時候會作一層隱式的轉換,比如 varchar 類型字段通過 數字去查詢# 正例
EXPLAIN SELECT * FROM `user_coll` where pid = '1';
type:ref
ref:const    
rows:1    
Extra:Using index condition
 
# 反例
EXPLAIN SELECT * FROM `user_coll` where pid = 1;
type:index
ref:NULL    
rows:3(總記錄數)
Extra:Using where; Using index
 
# 說明
pid字段有相應索引,且格式爲varchar 

9、儘量用 union all 替換 union

union和union all的差異主要是前者需要將兩個(或者多個)結果集合並後再進行唯一性過濾操作,這就會涉及到排序,增加大量的cpu運算,加大資源消耗及延遲。所以當我們可以確認不可能出現重複結果集或者不在乎重複結果集的時候,儘量使用union all而不是union

10、Inner join 和 left join、right join、子查詢

  • 第一:inner join內連接也叫等值連接是,left/rightjoin是外連接。
SELECT A.id,A.name,B.id,B.name FROM A LEFT JOIN B ON A.id =B.id;

SELECT A.id,A.name,B.id,B.name FROM A RIGHT JOIN ON B A.id= B.id;

SELECT A.id,A.name,B.id,B.name FROM A INNER JOIN ON A.id =B.id;
複製代碼

經過來之多方面的證實 inner join性能比較快,因爲inner join是等值連接,或許返回的行數比較少。但是我們要記得有些語句隱形的用到了等值連接,如:

SELECT A.id,A.name,B.id,B.name FROM A,B WHERE A.id = B.id;

推薦:能用inner join連接儘量使用inner join連接

  • 第二:子查詢的性能又比外連接性能慢,儘量用外連接來替換子查詢。

反例

mysql是先對外表A執行全表查詢,然後根據uuid逐次執行子查詢,如果外層表是一個很大的表,我們可以想象查詢性能會表現比這個更加糟糕。

Select* from A where exists (select * from B where id>=3000 and A.uuid=B.uuid);
複製代碼

執行時間:2s左右

正例

Select* from A inner join B ON A.uuid=B.uuid where b.uuid>=3000;  這個語句執行測試不到一秒;
複製代碼

執行時間:1s不到

  • 第三:使用JOIN時候,應該用小的結果驅動大的結果

left join 左邊表結果儘量小,如果有條件應該放到左邊先處理,right join同理反向。如:

反例

Select * from A left join B A.id=B.ref_id where  A.id>10
複製代碼

正例

select * from (select * from A wehre id >10) T1 left join B on T1.id=B.ref_id;

11、exist & in 優化

SELECT * from A WHERE id in ( SELECT id from B )
複製代碼
SELECT * from A WHERE id EXISTS ( SELECT 1 from A.id= B.id )
複製代碼

分析:

in 是在內存中遍歷比較in()適合B表比A表數據小的情況

exist 需要查詢數據庫,所以當B的數據量比較大時,exists效率優於in**

in()只執行一次,把B表中的所有id字段緩存起來,之後檢查A表的id是否與B表中的id相等,如果id相等則將A表的記錄加入到結果集中,直到遍歷完A表的所有記錄。

測試表:

CREATE TABLE `oa_user` (
  `id` varchar(255) NOT NULL,
  `user_name` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `card_no` varchar(255) DEFAULT NULL,
  `sex` varchar(255) DEFAULT NULL,
  `depat` varchar(255) DEFAULT NULL,
  `createtime` varchar(255) DEFAULT NULL,
  `tel` varchar(255) DEFAULT NULL,
  `email` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;



測試數據:
BEGIN
	#Routine body goes here...
		DECLARE user_name VARCHAR(60);
    DECLARE email VARCHAR(150);
    DECLARE rand_id VARCHAR(120);
    DECLARE id VARCHAR(120);
    DECLARE i INT DEFAULT 1;
    DECLARE createtime DATETIME;
    DECLARE tel_body VARCHAR(40);
    DECLARE tel VARCHAR(60);

-- 調試過程, 先插入5條
    WHILE i <= 1000000 DO
      -- user_name = test + i
      SET user_name = CONCAT('test', i);
      SET email = CONCAT(user_name,'@qq.com');
      SET rand_id= SUBSTRING(MD5(RAND()),1,28);
      -- id = rand_id + i, +i的目的主要是爲了區分測試數據與user_name對應
      SET id = CONCAT(rand_id, i);
      SET createtime = NOW();
      SET tel_body = FLOOR(RAND()*100000000);
      -- tel = 159開頭隨機號碼
      SET tel = CONCAT('159', tel_body);
 
      INSERT INTO  `oa_user`
      VALUES(id,
        user_name, 
       '202cb962ac59075b964b07152d234b70', 
			 '202cb962ac59075b964b1213213',
       'M',
       '100001', 
       createtime,
       tel,
       email
       );
      SET i=i+1;
      END WHILE;
END


————————————————
版權聲明:本文爲CSDN博主「山人宿置酒」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/weixin_40834464/article/details/105714746

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