2017年4月15日 星期六

如何在現有的資料表(Table)使用Partition

通常在刪除大量資料時,例如一次要看幾百萬甚至千萬筆的資料,使用Delete會砍到天荒地老,而且一不小心LOG還會大到爆炸,但一般建立Partition Table都是在一開始建立Table時就給定Partition Schema,那已經建立的Table但尚未套用Partition Schema是不是就無法使用Partition了呢?
情況:
上個禮拜同事的報表資料庫要進行刪除舊資料的動作,為了避免資料庫的紀錄檔(LDF)過大,同事每50萬筆進行Commit一次,於是悲劇就要開始發生,刪除到一半發山每50萬筆需要花費25分鐘,但是要刪除的資料有1700萬筆,而且後面還有二個Table要刪除資料量也不小,於是使用Partition的念頭就油然而生了。
發現:
公司的某些資料倉儲當初尚未規劃Partition,原因有很多,一是不知道Partition,一是當初的資料庫是SQL SERVER 2000,但不論是哪一種在大量異動資料(新增、更新與刪除)時效能會非常差,有規劃PartitionTable都是一開始建立好的,所以我開始思考如何在現有的Table加入Partition
解決方案:
於是上網問了一下G大神,終於在這篇文章找到方法,除了新建一個有PartitionTable後再將資料從舊資料表匯入之外,另一種方式就是使用Cluster Index的方式套用Partition
執行過程:
第一步:建立原始資料表
--STEP 1
--建立原始資料表
IF OBJECT_ID('partTable') IS NOT NULL
     BEGIN
         DROP TABLE partTable
     END
 
CREATE TABLE partTable (col1 VARCHAR(6), col2 VARCHAR(20)) ;
GO

 --建立CLUSTERED INDEX
IF EXISTS (SELECT NAME FROM SYS.INDEXES WHERE NAME = 'ClusteredIndex_partTable')
     BEGIN
         DROP INDEX [ClusteredIndex_partTable] ON [dbo].[partTable]
     END
CREATE CLUSTERED INDEX [ClusteredIndex_partTable] ON [dbo].[partTable]([col1] ASC)

--建立NONCLUSTERED INDEX
IF EXISTS (SELECT NAME FROM SYS.INDEXES WHERE NAME = 'NonClusteredIndex_partTable')
     BEGIN
         DROP INDEX [NonClusteredIndex_partTable] ON [dbo].[partTable]
     END
CREATE INDEX [NonClusteredIndex_partTable] ON [dbo].[partTable] ([col2] ASC)

第二步:建立Partition FunctionSchema


--建立 PARTITION FUNCTION
IF EXISTS (SELECT NAME FROM SYS.PARTITION_FUNCTIONS WHERE name = 'partRange1')
     BEGIN
         DROP PARTITION FUNCTION partRange1
     END
GO
CREATE PARTITION FUNCTION partRange1 (VARCHAR(6))
AS RANGE LEFT FOR VALUES ('201604') ;
GO
 
--建立 PARTITION SCHEME
IF EXISTS (SELECT NAME FROM SYS.PARTITION_SCHEMES WHERE name = 'partScheme1')
     BEGIN
         DROP PARTITION SCHEME partScheme1
     END
CREATE PARTITION SCHEME partScheme1
AS PARTITION partRange1
ALL TO ([PRIMARY]) ;
GO


第三步:ClusterNonCluster Index刪除並重建,重建時必須套用Partition Schema



--建立CLUSTERED INDEX,並套用PARTITION SCHEME
IF EXISTS (SELECT NAME FROM SYS.INDEXES WHERE NAME = 'ClusteredIndex_partTable')
     BEGIN
         DROP INDEX [ClusteredIndex_partTable] ON [dbo].[partTable]
     END
CREATE CLUSTERED INDEX [ClusteredIndex_partTable] ON [dbo].[partTable] ([col1] ASC) ON partScheme1([col1])

--建立NONCLUSTERED INDEX,並套用PARTITION SCHEME
IF EXISTS (SELECT NAME FROM SYS.INDEXES WHERE NAME = 'NonClusteredIndex_partTable')
     BEGIN
         DROP INDEX [NonClusteredIndex_partTable] ON [dbo].[partTable]
     END
CREATE INDEX [NonClusteredIndex_partTable] ON [dbo].[partTable] ([col2] ASC) ON partScheme1([col1])



第四步:確認Partition套用成功


結論:

由上得知,在已經建立好的資料表可以透過重新建立Cluster Index的方式套用Partition,這對日後維護大資料的Table有很大的幫助,下一節將示範如何在已經有Index的情況下進行資料的轉換(SWITCH)

2017年4月3日 星期一

淺談Parameter Sniffing

情況
為何執行Stored Procedure有時會一下快有時一下慢?
為何同一句SQL包在Stored Procedure有時候很慢,但直接執行SQL Command就很快?相信大家都有類似的經驗,平常執行某支Stored Procedure時都跑的好好的,但是特定在某個時間點就跑的很慢,這種情況我也遇到過,我們來看看為何吧?
發現:
於是我觀察在特定時間點的參數與執行結果,發現在月底做月報表時呼叫Stored Procedure,時間參數區間會比較長,傳回結果也較多,但執行速度卻慢很多,但是同樣參數直接使用SQL Command差異卻不大,在摸不著頭緒時我就找G大神求救了。
思考:
很快的找到答案了,原來這種靈異現象是因為Parameter Sniffing而產生,(英文爛不知道怎麼翻譯就直接貼出來了),那甚麼是Parameter Sniffing?
故事就要從頭說起了,SQL Server在第一次執行Stored Procedure時都會建立一個執行計畫優化執行效能,在建立執行計畫時會藉著第一次傳入的參數為Base而建立,Stored Procedure因傳入的參數而產生執行過程就是Parameter SniffingParameter Sniffing所帶來的影響就是沒有效率的執行計畫進而讓直行效率下滑。

測試:
我們來實際測試一下,測試情境為使用不同的參數查詢資料筆數為1000000的資料表。

1.      建立測試資料表


2.      建立測試SP

3.      第一次測試SP

4.      第一次測試SP結果
測試結果得到資料筆數分別為109484885,執行計畫兩者都是使用Index Scan(參數為1999-01-011999-12-31的執行計畫),消耗的邏輯讀取IO皆為3593

執行結果

執行計畫

執行SP所消耗的IO

5.      第二次測試SP

6.      第二次測試SP結果
測試結果得到資料筆數分別為885109484,執行計畫兩者都是使用索引搜尋加上索引鍵查閱(參數為2005-01-012005-01-03的執行計畫),消耗的邏輯讀取IO分別為2725335550

執行結果

執行計畫

執行SP所消耗的IO


解決方案:
如上述,Parameter Sniffing是第一次執行Stored Procedure 時,SQL Server根據傳入的參數,建立以該參數為基礎的最佳的執行計畫,要特別注意的是這裡說的第一次是指在compile recompile時還有沒任何procedure cache(我知道這一句有比較難懂),我們可以藉由使用下列方式來避免Parameter Sniffing的發生。
(1)   使用RECOMPILE
使用RECOMPILE的好處就是每次執行時每次都用重新建立執行計畫,如此就可以避免Parameter Sniffing的發生。

(2)   使用LOCAL參數將Parameter Sniffing停用
Stored Procedure宣告Loacl參數,使用Loacl參數存取外界傳進來的參數,如本範例使用@StartDate@StopDate來存取@BeginDate@EndDate,使用Loacl參數時當作WHERE條件時,由於SQL Server不知道 Local參數的內容為何,所以會建立一個通用的執行計畫。

(3)   依據需求建立多個適當的Stored Procedures
這個方法是依程式開發的需求,例如月報表就建立一支SP,而平日的日報表也建立一支SP,利用不同的SP來區隔不同情況的執行計畫。

結論:
有人會問使用Stored Procedures的其中一個優點不就是建立執行計畫的快取,下一次執行SP時就不用再次建立執行計畫進而提升效能,如果重新RECOMPILE不就是失去這項優勢了嗎?
我的想法是當重新產生執行計畫的Overhead遠小於Parameter Sniffing所帶來的衝擊,那就RECOMPILE吧,如果程式呼叫SP的條件與回傳資料數目差異不多時就用快取的執行計畫,簡而言之視情況而定。
此外同一句SQLWHERE條件直接給予特定內容(如下圖),執行SQL指令時就不會有Parameter Sniffing的情況,因為每一次執行時SQL Server Engine已經知道傳入的內容,故可以依據WHERE條件產生最佳的執行計畫,這就是為何同一句SQL包在Stored Procedure有時候很慢,但直接執行很快的原因了。



2017年4月1日 星期六

為何使用SQL SERVER Data Compression會讓查詢效能變慢與資料空間上升

啟用資料表Page壓縮反而讓查詢效能下降與資料空間上升?

情況:
在公司遇到一個很奇怪的現象, A是舊機器,B是新機器,BCPU、記憶體與Storage性能上都比A好上很多,同一支程式的執行結果A機器比B機器較能還好(1VS 2分鐘),當下檢查伺服器設定與更新資料表的統計資訊,在確定幾個比較重要的設定與統計資訊沒問題之後,再次執行同一支程式後結果還是相同。
發現:
於是開始比對兩邊資料庫的環境,結果發現某個資料表的資料筆數相同,但資料空間卻差異很大,在A的資料空間不到1MB,在B將近16MB,檢查相關設定之後發現B的資料表啟用了Page壓縮,在使用sp_estimate_data_compression_savings評估之後,當Table沒有啟用壓縮後,資料容量變回不到1MB,再次測試的結果兩邊執行的結果皆為1秒左右。
思考:
當下的疑問是為何啟用資料表壓縮後,資料量反而會變大呢?查了一下MSDN,發現由於B機器是使用Page壓縮,而Page壓縮使用的是Prefix CompressionDictionary Compression兩種壓縮技術,使用這兩種技術壓縮會產生metadata,如下圖,
原始Table

使用Prefix Compression壓縮產生的metadata

使用Dictionary Compression技術產生的metadata


簡而言之,壓縮資料時會產生metadata,而產生metadata的大於原本要壓縮的資料時,就會產生啟用資料壓縮時資料容量(MB)大於尚未壓縮的情況。

解決方案:
那麼要如何避免這種慘劇發生呢?答案是使用sp_estimate_data_compression_savings這是Stored Procedure來評估啟用資料壓縮後空間容量的變化。

1.      啟用資料壓縮後資料容量上升
首先來模擬資料啟用壓縮資料會讓空間變大的情境,壓縮資料通常是資料長的很像壓縮比才會提升,所以使用NEWID()這種函數,讓產生內容都是唯一值,如此啟用資料壓縮後資料容量會變大。

測試SCRIPT如下

USE MASTER
GO
ALTER DATABASE TEST SET RECOVERY SIMPLE
GO
CHECKPOINT
USE TEST
SET NOCOUNT ON
GO
IF EXISTS ( SELECT t.name FROM sys.tables t
                        INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
                        WHERE t.name = 'TestTable' AND s.name = 'dbo'
                  )
BEGIN
        DROP TABLE dbo.TestTable
END
GO
CREATE TABLE dbo.TestTable (
 C1 CHAR(36) )
GO
INSERT INTO TestTable SELECT NEWID()
GO 20000

檢視建立的資料表空間內容,資料表空間為0.875MB,資料壓縮類型為None(就是不壓縮)



我們使用sp_estimate_data_compression_savings來評估壓縮前後的空間容量。
由上圖可知啟用資料表壓縮,資料空間反而比壓縮前還大。


接下來進入啟用PAGE壓縮,壓縮SCRIPT如下

USE [Test]

ALTER TABLE [dbo].[TestTable] REBUILD PARTITION = ALL
WITH
(DATA_COMPRESSION = PAGE
)

啟用PAGE壓縮後,我們發現資料空間確實比壓縮前還大。


那會有甚麼影響呢?有的,資料空間變大,意味著要用到的PAGE變多,使用的記憶體回提升,這代表了邏輯存取會提高,進而讓查詢效能降低。

我們來看看驗證結果吧:
當不啟用資料壓縮時
測試SCRIPT
USE [Test]
GO
SET STATISTICS TIME ON
SET STATISTICS IO ON
GO

ALTER TABLE [dbo].[TestTable] REBUILD PARTITION = ALL
WITH
(DATA_COMPRESSION = NONE
)
GO

DBCC DROPCLEANBUFFERS
DBCC FREEPROCCACHE
GO

SELECT * FROM [dbo].[TestTable]




執行結果
尚未啟用資料壓縮時,邏輯讀取為112,執行時間為184


當啟用資料壓縮為Page

啟用資料壓縮為PAGE時,邏輯讀取為114,執行時間為194


結論:
啟用資料表的資料壓縮大部份都可以提升效能和減少資料空間,但也有特例,所以在啟用壓縮之前,要使用sp_estimate_data_compression_savings去評估啟用資料表壓縮後空間的變化,才不會弄巧成拙。

後話:

如果各位測試差距不明顯的話,可以將資料數目從2萬變成5萬看看,雖然說知道會有狀況,不過遇到公司提升快16MB的情況還是第一次遇到。最重要的是我產生全資料庫所有資料表資料壓縮的SCRIPT需要修改了。Orz