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

2023-04-25 15:03:18

概述

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

場景一: 統計資料(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