SQL性能优化-查询条件与字段分开执行,union代替in与or,存储过程代替union
PS:概要、背景、結語都是日常“裝X”,可以跳過直接看優化歷程
環境:SQL Server 2008 R2、阿里云RDS;輔助工具:SQL 審計
概要
一個訂單列表分頁查詢功能,單從SQL性能來講,從幾十萬數據量時,適當加一些索引隨便寫SQL;到百來萬數據量時,需要做一些SQL語句優化;再到幾百萬上千萬的數據量情況下,很多意想不到的情況就出現了(在大部分中小公司沒有專業DBA的情況下,“萬能”的研發就得頂上去了)。
?
背景
進入公司后,系統已經初具規模,已有成型的框架。隨著業務的不斷增長,系統功能的不斷增加,一些性能問題開始涌現出來。
以下訂單列表查詢頁面的優化歷程,有幸參與了整個過程,以前做研發或項目管理的時候,整天撲在功能開發上,很難有時間去深入研究一些比較難解決的問題。現在做團隊管理,反而有更多的時間讓我來思考與總結,以下的每一步優化其實都不是一開始就想到的,而是經過無數次的摸索以及線上測試驗證,最終找到合適于現階段的優化方案。
本人非專業DBA,目前的崗位是研發管理,園友們如果有更好的解決方案歡迎討論。當然我們也有想過一些更徹底的解決方案,我會在第五部分進行描述,如:冗余數據、數據庫分區、分表、分庫等,但受制于研發資源、開發周期等只能擱置。從公司的角度來講,永遠都是用最小的成本去實現最大的價值。
優化歷程
以下所有“執行統計信息”都是在現有的數據量情況下,且所有條件都是頁面默認打開的情況下(訂單主表大概1千萬左右,其他附屬表最大1億左右,以下所有表名、字段名都替換過且有些刪減)。統計信息限于篇幅,只貼出了執行時間,具體分析用的掃描次數、邏輯讀次數就沒貼了,時間只是其中一個參考值,掃描次數,邏輯讀次數也是很重要的參考值。
(一)、一條SQL語句實現查詢條件,返回字段
1、查詢總記錄數:
執行統計信息: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、查詢第一頁訂單信息
具體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執行統計信息: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.
小結:
在最開始的時候,很常見的寫法就是一條SQL、加一些合適的索引,就實現所有功能,在數據量小的情況其實是最優的,甚至索引都是越少越好,因為索引越多插入更新的速度會更慢。但在現有的數據量的情況下,已經完全無法接受了,我們拋開編譯時間,查詢總記錄數耗時111.9秒,查詢第一頁耗時45.9秒。
這是我們3年前的實現方式,當時也是因為數據量的增長,查詢慢,通過不斷的測試,最后有了第二部分。
?
(二)、SQL拆分:先查詢滿足條件的訂單號、再根據訂單號查詢數據(in)
1、查詢總記錄數:
具體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執行統計信息: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、查詢第一頁訂單號
具體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執行統計信息: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、根據訂單號查詢信息
具體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執行統計信息: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.小結:
從上面的結果可以看出來,獲取訂單總記錄數、獲取第一頁的訂單信息總時間在150毫秒左右就可以查出來,不過在根據訂單號in查詢訂單信息的時候,發現耗時2秒。細心的園友應該也發現我在這個“統計信息”里面多復制了一些東西,有兩個表的掃描次數、邏輯讀次數都不正常,其中主表617278次,導致總耗時2秒。但這條語句最特殊的是不同的訂單號執行結果完全不一樣,我開始以為是訂單號跨度越大越慢(我的猜測依據是訂單號是主鍵且升序排列),但在我測試的過程中發現完全沒有規律,甚至在我查8條記錄的情況下,耗時2秒(執行多次結果一致),我再隨便找2個17年的訂單號一起查詢,結果可以幾十毫秒出來。我們通過SQL審計發現至少有一半以上該語句執行時間在1-2秒,甚至有一些達到3-4秒,不穩定(一直沒有找到原因,現在只是找了替代方法滿足了穩定了性能,參考第三,第四部分)。
這種寫法是3年前我們優化的結果,在當時的執行統計信息不是今天這樣的結果,基本可以在幾百毫秒完成查詢,運行到前段時間基本也沒有用戶再抱怨此功能慢的問題。但最近又開始陸續有用戶抱怨這里慢的問題,我們通過SQL審計發現3年前很“優秀”的SQL,現在不靈了,性能不穩定。
下面先分析一下這種寫法:
1、可以看出來,把顯示字段跟查詢條件分開后,查詢的時候關聯的表大大減少,大大提高的索引查詢、分頁查詢的速度;
2、而在需要關聯多個表查詢顯示字段時,已經是聚焦索引查找了,在正常情況下基本可以毫秒級的完成;
?
(三)、用union代替in與or
? 查詢總記錄數、第一頁的訂單號跟第二部分是一樣的,沒變。
3、根據訂單號查詢信息
具體SQL如下(PS:因為SQL但長,只列了前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執行統計信息: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.
小結:
在實驗了很多種寫法,臨時表,表變量,Wtih等后,還是沒法穩定性能,在慢的那幾個訂單號面前,依舊兵敗如山倒。當然我們也有想過可能是索引出了問題,需要重建或碎片整理,但對于投產的庫,沒有十足的把握,以及成熟的方案情況下,不敢去實施。
一個偶然的機會,我拿一個訂單號做實驗發現很快,于是想到了union,動手測試發現每一條不同SQL第一次的編譯時間為1秒左右,執行時間只有22毫秒。真實情況下,每一次的訂單號都不一樣的,執行總時間基本都會在1.1秒左右,超出我們的期望值,且SQL語句太長,不直觀。這個方案其實是沒有在我們的生產環境最終實施的,只是一個中間方案,不過它給了我一個方向,單訂單號的情況執行性能很穩定,那現在唯一要解決的就是編譯時間的問題,要減少編譯時間那基本就想到存儲過程了。
?
(四)、用存儲過程代替union
存儲過程創建:
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、根據訂單號查詢信息
具體SQL如下(使用.net的DataSet獲取多行數據)EXEC pro_OrderList_Select @OrderNo = '10002'
EXEC pro_OrderList_Select @OrderNo = '10003'
執行統計信息:由于是分開執行,執行計劃太多,我只列其中第四部分有問題的那個表的信息,執行總時間通過客戶端統計信息查看,平均在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.
小結:
存儲過程的最大優點就是它是預編譯的,編譯時間短,我們這次也是充分利于它的優點。把單條訂單信息查詢創建一個存儲過程,然后定義一個表變量,把從存儲過程中查詢的信息插入表變量中,再查詢表變量。總執行時間基本穩定在100毫秒左右,總算是解決了問題。
接下來我們來說說存儲過程的缺點:維護成本高,版本控制不方便,并且對于數據庫來說,要盡量減少業務邏輯,因為對于增加程序服務器是很容易的,增加一臺服務器做負載均衡即可。但數據庫雖然你可以加很多從庫,但在沒有分庫策略的情況下,主庫還是只有一個,所以我們的代碼規范里面有一條就是禁止使用存儲過程。通過跟架構師討論,最后決定為了解決性能問題,放開了使用存儲過程的限制,但只允許用于此類列表查詢用,其他功能依舊不允許使用存儲過程。
?
(五)、更高一層次的,冗余數據,分區,分表,分庫
冗余數據:針對列表顯示字段,涉及一對多關系的表,使用冗余字段保存起來;
分區:分區需要對每條SQL進行特定優化,要保證大部分查詢都在一個分區內解決,不然可能比沒分區之前更慢。
分表,分庫,跟分區類似,只是更徹底,對于一個已經成熟且規模龐大的系統,無論是風險還是工作量,都是巨大的,相當于小重構。
當然,未來隨著業務量的增長,可能有一天會去做這件事情,不過可能那時會是整個系統的重構,因為一些歷史遺留問題,系統拆分不合理,這個訂單處理系統已經不堪負重了。
?
結語
任何一個系統的完善都不是一蹴而就的, 以上優化其實歷時3年,都是在系統運行過程中,業務量的不斷增長,性能問題開始突顯,以及對系統要求的不斷提高,而驅動研發去不斷的優化。每個階段優化的方案也有所不同,最好的不一定是最優的,找合適系統現階段發展的才是最優的。這是一個系統不斷優化的過程,也是整個團隊能力不斷提升的過程。
轉載于:https://www.cnblogs.com/tihb666/p/9274611.html
總結
以上是生活随笔為你收集整理的SQL性能优化-查询条件与字段分开执行,union代替in与or,存储过程代替union的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: newcode wyh的吃鸡(优势队列+
- 下一篇: linux cmake编译源码,linu