SQL Server優化技巧之SQL Server中的"MapReduce"


日常的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
View Code

 

當我們針對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是我的回答)

 關於SQL Server排序使用內存的討論

在介紹”類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相關的培訓或是優化,架構等方面的需求可以聯系我.為了小藍天,為了家要更拼些.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM