springboot 整合mybatis(ssm框架)
優化成本:硬體>系統配置>資料庫表結構>SQL 及索引。
優化效果:硬體<系統配置<資料庫表結構<SQL 及索引。
首先,對於MySQL層優化我一般遵從五個原則:
-
減少資料訪問:設定合理的欄位型別,啟用壓縮,通過索引訪問等減少磁碟 IO。
-
返回更少的資料:只返回需要的欄位和資料分頁處理,減少磁碟 IO 及網路 IO。
-
減少互動次數:批量 DML 操作,函式儲存等減少資料連線次數。
-
減少伺服器 CPU 開銷:儘量減少資料庫排序操作以及全表查詢,減少 CPU 記憶體佔用。
-
利用更多資源:使用表分割槽,可以增加並行操作,更大限度利用 CPU 資源。
總結到 SQL 優化中,就如下三點:
-
最大化利用索引。
-
儘可能避免全表掃描。
-
減少無效資料的查詢。
理解 SQL 優化原理 ,首先要搞清楚 SQL 執行順序。
SELECT 語句,語法順序如下:
1.SELECT
2.DISTINCT<select_list>
3.FROM<left_table>
4.<join_type>JOIN<right_table>
5.ON<join_condition>
6.WHERE<where_condition>
7.GROUPBY<group_by_list>
8.HAVING<having_condition>
9.ORDERBY<order_by_condition>
10.LIMIT<limit_number>
SELECT 語句,執行順序如下:
FROM
<表名>#選取表,將多個表資料通過笛卡爾積變成一個表。
ON
<篩選條件>#對笛卡爾積的虛表進行篩選
JOIN<join,leftjoin,rightjoin...>
<join表>#指定join,用於新增資料到on之後的虛表中,例如leftjoin會將左表的剩餘資料新增到虛表中
WHERE
<where條件>#對上述虛表進行篩選
GROUPBY
<分組條件>#分組
<SUM()等聚合函式>#用於having子句進行判斷,在書寫上這類聚合函式是寫在having判斷裡面的
HAVING
<分組篩選>#對分組後的結果進行聚合篩選
SELECT
<返回資料列表>#返回的單列必須在groupby子句中,聚合函式除外
DISTINCT
#資料除重
ORDERBY
<排序條件>#排序
LIMIT
<行數限制>
以下 SQL 優化策略適用於資料量較大的場景下,如果資料量較小,沒必要以此為準,以免畫蛇添足。
避免不走索引的場景
①儘量避免在欄位開頭模糊查詢,會導致資料庫引擎放棄索引進行全表掃描
如下:
SELECT*FROMtWHEREusernameLIKE'%陳%'
優化方式:儘量在欄位後面使用模糊查詢。
如下:
SELECT*FROMtWHEREusernameLIKE'陳%'
如果需求是要在前面使用模糊查詢:
-
使用 MySQL 內建函式 INSTR(str,substr)來匹配,作用類似於 Java 中的 indexOf(),查詢字串出現的角標位置。
-
使用 FullText 全文索引,用 match against 檢索。
-
資料量較大的情況,建議引用 ElasticSearch、Solr,億級資料量檢索速度秒級。
-
當表資料量較少(幾千條兒那種),別整花裡胡哨的,直接用 like '%xx%'。
②儘量避免使用 in 和 not in,會導致引擎走全表掃描
如下:
SELECT*FROMtWHEREidIN(2,3)
優化方式:如果是連續數值,可以用 between 代替。
如下:
SELECT*FROMtWHEREidBETWEEN2AND3
如果是子查詢,可以用 exists 代替。
如下:
--不走索引
select*fromAwhereA.idin(selectidfromB);
--走索引
select*fromAwhereexists(select*fromBwhereB.id=A.id);
③儘量避免使用 or,會導致資料庫引擎放棄索引進行全表掃描
如下:
SELECT*FROMtWHEREid=1ORid=3
優化方式:可以用 union 代替 or。
如下:
SELECT*FROMtWHEREid=1
UNION
SELECT*FROMtWHEREid=3
④儘量避免進行 null 值的判斷,會導致資料庫引擎放棄索引進行全表掃描
如下:
SELECT*FROMtWHEREscoreISNULL
優化方式:可以給欄位新增預設值 0,對 0 值進行判斷。
如下:
SELECT*FROMtWHEREscore=0
⑤儘量避免在 where 條件中等號的左側進行表示式、函式操作,會導致資料庫引擎放棄索引進行全表掃描
可以將表示式、函式操作移動到等號右側,如下:
--全表掃描
SELECT*FROMTWHEREscore/10=9
--走索引
SELECT*FROMTWHEREscore=10*9
⑥當資料量大時,避免使用 where 1=1 的條件
通常為了方便拼裝查詢條件,我們會預設使用該條件,資料庫引擎會放棄索引進行全表掃描。
如下:
SELECTusername,age,sexFROMTWHERE1=1
優化方式:用程式碼拼裝 SQL 時進行判斷,沒 where 條件就去掉 where,有 where 條件就加 and。
⑦查詢條件不能用 <> 或者 !=
使用索引列作為條件進行查詢時,需要避免使用<>或者!=等判斷條件。
如確實業務需要,使用到不等於符號,需要在重新評估索引建立,避免在此欄位上建立索引,改由查詢條件中其他索引欄位代替。
⑧where 條件僅包含複合索引非前置列
如下:複合(聯合)索引包含 key_part1,key_part2,key_part3 三列,但 SQL 語句沒有包含索引前置列"key_part1",按照 MySQL 聯合索引的最左匹配原則,不會走聯合索引。
selectcol1fromtablewherekey_part2=1andkey_part3=2
⑨隱式型別轉換造成不使用索引
如下 SQL 語句由於索引對列型別為 varchar,但給定的值為數值,涉及隱式型別轉換,造成不能正確走索引。
selectcol1fromtablewherecol_varchar=123;
⑩order by 條件要與 where 中條件一致,否則 order by 不會利用索引進行排序
如下:
--不走age索引
SELECT*FROMtorderbyage;
--走age索引
SELECT*FROMtwhereage>0orderbyage;
對於上面的語句,資料庫的處理順序是:
-
第一步:根據 where 條件和統計資訊生成執行計劃,得到資料。
-
第二步:將得到的資料排序。當執行處理資料(order by)時,資料庫會先檢視第一步的執行計劃,看 order by 的欄位是否在執行計劃中利用了索引。如果是,則可以利用索引順序而直接取得已經排好序的資料。如果不是,則重新進行排序操作。
-
第三步:返回排序後的資料。
當 order by 中的欄位出現在 where 條件中時,才會利用索引而不再二次排序,更準確的說,order by 中的欄位在執行計劃中利用了索引時,不用排序操作。
這個結論不僅對 order by 有效,對其他需要排序的操作也有效。比如 group by 、union 、distinct 等。
⑪正確使用 hint 優化語句
MySQL 中可以使用 hint 指定優化器在執行時選擇或忽略特定的索引。
一般而言,處於版本變更帶來的表結構索引變化,更建議避免使用 hint,而是通過 Analyze table 多收集統計資訊。
但在特定場合下,指定 hint 可以排除其他索引干擾而指定更優的執行計劃:
-
USE INDEX 在你查詢語句中表名的後面,新增 USE INDEX 來提供希望 MySQL 去參考的索引列表,就可以讓 MySQL 不再考慮其他可用的索引。
例子: SELECT col1 FROM table USE INDEX (mod_time, name)...
-
IGNORE INDEX 如果只是單純的想讓 MySQL 忽略一個或者多個索引,可以使用 IGNORE INDEX 作為 Hint。
例子: SELECT col1 FROM table IGNORE INDEX (priority) ...
-
FORCE INDEX 為強制 MySQL 使用一個特定的索引,可在查詢中使用FORCE INDEX 作為 Hint。
例子: SELECT col1 FROM table FORCE INDEX (mod_time) ...
在查詢的時候,資料庫系統會自動分析查詢語句,並選擇一個最合適的索引。但是很多時候,資料庫系統的查詢優化器並不一定總是能使用最優索引。
如果我們知道如何選擇索引,可以使用 FORCE INDEX 強制查詢使用指定的索引。
例如:
SELECT*FROMstudentsFORCEINDEX(idx_class_id)WHEREclass_id=1ORDERBYidDESC;
SELECT 語句其他優化
①避免出現 select *
首先,select * 操作在任何型別資料庫中都不是一個好的 SQL 編寫習慣。
使用 select * 取出全部列,會讓優化器無法完成索引覆蓋掃描這類優化,會影響優化器對執行計劃的選擇,也會增加網路頻寬消耗,更會帶來額外的 I/O,記憶體和 CPU 消耗。
建議提出業務實際需要的列數,將指定列名以取代 select *。
②避免出現不確定結果的函式
特定針對主從複製這類業務場景。由於原理上從庫複製的是主庫執行的語句,使用如 now()、rand()、sysdate()、current_user() 等不確定結果的函式很容易導致主庫與從庫相應的資料不一致。
另外不確定值的函式,產生的 SQL 語句無法利用 query cache。
③多表關聯查詢時,小表在前,大表在後
在 MySQL 中,執行 from 後的表關聯查詢是從左往右執行的(Oracle 相反),第一張表會涉及到全表掃描。
所以將小表放在前面,先掃小表,掃描快效率較高,在掃描後面的大表,或許只掃描大表的前 100 行就符合返回條件並 return 了。
例如:表 1 有 50 條資料,表 2 有 30 億條資料;如果全表掃描表 2,你品,那就先去吃個飯再說吧是吧。
④使用表的別名
當在 SQL 語句中連線多個表時,請使用表的別名並把別名字首於每個列名上。這樣就可以減少解析的時間並減少哪些友列名歧義引起的語法錯誤。
⑤用 where 字句替換 HAVING 字句
避免使用 HAVING 字句,因為 HAVING 只會在檢索出所有記錄之後才對結果集進行過濾,而 where 則是在聚合前刷選記錄,如果能通過 where 字句限制記錄的數目,那就能減少這方面的開銷。
HAVING 中的條件一般用於聚合函式的過濾,除此之外,應該將條件寫在 where 字句中。
where 和 having 的區別:where 後面不能使用組函式。
⑥調整 Where 字句中的連線順序
MySQL 採用從左往右,自上而下的順序解析 where 子句。根據這個原理,應將過濾資料多的條件往前放,最快速度縮小結果集。
增刪改 DML 語句優化
①大批量插入資料
如果同時執行大量的插入,建議使用多個值的 INSERT 語句(方法二)。這比使用分開 INSERT 語句快(方法一),一般情況下批量插入效率有幾倍的差別。
方法一:
insertintoTvalues(1,2);
insertintoTvalues(1,3);
insertintoTvalues(1,4);
方法二:
InsertintoTvalues(1,2),(1,3),(1,4);
選擇後一種方法的原因有三:
-
減少 SQL 語句解析的操作,MySQL 沒有類似 Oracle 的 share pool,採用方法二,只需要解析一次就能進行資料的插入操作。
-
在特定場景可以減少對 DB 連線次數。
-
SQL 語句較短,可以減少網路傳輸的 IO。
②適當使用 commit
適當使用 commit 可以釋放事務佔用的資源而減少消耗,commit 後能釋放的資源如下:
-
事務佔用的 undo 資料塊。
-
事務在 redo log 中記錄的資料塊。
-
釋放事務施加的,減少鎖爭用影響效能。特別是在需要使用 delete 刪除大量資料的時候,必須分解刪除量並定期 commit。
③避免重複查詢更新的資料
針對業務中經常出現的更新行同時又希望獲得改行資訊的需求,MySQL 並不支援 PostgreSQL 那樣的 UPDATE RETURNING 語法,在 MySQL 中可以通過變數實現。
例如,更新一行記錄的時間戳,同時希望查詢當前記錄中存放的時間戳是什麼?
簡單方法實現:
Updatet1settime=now()wherecol1=1;
Selecttimefromt1whereid=1;
使用變數,可以重寫為以下方式:
Updatet1settime=now()wherecol1=1and@now:=now();
Select@now;
前後二者都需要兩次網路來回,但使用變數避免了再次訪問資料表,特別是當 t1 表資料量較大時,後者比前者快很多。
④查詢優先還是更新(insert、update、delete)優先
MySQL 還允許改變語句排程的優先順序,它可以使來自多個客戶端的查詢更好地協作,這樣單個客戶端就不會由於鎖定而等待很長時間。改變優先順序還可以確保特定型別的查詢被處理得更快。
我們首先應該確定應用的型別,判斷應用是以查詢為主還是以更新為主的,是確保查詢效率還是確保更新的效率,決定是查詢優先還是更新優先。
下面我們提到的改變排程策略的方法主要是針對只存在表鎖的儲存引擎,比如 MyISAM 、MEMROY、MERGE,對於 Innodb 儲存引擎,語句的執行是由獲得行鎖的順序決定的。
MySQL 的預設的排程策略可用總結如下:
-
寫入操作優先於讀取操作。
-
對某張資料表的寫入操作某一時刻只能發生一次,寫入請求按照它們到達的次序來處理。
-
對某張資料表的多個讀取操作可以同時地進行。
MySQL 提供了幾個語句調節符,允許你修改它的排程策略:
-
LOW_PRIORITY 關鍵字應用於 DELETE、INSERT、LOAD DATA、REPLACE 和 UPDATE。
-
HIGH_PRIORITY 關鍵字應用於 SELECT 和 INSERT 語句。
-
DELAYED 關鍵字應用於 INSERT 和 REPLACE 語句。
如果寫入操作是一個 LOW_PRIORITY(低優先順序)請求,那麼系統就不會認為它的優先順序高於讀取操作。
在這種情況下,如果寫入者在等待的時候,第二個讀取者到達了,那麼就允許第二個讀取者插到寫入者之前。
只有在沒有其它的讀取者的時候,才允許寫入者開始操作。這種排程修改可能存在 LOW_PRIORITY 寫入操作永遠被阻塞的情況。
SELECT 查詢的 HIGH_PRIORITY(高優先順序)關鍵字也類似。它允許 SELECT 插入正在等待的寫入操作之前,即使在正常情況下寫入操作的優先順序更高。
另外一種影響是,高優先順序的 SELECT 在正常的 SELECT 語句之前執行,因為這些語句會被寫入操作阻塞。
如果希望所有支援 LOW_PRIORITY 選項的語句都預設地按照低優先順序來處理,那麼請使用--low-priority-updates 選項來啟動伺服器。
通過使用 INSERTHIGH_PRIORITY 來把 INSERT 語句提高到正常的寫入優先順序,可以消除該選項對單個 INSERT 語句的影響。
查詢條件優化
①對於複雜的查詢,可以使用中間臨時表暫存資料
②優化 group by 語句
預設情況下,MySQL 會對 GROUP BY 分組的所有值進行排序,如 “GROUP BY col1,col2,....;” 查詢的方法如同在查詢中指定 “ORDER BY col1,col2,...;” 。
如果顯式包括一個包含相同的列的 ORDER BY 子句,MySQL 可以毫不減速地對它進行優化,儘管仍然進行排序。
因此,如果查詢包括 GROUP BY 但你並不想對分組的值進行排序,你可以指定 ORDER BY NULL 禁止排序。
例如:
SELECTcol1,col2,COUNT(*)FROMtableGROUPBYcol1,col2ORDERBYNULL;
③優化 join 語句
MySQL 中可以通過子查詢來使用 SELECT 語句來建立一個單列的查詢結果,然後把這個結果作為過濾條件用在另一個查詢中。
使用子查詢可以一次性的完成很多邏輯上需要多個步驟才能完成的 SQL 操作,同時也可以避免事務或者表鎖死,並且寫起來也很容易。但是,有些情況下,子查詢可以被更有效率的連線(JOIN)..替代。
例子:假設要將所有沒有訂單記錄的使用者取出來,可以用下面這個查詢完成:
SELECTcol1FROMcustomerinfoWHERECustomerIDNOTin(SELECTCustomerIDFROMsalesinfo)
如果使用連線(JOIN)..來完成這個查詢工作,速度將會有所提升。
尤其是當 salesinfo 表中對 CustomerID 建有索引的話,效能將會更好,查詢如下:
SELECTcol1FROMcustomerinfo
LEFTJOINsalesinfoONcustomerinfo.CustomerID=salesinfo.CustomerID
WHEREsalesinfo.CustomerIDISNULL
連線(JOIN)..之所以更有效率一些,是因為 MySQL 不需要在記憶體中建立臨時表來完成這個邏輯上的需要兩個步驟的查詢工作。
④優化 union 查詢
MySQL 通過建立並填充臨時表的方式來執行 union 查詢。除非確實要消除重複的行,否則建議使用 union all。
原因在於如果沒有 all 這個關鍵詞,MySQL 會給臨時表加上 distinct 選項,這會導致對整個臨時表的資料做唯一性校驗,這樣做的消耗相當高。
高效:
SELECTCOL1,COL2,COL3FROMTABLEWHERECOL1=10
UNIONALL
SELECTCOL1,COL2,COL3FROMTABLEWHERECOL3='TEST';
低效:
SELECTCOL1,COL2,COL3FROMTABLEWHERECOL1=10
UNION
SELECTCOL1,COL2,COL3FROMTABLEWHERECOL3='TEST';
⑤拆分複雜 SQL 為多個小 SQL,避免大事務
如下:
-
簡單的 SQL 容易使用到 MySQL 的 QUERY CACHE。
-
減少鎖表時間特別是使用 MyISAM 儲存引擎的表。
-
可以使用多核 CPU。
⑥使用 truncate 代替 delete
當刪除全表中記錄時,使用 delete 語句的操作會被記錄到 undo 塊中,刪除記錄也記錄 binlog。
當確認需要刪除全表時,會產生很大量的 binlog 並佔用大量的 undo 資料塊,此時既沒有很好的效率也佔用了大量的資源。
使用 truncate 替代,不會記錄可恢復的資訊,資料不能被恢復。也因此使用 truncate 操作有其極少的資源佔用與極快的時間。另外,使用 truncate 可以回收表的水位,使自增欄位值歸零。
⑦使用合理的分頁方式以提高分頁效率
使用合理的分頁方式以提高分頁效率 針對展現等分頁需求,合適的分頁方式能夠提高分頁的效率。
案例 1:
select*fromtwherethread_id=10000anddeleted=0
orderbygmt_createasclimit0,15;
上述例子通過一次性根據過濾條件取出所有欄位進行排序返回。資料訪問開銷=索引 IO+索引全部記錄結果對應的表資料 IO。
因此,該種寫法越翻到後面執行效率越差,時間越長,尤其表資料量很大的時候。
適用場景:當中間結果集很小(10000 行以下)或者查詢條件複雜(指涉及多個不同查詢欄位或者多表連線)時適用。
案例 2:
selectt.*from(selectidfromtwherethread_id=10000anddeleted=0
orderbygmt_createasclimit0,15)a,t
wherea.id=t.id;
上述例子必須滿足 t 表主鍵是 id 列,且有覆蓋索引 secondary key:(thread_id, deleted, gmt_create)。
通過先根據過濾條件利用覆蓋索引取出主鍵 id 進行排序,再進行 join 操作取出其他欄位。
資料訪問開銷=索引 IO+索引分頁後結果(例子中是 15 行)對應的表資料 IO。因此,該寫法每次翻頁消耗的資源和時間都基本相同,就像翻第一頁一樣。
適用場景:當查詢和排序欄位(即 where 子句和 order by 子句涉及的欄位)有對應覆蓋索引時,且中間結果集很大的情況時適用。
建表優化
①在表中建立索引,優先考慮 where、order by 使用到的欄位。
②儘量使用數字型欄位(如性別,男:1 女:2),若只含數值資訊的欄位儘量不要設計為字元型,這會降低查詢和連線的效能,並會增加儲存開銷。
這是因為引擎在處理查詢和連線時會 逐個比較字串中每一個字元,而對於數字型而言只需要比較一次就夠了。
③查詢資料量大的表 會造成查詢緩慢。主要的原因是掃描行數過多。這個時候可以通過程式,分段分頁進行查詢,迴圈遍歷,將結果合併處理進行展示。
要查詢 100000 到 100050 的資料,如下:
SELECT*FROM(SELECTROW_NUMBER()OVER(ORDERBYIDASC)ASrowid,*
FROMinfoTab)tWHEREt.rowid>100000ANDt.rowid<=100050
④用 varchar/nvarchar 代替 char/nchar。
儘可能的使用 varchar/nvarchar 代替 char/nchar ,因為首先變長欄位儲存空間小,可以節省儲存空間,其次對於查詢來說,在一個相對較小的欄位內搜尋效率顯然要高些。
不要以為 NULL 不需要空間,比如:char(100) 型,在欄位建立時,空間就固定了, 不管是否插入值(NULL 也包含在內),都是佔用 100 個字元的空間的,如果是 varchar 這樣的變長欄位, null 不佔用空間。