2012以后提供了一種不同於傳統B樹結構的索引類型,就是內存列存儲索引。這種索引應用了一種基於列的存儲模式,也是一種新的查詢執行的批處理模式,並且為特定的負載提供了巨大的性能提升。它是如何構建?如何工作?又是為什么能對性能有如此大的提升,接下來我們用簡明的描述和詳盡的示例來解釋說明。
那么列存儲索引究竟是什么?大多數時候,列存儲索引被描述作為一種數據倉庫和數據報表的功能。事實上,你最有可能就是在這種情況下利用這種索引。然而,即使在OLTP數據庫中,你也會遇到一些要從大量數據表中獲取數據的報表,它們是非常緩慢的。在合適的計划和謹慎的使用下,甚至這些報表也能利用列存儲索引得到性能的提高。一個重要的前提是數據非常大,列存儲索引是用來與大數據表一起使用的。雖然沒有明確的最小要求,但是作為經驗,我建議至少要有一千萬的行數據在一個單表中才能受益於列存儲索引。
對於這個系列中的例子,將使用 ContosoRetailDW
作為演示 數據庫,下載地址:http://www.microsoft.com/en-us/download/details.aspx?id=18279,這是一個626MB的數據庫備份,大概1.2GB大小的數據庫,對於列存儲索引而言有點小,但是對於演示功能來說足夠大了。這個數據庫本身不包含任何列存儲索引,事實上不是一個壞事,為了能更好的體現列存儲索引的優點,我們將對同一查詢對比帶和不帶列存儲索引的性能。下面的例子是一個典型的來自於BI信息工作人員的查詢。
WITH ContosoProducts AS (SELECT * FROM dbo.DimProduct WHERE BrandName = 'Contoso') SELECT cp.ProductName, dd.CalendarQuarter, COUNT(fos.SalesOrderNumber) AS NumOrders, SUM(fos.SalesQuantity) AS QuantitySold FROM dbo.FactOnlineSales AS fos INNER JOIN dbo.DimDate AS dd ON dd.Datekey = fos.DateKey INNER JOIN ContosoProducts AS cp ON cp.ProductKey = fos.ProductKey GROUP BY cp.ProductName, dd.CalendarQuarter ORDER BY cp.ProductName, dd.CalendarQuarter;
Listing 1: 典型的BI查詢
在我的筆記本上,這個查詢平均花費了6.27秒來讀取已經在緩存中的數據,假如數據被直接從硬盤上讀取這個執行將花費8.11秒。由於FactOnlineSales
表中有超過12500000行的數據,這個查詢必須掃描整個聚集索引,其實這樣還不錯,但是假如你整天面對這樣的查詢,這樣的遲緩的響應將變成一個非常惡心的事情,同時也能聯想到如果數據庫是十倍甚至百倍大小時回事什么樣的性能表現?
注意這些執行時間是基於硬件設備的使用,假如重復執行這些測試在一個高端設備上,這些查詢可能會非常迅速。當然如果在一個三年前的廉價筆記本上,將更緩慢的執行。不過,即使如此,我們也將看到在創建列存儲索引后將會極大的提升執行效率。
創建列存儲索引
列存儲索引有兩個類型:聚集和非聚集。有很多相似之處兩者之間,也有很多不同。其中一個不同是在2012中只有非聚集列存儲索引。2014中才加入了聚集的版本。我們將創建一個非聚集列存儲索引,以便讀者能在沒SQLServer2014的情況下實現。
CREATE NONCLUSTERED COLUMNSTORE INDEX NCI_FactOnlineSales ON dbo.FactOnlineSales (OnlineSalesKey, DateKey, StoreKey, ProductKey, PromotionKey, CurrencyKey, CustomerKey, SalesOrderNumber, SalesOrderLineNumber, SalesQuantity, SalesAmount, ReturnQuantity, ReturnAmount, DiscountQuantity, DiscountAmount, TotalCost, UnitCost, UnitPrice, ETLLoadID, LoadDate, UpdateDate);
Listing 2: 創建非聚集存儲索引
執行這個創建將花費一些時間(我必須要等待接近43秒),但是這是一個一次性的操作,在真實的數據倉庫中會在夜間完成這一典型的操作。一旦索引被創建,它會提高SQLServer 中很多查詢的效率。
我們獲得了什么?(優點)
當我們再次運行listing 1的代碼,結果和以前的一樣,但是這個結果幾乎是即刻返回的。整個查詢只用了0.34秒,是之前沒有加入列存儲索引速度的18倍多。當然如果從硬盤上讀取的話,即使是列存儲索引也會變慢,大約需要1.54秒,不過這仍然要比之前的8.11秒快了5倍多。
缺點
這個由非聚集列存儲索引獲得的性能提升令人印象深刻的,但是也需要在書寫查詢的時候非常小心。幾乎每個帶有列存儲索引的表查詢都能提高效率,但是你必須帶着許多限制來書寫代碼從而獲得更大的性能潛力。比如其中一個這樣限制是有關於外部連接的。
假如編寫 listing 1代碼的編程人員打算將BrandName為“Contoso ”的所有產品,即使沒有賣出去過的,都包含在結果中,那么就需要將Inner Join 變為Right Outer Join,如下listing3 中所示:
WITH ContosoProducts
AS (SELECT *
FROM dbo.DimProduct
WHERE BrandName = 'Contoso')
SELECT cp.ProductName,
dd.CalendarQuarter,
COUNT(fos.SalesOrderNumber) AS NumOrders,
SUM(fos.SalesQuantity) AS QuantitySold
FROM dbo.FactOnlineSales AS fos
INNER JOIN dbo.DimDate AS dd
ON dd.Datekey = fos.DateKey
RIGHT JOIN ContosoProducts AS cp
ON cp.ProductKey = fos.ProductKey
GROUP BY cp.ProductName,
dd.CalendarQuarter
ORDER BY cp.ProductName,
dd.CalendarQuarter;
Listing 3: 引入一個外鏈接
在沒有列存儲索引的情況下(或者帶有暗示模仿忽視列存儲索引的情況),當數據已經在緩存中時,這個查詢運行了6.71秒。包含了變化造成的在執行計划中的額外消耗,這部分大概花費了0.44秒在,耗時增加了接近百分之7。
當在我的SQLServer2012中不帶提示的去運行這個查詢時,優化器將立即選擇一個帶有列存儲索引的執行計划,結果正如期望是更快的,接近4.24秒。當然這依然是要比6.71秒那種不含列存儲索引的效率高的,但是與之前0.34秒的情況比較起來沒有明顯變化,那到底是為什么在同時都應用了列存儲索引的情況下,僅僅從inner改為了outer 就產生了如此大的性能變化呢?
批處理模式
列存儲索引是由於使用了一種叫做“批處理執行模式”的模式,用一種完全不同的方式來執行查詢,但是在2012中這一模式是有很多限制的,僅有少量操作符可以用來使用這一模式,只要使用了不再這些操作符中的操作符,這個查詢將返回到原來的查詢模式中。比如Outer Join就是這樣的操作符,將會引起查詢返回到行模式中,雖然也能獲取一部分性能提升,但是不能從批處理模式中得到顯著提升。
最快速的方式去核實這個模式就是通過執行計划來查看該查詢在SSMS 中的圖像。檢查兩個屬性“Estimated Execution Mode” 和“Actual Execution Mode”,下圖極為在批處理模式下查詢執行計划的示例,兩個屬性都為batch。
Figure 1-1: 執行計划顯示為Batch
當然在2014中批處理模式的操作符增加很多,其中outer join 也是其中之一,總之在性能和限制上,2014都有顯著的提高,這一點是毋庸置疑的。
對比效果.
沒有一種簡單的方式去預測當你創建列存儲索引后性能的提升。目前只有通過在真實環境下比較查詢性能或者在一個盡可能真實的測試環境下來測試比較,它帶來的好處。
對於能夠運行在批處理模式下的查詢而言,我們已經能看到在添加列存儲索引后性能提升了5到70倍,相比較於行模式的查詢,性能的提升永遠是更小的,一般為50%到20倍的提升。
總結
通過使用列存儲索引通過兩個因素來提升性能。一個是通過新的索引架構來節省I/O,另一個是批處理模式。很不幸的是,在SQLServer2012中僅有少量操作符可以使用列存儲索引,造成許多查詢被迫采用行模式執行,喪失了批處理模式的性能獲得。不過好消息是,絕大多數的限制在SQLServer 2014 中得到了完善。
翻譯自StairWay