SQL性能优化-查询条件与字段分开执行,union代替in与or,存储过程代替union
PS:概要、背景、結(jié)語(yǔ)都是日?!把bX”,可以跳過(guò)直接看優(yōu)化歷程
環(huán)境:SQL Server 2008 R2、阿里云RDS;輔助工具:SQL 審計(jì)
概要
一個(gè)訂單列表分頁(yè)查詢功能,單從SQL性能來(lái)講,從幾十萬(wàn)數(shù)據(jù)量時(shí),適當(dāng)加一些索引隨便寫(xiě)SQL;到百來(lái)萬(wàn)數(shù)據(jù)量時(shí),需要做一些SQL語(yǔ)句優(yōu)化;再到幾百萬(wàn)上千萬(wàn)的數(shù)據(jù)量情況下,很多意想不到的情況就出現(xiàn)了(在大部分中小公司沒(méi)有專業(yè)DBA的情況下,“萬(wàn)能”的研發(fā)就得頂上去了)。
?
背景
進(jìn)入公司后,系統(tǒng)已經(jīng)初具規(guī)模,已有成型的框架。隨著業(yè)務(wù)的不斷增長(zhǎng),系統(tǒng)功能的不斷增加,一些性能問(wèn)題開(kāi)始涌現(xiàn)出來(lái)。
以下訂單列表查詢頁(yè)面的優(yōu)化歷程,有幸參與了整個(gè)過(guò)程,以前做研發(fā)或項(xiàng)目管理的時(shí)候,整天撲在功能開(kāi)發(fā)上,很難有時(shí)間去深入研究一些比較難解決的問(wèn)題。現(xiàn)在做團(tuán)隊(duì)管理,反而有更多的時(shí)間讓我來(lái)思考與總結(jié),以下的每一步優(yōu)化其實(shí)都不是一開(kāi)始就想到的,而是經(jīng)過(guò)無(wú)數(shù)次的摸索以及線上測(cè)試驗(yàn)證,最終找到合適于現(xiàn)階段的優(yōu)化方案。
本人非專業(yè)DBA,目前的崗位是研發(fā)管理,園友們?nèi)绻懈玫慕鉀Q方案歡迎討論。當(dāng)然我們也有想過(guò)一些更徹底的解決方案,我會(huì)在第五部分進(jìn)行描述,如:冗余數(shù)據(jù)、數(shù)據(jù)庫(kù)分區(qū)、分表、分庫(kù)等,但受制于研發(fā)資源、開(kāi)發(fā)周期等只能擱置。從公司的角度來(lái)講,永遠(yuǎn)都是用最小的成本去實(shí)現(xiàn)最大的價(jià)值。
優(yōu)化歷程
以下所有“執(zhí)行統(tǒng)計(jì)信息”都是在現(xiàn)有的數(shù)據(jù)量情況下,且所有條件都是頁(yè)面默認(rèn)打開(kāi)的情況下(訂單主表大概1千萬(wàn)左右,其他附屬表最大1億左右,以下所有表名、字段名都替換過(guò)且有些刪減)。統(tǒng)計(jì)信息限于篇幅,只貼出了執(zhí)行時(shí)間,具體分析用的掃描次數(shù)、邏輯讀次數(shù)就沒(méi)貼了,時(shí)間只是其中一個(gè)參考值,掃描次數(shù),邏輯讀次數(shù)也是很重要的參考值。
(一)、一條SQL語(yǔ)句實(shí)現(xiàn)查詢條件,返回字段
1、查詢總記錄數(shù):
執(zhí)行統(tǒng)計(jì)信息:SQL Server parse and compile time: CPU time = 421 ms, elapsed time = 439 ms.SQL Server Execution Times:CPU time = 71621 ms, elapsed time = 111959 ms.
2、查詢第一頁(yè)訂單信息
具體SQL如下:SELECT * FROM (select ROW_NUMBER() Over(ORDER BY temps.OpDate) as RowId,*from ( SELECT DISTINCT *,(SELECT '@#@#@' + rtrim (pd.Remark) FROM dbo.wf_pda pd WHERE pd.ProcessID=temp.ProcessID AND pd.ActID=-2 FOR XML PATH('')) AS BackReasonFROM (SELECT tpo.OrderNo,emark.Remark,finance.a,sup.b,ph.c,ph.ProcessID,re.d,tpro.eFROM dbo.tp_orderMain tpo INNER JOIN dbo.tp_remark remark ON tpo.OrderNo = remark.OrderNoINNER JOIN dbo.tp_finance finance ON tpo.OrderNo = finance.OrderNoINNER JOIN dbo.tp_tpro tpro ON tpo.OrderNo = tpro.OrderNoLEFT JOIN dbo.wf_reph ph ON tpo.OrderNo=ph.WareCodeLEFT JOIN dbo.tp_re re ON tpo.OrderNo=re.OrderNoLEFT JOIN dbo.tp_sup sup ON tpo.OrderNo = sup.OrderNoINNER JOIN dbo.com_juris AS juris ON juris.UserId = '200000' AND juris.CompanyId = tpo.ClientCode WHERE tpo.OpDate BETWEEN '2018-06-28' AND '2018-07-06' ) AS temp ) as temps where 1=1) as temp_table_temp where RowId between 1 and 10執(zhí)行統(tǒng)計(jì)信息:SQL Server parse and compile time: CPU time = 1092 ms, elapsed time = 1122 ms.SQL Server Execution Times:CPU time = 36223 ms, elapsed time = 45982 ms.
小結(jié):
在最開(kāi)始的時(shí)候,很常見(jiàn)的寫(xiě)法就是一條SQL、加一些合適的索引,就實(shí)現(xiàn)所有功能,在數(shù)據(jù)量小的情況其實(shí)是最優(yōu)的,甚至索引都是越少越好,因?yàn)樗饕蕉嗖迦敫碌乃俣葧?huì)更慢。但在現(xiàn)有的數(shù)據(jù)量的情況下,已經(jīng)完全無(wú)法接受了,我們拋開(kāi)編譯時(shí)間,查詢總記錄數(shù)耗時(shí)111.9秒,查詢第一頁(yè)耗時(shí)45.9秒。
這是我們3年前的實(shí)現(xiàn)方式,當(dāng)時(shí)也是因?yàn)閿?shù)據(jù)量的增長(zhǎng),查詢慢,通過(guò)不斷的測(cè)試,最后有了第二部分。
?
(二)、SQL拆分:先查詢滿足條件的訂單號(hào)、再根據(jù)訂單號(hào)查詢數(shù)據(jù)(in)
1、查詢總記錄數(shù):
具體SQL如下:select sum(a) from (select 1 a from (SELECT tpo.OrderNo,tpo.OpDateFROM dbo.tp_orderMain tpo INNER JOIN dbo.com_juris AS juris ON juris.UserId = '200000' AND juris.CompanyId = tpo.ClientCode WHERE tpo.OpDate BETWEEN '2018-06-28' AND '2018-07-06') as temps where 1=1 ) p執(zhí)行統(tǒng)計(jì)信息:SQL Server parse and compile time: CPU time = 0 ms, elapsed time = 27 ms.SQL Server Execution Times:CPU time = 31 ms, elapsed time = 124 ms.
2、查詢第一頁(yè)訂單號(hào)
具體SQL如下:SELECT * FROM (select ROW_NUMBER() Over(ORDER BY temps.OpDate) as RowId,*from (SELECT tpo.OrderNo,tpo.OpDateFROM dbo.tp_orderMain tpo INNER JOIN dbo.com_juris AS juris ON juris.UserId = '200000' AND juris.CompanyId = tpo.ClientCode WHERE tpo.OpDate BETWEEN '2018-06-28' AND '2018-07-06') as temps where 1=1 ) as temp_table_temp where RowId between 1 and 10執(zhí)行統(tǒng)計(jì)信息:SQL Server parse and compile time: CPU time = 0 ms, elapsed time = 4 ms.SQL Server Execution Times:CPU time = 0 ms, elapsed time = 0 ms.
3、根據(jù)訂單號(hào)查詢信息
具體SQL如下:SELECT *,(SELECT '@#@#@' + rtrim (pd.Remark) FROM dbo.wf_pda pd WHERE pd.ProcessID=temp.ProcessID AND pd.ActID=-2 FOR XML PATH('')) AS BackReasonFROM (SELECT tpo.OrderNo,emark.Remark,finance.a,sup.b,ph.c,ph.ProcessID,re.d,tpro.eFROM dbo.tp_orderMain tpo INNER JOIN dbo.tp_remark remark ON tpo.OrderNo = remark.OrderNoINNER JOIN dbo.tp_finance finance ON tpo.OrderNo = finance.OrderNoINNER JOIN dbo.tp_tpro tpro ON tpo.OrderNo = tpro.OrderNoLEFT JOIN dbo.wf_reph ph ON tpo.OrderNo=ph.WareCodeLEFT JOIN dbo.tp_re re ON tpo.OrderNo=re.OrderNoLEFT JOIN dbo.tp_sup sup ON tpo.OrderNo = sup.OrderNoINNER JOIN dbo.com_juris AS juris ON juris.UserId = '200000' AND juris.CompanyId = tpo.ClientCode where tpo.OrderNo IN ('10001','10002','10003','10004','10005','10006','10007','10008','10009') ) AS temp ORDER BY temp.OpDate DESC執(zhí)行統(tǒng)計(jì)信息:SQL Server parse and compile time: CPU time = 46 ms, elapsed time = 59 ms.Table 'tp_orderMain '. Scan count 16, logical reads 617278, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.Table 'tp_re'. Scan count 8, logical reads 37312, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.SQL Server Execution Times:CPU time = 2044 ms, elapsed time = 2075 ms.小結(jié):
從上面的結(jié)果可以看出來(lái),獲取訂單總記錄數(shù)、獲取第一頁(yè)的訂單信息總時(shí)間在150毫秒左右就可以查出來(lái),不過(guò)在根據(jù)訂單號(hào)in查詢訂單信息的時(shí)候,發(fā)現(xiàn)耗時(shí)2秒。細(xì)心的園友應(yīng)該也發(fā)現(xiàn)我在這個(gè)“統(tǒng)計(jì)信息”里面多復(fù)制了一些東西,有兩個(gè)表的掃描次數(shù)、邏輯讀次數(shù)都不正常,其中主表617278次,導(dǎo)致總耗時(shí)2秒。但這條語(yǔ)句最特殊的是不同的訂單號(hào)執(zhí)行結(jié)果完全不一樣,我開(kāi)始以為是訂單號(hào)跨度越大越慢(我的猜測(cè)依據(jù)是訂單號(hào)是主鍵且升序排列),但在我測(cè)試的過(guò)程中發(fā)現(xiàn)完全沒(méi)有規(guī)律,甚至在我查8條記錄的情況下,耗時(shí)2秒(執(zhí)行多次結(jié)果一致),我再隨便找2個(gè)17年的訂單號(hào)一起查詢,結(jié)果可以幾十毫秒出來(lái)。我們通過(guò)SQL審計(jì)發(fā)現(xiàn)至少有一半以上該語(yǔ)句執(zhí)行時(shí)間在1-2秒,甚至有一些達(dá)到3-4秒,不穩(wěn)定(一直沒(méi)有找到原因,現(xiàn)在只是找了替代方法滿足了穩(wěn)定了性能,參考第三,第四部分)。
這種寫(xiě)法是3年前我們優(yōu)化的結(jié)果,在當(dāng)時(shí)的執(zhí)行統(tǒng)計(jì)信息不是今天這樣的結(jié)果,基本可以在幾百毫秒完成查詢,運(yùn)行到前段時(shí)間基本也沒(méi)有用戶再抱怨此功能慢的問(wèn)題。但最近又開(kāi)始陸續(xù)有用戶抱怨這里慢的問(wèn)題,我們通過(guò)SQL審計(jì)發(fā)現(xiàn)3年前很“優(yōu)秀”的SQL,現(xiàn)在不靈了,性能不穩(wěn)定。
下面先分析一下這種寫(xiě)法:
1、可以看出來(lái),把顯示字段跟查詢條件分開(kāi)后,查詢的時(shí)候關(guān)聯(lián)的表大大減少,大大提高的索引查詢、分頁(yè)查詢的速度;
2、而在需要關(guān)聯(lián)多個(gè)表查詢顯示字段時(shí),已經(jīng)是聚焦索引查找了,在正常情況下基本可以毫秒級(jí)的完成;
?
(三)、用union代替in與or
? 查詢總記錄數(shù)、第一頁(yè)的訂單號(hào)跟第二部分是一樣的,沒(méi)變。
3、根據(jù)訂單號(hào)查詢信息
具體SQL如下(PS:因?yàn)镾QL但長(zhǎng),只列了前2條):SELECT *,(SELECT '@#@#@' + rtrim (pd.Remark) FROM dbo.wf_pda pd WHERE pd.ProcessID=temp.ProcessID AND pd.ActID=-2 FOR XML PATH('')) AS BackReasonFROM (SELECT tpo.OrderNo,emark.Remark,finance.a,sup.b,ph.c,ph.ProcessID,re.d,tpro.eFROM dbo.tp_orderMain tpo INNER JOIN dbo.tp_remark remark ON tpo.OrderNo = remark.OrderNoINNER JOIN dbo.tp_finance finance ON tpo.OrderNo = finance.OrderNoINNER JOIN dbo.tp_tpro tpro ON tpo.OrderNo = tpro.OrderNoLEFT JOIN dbo.wf_reph ph ON tpo.OrderNo=ph.WareCodeLEFT JOIN dbo.tp_re re ON tpo.OrderNo=re.OrderNoLEFT JOIN dbo.tp_sup sup ON tpo.OrderNo = sup.OrderNoINNER JOIN dbo.com_juris AS juris ON juris.UserId = '200000' AND juris.CompanyId = tpo.ClientCode where tpo.OrderNo = '10002') AS temp UNIONSELECT *,(SELECT '@#@#@' + rtrim (pd.Remark) FROM dbo.wf_pda pd WHERE pd.ProcessID=temp.ProcessID AND pd.ActID=-2 FOR XML PATH('')) AS BackReason --訂單回滾原因FROM (SELECT tpo.OrderNo,emark.Remark,finance.a,sup.b,ph.c,ph.ProcessID,re.d,tpro.eFROM dbo.tp_orderMain tpo INNER JOIN dbo.tp_remark remark ON tpo.OrderNo = remark.OrderNoINNER JOIN dbo.tp_finance finance ON tpo.OrderNo = finance.OrderNoINNER JOIN dbo.tp_tpro tpro ON tpo.OrderNo = tpro.OrderNoLEFT JOIN dbo.wf_reph ph ON tpo.OrderNo=ph.WareCodeLEFT JOIN dbo.tp_re re ON tpo.OrderNo=re.OrderNoLEFT JOIN dbo.tp_sup sup ON tpo.OrderNo = sup.OrderNoINNER JOIN dbo.com_juris AS juris ON juris.UserId = '200000' AND juris.CompanyId = tpo.ClientCode where tpo.OrderNo = '10003') AS temp執(zhí)行統(tǒng)計(jì)信息:SQL Server parse and compile time: CPU time = 967 ms, elapsed time = 1023 ms.SQL Server Execution Times:CPU time = 32 ms, elapsed time = 22 ms.
小結(jié):
在實(shí)驗(yàn)了很多種寫(xiě)法,臨時(shí)表,表變量,Wtih等后,還是沒(méi)法穩(wěn)定性能,在慢的那幾個(gè)訂單號(hào)面前,依舊兵敗如山倒。當(dāng)然我們也有想過(guò)可能是索引出了問(wèn)題,需要重建或碎片整理,但對(duì)于投產(chǎn)的庫(kù),沒(méi)有十足的把握,以及成熟的方案情況下,不敢去實(shí)施。
一個(gè)偶然的機(jī)會(huì),我拿一個(gè)訂單號(hào)做實(shí)驗(yàn)發(fā)現(xiàn)很快,于是想到了union,動(dòng)手測(cè)試發(fā)現(xiàn)每一條不同SQL第一次的編譯時(shí)間為1秒左右,執(zhí)行時(shí)間只有22毫秒。真實(shí)情況下,每一次的訂單號(hào)都不一樣的,執(zhí)行總時(shí)間基本都會(huì)在1.1秒左右,超出我們的期望值,且SQL語(yǔ)句太長(zhǎng),不直觀。這個(gè)方案其實(shí)是沒(méi)有在我們的生產(chǎn)環(huán)境最終實(shí)施的,只是一個(gè)中間方案,不過(guò)它給了我一個(gè)方向,單訂單號(hào)的情況執(zhí)行性能很穩(wěn)定,那現(xiàn)在唯一要解決的就是編譯時(shí)間的問(wèn)題,要減少編譯時(shí)間那基本就想到存儲(chǔ)過(guò)程了。
?
(四)、用存儲(chǔ)過(guò)程代替union
存儲(chǔ)過(guò)程創(chuàng)建:
CREATE PROCEDURE [dbo].[pro_OrderList_Select]@OrderNo VARCHAR(50)ASBEGIN SELECT *,(SELECT '@#@#@' + rtrim (pd.Remark) FROM dbo.wf_pda pd WHERE pd.ProcessID=temp.ProcessID AND pd.ActID=-2 FOR XML PATH('')) AS BackReasonFROM (SELECT tpo.OrderNo,emark.Remark,finance.a,sup.b,ph.c,ph.ProcessID,re.d,tpro.eFROM dbo.tp_orderMain tpo INNER JOIN dbo.tp_remark remark ON tpo.OrderNo = remark.OrderNoINNER JOIN dbo.tp_finance finance ON tpo.OrderNo = finance.OrderNoINNER JOIN dbo.tp_tpro tpro ON tpo.OrderNo = tpro.OrderNoLEFT JOIN dbo.wf_reph ph ON tpo.OrderNo=ph.WareCodeLEFT JOIN dbo.tp_re re ON tpo.OrderNo=re.OrderNoLEFT JOIN dbo.tp_sup sup ON tpo.OrderNo = sup.OrderNoINNER JOIN dbo.com_juris AS juris ON juris.UserId = '200000' AND juris.CompanyId = tpo.ClientCode where tpo.OrderNo = @OrderNo ) AS temp ENDGO3、根據(jù)訂單號(hào)查詢信息
具體SQL如下(使用.net的DataSet獲取多行數(shù)據(jù))EXEC pro_OrderList_Select @OrderNo = '10002'
EXEC pro_OrderList_Select @OrderNo = '10003'
執(zhí)行統(tǒng)計(jì)信息:由于是分開(kāi)執(zhí)行,執(zhí)行計(jì)劃太多,我只列其中第四部分有問(wèn)題的那個(gè)表的信息,執(zhí)行總時(shí)間通過(guò)客戶端統(tǒng)計(jì)信息查看,平均在100毫秒左右。Table 'tp_orderMain'. Scan count 2, logical reads 14, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.Table 'tp_re'. Scan count 0, logical reads 4, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
小結(jié):
存儲(chǔ)過(guò)程的最大優(yōu)點(diǎn)就是它是預(yù)編譯的,編譯時(shí)間短,我們這次也是充分利于它的優(yōu)點(diǎn)。把單條訂單信息查詢創(chuàng)建一個(gè)存儲(chǔ)過(guò)程,然后定義一個(gè)表變量,把從存儲(chǔ)過(guò)程中查詢的信息插入表變量中,再查詢表變量??倛?zhí)行時(shí)間基本穩(wěn)定在100毫秒左右,總算是解決了問(wèn)題。
接下來(lái)我們來(lái)說(shuō)說(shuō)存儲(chǔ)過(guò)程的缺點(diǎn):維護(hù)成本高,版本控制不方便,并且對(duì)于數(shù)據(jù)庫(kù)來(lái)說(shuō),要盡量減少業(yè)務(wù)邏輯,因?yàn)閷?duì)于增加程序服務(wù)器是很容易的,增加一臺(tái)服務(wù)器做負(fù)載均衡即可。但數(shù)據(jù)庫(kù)雖然你可以加很多從庫(kù),但在沒(méi)有分庫(kù)策略的情況下,主庫(kù)還是只有一個(gè),所以我們的代碼規(guī)范里面有一條就是禁止使用存儲(chǔ)過(guò)程。通過(guò)跟架構(gòu)師討論,最后決定為了解決性能問(wèn)題,放開(kāi)了使用存儲(chǔ)過(guò)程的限制,但只允許用于此類列表查詢用,其他功能依舊不允許使用存儲(chǔ)過(guò)程。
?
(五)、更高一層次的,冗余數(shù)據(jù),分區(qū),分表,分庫(kù)
冗余數(shù)據(jù):針對(duì)列表顯示字段,涉及一對(duì)多關(guān)系的表,使用冗余字段保存起來(lái);
分區(qū):分區(qū)需要對(duì)每條SQL進(jìn)行特定優(yōu)化,要保證大部分查詢都在一個(gè)分區(qū)內(nèi)解決,不然可能比沒(méi)分區(qū)之前更慢。
分表,分庫(kù),跟分區(qū)類似,只是更徹底,對(duì)于一個(gè)已經(jīng)成熟且規(guī)模龐大的系統(tǒng),無(wú)論是風(fēng)險(xiǎn)還是工作量,都是巨大的,相當(dāng)于小重構(gòu)。
當(dāng)然,未來(lái)隨著業(yè)務(wù)量的增長(zhǎng),可能有一天會(huì)去做這件事情,不過(guò)可能那時(shí)會(huì)是整個(gè)系統(tǒng)的重構(gòu),因?yàn)橐恍v史遺留問(wèn)題,系統(tǒng)拆分不合理,這個(gè)訂單處理系統(tǒng)已經(jīng)不堪負(fù)重了。
?
結(jié)語(yǔ)
任何一個(gè)系統(tǒng)的完善都不是一蹴而就的, 以上優(yōu)化其實(shí)歷時(shí)3年,都是在系統(tǒng)運(yùn)行過(guò)程中,業(yè)務(wù)量的不斷增長(zhǎng),性能問(wèn)題開(kāi)始突顯,以及對(duì)系統(tǒng)要求的不斷提高,而驅(qū)動(dòng)研發(fā)去不斷的優(yōu)化。每個(gè)階段優(yōu)化的方案也有所不同,最好的不一定是最優(yōu)的,找合適系統(tǒng)現(xiàn)階段發(fā)展的才是最優(yōu)的。這是一個(gè)系統(tǒng)不斷優(yōu)化的過(guò)程,也是整個(gè)團(tuán)隊(duì)能力不斷提升的過(guò)程。
轉(zhuǎn)載于:https://www.cnblogs.com/tihb666/p/9274611.html
總結(jié)
以上是生活随笔為你收集整理的SQL性能优化-查询条件与字段分开执行,union代替in与or,存储过程代替union的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: newcode wyh的吃鸡(优势队列+
- 下一篇: MySQL之事务、锁