接上文:SQL Server 列存儲索引性能總結(4)——列存儲壓縮,本文介紹列存儲相關的鎖
上週六,我在加班,爲公司的Azure SQL DB測試聚集列存儲索引,按照網上的說法,對堆表建立聚集列存儲索引應該很快的,何況我使用的是SQL DB中最高I/O的Pricing Tier——Business Critical vCore 80,號稱IOPS 可達204800,Log的吞吐量也可以達96 Mbps。
可是事與願違,我的表500多列,1300萬行,堆表大小100G,創建時間整整50分鐘。我很納悶,我在一臺普通的Linux VM裏面的SQL Server 2019,對三千萬數據建列存儲也只是需要2分鐘左右,這個太不合理了。所以我刪了索引再次創建,並且查看發生了什麼事。
最好的切入點往往就是事務和等待。如果事務沒有明顯的異常,那很可能就是存在等待/阻塞。由於SQL DB不支持很多很好的工具如sp_whoisactive,所以只能簡單查看sys.sysprocesses。發現存在幾個等待類型——CXPACKET 、cxconsumer和columnstore_build_throttle。而且持續非長久的時間。那麼下面我們來了解一下這三個等待類型是關於什麼的。
CXPACKET
很好理解也挺常見,你可以理解爲出現了並行執行,如果服務器有多CPU,那麼這個類型是很常見的。除非你把並行度設爲1,不過這樣比較不合理。特別是對於我公司環境裏面偏DW類型的應用而言。
這個類型主要是SQL Server認爲需要進行並行執行某個session的SQL,使用多線程(Thread)來實現,線程之間出現了阻塞或者等待(水桶效應)。
我們在TempDB上建一個表然後循環插入80萬數據,然後更新統計信息:
use tempdb
go
CREATE TABLE orders (d_id INT, o_id INT, o_amount INT, o_description CHAR(2000))
CREATE UNIQUE CLUSTERED INDEX test ON orders(d_id, o_id)
go
BEGIN TRAN
DECLARE @i INT
SET @i = 1
WHILE @i <= 800000
BEGIN
INSERT INTO orders VALUES (@i % 8, @i, RAND() * 800000, REPLICATE('a', 2000))
SET @i = @i + 1
END
COMMIT TRAN
GO
UPDATE STATISTICS orders WITH fullscan
GO
CREATE TABLE #department (d_id INT)
INSERT INTO #department VALUES(0)
INSERT INTO #department VALUES(1)
INSERT INTO #department VALUES(2)
INSERT INTO #department VALUES(3)
INSERT INTO #department VALUES(4)
INSERT INTO #department VALUES(5)
INSERT INTO #department VALUES(6)
INSERT INTO #department VALUES(7)
GO
接下來清空統計信息,我們使用OPTION(MAXDOP 1)來限定不允許並行,然後使用SET STATISTICS TIME ON來統計一下時間,並且檢查CXPACKET的信息:
SET STATISTICS time ON
GO
DBCC sqlperf('sys.dm_os_wait_stats', clear)
DECLARE @order_amount INT
SELECT @order_amount = MAX(o_amount)
FROM orders o INNER JOIN #department d ON (o.d_id = d.d_id)
OPTION (maxdop 1)
SELECT * FROM sys.dm_os_wait_stats
WHERE wait_type = 'CXPACKET';
執行時間如下:
SQL Server Execution Times:
CPU time = 815 ms, elapsed time = 815 ms.
接下來按同樣的方式測試不限制並行度,也就是MAXDOP爲0:
DBCC sqlperf('sys.dm_os_wait_stats', clear)
DECLARE @order_amount INT
SELECT @order_amount = MAX(o_amount)
FROM orders o INNER JOIN #department d ON (o.d_id = d.d_id)
OPTION (maxdop 0)
SELECT * FROM sys.dm_os_wait_stats
WHERE wait_type = 'CXPACKET'
GO
執行時間如下:
SQL Server Execution Times:
CPU time = 983 ms, elapsed time = 306 ms.
從這兩個測試可以對比出,並行執行首先可能出現waiting,然後CPU time會高,因爲使用的CPU數量相對也多。但是從elapsed time也就是運行時間來看,並行執行的時間相對較短。
但是這並不代表什麼,對於OLTP系統而言,由於操作相對頻繁,量少,並行執行並不能從中獲益太多。對DW/OLAP而言,則可以充分利用CPU資源。所以對於我個人而言,由於長期運維OLTP系統,所以並不很喜歡看到這種等待狀態。
COLUMNSTORE_BUILD_THROTTLE
這個等待類型會在列存儲索引創建和重建的過程中出現。主要原因是第一個線程會決定內存的使用量和可能需要的線程數,這就導致了所有的線程都需要等待第一個線程分析並完成這些信息收集和處理。所以出現了這部分的等待。
如果沒有足夠內存,那麼總線程數就會降低,這個過程也會算入等待時間。
有一個相關的擴展事件——column_store_index_build_throttle。
這一類等待也通常會在內存壓力或者Dictionary(後續章節會說到) 壓力發生時出現。
下面創建一個測試來演示dictionary壓力和哪些行組會被非常明顯地修剪。
DROP TABLE IF EXISTS dbo.t_colstore;
CREATE TABLE dbo.t_colstore (
c1 int NOT NULL,
c2 INT NOT NULL,
c3 char(40) NOT NULL,
c4 char(800) NOT NULL
);
set nocount on
declare @outerloop int = 0
declare @i int = 0
while (@outerloop < 1100000)
begin
Select @i = 0
begin tran
while (@i < 2000)
begin
insert t_colstore values (@i + @outerloop, @i + @outerloop, 'a',
concat (CONVERT(varchar, @i + @outerloop), (replicate ('b', 750))))
set @i += 1;
end
commit
set @outerloop = @outerloop + @i
set @i = 0
end
go
然後開啓實際執行計劃並創建聚集列存儲索引:
CREATE CLUSTERED COLUMNSTORE INDEX CCI ON dbo.t_colstore;
從insert運算符的屬性中的“WaitStats”可以看到,其中會有一個等待狀態是COLUMNSTORE_BUILD_THROTTLE,也就是說有1.183秒(來自WaitTimeMS)是用於第一個線程的修剪操作。出現這種情況,一般就需要修改表設計。
cxconsumer
這是從SQL 2016 SP2開始引入的新等待狀態。這個等待狀態意味着有並行計劃在運行。良性 CXPACKET 等待現在顯示爲 CXCONSUMER 等待,可以安全地忽略。
不過凡事也有個但是,對於執行計劃操作符而言,有生產者(producer)和消耗者(consumer)線程。在並行過程中,生產者線程會實際影響性能,導致消耗者線程只能等待生產者提供數據。
對於這種情況,切記不要單純降低MAXDOP,需要優化性能。
但是在我的環境中比較悲劇,因爲這是創建聚集列存儲索引,這一點很難進行優化。
總結
綜合前面所述,在有足夠CPU的前提下,不限制MAXDOP的話,創建大表的索引(不管是否列存儲),都可能會使用並行運行,這樣就會出現線程之間的等待,並行執行對於OLTP來說並不一定是好事,不過也不一定是壞事。另外由於本系列是基於列存儲的,所以更重要的我想介紹COLUMNSTORE_BUILD_THROTTLE這種等待類型。這種等待類型出現的時候,注意檢查是否有內存壓力和Dictionary的壓力,另外表的設計是否合理。關於這部分在後續文章再介紹。
在本人工作的那個例子中,我分別試了1/4/8/16/24/32 的MAXDOP,但是發現越大的MAXDOP配置,速度越快。所以凡是都有個例外。