索引工做原理

什麼是索引(What is indexing)?

索引是對記錄集的多個字段進行排序的方法。在一張表中爲一個字段建立一個索引,將建立另一個數據結構,包含字段數值以及指向相關記錄的指針,而後對這個索引結構進行排序,容許在該數據上進行二分法排序。所謂索引,就是以某個字段爲關鍵字的B樹文件。
反作用是索引須要額外的磁盤空間,對於MyISAM引擎而言,這些索引是被統一保存在一張表中的,這個文件將很快到達底層文件系統所可以支持的大小限制,若是不少字段都創建了索引的話。html

1 建索引的幾大原則

1.1 最左前綴匹配原則

        很是重要的原則,mysql會一直向右匹配直到遇到範圍查詢(>、<、between、like)就中止匹配,好比a = 1 and b = 2 and c > 3 and d = 4 若是創建(a,b,c,d)順序的索引,d是用不到索引的,若是創建(a,b,d,c)的索引則均可以用到,a,b,d的順序能夠任意調整。mysql

1.2 =和in能夠亂序

        好比a = 1 and b = 2 and c = 3 創建(a,b,c)索引能夠任意順序,mysql的查詢優化器會幫你優化成索引能夠識別的形式sql

1.3 儘可能選擇區分度高的列做爲索引

        區分度的公式是count(distinct col)/count(*),表示字段不重複的比例,比例越大咱們掃描的記錄數越少,惟一鍵的區分度是1,而一些狀態、性別字段可能在大數據面前區分度就是0,那可能有人會問,這個比例有什麼經驗值嗎?使用場景不一樣,這個值也很難肯定,通常須要join的字段咱們都要求是0.1以上,即平均1條掃描10條記錄。數據結構

1.4 索引列不能參與計算

        保持列「乾淨」,好比from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,緣由很簡單,b+樹中存的都是數據表中的字段值,但進行檢索時,須要把全部元素都應用函數才能比較,顯然成本太大。因此語句應該寫成create_time = unix_timestamp(’2014-05-29’);函數

1.5 儘可能的擴展索引,不要新建索引

        好比表中已經有a的索引,如今要加(a,b)的索引,那麼只須要修改原來的索引便可性能

2  回到開始的慢查詢

        根據最左匹配原則,最開始的sql語句的索引應該是status、operator_id、type、operate_time的聯合索引;其中status、operator_id、type的順序能夠顛倒,因此我纔會說,把這個表的全部相關查詢都找到,會綜合分析;大數據

好比還有以下查詢優化

select * from task where status = 0 and type = 12 limit 10;
select count(*) from task where status = 0 ;

那麼索引創建成(status,type,operator_id,operate_time)就是很是正確的,由於能夠覆蓋到全部狀況。這個就是利用了索引的最左匹配的原則spa

3 查詢優化神器 - explain命令

        關於explain命令相信你們並不陌生,具體用法和字段含義能夠參考官網 explain-output ,這裏須要強調rows是核心指標,絕大部分rows小的語句執行必定很快(有例外,下面會講到)。因此優化語句基本上都是在優化rows。unix

4  慢查詢優化基本步驟

(1)先運行看看是否真的很慢,注意設置SQL_NO_CACHE

(2)where條件單表查,鎖定最小返回記錄表。這句話的意思是把查詢語句的where都應用到表中返回的記錄數最小的表開始查起,單表每一個字段分別查詢,看哪一個字段的區分度最高

(3)explain查看執行計劃,是否與1預期一致(從鎖定記錄較少的表開始查詢)

(4)order by limit 形式的sql語句讓排序的表優先查

(5)瞭解業務方使用場景

(6)加索引時參照建索引的幾大原則

(7)觀察結果,不符合預期繼續從0分析

五、 幾個慢查詢案例

下面幾個例子詳細解釋瞭如何分析和優化慢查詢

5.1  複雜語句寫法

不少狀況下,咱們寫SQL只是爲了實現功能,這只是第一步,不一樣的語句書寫方式對於效率每每有本質的差異,這要求咱們對mysql的執行計劃和索引原則有很是清楚的認識,請看下面的語句

select
  distinct cert.emp_id 
from
  cm_log cl 
inner join
  (
    select
      emp.id as emp_id,
      emp_cert.id as cert_id 
    from
      employee emp 
    left join
      emp_certificate emp_cert 
        on emp.id = emp_cert.emp_id 
    where
      emp.is_deleted=0
  ) cert 
    on (
      cl.ref_table='Employee' 
      and cl.ref_oid= cert.emp_id
    ) 
    or (
      cl.ref_table='EmpCertificate' 
      and cl.ref_oid= cert.cert_id
    ) 
where
  cl.last_upd_date >='2013-11-07 15:03:00' 
  and cl.last_upd_date<='2013-11-08 16:00:00';

(1)先運行一下,53條記錄 1.87秒,又沒有用聚合語句,比較慢

53 rows in set (1.87 sec)

(2)explain

+----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+
| id | select_type | table      | type  | possible_keys                   | key                   | key_len | ref               | rows  | Extra                          |
+----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+
|  1 | PRIMARY     | cl         | range | cm_log_cls_id,idx_last_upd_date | idx_last_upd_date     | 8       | NULL              |   379 | Using where; Using temporary   |
|  1 | PRIMARY     | <derived2> | ALL   | NULL                            | NULL                  | NULL    | NULL              | 63727 | Using where; Using join buffer |
|  2 | DERIVED     | emp        | ALL   | NULL                            | NULL                  | NULL    | NULL              | 13317 | Using where                    |
|  2 | DERIVED     | emp_cert   | ref   | emp_certificate_empid           | emp_certificate_empid | 4       | meituanorg.emp.id |     1 | Using index                    |
+----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+

簡述一下執行計劃,首先mysql根據idx_last_upd_date索引掃描cm_log表得到379條記錄;而後查表掃描了63727條記錄,分爲兩部分,derived表示構造表,也就是不存在的表,能夠簡單理解成是一個語句造成的結果集,後面的數字表示語句的ID。derived2表示的是ID = 2的查詢構造了虛擬表,而且返回了63727條記錄。咱們再來看看ID = 2的語句究竟作了寫什麼返回了這麼大量的數據,首先全表掃描employee表13317條記錄,而後根據索引emp_certificate_empid關聯emp_certificate表,rows = 1表示,每一個關聯都只鎖定了一條記錄,效率比較高。得到後,再和cm_log的379條記錄根據規則關聯。從執行過程上能夠看出返回了太多的數據,返回的數據絕大部分cm_log都用不到,由於cm_log只鎖定了379條記錄。

如何優化呢?能夠看到咱們在運行完後仍是要和cm_log作join,那麼咱們能不能以前和cm_log作join呢?仔細分析語句不難發現,其基本思想是若是cm_log的ref_table是EmpCertificate就關聯emp_certificate表,若是ref_table是Employee就關聯employee表,咱們徹底能夠拆成兩部分,並用union鏈接起來,注意這裏用union,而不用union all是由於原語句有「distinct」來獲得惟一的記錄,而union剛好具有了這種功能。若是原語句中沒有distinct不須要去重,咱們就能夠直接使用union all了,由於使用union須要去重的動做,會影響SQL性能。

優化過的語句以下

select
  emp.id 
from
  cm_log cl 
inner join
  employee emp 
    on cl.ref_table = 'Employee' 
    and cl.ref_oid = emp.id  
where
  cl.last_upd_date >='2013-11-07 15:03:00' 
  and cl.last_upd_date<='2013-11-08 16:00:00' 
  and emp.is_deleted = 0  
union
select
  emp.id 
from
  cm_log cl 
inner join
  emp_certificate ec 
    on cl.ref_table = 'EmpCertificate' 
    and cl.ref_oid = ec.id  
inner join
  employee emp 
    on emp.id = ec.emp_id  
where
  cl.last_upd_date >='2013-11-07 15:03:00' 
  and cl.last_upd_date<='2013-11-08 16:00:00' 
  and emp.is_deleted = 0

(3)不須要了解業務場景,只須要改造的語句和改造以前的語句保持結果一致

(4)現有索引能夠知足,不須要建索引

(5)用改造後的語句實驗一下,只須要10ms 下降了近200倍!

+----+--------------+------------+--------+---------------------------------+-------------------+---------+-----------------------+------+-------------+
| id | select_type  | table      | type   | possible_keys                   | key               | key_len | ref                   | rows | Extra       |
+----+--------------+------------+--------+---------------------------------+-------------------+---------+-----------------------+------+-------------+
|  1 | PRIMARY      | cl         | range  | cm_log_cls_id,idx_last_upd_date | idx_last_upd_date | 8       | NULL                  |  379 | Using where |
|  1 | PRIMARY      | emp        | eq_ref | PRIMARY                         | PRIMARY           | 4       | meituanorg.cl.ref_oid |    1 | Using where |
|  2 | UNION        | cl         | range  | cm_log_cls_id,idx_last_upd_date | idx_last_upd_date | 8       | NULL                  |  379 | Using where |
|  2 | UNION        | ec         | eq_ref | PRIMARY,emp_certificate_empid   | PRIMARY           | 4       | meituanorg.cl.ref_oid |    1 |             |
|  2 | UNION        | emp        | eq_ref | PRIMARY                         | PRIMARY           | 4       | meituanorg.ec.emp_id  |    1 | Using where |
| NULL | UNION RESULT | <union1,2> | ALL    | NULL                            | NULL              | NULL    | NULL                  | NULL |             |
+----+--------------+------------+--------+---------------------------------+-------------------+---------+-----------------------+------+-------------+
53 rows in set (0.01 sec)

5.2  明確應用場景

舉這個例子的目的在於顛覆咱們對列的區分度的認知,通常上咱們認爲區分度越高的列,越容易鎖定更少的記錄,但在一些特殊的狀況下,這種理論是有侷限性的

select
  * 
from
  stage_poi sp 
where
  sp.accurate_result=1 
  and (
    sp.sync_status=0 
    or sp.sync_status=2 
    or sp.sync_status=4
  );

(1)先看看運行多長時間,951條數據6.22秒,真的很慢

951 rows in set (6.22 sec)

(2)先explain,rows達到了361萬,type = ALL代表是全表掃描

+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows    | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
|  1 | SIMPLE      | sp    | ALL  | NULL          | NULL | NULL    | NULL | 3613155 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+

(3)全部字段都應用查詢返回記錄數,由於是單表查詢 0已經作過了951條

 

(4)讓explain的rows 儘可能逼近951,看一下accurate_result = 1的記錄數

 

select count(*),accurate_result from stage_poi  group by accurate_result;
+----------+-----------------+
| count(*) | accurate_result |
+----------+-----------------+
|     1023 |              -1 |
|  2114655 |               0 |
|   972815 |               1 |
+----------+-----------------+

咱們看到accurate_result這個字段的區分度很是低,整個表只有-1,0,1三個值,加上索引也沒法鎖定特別少許的數據

再看一下sync_status字段的狀況

select count(*),sync_status from stage_poi  group by sync_status;
+----------+-------------+
| count(*) | sync_status |
+----------+-------------+
|     3080 |           0 |
|  3085413 |           3 |
+----------+-------------+

一樣的區分度也很低,根據理論,也不適合創建索引

問題分析到這,好像得出了這個表沒法優化的結論,兩個列的區分度都很低,即使加上索引也只能適應這種狀況,很難作廣泛性的優化,好比當sync_status 0、3分佈的很平均,那麼鎖定記錄也是百萬級別的

(5)找業務方去溝通,看看使用場景。業務方是這麼來使用這個SQL語句的,每隔五分鐘會掃描符合條件的數據,處理完成後把sync_status這個字段變成1,五分鐘符合條件的記錄數並不會太多,1000個左右。瞭解了業務方的使用場景後,優化這個SQL就變得簡單了,由於業務方保證了數據的不平衡,若是加上索引能夠過濾掉絕大部分不須要的數據

(6)根據創建索引規則,使用以下語句創建索引

alter table stage_poi add index idx_acc_status(accurate_result,sync_status);

(7)觀察預期結果,發現只須要200ms,快了30多倍。

952 rows in set (0.20 sec)

咱們再來回顧一下分析問題的過程,單表查詢相對來講比較好優化,大部分時候只須要把where條件裏面的字段依照規則加上索引就好,若是隻是這種「無腦」優化的話,顯然一些區分度很是低的列,不該該加索引的列也會被加上索引,這樣會對插入、更新性能形成嚴重的影響,同時也有可能影響其它的查詢語句。因此咱們第4步調差SQL的使用場景很是關鍵,咱們只有知道這個業務場景,才能更好地輔助咱們更好的分析和優化查詢語句。

相關文章
相關標籤/搜索