15種sql優化

1 避免使用 select *

很多時候,我們寫sql語句時,爲了方便,喜歡直接使用select *,一次性查出表中所有列的數據。
反例:

select * from user where id=1;

在實際業務場景中,可能我們真正需要使用的只有其中一兩列。查了很多數據,但是不用,白白浪費了數據庫資源,比如:內存或者cpu。
此外,多查出來的數據,通過網絡I0傳輸的過程中,也會增加數據傳輸的時間。
還有一個最重要的問題是:“select *不會走覆蓋索引,會出現大量的回表操作,而從導致查詢sgl的性能很低。
那麼,如何優化呢?
正例:

select name ,age from user where id=1;

sql 語句查詢時,只查需要用到的列,多餘的列根本無需查出來。

2 用unionall代替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);

除非是有些特殊的場景,比如unionall之後,結果集中出現了重複數據,而業務場景中是不允許產生重複數據的,這時可以使用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',101),values(123, 001',102).values(123, 001',103);

這樣只需要遠程請求一次數據庫,sql性能會得到提升,數據量越多,提升越大。

但需要注意的是,不建議一次批量操作太多的數據,如果數據太多數據庫響應也會很慢。批量操作需要把握一個度。

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_datefrom orderwhere user id=123order by create date aslimit 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 Bus inessException("-次最多允許查詢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,agefrom 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 orderwhere user_id in ( select id from user where status=1 )

子查詢語句可以通過in關鍵字實現,一個查詢語句的條件落在另一個select語句的查詢結果中。程序先運行在嵌套在最內層的語句。
子查詢語句的優點是簡單,結構化,如果涉及的表數量不多的話。
但缺點是mysq1執行子查詢時,需要創建臨時表,查詢完畢後,需要再刪除這些臨時表,有一些額外的性能消耗。
這時可以改成連接查詢。具體例子如下:

select o.* from order oinner join user u on o.user id = u.idwhere u.status=1

10 join的表不宜過多

根據阿里巴巴開發者手冊的規定,join 表的數量不應該超過3個。
反例:

select a.name,b.name,c.name,d.namefrom ainner join b on a.id = b.a_idinner join C on c.b_id=b.idinner join d on d.c_id=c.idinner join e on e.d_id=d.idinner join f on f.e_id=e.idinner join g on g.f_id=f.id

如果join太多,mysg!在選擇索引的時候會非常複雜,很容易選錯索引。
並且如果沒有命中中,nested loopjoin就是分別從兩個表讀一行數據進行兩兩對比,複雜度是n^2。
所以我們應該儘量控制join表的數量。
正例:

select a.name,b.name.c.name,a.d_name
from a
inner join b on 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;

如果兩張表使用innerjoin關聯,mysgl 會自動選擇兩張表中的小表,去驅動大表,所以性能上不會有太大的問題。
使用leftjoin 的示例如下:

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語句在做一些耗時的操作之前,應儘可能縮小數據範圍,這樣能提升效率。

15 索引優化

sql優化當中,有一個非常重要的內容就是: 索引優化。
很多時候sql語句,走了索引,和沒有走索引,執行效率差別很大。所以索引優化被作爲sql優化的首選。
索引優化的第一步是:檢查sql語句有沒有走索引。
那麼,如何查看sql走了索引沒?
可以使用explain命令,查看mysql的執行計劃。
例如:

explain select * from order where code= '002';

image-20211229172603166

此外,你有沒有遇到過這樣一種情況:明明是同一條sql,只有入參不同而已。有的時候走的索引a,有的時候卻走的索引b?
沒錯,有時候mysql會選錯索引。
必要時可以使用force index 來強制查詢sgl走某個索引。

例如:

select customer, count(1) C
from upv_**
 where created between "2015-07-06" and “2015-07-07"
group by customer
having C > 20
 order by C desc

執行explain,發現這個sq|掃描了8000W條記錄到磁盤上。然後再進行篩選。type=index說明整 個索引樹都被掃描了,效果顯然不理想。

image-20211229173239307

僅僅加了個force index,花了1s多就出結果了。修改後的SQL如下:

select customer, count(1) C
from upv_** force index(idx_created)
where created between "2015-07-06" and "2015-07-07”
group by customer
having C > 15
order by C desc

同樣執行以下explain命令,這個SQL僅僅掃描了磁盤的110W行記錄。也就是上一個SQL的80分之一。大家都知道,掃描磁盤是很耗時的IO操作,比內存操作慢幾個數量級。type=range,說明索引樹僅僅被部分掃描,要優於前面那個SQL。

image-20211229173823262

除了磁盤掃描的行數的不-樣,還有采用的索引的不同,上面的sq|用的是聯合索引, 而下面的是單純的created字段的索引。由於用的是created的索引,驅動條件就是created的區間,需要掃描的數據就立刻變小了,因爲時間區間小。後面的SQL的key_ len要遠遠小於前面的SQL,也就意味着要掃描的磁盤上的索引數據量要遠遠小於前面的SQL。

第一個sql使用的是錯誤的索引,帶來低效的查詢。然後每條SQL只可能使用一個索引。 通過上面的分析就可以發現,force index()指令可以指定本次查詢使用哪個索引! 一條sql只會用到一個索引,mysql優化器會計算出一個合適的索引,但是這個索引不一定是最好的。 force index()指令可以避免MySql優化器用到了一
個低效的索引。

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