SQL Server 列存储索引性能总结(5)——列存储等待信息

接上文: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'; 

CXPACKET=1的情况
  执行时间如下:

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配置,速度越快。所以凡是都有个例外。

   下一篇:SQL Server 列存储索引性能总结(6)——聚集和非聚集列存储索引的压缩

发布了192 篇原创文章 · 获赞 1268 · 访问量 250万+

猜你喜欢

转载自blog.csdn.net/DBA_Huangzj/article/details/104910789