日常的OLTP環境中,有時會涉及到一些統計方面的SQL語句,這些語句可能消耗巨大,進而影響整體運行環境,這里我為大家介紹如何利用SQL Server中的”類MapReduce”方式,在特定的統計情形中不犧牲響應速度的情形下減少資源消耗.
我們可能經常會利用開窗函數對巨大的數據集進行分組統計排序.比如下面的例子:
腳本環境

/* This script creates two new tables in AdventureWorks: dbo.bigProduct dbo.bigTransactionHistory */ USE AdventureWorks GO SELECT p.ProductID + (a.number * 1000) AS ProductID, p.Name + CONVERT(VARCHAR, (a.number * 1000)) AS Name, p.ProductNumber + '-' + CONVERT(VARCHAR, (a.number * 1000)) AS ProductNumber, p.MakeFlag, p.FinishedGoodsFlag, p.Color, p.SafetyStockLevel, p.ReorderPoint, p.StandardCost, p.ListPrice, p.Size, p.SizeUnitMeasureCode, p.WeightUnitMeasureCode, p.Weight, p.DaysToManufacture, p.ProductLine, p.Class, p.Style, p.ProductSubcategoryID, p.ProductModelID, p.SellStartDate, p.SellEndDate, p.DiscontinuedDate INTO bigProduct FROM Production.Product AS p CROSS JOIN master..spt_values AS a WHERE a.type = 'p' AND a.number BETWEEN 1 AND 50 GO ALTER TABLE bigProduct ALTER COLUMN ProductId INT NOT NULL GO ALTER TABLE bigProduct ADD CONSTRAINT pk_bigProduct PRIMARY KEY (ProductId) GO SELECT ROW_NUMBER() OVER ( ORDER BY x.TransactionDate, (SELECT NEWID()) ) AS TransactionID, p1.ProductID, x.TransactionDate, x.Quantity, CONVERT(MONEY, p1.ListPrice * x.Quantity * RAND(CHECKSUM(NEWID())) * 2) AS ActualCost INTO bigTransactionHistory FROM ( SELECT p.ProductID, p.ListPrice, CASE WHEN p.productid % 26 = 0 THEN 26 WHEN p.productid % 25 = 0 THEN 25 WHEN p.productid % 24 = 0 THEN 24 WHEN p.productid % 23 = 0 THEN 23 WHEN p.productid % 22 = 0 THEN 22 WHEN p.productid % 21 = 0 THEN 21 WHEN p.productid % 20 = 0 THEN 20 WHEN p.productid % 19 = 0 THEN 19 WHEN p.productid % 18 = 0 THEN 18 WHEN p.productid % 17 = 0 THEN 17 WHEN p.productid % 16 = 0 THEN 16 WHEN p.productid % 15 = 0 THEN 15 WHEN p.productid % 14 = 0 THEN 14 WHEN p.productid % 13 = 0 THEN 13 WHEN p.productid % 12 = 0 THEN 12 WHEN p.productid % 11 = 0 THEN 11 WHEN p.productid % 10 = 0 THEN 10 WHEN p.productid % 9 = 0 THEN 9 WHEN p.productid % 8 = 0 THEN 8 WHEN p.productid % 7 = 0 THEN 7 WHEN p.productid % 6 = 0 THEN 6 WHEN p.productid % 5 = 0 THEN 5 WHEN p.productid % 4 = 0 THEN 4 WHEN p.productid % 3 = 0 THEN 3 WHEN p.productid % 2 = 0 THEN 2 ELSE 1 END AS ProductGroup FROM bigproduct p ) AS p1 CROSS APPLY ( SELECT transactionDate, CONVERT(INT, (RAND(CHECKSUM(NEWID())) * 100) + 1) AS Quantity FROM ( SELECT DATEADD(dd, number, '20050101') AS transactionDate, NTILE(p1.ProductGroup) OVER ( ORDER BY number ) AS groupRange FROM master..spt_values WHERE type = 'p' ) AS z WHERE z.groupRange % 2 = 1 ) AS x ALTER TABLE bigTransactionHistory ALTER COLUMN TransactionID INT NOT NULL GO ALTER TABLE bigTransactionHistory ADD CONSTRAINT pk_bigTransactionHistory PRIMARY KEY (TransactionID) GO CREATE NONCLUSTERED INDEX IX_ProductId_TransactionDate ON bigTransactionHistory ( ProductId, TransactionDate ) INCLUDE ( Quantity, ActualCost ) GO
當我們針對bigProduct表的productid分組,並按照bigTransactionHistory的actualcost
及quantity分別排序取結果集語句如下:
code
Declare @p1 int, @p2 nvarchar(56), @p3 smallint, @p4 int, @p5 bigint, @p6 bigint select @p1=p.productid, @p2=p.productnumber, @p3=p.reorderpoint, @p4=th.transactionid, @p5=rank()over (partition by p.productid order by th.actualcost desc), @p6=rank()over (partition by p.productid order by th.quantity desc) from bigproduct as p join bigtransactionhistory as th on th.productid=p.productid where p.productid between 1001 and 3001
執行此語句並輸出實際執行計划如圖1-1
圖1-1
可以看出我的這條語句由於對大量結果集進行排序,致使消耗了365MB的內存,並且由於分別對actualcost, quantity排序使得在進行第二個排序時內存不足並溢出,排序的操作只能在tempdb中進行.
Sort由於是典型的計算密集型運算符,此查詢在我的機器上執行時間為5s
大量的內存被個別查詢長時間獨占,使得Buffer Pool的穩定性下降,進而可能影響整體吞吐.
這里關於Sort運算的資源消耗我就不細說了,SQL Server的資深從業者鄒建曾經發帖問及過關於排序內存消耗的問題,我在跟帖中解答過,有興趣的朋友可以看看(shanks_gao是我的回答)
在介紹”類MapReduce”之前,我想先接着上面Sort溢出的現象給大家簡單介紹下通過Query hints 來影響優化器的資源分配.
廢話不說,直接上菜:
code
Declare @p1 int, @p2 nvarchar(56), @p3 smallint, @p4 int, @p5 bigint, @p6 bigint, @i int select @i=3001; with p as ( select productid, ProductNumber=convert(nvarchar(56),ProductNumber), reorderpoint from bigproduct as bp ) select @p1=p.productid, @p2=p.productnumber, @p3=p.reorderpoint, @p4=th.transactionid, @p5=rank()over (partition by p.productid order by th.actualcost desc), @p6=rank()over (partition by p.productid order by th.quantity desc) from bigproduct as p join bigtransactionhistory as th on th.productid=p.productid where p.productid between 1001 and @i option(OPTIMIZE FOR (@i=5001))
通過查詢可以看出由於我加了Query Hint,改變了優化器的資源評估標准,使得優化器認為productid本身需要資源從1001 and 3001分配變為了1001 and 5001分配,內存申請由365MB變為了685MB,接近一倍的增長,避免了溢出.並且執行時間也由5S變為了2S.提升了用戶體驗
如圖1-2
圖1-2
可以看到溢出與不溢出在查詢消耗時間上差別很大,但這樣就是好了嗎?其實未必,畢竟即便在非溢出的情形中將近700MB的內存近2s內被這個查詢占用,這在高並發的OLTP環境中是傷全局的.那更理想的解決方式呢?
在並行執行計划中是多個線程(CPU核)協同工作,這里面的Sort面對大量數據結果集時即便多核同時進行,在復雜的預算面前也是有些力不從心.在分布式的思想中,講究分而治之,我們只要將大的結果集化為多個小的部分並多核同時進行排序,這樣就達到了分而治之的效果.也就是標題說的”MapReduce”
幸好,在SQL Server實現並行運算的運算符”nestloop”與之相似.
並行Nest loop Join實現方式
在並行循環嵌套中,外表數據Scan,seek多線程(threads)同時進行(Map),而內表的在每個thread上串行執行(Reduce).
優點:可以減少執行過程中各線程數據流的數據交換
顯著的減少內存需求.
上述查詢我用如下的方式實現:
code
Declare @p1 int, @p2 nvarchar(56), @p3 smallint, @p4 int, @p5 bigint, @p6 bigint select @p1=p.productid, @p2=p.productnumber, @p3=p.reorderpoint, @p4=ca.transactionid, @p5=ca.linetotalrank, @p6=ca.orderqtyrank from bigproduct as p cross apply ( select th.transactionid, linetotalrank=rank()over( order by th.actualcost desc), orderqtyrank=rank() over( order by th.quantity desc) from bigtransactionhistory as th where th.productid=p.productid ) as ca where p.productid between 1001 and 3001
執行中輸出實際執行計划可以看出,此計划中消耗的內存15MB,和上述的執行計划相比有指數級的下降,同時執行時間為不到2s,保證執行時間的同時明顯降低了資源消耗,從而避免了實例級的影響.
已經很美好了:)
如圖1-3
圖1-3
到這里其實我們已經達到了我們想要的效果,但還可以更好嗎?我們還需要多了解些.
上面我講到了並行nest loops的優點,少資源占用,少數據交換.但就像在我以前的博客中說的那樣:”任何術都是有缺陷的”,並行中很可能造成數據的傾斜,如上圖1-3中藍線中標注的外表seek,實際是只在一個thread中完成的.優化器為我們加了數據交換,使得外部的數據在多個threads下分布均衡與內表匹配提升效率,但優化器可不會每次都如此”好心”(智能).
其實在並行seek,scan中由於實現方式在05到08的過程變化很大,使得操作更需注意,這里我就先不細說了,在之后的博客或是講座中我再分享.
我們直接上解決方案:
select bp.productid, bp.productnumber, bp.reorderpoint into #p from bigproduct as bp where bp.productid between 1001 and 3001 alter table #p add primary key (productid) Declare @p1 int, @p2 nvarchar(56), @p3 smallint, @p4 int, @p5 bigint, @p6 bigint select @p1=p.productid, @p2=p.productnumber, @p3=p.reorderpoint, @p4=ca.transactionid, @p5=ca.linetotalrank, @p6=ca.orderqtyrank from #p as p cross apply ( select th.transactionid, linetotalrank=rank()over( order by th.actualcost desc), orderqtyrank=rank() over( order by th.quantity desc) from bigtransactionhistory as th where th.productid=p.productid ) as ca drop table #p
通過查詢時輸出執行計划 如圖1-4所示
我們可以看到通過將外表數據放入臨時表中,使得內存消耗進一步降低,而數據較為平均的分布到多個threads中,你可能看到其中不少threads是沒有數據的,其實有時需要我們根據查詢管控並行度的.而在執行時間上有可能得到進一步的改善!
圖1-4
說點體外話,不少朋友認為SQL Server是小兒科,沒內容,技術含量不高.而且在國內的互聯網公司中又顯得格格不入.這里我可以告訴大家,SQL Server,乃至關系型數據庫的水很深.舉個簡單的例子在雙11當晚,我對我們的一個實例調整了一個大家可能都知道的參數就使得CPU消耗明顯下降而訪問量繼續增加,但調整這個參數的過程遠沒有動動手那么簡單..如果你是相關的從業者,全身心的投入進來吧,其實很好玩.
結語:作為一個DBA,一個IT從業者處理問題時時刻需要我們權衡,權衡的基礎就是我們的知識儲備及經驗,願我們大家一起努力,一起成長.
/*******************************************************************/
最后奉上我兒子小藍天的靚照.
小寶貝出生了,壓力增加,動力更強了,哪些朋友如果有SQL Server相關的培訓或是優化,架構等方面的需求可以聯系我.為了小藍天,為了家要更拼些.