SQL優化必懂的知識點

1. 基數


單個列唯一鍵(distict_keys)的數量叫做基數。比如性別列,該列只有男女之分,拋開中性,所以這一列基數就是主鍵列的基數等於表的總行數。基數的高低影響列的數據分佈。

test 表的總函數是 7,gender 列的基數是 2,說明 gender 列裏面有大量重複值,phone 列的基數等於總行數,說明 phone 列沒有重複值,相當於主鍵。gender 列的數據分佈如下:

gender 列的數據分佈極其不均衡,運行如下 SQL。

gender 爲 1 有 4 條數據,從 7 條數據裏查詢 4 條數據,也就是說要返回表中超過 50% 的數據。

MariaDB [test]>select4/7*100"percent from dual";

+-------------------+

| percentfromdual |

+-------------------+

|57.1429|

+-------------------+

1rowinset(0.00sec)

那麼請思考,你認爲如上查詢應該使用索引?現在我們換一種查詢語句。

MariaDB [test]> select * fromtestwherephone='13054480665';

+----+--------+-------------+

| id | gender | phone       |

+----+--------+-------------+

|  1 |      1 | 13054480665 |

+----+--------+-------------+

1 rowinset(0.00 sec)

phone 等值條件有 1 條數據,從 7 條數據裏查詢 1 條數據,也就是說要返回表中 14% 的數據。

MariaDB [test]>select1/7*100"percent from dual";

+-------------------+

| percentfromdual |

+-------------------+

|14.2857|

+-------------------+

1rowinset(0.00sec)

請思考,返回表中 14% 的數據是否走索引?

如果你還不懂索引,沒關係,可以看下筆者其他相關的 chat。如果你回答不了上述問題,我們先提醒一下。當查詢結果返回表中 30% 內的數據時,應該走索引(表中數據量小,其實 phone 的等值查詢也是);當查詢結果返回的是超過表中 30% 數據時,基本會走全表掃描。

當然了,返回表中 30% 內的數據會走索引,返回超過 30% 數據就使用全表掃描,這個結論太絕對了,但其實大多場景下,你先記住這個 30% 這個界限吧。這裏之所以讓記住 30% 這個界限,是不想讓初學者爲了答案糾結,其實工作中真返回超過 30% 的數據量,本身業務角度就有問題,尤其在 oltp 業務下。

現在有如下查詢語句:

select*fromtestwheregender=:b1;

語句中 :b1 是綁定變量,可以傳入任何值,該查詢可能走索引也可能走全表掃描。

現在得到一個結論:如果一個列基數很低,該列數據分佈不均衡,由於該列數據分佈極度不均衡,會導致 SQL 查詢可能走索引,也可能走全表掃描。

在做 SQL 優化時,如果懷疑該列數據分佈不均衡,我們可以使用 select 列,count(*) from 表 group by 列 order by 2 desc 來查看列的數據分佈。

如果 SQL 語句是單表訪問,可能走索引掃描,也可能走全表掃描,也可能走物理物化視圖掃描。在不考慮物理物化視圖的情況下,單表訪問要麼走索引掃描,要麼走全表掃描。

現在,回憶一下,走索引的條件:返回表中 30% 內的數據要麼走索引,要麼走全表掃描。相信大家看到這裏,已經懂得單表訪問的優化方法。

我們來看如下查詢:

select*fromtestwherephone=:b1;

不管 phone 傳入任何值,都應該走索引。


2.選擇性


基數與總行數的比值再乘 100% 就是一個列的選擇性。

在進行 SQL 優化的時候,單獨看列的基數是沒有任何意義的,基數相對於總行數纔有實際意義,正是這個原因,我們才引出選擇性這個概念。

請思考,什麼樣的列必須建立索引?

有人說基數高的列,有人說在 where 條件中的列。這些答案並不完美。基數高究竟多高?沒有和總行數對比,始終不知道有多高。

比如一個列的基數是幾萬行,但是總數是十幾億行,那麼這個列的基數還高?這就是引出選擇性的根本原因。

對於如下 SQL

select*fromtestwherephone=:b1;

不管 phone 傳入任何值,最多返回1條。

什麼樣的列必須要創建索引呢?當一個列出現在 where 條件中,該列沒有創建索引並且選擇性大於 20% 時,那麼該列必須創建索引,從而提升 SQL 查詢性能。當然了,如果表只有幾百條數據,那我們就不用創建索引了。

下面拋出 SQL 優化核心的第一個觀點:只有大表纔會產生性能問題。

也許有人會說:“我有個表很小,只有幾百條,但是經常進行 DML,會產生熱點塊,也會出性能問題。”對此我們並不想過多的討論,這屬於應用程序設計問題,不屬於 SQL 優化的範疇。


3.回表


當對一個列創建索引之後,索引會包含該列的鍵值及鍵值對應行所在的 rowid。通過索引中記錄的 rowid 訪問表中的數據就叫回表。

回表一般是單塊讀,回表次數太多會嚴重影響 SQL 性能,如果回表次數太多,就不應該走索引掃描,應該直接走全表掃描。

在進行 SQL 優化時,一定要注意回表次數!特別是注意回表的物理 IO 次數。

MariaDB [test]> explain select * from test where gender=1;

+------+-------------+-------+------+---------------+------+---------+------+------+-------------+

| id   |select_type| table |type| possible_keys |key| key_len |ref| rows |Extra|

+------+-------------+-------+------+---------------+------+---------+------+------+-------------+

|1| SIMPLE      |test| ALL  |NULL| NULL |NULL| NULL |7| Using where |

+------+-------------+-------+------+---------------+------+---------+------+------+-------------+

1rowinset (0.00sec)

此 SQL 執行計劃的 ALL,也就是說全表掃描,但是 select * 需要查詢表中所有的列,也就是回表。

在此處,引出 SQL 優化有一核心點:減少回表,也就是網絡傳輸消耗。筆者公司前段時間剛出現大量回表導致小部分用戶,使用功能超時。

什麼樣的 SQL 必須要回表?

select*fromtestwhere.......;

這樣的 SQL 就必須要回表,所以我們一般禁止使用 select *。那麼什麼樣的 SQL 不需要回表?

selectcount(*)fromtest;

這樣的 SQL 就無需回表。

當要查詢的列也包含在索引中,這個時候就不需要回表,所以我們往往會建立組合索引來消除回表,從而提升性能。

當一個 SQL 有多個過濾條件但是隻有一個列或者部分列建立了索引,這個時候回出現回表再過濾,也需要創建組合索引,進而消除回表再過濾,從而提升查詢性能。

關於回表有些專業名詞,筆者是借用 Oracle 數據中的,其實思想是想通的。


4.集羣因子


集羣因子用於判斷索引回表需要消耗的物理 IO 次數。

我們先對測試表 test 的 object_id 累創建一個索引 idx_id。

createindexidx_idontest(object_id);

然後我們查看該索引的集羣因子。(基於 Oracle 的)

selectonwer,index_name,clustering_factorfromdba_indexeswhereowner="scott"andindex_name="IDX_ID';

索引 idx_id 的葉子塊中有序存儲了索引的鍵值及鍵值對應行所在的 rowid。

sql>select*from(selectobject_id,rowidfromtestwhereobject_idisnotnullorderbyobject_id)whererownum<=5;

object_id     rowid

2             AAASNJAAEAAAAAITAAw

3             AAASNJAAEAAAAAITAAf

4             AAASNJAAEAAAAAITAAx

5             AAASNJAAEAAAAAITAAa

6             AAASNJAAEAAAAAITAAV

集羣因子的算法如下:

首先我們比較 2、3 對應的 rowid 是否在同一個塊,如果在同一個塊 clustering_factor+0,如果不在同一個數據塊,clustering_factor+1。

然後我們比較 3、4 對應的 rowid 是否在同一個數據塊,如果在同一個塊 clustering_factor+0,如果不在同一個數據塊,clustering_factor+1。

如上面步驟一樣,一直這樣有序的比較下去,直到比較完索引中最後一個鍵值。

根據算法,我們直到集羣因子介於表的塊數和表的行數之間。

如果集羣因子與塊數接近,表明表的數據基本上是有序的,而且其順序基本與索引順序一致。這樣在進行索引範圍掃描或者全索引掃描時,回表只需要讀取少量的數據塊就能完成。

如果集羣因子與表記錄數接近時,表明表的數據和索引順序差別很大,在進行索引範圍掃描或者索引全掃描的時候,回表會讀取更多的數據塊。

集羣因子只會影響索引範圍掃描及索引全掃描,因爲只有這兩種索引掃描數據會有大量數據回表。

集羣因子不會影響索引唯一掃描,因爲索引唯一掃描只返回一條數據。集羣因子更不會影響索引快速掃描,因爲索引快速掃描不回表。

集羣因子到底影響的是什麼性能?集羣因子影響的是索引回表的物理 IO 次數。

我們假設索引範圍掃描返回了 1000 行數據,如果 buffer  cache 中沒有緩存表的數據塊,假設這 1000 行數據都在同一數據塊中,那麼回表就需要耗費 1 個物理 IO;

假設這 1000 行數據都在不同的數據塊中,那麼回表就需要消耗 1000 個物理 IO。因此集羣因子影響索引回表的物理 IO 次數。

請注意,不要嘗試重建索引來降低集羣因子,這根本沒用,因爲表中的數據順序始終沒變。

唯一能降低集羣因子的辦法就是根據索引列排序對錶進行重建(creeate table new_table as select * from old_table order by 索引列),但是這在實際操作中是不可取的,因爲我們無法照顧到每一個索引。

怎麼才能避免集羣因子對 SQL 查詢性能產生影響?集羣因子隻影響索引範圍掃描和索引全掃描。當索引範圍掃描,索引全掃描不回表或者返回數據量很少的時候,不管集羣因子多大,對SQL查詢性能幾乎不受影響。

重點強調一下,在進行 SQL 優化時,大多會建立合適的組合索引消除回表,或者建立組合索引儘量減少回表次數。


5.表與表之間的關係


關係數據庫中,表與表之間會進行關聯,在進行關聯的時候,我們一定要清楚表與表之間的關係。表與表之間存在三種關係。

一種是 1:1,一種是 1:n,最後一種是 n:n 關係。搞懂表與表之間的關係,對於 SQL 優化、SQL 等價改寫、表設計及分表分庫都有幫助。

兩表再進行關聯的時候,如果兩表屬於 1:1 關係,關聯之後返回的機構也屬於 1:1 的關係,數據不會重複。如果兩表屬於 1:n 關係,關聯之後的結果集屬於 1:n 的關係。

如果兩表屬於 n:n 關係,關聯之後的結果集會產生局部的笛卡爾積,n:n 關係一般不存在內 / 外連接中,只能存在於半連接或者反連接中。

如果我們不知道業務,不知道數據字典,怎麼判斷兩表是什麼關係?我們用下面 SQL:

select*fromemp e,dept dwheree.deptno=d.deptno;

我們只需要對兩表進行彙總就可以知道兩表什麼關係:

selectdeptno,count(*)fromempgroupbydeptnoorderby2desc;

selectdeptno,count(*)fromdeptgroupbydeptnoorderby2desc;

從上面 SQL 的結果集發現,emp 和 dept 是 n:1 的關係。搞清楚表與表之間的關係對於 SQL 優化很有用,這是最基本的,也是應用程序設計的基礎。

總之所述知識點都是 SQL 優化的基礎點,更是一個應用程序開發的基礎,如果連基數、表關係等都不懂,盲目去做業務開發和表設計,基本是災難。

在筆者公司的技術人員裏,不少資深應用程序開發老司機設計表簡直就是瞎玩,SQL 出現慢就只知道加索引,壓根不看基數,連那種狀態值的列也加。

本來就是大表,經常查詢的狀態值得結果集都是超過 30%。程序研發是操作數據,可是數據相關的基礎都沒有,縱使你換最牛逼的語言也是歇菜。

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