開發人員MySQL調優-實戰篇2-讓SQL使用索引詳解

建議先看看開發人員MySQL調優-實戰篇0

讓執行的SQL使用索引

雖然DBA給咱們建了不少索引,但沒有經驗的開發人員每每只看錶結構,不太關注索引和如何利用索引提升SQL執行速度,下面羅列一些經驗,讓你寫的SQL更加高效的利用索引mysql

在作實驗以前最好先想想索引的數據結構與排序,以及索引工做的方式,才能更快的理解與記住這些點,不然在工做中遇到更復雜的狀況,極可能就不會處理了正則表達式

  • 查詢字段與某一個索引的結構徹底一致,包括字段和順序(徹底匹配)sql

create index idx_tv_v_user_u_a_g on tb_v_user(user_name,age,gendor);
select * 
  from tb_v_user 
 where user_name = '1F7sJ' 
   and age = 44 
   and gendor = 1;

查詢條件與索引的列與順序徹底一致,執行計劃掃描類型爲ref。有興趣的同窗能夠試試分別將user_name、age、gendor查詢條件刪除,看看執行計劃是什麼狀況數據庫

  • 不在列上作任何附加操做,好比加函數數據結構

select * 
  from tb_v_user 
 where left(user_name,5) = '1F7sJ' and age=44 and gendor = 1;

這裏的SQL和上面的SQL執行結果是如出一轍,寫法上區別是在user_name上使用了left函數,看看執行計劃oracle

變全表掃描了socket

若是在實際生產中,確實須要經過函數來處理列再查詢怎麼辦?函數

1.若是你的MySQL是5.7以上版本,能夠建立一個虛擬的列,而後在該列上建立函數索引。oracle能夠直接建立函數索引,比這個簡單一些工具

alter table tb_v_user add column user_name_t varchar as (left(user_name,5)) stored;
或者
alter table tb_v_user add column user_name_t varchar as (left(user_name,5)) virtual;
​
alter table tb_v_user add key idx_tb_v_user_ut(user_name_t);

2.若是你的MySQL是5.7如下版本oop

現有表上冗餘一個字段,浪費存儲,最重要的是還得修改之前的代碼

用觸發器往另一張表同步數據,使整個事務的流程更長,增長了不穩定性

 

  • 查詢時索引中的某一列已是range掃描了,那麼索引中後面列的掃描就沒法再使用索引了

select * 
  from tb_v_user 
 where user_name = '1F7sJ' 
   and age > 30 
   and gendor = 1;

雖然查詢條件和索引的列徹底一致,可是由於在age使用了range掃描,後面gendor的條件是沒法再使用索引的

  • 儘可能使用覆蓋索引

select user_name,age,gendor 
  from tb_v_user 
 where user_name = '1F7sJ' 
   and age = 44 
   and gendor = 1;

SQL語句只是將*替換成了指定的列,並且這些列都在索引中存在。像這種狀況,及時gendor查詢條件不能再走索引的掃描,可是數據的讀取仍是能夠走索引的,這樣能夠減小IO的次數

  • 減小使用不等於、is null 、is not null、or,會觸發全表掃描

    這個應該很明確吧,都屬於範圍類型的查詢,固然無法走索引掃描咯

  • like 使用時優先考慮右則使用通配符,若是最左側也須要通配符,儘可能走索引全掃描(index)

select user_id,user_name,age,gendor
  from tb_v_user 
 where user_name like '%1F7sJ%';

select user_id,user_name,age,gendor
  from tb_v_user 
 where user_name like '1F7sJ%';

全表(all)掃描變成了範圍(range)掃描

注:雖然like也是範圍掃描,可是like畢竟可以最終確認出一個指定的結果集,所以跟在like後面的查詢條件是能夠用到索引的。這一點和'>'出現的範圍掃描不同

若是需求必需要根據列中間的部分作模糊查詢,那麼儘可能使用索引覆蓋,減小IO次數

select user_name,age,gendor
  from tb_v_user 
 where user_name like '%1F7sJ%';

就是SQL查詢的列在索引列中存在

  • 若是列是字符串,必定以及千萬要加單引號,若是這個都不記得的話,估計你的實習期就過不了

    表某些列的值雖然都是阿拉伯數字,可是列類型倒是varchar,此時就很容易在寫SQL的時候當成數值類型,這樣寫會致使沒辦法使用到索引,好比給值加上單引號

  • 查詢條件+排序+分組的時候,也儘可能符合徹底匹配原則,特別是排序要緊隨查詢條件後面的列,不然會出現Using filesort

    總結:上面雖然羅列了很多內容,最終概況起來是:利用現有索引,再改變需求的狀況下,儘可能使用索引覆蓋,保持列的類型不變(不加函數、指明類型),減小模糊查詢。若是現有索引知足不了,而需求又必須實現,那麼就把DBA和需求負責人一塊兒拉過來討論討論吧,找一個折中的辦法,工做中通常碼農和維護人員都會屈服於需求人員的淫威

小表驅動大表

顧名思義就是用數據量小的表與數據量大的表作匹配,減少IO和比較次數。比較常見的三種場景以下,他們三者功能很相似,均可以用來作過濾,可是join經過豐富的關聯能實現不少集合操做。詳情能夠掉頭看看我以前的帖子

先準備點數據

create table tb_v_s_user 
select user_name,count(1) as cnt 
  from tb_v_user 
 group by user_name 
having COUNT(1)>5;
#由於後面的測試是基於user_name作的關聯,爲了減小IO次數,我使用了索引覆蓋優化方式
create index idx_tb_v_s_user_u on tb_v_s_user(user_name);

1.in

select user_name from tb_v_user where user_name in(select user_name from tb_v_s_user);

先從tb_v_s_user的索引中讀取出全部的user_name,而後在循環和tb_v_user索引中的user_name作比較(ref掃描),若是我把兩個表的順序換一下會如何

select user_name from tb_v_s_user where user_name in(select user_name from tb_v_user);

看來MySQL的優化器作了自動優化,並無傻傻的先加載大表tb_v_user。再使用in的時候,MySQL會自動以小表驅動大表的優化策略優化SQL

兩種編寫方式估算出來的rows都等於94=(93+1)

但我仍是推薦第一種寫法

2.exists

先將in的第一種寫法翻譯過來看看

select * from tb_v_user a
 where exists ( select 1 from tb_v_s_user b where a.user_name = b.user_name);

雖然都用到了索引,可是走了大表的索引全掃描,明顯IO的次數比in的第一種寫法會多不少不少,效率必定會查不少,這種方式就成了大表驅動小表,固然不是咱們想要的結果。接下來翻譯in的第二種寫法

select a.user_name from tb_v_s_user a
 where exists ( select 1 from tb_v_user b where a.user_name = b.user_name);

這下就變成了咱們想要的結果,小表驅動大表,而後估算出來的rows=94

3.join

大表寫在前:

select a.user_name from tb_v_user a join tb_v_s_user b on a.user_name = b.user_name;

小表寫在前:

select a.user_name from tb_v_s_user a join tb_v_user b on a.user_name = b.user_name;

能夠看出執行計劃並無改變,和用in同樣,MySQL仍是很智能的。

總結:in exists join三種方式均可以實現過濾,可是若是對原理掌握不是很好的話,可使用in 和 join,不過如今智能的東西不必定每次都對,所以在實際開發中,最好仍是看看執行計劃,不要太相信優化器。

排序

調優SQL

create index idx_tv_v_user_u_a_g on tb_v_user(user_name,age,gendor);
select user_name,age,gendor 
  from tb_v_user 
 where user_name = '1F7sJ' 
 order by age,gendor;
select user_name,age,gendor 
  from tb_v_user 
 where user_name > '1F7sJ' 
 order by age,gendor;

有上面的全部,先想一想這兩個誰會出現Using filesort,爲何它會出現,另一個不會出現

答案是第二種有Using filesort出現,第一種沒有,爲何喃?由於第一種最終查詢結果集user_name確定全都是1F7sJ,這一列再也不須要排序,而第二種user_name是不肯定的,且order by 子句與索引列不是前綴匹配(看下圖),可是結果集又包含了user_name,因此MySQL沒有已排好序的數據可拿,只能再作一次排序。

先來一個簡單一點的:

再來一個複雜一點的:

例子1:select user_name from tb_v_user order by user_name[,age,gendor]

列子2:select user_name from tb_v_user order where user_name = '1F7sJ' order by user_name[,age,gendor]

它們兩個都不會產生Using filesort

若是上面的還不能理解,但願下面的圖(來自於尚硅谷-周陽)能夠有用

 

注意:這裏查詢的列都指定爲了user_name而不是'*'。若是改爲'*'可能就演示不出來效果

調優參數

1.若是查詢語句有order by ,千萬當心使用select *

2.聯繫DBA調整sort_buffer_size參數

3.聯繫DBA調整max_length_for_sort_data

雖然能夠調大緩衝區的大小,可是針對大表,而又不可避免對大數據量的排序,並且仍是filesort,遇到這種狀況,建議在系統負載比較悠閒的時間段作,或者乾脆去離線的大數據方式解決

分組

首先得先明白什麼是分組,若是不懂的,出門左轉百度一下數據庫的分組是什麼意思,分組以後數據長什麼樣,能夠作哪些統計操做等。

分組其實就是先排序,而後對分組字段作統計操做,因此要優化分組,必須得先優化排序。所以排序的優化策略是徹底適用的,不一樣的是要減小寫分組過濾條件(having),能用where代替的就使用where,比較 having 後面跟的都是統計函數

慢查詢日誌

雖然咱們在測試環境對每一個SQL的執行計劃都作過驗證,可是並不表明在生產環境MySQL就乖乖的按照測試環境的執行計劃來跑,由於數據量徹底不同。因此當有新需求上線、數據割接應該打開MySQL的慢查詢日誌(通常數據量大的系統還會按期作慢查詢日誌分析),收集查詢時間大於long_query_time的SQL

查看慢查詢日誌相關參數

show variables like '%slow_query_log%';
+------------------------------------+----------------------------------+
| Variable_name                      | Value                            |
+------------------------------------+----------------------------------+
| slow_query_log                     | OFF                              |
| slow_query_log_always_write_time   | 10.000000                        |
| slow_query_log_file                | /var/lib/mysql/hadoop00-slow.log |
| slow_query_log_timestamp_always    | OFF                              |
| slow_query_log_timestamp_precision | second                           |
| slow_query_log_use_global_control  |                                  |
+------------------------------------+----------------------------------+
6 rows in set

查看慢查詢日誌記錄數

mysql> show global status like '%Slow_queries%'
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Slow_queries  | 1     |
+---------------+-------+
1 row in set

因此若是執行這個命令看到value很大,那麼你的系統必定是很慢的

臨時設置:

下面的設置只對當前會話有效,時間單位是秒

set global slow_query_log=1;
set long_query_time=3;
select user_id,count(1) from tb_vote group by user_id;

到主機目錄下查看有沒有將SQL打印出來

[root@hadoop00 /var/lib/mysql]$ cat hadoop00-slow.log 
/usr/sbin/mysqld, Version: 5.6.24-72.2 (Percona Server (GPL), Release 72.2, Revision 8d0f85b). started with:
Tcp port: 3306  Unix socket: /var/lib/mysql/mysql.sock
Time                 Id Command    Argument
# Time: 180612  7:19:47
# User@Host: root[root] @  [192.168.245.1]  Id:    10
# Schema: mydb  Last_errno: 0  Killed: 0
# Query_time: 3.603351  Lock_time: 0.000180  Rows_sent: 400000  Rows_examined: 1200000  Rows_affected: 0
# Bytes_sent: 10800115
use mydb;
SET timestamp=1528813187;
select user_id,count(1) from tb_vote group by user_id;

確實打印出來了

永久設置:

打開/etc/my.cnf文件,添加以下配置,時間單位是秒

#還能夠slow_query_log=true
slow_query_log=1
long_query_time=3
#還能夠設置日誌文件目錄和名稱
slow_query_log_file=/home/hadoop/data/mysql/mysql-slow.log

通常開發人員是沒有權限去修改這些的,須要聯繫DBA配合,並且長時間打開這對性能也是有必定影響的

DBA或者項目經理能夠藉助MySQL提供的mysqldumpslow工具對慢查詢日誌作分析

mysqldumpslow工具簡介

mysqldumpslow參數

s:按那種方式排序

c:訪問次數

l:鎖定時間

r:返回記錄

t:查詢時間

al:平均鎖定時間

ar:平均返回記錄數

at:平均查詢時間

t:返回前面多少條的數據

g:後半搭配一個正則表達式,忽略大小寫

舉例

1.獲得返回記錄最多的10個SQL

mysqldumpslow -s r -t 10 /home/hadoop/data/mysql/1.log

2.獲得訪問次數最多的10個SQL

mysqldumpslow -s c -t 10 /home/hadoop/data/mysql/2.log

3.獲得按照時間排序的前10條含有左鏈接的SQL

mysqldumpslow -s t -t 10 -g "left join" /home/hadoop/data/mysql/3.log

4.還能夠結果管道(|)和more使用

mysqldumpslow -s r -t 10 /home/hadoop/data/mysql/4.log | more

這個確實很簡單,對於通常開發人員估計不會讓你幹這種事情,都是DBA和項目經理來幹,須要對mysqldumpslow工具使用熟練,並且有多年的經驗,真的該如何去分析。新手拿到日誌文件,並且知道每一個參數的意思,可能短期也拿不出比較切實有效的統計結果

相關文章
相關標籤/搜索