索引是對記錄集的多個字段進行排序的方法。在一張表中爲一個字段建立一個索引,將建立另一個數據結構,包含字段數值以及指向相關記錄的指針,而後對這個索引結構進行排序,容許在該數據上進行二分法排序。所謂索引,就是以某個字段爲關鍵字的B樹文件。
反作用是索引須要額外的磁盤空間,對於MyISAM引擎而言,這些索引是被統一保存在一張表中的,這個文件將很快到達底層文件系統所可以支持的大小限制,若是不少字段都創建了索引的話。html
很是重要的原則,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
好比a = 1 and b = 2 and c = 3 創建(a,b,c)索引能夠任意順序,mysql的查詢優化器會幫你優化成索引能夠識別的形式sql
區分度的公式是count(distinct col)/count(*),表示字段不重複的比例,比例越大咱們掃描的記錄數越少,惟一鍵的區分度是1,而一些狀態、性別字段可能在大數據面前區分度就是0,那可能有人會問,這個比例有什麼經驗值嗎?使用場景不一樣,這個值也很難肯定,通常須要join的字段咱們都要求是0.1以上,即平均1條掃描10條記錄。數據結構
保持列「乾淨」,好比from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,緣由很簡單,b+樹中存的都是數據表中的字段值,但進行檢索時,須要把全部元素都應用函數才能比較,顯然成本太大。因此語句應該寫成create_time = unix_timestamp(’2014-05-29’);函數
好比表中已經有a的索引,如今要加(a,b)的索引,那麼只須要修改原來的索引便可性能
根據最左匹配原則,最開始的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
關於explain命令相信你們並不陌生,具體用法和字段含義能夠參考官網 explain-output ,這裏須要強調rows是核心指標,絕大部分rows小的語句執行必定很快(有例外,下面會講到)。因此優化語句基本上都是在優化rows。unix
(1)先運行看看是否真的很慢,注意設置SQL_NO_CACHE
(2)where條件單表查,鎖定最小返回記錄表。這句話的意思是把查詢語句的where都應用到表中返回的記錄數最小的表開始查起,單表每一個字段分別查詢,看哪一個字段的區分度最高
(3)explain查看執行計劃,是否與1預期一致(從鎖定記錄較少的表開始查詢)
(4)order by limit 形式的sql語句讓排序的表優先查
(5)瞭解業務方使用場景
(6)加索引時參照建索引的幾大原則
(7)觀察結果,不符合預期繼續從0分析
下面幾個例子詳細解釋瞭如何分析和優化慢查詢
不少狀況下,咱們寫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)
舉這個例子的目的在於顛覆咱們對列的區分度的認知,通常上咱們認爲區分度越高的列,越容易鎖定更少的記錄,但在一些特殊的狀況下,這種理論是有侷限性的
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的使用場景很是關鍵,咱們只有知道這個業務場景,才能更好地輔助咱們更好的分析和優化查詢語句。