聊聊sql優化的15個小技巧
前言
sql優化是一個大家都比較關注的熱門話題,無論你在面試,還是工作中,都很有可能會遇到。
如果某天你負責的某個線上介面,出現了效能問題,需要做優化。那麼你首先想到的很有可能是優化sql語句,因為它的改造成本相對於程式碼來說也要小得多。
那麼,如何優化sql語句呢?
這篇文章從15個方面,分享了sql優化的一些小技巧,希望對你有所幫助。
1 避免使用select *
很多時候,我們寫sql語句時,為了方便,喜歡直接使用select *
,一次性查出表中所有列的資料。
反例:
select * from user where id=1;
在實際業務場景中,可能我們真正需要使用的只有其中一兩列。查了很多資料,但是不用,白白浪費了資料庫資源,比如:記憶體或者cpu。
此外,多查出來的資料,通過網路IO傳輸的過程中,也會增加資料傳輸的時間。
還有一個最重要的問題是:select *
不會走覆蓋索引
,會出現大量的回表
操作,而從導致查詢sql的效能很低。
那麼,如何優化呢?
正例:
select name,age from user where id=1;
sql語句查詢時,只查需要用到的列,多餘的列根本無需查出來。
2 用union all代替union
我們都知道sql語句使用union
關鍵字後,可以獲取排重後的資料。
而如果使用union all
關鍵字,可以獲取所有資料,包含重複的資料。
反例:
(select * from user where id=1) union (select * from user where id=2);
排重的過程需要遍歷、排序和比較,它更耗時,更消耗cpu資源。
所以如果能用union all的時候,儘量不用union。
正例:
(select * from user where id=1)
union all
(select * from user where id=2);
除非是有些特殊的場景,比如union all之後,結果集中出現了重複資料,而業務場景中是不允許產生重複資料的,這時可以使用union。
3 小表驅動大表
小表驅動大表,也就是說用小表的資料集驅動大表的資料集。
假如有order和user兩張表,其中order表有10000條資料,而user表有100條資料。
這時如果想查一下,所有有效的使用者下過的訂單列表。
可以使用in
關鍵字實現:
select * from order
where user_id in (select id from user where status=1)
也可以使用exists
關鍵字實現:
select * from order
where exists (select 1 from user where order.user_id = user.id and status=1)
前面提到的這種業務場景,使用in關鍵字去實現業務需求,更加合適。
為什麼呢?
因為如果sql語句中包含了in關鍵字,則它會優先執行in裡面的子查詢語句
,然後再執行in外面的語句。如果in裡面的資料量很少,作為條件查詢速度更快。
而如果sql語句中包含了exists關鍵字,它優先執行exists左邊的語句(即主查詢語句)。然後把它作為條件,去跟右邊的語句匹配。如果匹配上,則可以查詢出資料。如果匹配不上,資料就被過濾掉了。
這個需求中,order表有10000條資料,而user表有100條資料。order表是大表,user表是小表。如果order表在左邊,則用in關鍵字效能更好。
總結一下:
in
適用於左邊大表,右邊小表。exists
適用於左邊小表,右邊大表。
不管是用in,還是exists關鍵字,其核心思想都是用小表驅動大表。
4 批量操作
如果你有一批資料經過業務處理之後,需要插入資料,該怎麼辦?
反例:
for(Order order: list){
orderMapper.insert(order):
}
在迴圈中逐條插入資料。
insert into order(id,code,user_id)
values(123,'001',100);
該操作需要多次請求資料庫,才能完成這批資料的插入。
但眾所周知,我們在程式碼中,每次遠端請求資料庫,是會消耗一定效能的。而如果我們的程式碼需要請求多次資料庫,才能完成本次業務功能,勢必會消耗更多的效能。
那麼如何優化呢?
正例:
orderMapper.insertBatch(list):
提供一個批量插入資料的方法。
insert into order(id,code,user_id)
values(123,'001',100),(124,'002',100),(125,'003',101);
這樣只需要遠端請求一次資料庫,sql效能會得到提升,資料量越多,提升越大。
但需要注意的是,不建議一次批量操作太多的資料,如果資料太多資料庫響應也會很慢。批量操作需要把握一個度,建議每批資料儘量控制在500以內。如果資料多於500,則分多批次處理。
5 多用limit
有時候,我們需要查詢某些資料中的第一條,比如:查詢某個使用者下的第一個訂單,想看看他第一次的首單時間。
反例:
select id, create_date
from order
where user_id=123
order by create_date asc;
根據使用者id查詢訂單,按下單時間排序,先查出該使用者所有的訂單資料,得到一個訂單集合。 然後在程式碼中,獲取第一個元素的資料,即首單的資料,就能獲取首單時間。
List<Order> list = orderMapper.getOrderList();
Order order = list.get(0);
雖說這種做法在功能上沒有問題,但它的效率非常不高,需要先查詢出所有的資料,有點浪費資源。
那麼,如何優化呢?
正例:
select id, create_date
from order
where user_id=123
order by create_date asc
limit 1;
使用limit 1
,只返回該使用者下單時間最小的那一條資料即可。
此外,在刪除或者修改資料時,為了防止誤操作,導致刪除或修改了不相干的資料,也可以在sql語句最後加上limit。
例如:
update order set status=0,edit_time=now(3)
where id>=100 and id<200 limit 100;
這樣即使誤操作,比如把id搞錯了,也不會對太多的資料造成影響。
6 in中值太多
對於批量查詢介面,我們通常會使用in
關鍵字過濾出資料。比如:想通過指定的一些id,批量查詢出使用者資訊。
sql語句如下:
select id,name from category
where id in (1,2,3...100000000);
如果我們不做任何限制,該查詢語句一次性可能會查詢出非常多的資料,很容易導致介面超時。
這時該怎麼辦呢?
select id,name from category
where id in (1,2,3...100)
limit 500;
可以在sql中對資料用limit做限制。
不過我們更多的是要在業務程式碼中加限制,虛擬碼如下:
public List<Category> getCategory(List<Long> ids) {
if(CollectionUtils.isEmpty(ids)) {
return null;
}
if(ids.size() > 500) {
throw new BusinessException("一次最多允許查詢500條記錄")
}
return mapper.getCategoryList(ids);
}
還有一個方案就是:如果ids超過500條記錄,可以分批用多執行緒去查詢資料。每批只查500條記錄,最後把查詢到的資料彙總到一起返回。
不過這只是一個臨時方案,不適合於ids實在太多的場景。因為ids太多,即使能快速查出資料,但如果返回的資料量太大了,網路傳輸也是非常消耗效能的,介面效能始終好不到哪裡去。
7 增量查詢
有時候,我們需要通過遠端介面查詢資料,然後同步到另外一個數據庫。
反例:
select * from user;
如果直接獲取所有的資料,然後同步過去。這樣雖說非常方便,但是帶來了一個非常大的問題,就是如果資料很多的話,查詢效能會非常差。
這時該怎麼辦呢?
正例:
select * from user
where id>#{lastId} and create_time >= #{lastCreateTime}
limit 100;
按id和時間升序,每次只同步一批資料,這一批資料只有100條記錄。每次同步完成之後,儲存這100條資料中最大的id和時間,給同步下一批資料的時候用。
通過這種增量查詢的方式,能夠提升單次查詢的效率。
8 高效的分頁
有時候,列表頁在查詢資料時,為了避免一次性返回過多的資料影響介面效能,我們一般會對查詢介面做分頁處理。
在mysql中分頁一般用的limit
關鍵字:
select id,name,age
from user limit 10,20;
如果表中資料量少,用limit關鍵字做分頁,沒啥問題。但如果表中資料量很多,用它就會出現效能問題。
比如現在分頁引數變成了:
select id,name,age
from user limit 1000000,20;
mysql會查到1000020條資料,然後丟棄前面的1000000條,只查後面的20條資料,這個是非常浪費資源的。
那麼,這種海量資料該怎麼分頁呢?
優化sql:
select id,name,age
from user where id > 1000000 limit 20;
先找到上次分頁最大的id,然後利用id上的索引查詢。不過該方案,要求id是連續的,並且有序的。
還能使用between
優化分頁。
select id,name,age
from user where id between 1000000 and 1000020;
需要注意的是between要在唯一索引上分頁,不然會出現每頁大小不一致的問題。
9 用連線查詢代替子查詢
mysql中如果需要從兩張以上的表中查詢出資料的話,一般有兩種實現方式:子查詢
和 連線查詢
。
子查詢的例子如下:
select * from order
where user_id in (select id from user where status=1)
子查詢語句可以通過in
關鍵字實現,一個查詢語句的條件落在另一個select語句的查詢結果中。程式先執行在巢狀在最內層的語句,再執行外層的語句。
子查詢語句的優點是簡單,結構化,如果涉及的表數量不多的話。
但缺點是mysql執行子查詢時,需要建立臨時表,查詢完畢後,需要再刪除這些臨時表,有一些額外的效能消耗。
這時可以改成連線查詢。 具體例子如下:
select o.* from order o
inner join user u on o.user_id = u.id
where u.status=1
10 join的表不宜過多
根據阿里巴巴開發者手冊的規定,join表的數量不應該超過3
個。
反例:
select a.name,b.name.c.name,d.name
from a
inner join b on a.id = b.a_id
inner join c on c.b_id = b.id
inner join d on d.c_id = c.id
inner join e on e.d_id = d.id
inner join f on f.e_id = e.id
inner join g on g.f_id = f.id
如果join太多,mysql在選擇索引的時候會非常複雜,很容易選錯索引。
並且如果沒有命中中,nested loop join 就是分別從兩個表讀一行資料進行兩兩對比,複雜度是 n^2。
所以我們應該儘量控制join表的數量。
正例:
select a.name,b.name.c.name,a.d_name
from a
inner join b on a.id = b.a_id
inner join c on c.b_id = b.id
如果實現業務場景中需要查詢出另外幾張表中的資料,可以在a、b、c表中冗餘專門的欄位
,比如:在表a中冗餘d_name欄位,儲存需要查詢出的資料。
不過我之前也見過有些ERP系統,併發量不大,但業務比較複雜,需要join十幾張表才能查詢出資料。
所以join表的數量要根據系統的實際情況決定,不能一概而論,儘量越少越好。
11 join時要注意
我們在涉及到多張表聯合查詢的時候,一般會使用join
關鍵字。
而join使用最多的是left join和inner join。
left join
:求兩個表的交集外加左表剩下的資料。inner join
:求兩個表交集的資料。
使用inner join的示例如下:
select o.id,o.code,u.name
from order o
inner join user u on o.user_id = u.id
where u.status=1;
如果兩張表使用inner join關聯,mysql會自動選擇兩張表中的小表,去驅動大表,所以效能上不會有太大的問題。
使用left join的示例如下:
select o.id,o.code,u.name
from order o
left join user u on o.user_id = u.id
where u.status=1;
如果兩張表使用left join關聯,mysql會預設用left join關鍵字左邊的表,去驅動它右邊的表。如果左邊的表資料很多時,就會出現效能問題。
要特別注意的是在用left join關聯查詢時,左邊要用小表,右邊可以用大表。如果能用inner join的地方,儘量少用left join。
12 控制索引的數量
眾所周知,索引能夠顯著的提升查詢sql的效能,但索引數量並非越多越好。
因為表中新增資料時,需要同時為它建立索引,而索引是需要額外的儲存空間的,而且還會有一定的效能消耗。
阿里巴巴的開發者手冊中規定,單表的索引數量應該儘量控制在5
個以內,並且單個索引中的欄位數不超過5
個。
mysql使用的B+樹的結構來儲存索引的,在insert、update和delete操作時,需要更新B+樹索引。如果索引過多,會消耗很多額外的效能。
那麼,問題來了,如果表中的索引太多,超過了5個該怎麼辦?
這個問題要辯證的看,如果你的系統併發量不高,表中的資料量也不多,其實超過5個也可以,只要不要超過太多就行。
但對於一些高併發的系統,請務必遵守單表索引數量不要超過5的限制。
那麼,高併發系統如何優化索引數量?
能夠建聯合索引,就別建單個索引,可以刪除無用的單個索引。
將部分查詢功能遷移到其他型別的資料庫中,比如:Elastic Seach、HBase等,在業務表中只需要建幾個關鍵索引即可。
13 選擇合理的欄位型別
char
表示固定字串型別,該型別的欄位儲存空間的固定的,會浪費儲存空間。
alter table order
add column code char(20) NOT NULL;
varchar
表示變長字串型別,該型別的欄位儲存空間會根據實際資料的長度調整,不會浪費儲存空間。
alter table order
add column code varchar(20) NOT NULL;
如果是長度固定的欄位,比如使用者手機號,一般都是11位的,可以定義成char型別,長度是11位元組。
但如果是企業名稱欄位,假如定義成char型別,就有問題了。
如果長度定義得太長,比如定義成了200位元組,而實際企業長度只有50位元組,則會浪費150位元組的儲存空間。
如果長度定義得太短,比如定義成了50位元組,但實際企業名稱有100位元組,就會儲存不下,而丟擲異常。
所以建議將企業名稱改成varchar型別,變長欄位儲存空間小,可以節省儲存空間,而且對於查詢來說,在一個相對較小的欄位內搜尋效率顯然要高些。
我們在選擇欄位型別時,應該遵循這樣的原則:
- 能用數字型別,就不用字串,因為字元的處理往往比數字要慢。
- 儘可能使用小的型別,比如:用bit存布林值,用tinyint存列舉值等。
- 長度固定的字串欄位,用char型別。
- 長度可變的字串欄位,用varchar型別。
- 金額欄位用decimal,避免精度丟失問題。
還有很多原則,這裡就不一一列舉了。
14 提升group by的效率
我們有很多業務場景需要使用group by
關鍵字,它主要的功能是去重和分組。
通常它會跟having
一起配合使用,表示分組後再根據一定的條件過濾資料。
反例:
select user_id,user_name from order
group by user_id
having user_id <= 200;
這種寫法效能不好,它先把所有的訂單根據使用者id分組之後,再去過濾使用者id大於等於200的使用者。
分組是一個相對耗時的操作,為什麼我們不先縮小資料的範圍之後,再分組呢?
正例:
select user_id,user_name from order
where user_id <= 200
group by user_id
使用where條件在分組前,就把多餘的資料過濾掉了,這樣分組時效率就會更高一些。
其實這是一種思路,不僅限於group by的優化。我們的sql語句在做一些耗時的操作之前,應儘可能縮小資料範圍,這樣能提升sql整體的效能。
15 索引優化
sql優化當中,有一個非常重要的內容就是:索引優化
。
很多時候sql語句,走了索引,和沒有走索引,執行效率差別很大。所以索引優化被作為sql優化的首選。
索引優化的第一步是:檢查sql語句有沒有走索引。
那麼,如何檢視sql走了索引沒?
可以使用explain
命令,檢視mysql的執行計劃。
例如:
explain select * from `order` where code='002';
結果:
通過這幾列可以判斷索引使用情況,執行計劃包含列的含義如下圖所示:
如果你想進一步瞭解explain的詳細用法,可以看看我的另一篇文章《explain | 索引優化的這把絕世好劍,你真的會用嗎?》
說實話,sql語句沒有走索引,排除沒有建索引之外,最大的可能性是索引失效了。
下面說說索引失效的常見原因:
如果不是上面的這些原因,則需要再進一步排查一下其他原因。
此外,你有沒有遇到過這樣一種情況:明明是同一條sql,只有入參不同而已。有的時候走的索引a,有的時候卻走的索引b?
沒錯,有時候mysql會選錯索引。
必要時可以使用force index
來強制查詢sql走某個索引。
至於為什麼mysql會選錯索引,後面有專門的文章介紹的,這裡先留點懸念。
最後說一句(求關注,別白嫖我)
如果這篇文章對您有所幫助,或者有所啟發的話,幫忙掃描下發二維碼關注一下,您的支援是我堅持寫作最大的動力。
求一鍵三連:點贊、轉發、在看。
關注公眾號:【蘇三說技術】,在公眾號中回覆:面試、程式碼神器、開發手冊、時間管理有超讚的粉絲福利,另外回覆:加群,可以跟很多BAT大廠的前輩交流和學習。