真實場景sql優化持續更新(老司機必備)

概述

下述場景,均來自實際產品線上經驗,出於保密考量,所有需求場景都是仿造的,模擬遇到過的真實場景。

場景一: 統計數據(Order by 不具備唯一性導致的分頁數據混亂)

需求

在實際業務場景中,我們經常遇到統計分析,比如現在有一張學生表student,現統計姓名爲xxx的總共有多少學生。

id name
1 張三
2 張三
3 李四
4 武器
5 大炮
6 大炮
7 李四
8 無用
9 劉可
10 狐狸
11 無話
12 敗給
13 事變
14 狐狸
15 何必
16 無話
17 無用
18 無話
19 李四

實現

常規思路一般用groub by ,然後再求和,再分頁。

查第一頁

SELECT
	t.name,
	COUNT(1) as num 
FROM
	test t 
WHERE
	1 = 1 
GROUP BY t.`name`
ORDER BY
	num DESC 
	LIMIT 0,
	5

查詢結果是這樣的:

name num
李四 3
無話 3
張三 2
大炮 2
狐狸 2

查第二頁


SELECT
	t.name,
	COUNT(1) as num 
FROM
	test t 
WHERE
	1 = 1 
GROUP BY t.`name`
ORDER BY
	num DESC 
	LIMIT 5,
	5

查詢結果是這樣的:

name num
狐狸 2
武器 1
劉可 1
敗給 1
事變 1

結果分析

顯然第二頁的'狐狸'不應該出現,他是第一頁的最後一條數據。這個問題在mysql官方是給予了答案的,其實只要是order by 的排序字段在結果集中不唯一,排序字段一致的行他返回的結果都是無序的,這一點不容易被重視,也不容易被測試所發現(單表一般需要較多重複數據和分頁才容易被發現),算是一個小坑。

優化

方案一

網上一般提供的思路: 既然排序字段不是唯一的,我們一般期望唯一排序,只需要在order by 中跟上唯一標識的字段即可,像下面這樣:


SELECT
	t.name,
	COUNT(1) as num 
FROM
	test t 
WHERE
	1 = 1 
GROUP BY t.`name`
ORDER BY
	num DESC,t.id desc
	LIMIT 5,
	5

但是這種方式有個致命問題,ORDER BY 後面接了兩個字段會讓索引失效,大數據場景下是不推薦這種方式的。

方案二

使用 ROW_NUMBER() OVER ( ORDER BY t.id) AS serial_number讓他按照指定方式排序,這基本也是萬機油解決方案,對代碼侵入程度很低。但是我們這個場景下兩種方式效率一樣,因爲本來num字段就沒有索引,但是當order by 存在一個字段可以用索引的話就不一樣了。


SELECT
	t.name,
	COUNT(1) as num ,
	ROW_NUMBER() OVER ( ORDER BY t.name) AS serial_number
FROM
	test t 
WHERE
	1 = 1 
GROUP BY t.`name`
ORDER BY
	num DESC
	LIMIT 5,
	5

場景二: 大表查詢優化問題(多租戶情景下的連表查詢規範)

需求

假設有這樣一個場景,要求查某公司的商品出售情況的數據,數據庫設計如下:

表名 備註
order 訂單表,有create_by 字段
goods 商品表
logistics 物流表
order_goods_mapping 商品與訂單關聯表
order_logistics_mapping 物流與訂單關聯表

實現

先不考慮數據庫設計是否合理,現在要分頁查詢商品銷售情況,在不考慮數據量的情況下一般這樣寫sql(僞sql):


select g.*,o.*,l.* from goods g
join order_goods_mapping ogm on(ogm.goods_id= g.goods_id)
join order o on(o.order_id= ogm.order_id)
join order_logistics_mapping olg on(olg.order_id = o.order_id)
join logistics l on(l.logistics_id = olg.logistics_id)

where l.company_id = #{companyId} limit 0,10

這些xxxid字段索引都有,當數據庫較小的時候看上去沒有任務問題。但是假設商品有1億種商品,這個sql可以預見性的劇卡。因爲join操作匹配本來就是nnn這樣的操作,由於只限制了logistics 的company_id,所以查詢出來的數據量依舊是巨大的。(親身經歷的一次因爲慢查詢,導致上線失敗的根本原因)

優化

要限制每張表的數據儘可能少,一般多租戶場景下,每張表要有租戶id, 這樣就可以按租戶維度進行數據隔離。由於很多時候我們沒有遇到過大表的情況,所以基本租戶隔離技術在sql聯表查詢沒有體現出來,往往只是限制了聯表的某一張表的租戶id等於登錄的租戶id,這是不可取的(有意思的是:難怪現在流行的多租戶方案要求每張表都要有租戶id,除了分庫分表有用,查詢優化也體現出了數據隔離的優勢,一個小小的字段竟然有這麼大的作用)。優化後的sql如下:


select g.*,o.*,l.* from goods g
join order_goods_mapping ogm on(ogm.goods_id= g.goods_id)
join order o on(o.order_id= ogm.order_id)
join order_logistics_mapping olg on(olg.order_id = o.order_id)
join logistics l on(l.logistics_id = olg.logistics_id)

where l.company_id = #{companyId} and g.company_id = #{companyId} and ogm..company_id = #{companyId} and o.company_id = #{companyId} and olg.company_id = #{companyId}limit 0,10

場景三: 子查詢導致的效率低下的問題(縱錶轉橫表的查詢,本質上是連表取交集問題的解決思路)

需求

mysql作爲關係型數據庫,他對行內關係的描述較弱,比如有這樣2個表,主表interface記錄接口表,子表itf_param記錄接口參數表。
itf_param假設構造如下:

字段名 描述
id 主鍵
itf_id 接口id
param_name 參數名稱
param_value 參數值

現在要查所有(參數名='code',參數值='12')和(參數名='route',參數值='gw')的interface記錄。

實現

通常我們會用如下sql實現:


select it.* from interface it where 1=1 
and exists(
  select 1  from itf_param p where p.param_name= 'code' and p.param_value='12'
)
and exists(
  select 1  from itf_param p where p.param_name= 'route' and p.param_value='gw'
)
where 1=1 limit 0,10

在數據量少的情況下,這個sql是沒有任何問題的,但是在大數據量場景下,此sql就難堪大任了,因爲一般來講子查詢效率都會較低(這裏即便分頁了也是如此,具體原因要問DB工程師了,估摸着limit是最後被執行,所以逐條過濾大量數據導致效率較低)。

優化

通常連表查詢效率高於子查詢,這裏採用縱錶轉橫表的方式對sql進行優化,如下所示(僞sql):


select it.* , 
MAX(CASE WHEN p.param_name= 'code' THEN p.param_value ELSE NULL END) AS codeParamValue,
MAX(CASE WHEN p.param_name= 'route' THEN p.param_value ELSE NULL END) AS routeParamValue,
from interface it join itf_param p on(it.itf_id = p.itf_id)
where 1=1 
group by it.*
having codeParamValue = '12' and routeParamValue='gw' 
limit 0,10 

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