数据库优化:SQL高性能优化指南,助你成就大神之路!
1、參數是子查詢時,使用 EXISTS 代替 IN
如果 IN 的參數是(1,2,3)這樣的值列表時,沒啥問題,但如果參數是子查詢時,就需要注意了。比如,現在有如下兩個表:
現在我們要查出同時存在于兩個表的員工,即田中和鈴木,則以下用 IN 和 EXISTS 返回的結果是一樣,但是用 EXISTS 的 SQL 會更快:
--?慢 SELECT?*?FROM?Class_A WHERE?id?IN?(SELECT?id?FROM??CLASS_B);--?快 SELECT?*FROM?Class_A?A?WHERE?EXISTS (SELECT?*?FROM?Class_B??BWHERE?A.id?=?B.id);為啥使用 EXISTS 的 SQL 運行更快呢,有兩個原因
可以`用到索引,如果連接列 (id) 上建立了索引,那么查詢 Class_B 時不用查實際的表,只需查索引就可以了。
如果使用 EXISTS,那么只要查到一行數據滿足條件就會終止查詢, 不用像使用 IN 時一樣掃描全表。在這一點上 NOT EXISTS 也一樣
另外如果 IN 后面如果跟著的是子查詢,由于 SQL 會先執行 IN 后面的子查詢,會將子查詢的結果保存在一張臨時的工作表里(內聯視圖),然后掃描整個視圖,顯然掃描整個視圖這個工作很多時候是非常耗時的,而用 EXISTS 不會生成臨時表。
當然了,如果 IN 的參數是子查詢時,也可以用連接來代替,如下:
--?使用連接代替?IN?SELECT?A.id,?A.name FROM?Class_A?A?INNER?JOIN?Class_B?B?ON?A.id?=?B.id;用到了 「id」列上的索引,而且由于沒有子查詢,也不會生成臨時表
2、避免排序
SQL 是聲明式語言,即對用戶來說,只關心它能做什么,不用關心它怎么做。這樣可能會產生潛在的性能問題:排序,會產生排序的代表性運算有下面這些
GROUP BY 子句
ORDER BY 子句
聚合函數(SUM、COUNT、AVG、MAX、MIN)
DISTINCT
集合運算符(UNION、INTERSECT、EXCEPT)
窗口函數(RANK、ROW_NUMBER 等)
如果在內存中排序還好,但如果內存不夠導致需要在硬盤上排序上的話,性能就會急劇下降,所以我們需要減少不必要的排序。怎樣做可以減少排序呢。
1、 使用集合運算符的 ALL 可選項
SQL 中有 UNION,INTERSECT,EXCEPT 三個集合運算符,默認情況下,這些運算符會為了避免重復數據而進行排序,對比一下使用 UNION 運算符加和不加 ALL 的情況:
注意:加 ALL 是優化性能非常有效的手段,不過前提是不在乎結果是否有重復數據。
2、使用 EXISTS 代表 DISTINCT
為了排除重復數據, DISTINCT 也會對結果進行排序,如果需要對兩張表的連接結果進行去重,可以考慮用 EXISTS 代替 DISTINCT,這樣可以避免排序。
如何找出有銷售記錄的商品,使用如下 DISTINCT 可以:
SELECT?DISTINCT?I.item_no FROM?Items?I?INNER?JOIN?SalesHistory?SH ON?I.?item_no?=?SH.?item_no;不過更好的方式是使用 EXISTS:
SELECT?item_no?FROM?Items?I WHERE?EXISTS?(SELECT?*FROM?SalesHistory?SHWHERE?I.item_no?=?SH.item_no);既用到了索引,又避免了排序對性能的損耗。
2、在極值函數中使用索引(MAX/MIN)
使用 MAX/ MIN 都會對進行排序,如果參數字段上沒加索引會導致全表掃描,如果建有索引,則只需要掃描索引即可,對比如下
--?這樣寫需要掃描全表? SELECT?MAX(item)FROM?Items;--?這樣寫能用到索引? SELECT?MAX(item_no)FROM?Items;注意:極值函數參數推薦為索引列中并不是不需要排序,而是優化了排序前的查找速度(畢竟索引本身就是有序排列的)。
3、能寫在 WHERE 子句里的條件不要寫在 HAVING 子句里
下列 SQL 語句返回的結果是一樣的:
--?聚合后使用?HAVING?子句過濾 SELECT?sale_date,?SUM(quantity)FROM?SalesHistory?GROUP?BY?sale_date HAVING?sale_date?=?'2007-10-01';--?聚合前使用?WHERE?子句過濾 SELECT?sale_date,?SUM(quantity)FROM?SalesHistoryWHERE?sale_date?=?'2007-10-01'?GROUP?BY?sale_date;使用第二條語句效率更高,原因主要有兩點
使用 GROUP BY 子句進行聚合時會進行排序,如果事先通過 WHERE 子句能篩選出一部分行,能減輕排序的負擔
在 WHERE 子句中可以使用索引,而 HAVING 子句是針對聚合后生成的視頻進行篩選的,但很多時候聚合后生成的視圖并沒有保留原表的索引結構
4、在 GROUP BY 子句和 ORDER BY 子句中使用索引
GROUP BY 子句和 ORDER BY 子句一般都會進行排序,以對行進行排列和替換,不過如果指定帶有索引的列作為這兩者的參數列,由于用到了索引,可以實現高速查詢,由于索引是有序的,排序本身都會被省略掉
5、使用索引時,條件表達式的左側應該是原始字段
假設我們在 col 列上建立了索引,則下面這些 SQL 語句無法用到索引
SELECT?*FROM?SomeTableWHERE?col?*?1.1?>?100;SELECT?*FROM?SomeTableWHERE?SUBSTR(col,?1,?1)?=?'a';以上第一個 SQL 在索引列上進行了運算, 第二個 SQL 對索引列使用了函數,均無法用到索引,正確方式是把列單獨放在左側,如下:
SELECT?*FROM?SomeTableWHERE?col_1?>?100?/?1.1;當然如果需要對此列使用函數,則無法避免在左側運算,可以考慮使用函數索引,不過一般不推薦隨意這么做。
6、盡量避免使用否定形式
如下的幾種否定形式不能用到索引:
<>
!=
NOT IN
所以以下 了SQL 語句會導致全表掃描
SELECT?*FROM?SomeTableWHERE?col_1?<>?100;可以改成以下形式
SELECT?*FROM?SomeTableWHERE?col_1?>?100?or?col_1?<?100;7、進行默認的類型轉換
假設 col 是 char 類型,則推薦使用以下第二,三條 SQL 的寫法,不推薦第一條 SQL 的寫法
×?SELECT?*?FROM?SomeTable?WHERE?col_1?=?10; ○?SELECT?*?FROM?SomeTable?WHERE?col_1?=?'10'; ○?SELECT?*?FROM?SomeTable?WHERE?col_1?=?CAST(10,?AS?CHAR(2));雖然第一條 SQL 會默認把 10 轉成 '10',但這種默認類型轉換不僅會增加額外的性能開銷,還會導致索引不可用,所以建議使用的時候進行類型轉換。
8、減少中間表
在 SQL 中,子查詢的結果會產生一張新表,不過如果不加限制大量使用中間表的話,會帶來兩個問題,一是展示數據需要消耗內存資源,二是原始表中的索引不容易用到,所以盡量減少中間表也可以提升性能。
9、靈活使用 HAVING 子句
這一點與上面第八條相呼應,對聚合結果指定篩選條件時,使用 HAVING 是基本的原則,可能一些工程師會傾向于使用下面這樣的寫法:
SELECT?*FROM?(SELECT?sale_date,?MAX(quantity)?AS?max_qtyFROM?SalesHistory?GROUP?BY?sale_date)?TMPWHERE?max_qty?>=?10;雖然上面這樣的寫法能達到目的,但會生成 TMP 這張臨時表,所以應該使用下面這樣的寫法:
SELECT?sale_date,?MAX(quantity)?FROM?SalesHistoryGROUP?BY?sale_date HAVING?MAX(quantity)?>=?10;HAVING 子句和聚合操作是同時執行的,所以比起生成中間表后再執行 HAVING 子句,效率會更高,代碼也更簡潔
10、需要對多個字段使用 IN 謂詞時,將它們匯總到一處
一個表的多個字段可能都使用了 IN 謂詞,如下:
SELECT?id,?state,?city?FROM?Addresses1?A1WHERE?state?IN?(SELECT?stateFROM?Addresses2?A2WHERE?A1.id?=?A2.id)?AND?city?IN?(SELECT?cityFROM?Addresses2?A2?WHERE?A1.id?=?A2.id);這段代碼用到了兩個子查詢,也就產生了兩個中間表,可以像下面這樣寫
SELECT?*FROM?Addresses1?A1WHERE?id?||?state?||?cityIN?(SELECT?id?||?state||?cityFROM?Addresses2?A2);這樣子查詢不用考慮關聯性,沒有中間表產生,而且只執行一次即可。
11、 使用延遲查詢優化 limit [offset], [rows]
經常出現類似以下的 SQL 語句:
SELECT?*?FROM?film?LIMIT?100000,?10offset 特別大!
這是我司出現很多慢 SQL 的主要原因之一,尤其是在跑任務需要分頁執行時,經常跑著跑著 offset 就跑到幾十萬了,導致任務越跑越慢。
LIMIT 能很好地解決分頁問題,但如果 offset 過大的話,會造成嚴重的性能問題,原因主要是因為 MySQL 每次會把一整行都掃描出來,掃描 offset 遍,找到 offset 之后會拋棄 offset 之前的數據,再從 offset 開始讀取 10 條數據,顯然,這樣的讀取方式問題。
可以通過延遲查詢的方式來優化
假設有以下 SQL,有組合索引(sex, rating)
SELECT?<cols>?FROM?profiles?where?sex='M'?order?by?rating?limit?100000,?10;則上述寫法可以改成如下寫法
SELECT?<cols>?FROM?profiles? inner?join (SELECT?id?form?FROM?profiles?where?x.sex='M'?order?by?rating?limit?100000,?10) as?x?using(id);這里利用了覆蓋索引的特性,先從覆蓋索引中獲取 100010 個 id,在丟充掉前 100000 條 id,保留最后 10 個 id 即可,丟掉 100000 條 id 不是什么大的開銷,所以這樣可以顯著提升性能
12、 利用 LIMIT 1 取得唯一行
數據庫引擎只要發現滿足條件的一行數據則立即停止掃描,,這種情況適用于只需查找一條滿足條件的數據的情況
13、 注意組合索引,要符合最左匹配原則才能生效
假設存在這樣順序的一個聯合索引“col_1, col_2, col_3”。這時,指定條件的順序就很重要。
○?SELECT?*?FROM?SomeTable?WHERE?col_1?=?10?AND?col_2?=?100?AND?col_3?=?500; ○?SELECT?*?FROM?SomeTable?WHERE?col_1?=?10?AND?col_2?=?100?; ×?SELECT?*?FROM?SomeTable?WHERE?col_2?=?100?AND?col_3?=?500?;前面兩條會命中索引,第三條由于沒有先匹配 col_1,導致無法命中索引, 另外如果無法保證查詢條件里列的順序與索引一致,可以考慮將聯合索引 拆分為多個索引。
14、使用 LIKE 謂詞時,只有前方一致的匹配才能用到索引(最左匹配原則)
×?SELECT?*?FROM?SomeTable?WHERE?col_1?LIKE?'%a'; ×?SELECT?*?FROM?SomeTable?WHERE?col_1?LIKE?'%a%'; ○?SELECT?*?FROM?SomeTable?WHERE?col_1?LIKE?'a%';上例中,只有第三條會命中索引,前面兩條進行后方一致或中間一致的匹配無法命中索引
15、 簡單字符串表達式
模型字符串可以使用 _ 時, 盡可能避免使用 %, 假設某一列上為 char(5)
不推薦
SELECT?first_name,?last_name,homeroom_nbrFROM?StudentsWHERE?homeroom_nbr?LIKE?'A-1%';推薦
SELECT?first_name,?last_name homeroom_nbrFROM?StudentsWHERE?homeroom_nbr?LIKE?'A-1__';?--模式字符串中包含了兩個下劃線16、盡量使用自增 id 作為主鍵
比如現在有一個用戶表,有人說身份證是唯一的,也可以用作主鍵,理論上確實可以,不過用身份證作主鍵的話,一是占用空間相對于自增主鍵大了很多,二是很容易引起頻繁的頁分裂,造成性能問題(什么是頁分裂,請參考這篇文章)
主鍵選擇的幾個原則:自增,盡量小,不要對主鍵進行修改
17、在無 WHERE 條件下要計算表的行數,優先使用 count(*)
優先使用以下語句來統計行數, innoDB 5.6之后已經對此語句進行了優化
SELECT?COUNT(*)?FROM?SomeTable按照效率排序的話,count(字段)<count(主鍵 id)<count(1)≈count(*),count(*) 會選用性能最好的索引來進行排序
18、避免使用 SELECT * ,盡量利用覆蓋索引來優化性能
SELECT *?會提取出一整行的數據,如果查詢條件中用的是組合索引進行查找,還會導致回表(先根據組合索引找到葉子節點,再根據葉子節點上的主鍵回表查詢一整行),降低性能,而如果我們所要的數據就在組合索引里,只需讀取組合索引列,這樣網絡帶寬將大大減少,假設有組合索引列 (col_1, col_2)
推薦用
SELECT?col_1,?col_2?FROM?SomeTable?WHERE?col_1?=?xxx?AND?col_2?=?xxx不推薦用
SELECT?*FROM?SomeTable?WHERE?col_1?=?xxx?AND??col_2?=?xxx19、 如有必要,使用 force index() 強制走某個索引
業務團隊曾經出現類似以下的慢 SQL 查詢
SELECT?*FROM??SomeTableWHERE?`status`?=?0AND?`gmt_create`?>?1490025600AND?`gmt_create`?<?1490630400AND?`id`?>?0AND?`post_id`?IN?('67778',?'67811',?'67833',?'67834',?'67839',?'67852',?'67861',?'67868',?'67870',?'67878',?'67909',?'67948',?'67951',?'67963',?'67977',?'67983',?'67985',?'67991',?'68032',?'68038'/*...?omitted?480?items?...*/) order?by?id?asc?limit?200;post_id 也加了索引,理論上走 post_id 索引會很快查詢出來,但實現了通過 EXPLAIN 發現走的卻是 id 的索引(這里隱含了一個常見考點,在多個索引的情況下, MySQL 會如何選擇索引),而 id > 0 這個查詢條件沒啥用,直接導致了全表掃描, 所以在有多個索引的情況下一定要慎用,可以使用 force index 來強制走某個索引,以這個例子為例,可以強制走 post_id 索引,效果立竿見影。
這種由于表中有多個索引導致 MySQL 誤選索引造成慢查詢的情況在業務中也是非常常見,一方面是表索引太多,另一方面也是由于 SQL 語句本身太過復雜導致, 針對本例這種復雜的 SQL 查詢,其實用 ElasticSearch 搜索引擎來查找更合適,有機會到時出一篇文章說說。
20、 使用 EXPLAIN 來查看 SQL 執行計劃
上個點說了,可以使用 EXPLAIN 來分析 SQL 的執行情況,如怎么發現上文中的最左匹配原則不生效呢,執行 「EXPLAIN + SQL 語句」可以發現 key 為 None ,說明確實沒有命中索引
我司在提供 SQL 查詢的同時,也貼心地加了一個 EXPLAIN 功能及 sql 的優化建議,建議各大公司效仿 ^_^,如圖示
21、 批量插入,速度更快
當需要插入數據時,批量插入比逐條插入性能更高
推薦用
--?批量插入 INSERT?INTO?TABLE?(id,?user_id,?title)?VALUES?(1,?2,?'a'),(2,3,'b');不推薦用
INSERT?INTO?TABLE?(id,?user_id,?title)?VALUES?(1,?2,?'a'); INSERT?INTO?TABLE?(id,?user_id,?title)?VALUES?(2,3,'b');批量插入 SQL 執行效率高的主要原因是合并后日志量 MySQL 的 binlog 和 innodb 的事務讓日志減少了,降低日志刷盤的數據量和頻率,從而提高了效率
22、 慢日志 SQL 定位
前面我們多次說了 SQL 的慢查詢,那么該怎么定位這些慢查詢 SQL 呢,主要用到了以下幾個參數
這幾個參數一定要配好,再根據每條慢查詢對癥下藥,像我司每天都會把這些慢查詢提取出來通過郵件給形式發送給各個業務團隊,以幫忙定位解決
IT技術分享社區
個人博客網站:https://programmerblog.xyz
文章推薦程序員效率:畫流程圖常用的工具程序員效率:整理常用的在線筆記軟件遠程辦公:常用的遠程協助軟件,你都知道嗎?51單片機程序下載、ISP及串口基礎知識硬件:斷路器、接觸器、繼電器基礎知識
總結
以上是生活随笔為你收集整理的数据库优化:SQL高性能优化指南,助你成就大神之路!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 正弦定理和余弦定理_那些让你加快解题速度
- 下一篇: 数据库:Redis数据库优点介绍